Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
92 commits
Select commit Hold shift + click to select a range
24a19f3
chore: add type hints, add helper method for loading JSON
liquidz00 Apr 20, 2025
b0fd8ed
chore: add type hints
liquidz00 Apr 20, 2025
dd0266d
chore: add ruff config for code formatting
liquidz00 Apr 21, 2025
1bb0ca9
chore: add type hints, format code to PEP standards with ruff
liquidz00 Apr 21, 2025
8b2c0d2
chore: ignore ruff cache
liquidz00 May 10, 2025
c64afb9
chore: default to double quotes instead of single quotes
liquidz00 May 10, 2025
a2f6bf1
refactor: format codebase with double quotes for consistency
liquidz00 May 10, 2025
c45d68e
Merge pull request #52 from liquidz00/type-hints
ball42 May 12, 2025
361e2d6
fix: corrected notice info for custom webhooks with custom auth
ball42 Jul 30, 2025
9f2e1b4
Merge pull request #55 from jamf/bugfix
ball42 Aug 5, 2025
e42ffb5
Merge branch 'main' into develop
ball42 Feb 11, 2026
dd486fb
feature: templates (#58)
ball42 Feb 11, 2026
f398b84
feature: unified automations (#61)
ball42 Feb 11, 2026
b2008f7
feat: add credential management and search functionality (#60)
ball42 Feb 11, 2026
661cda3
feature: UI refresh (#59)
ball42 Feb 11, 2026
c6cfb8d
chore: wire blueprints and cleanup (#57)
ball42 Feb 11, 2026
11d5714
feat: add session timeout warning modal and extend session duration t…
ball42 Feb 13, 2026
8c1eccc
feat: update static file references to use url_for for better cache m…
ball42 Feb 13, 2026
c0c50db
feat: enhance error handling and logging
ball42 Feb 13, 2026
e6d03db
feat: redesign error and success pages with improved UI components
ball42 Feb 13, 2026
580f78b
feat: enhance UI
ball42 Feb 13, 2026
609f975
feat: add smoke-test harness and CI (ruff + pytest) (#62)
ball42 Jul 7, 2026
81a5493
fix: template webhooks fire (B1) + reject path traversal in import (B…
ball42 Jul 8, 2026
81839d1
feat: real, admin-configurable session timeout + cookie hardening (B4…
ball42 Jul 9, 2026
0a6d735
fix: bug-fix batch — receiver, resource delete, error pages, login UX…
ball42 Jul 10, 2026
105c6ce
fix: link the Setup Required error page directly to /setup (#66)
ball42 Jul 10, 2026
dabb394
fix: make session-timeout modal survive sleep via absolute deadline (…
ball42 Jul 10, 2026
0b0061a
chore: remove dead code fossils (mongoengine, load_home, stale commen…
ball42 Jul 11, 2026
7f0f5f8
fix: J10 paper-cuts — upload cap, setup slash, shebang, copy, auth gu…
ball42 Jul 11, 2026
e02ab88
fix: guard legacy redirect routes against open-redirect (B8) (#70)
ball42 Jul 11, 2026
a4876ba
docs: add Jamf Routines comparison, bump to v3.2, add release notes
ball42 Jul 23, 2026
68ad3d9
docs: document the JAWA automation script contract
ball42 Jul 23, 2026
a530877
feat: note Jamf Routines as a hosted alternative on the setup page
ball42 Jul 23, 2026
36662ee
docs: scope script output/exit behavior to the webhook path
ball42 Jul 23, 2026
1237294
Merge Jamf Routines plug + v3.2 README + scripting docs into develop
ball42 Jul 23, 2026
b9371c7
fix: harden installer.sh and guard it in CI
ball42 Jul 23, 2026
52f0520
chore: stop tracking runtime webhooks.json, ignore scripts/
ball42 Jul 23, 2026
4119061
Merge installer.sh hardening + CI hygiene guard into develop
ball42 Jul 23, 2026
6187199
refactor: clean up the setup form layout
ball42 Jul 23, 2026
b780e71
fix: close bogus-credential login bypass via stale token (security)
ball42 Jul 23, 2026
4e6cd8d
fix: render a single autofocus on the login form
ball42 Jul 23, 2026
47f58c3
feat: add success-context session flash + PRG on automation create (J13)
ball42 Jul 24, 2026
dc906af
feat: convert automation edit + delete to PRG (J13)
ball42 Jul 24, 2026
0420d55
feat: convert /setup success to PRG (J13)
ball42 Jul 24, 2026
ee7de28
feat: give the success page forward actions instead of history.back()…
ball42 Jul 24, 2026
21ccb6e
test: assert smart-group notice survives the success flash (J13)
ball42 Jul 24, 2026
2aff5dc
Merge J13: success-page PRG + forward actions into develop
ball42 Jul 24, 2026
672493f
feat: add the Jamf webhook event catalog as static reference data (J15)
ball42 Jul 29, 2026
012ffa5
fix: close two escape paths in the webhook catalog fail-soft guard (J15)
ball42 Jul 29, 2026
af300e1
feat: serve the webhook event reference at /reference/webhooks (J15)
ball42 Jul 29, 2026
3b9c551
fix: degrade the webhook reference when only part of the catalog loads
ball42 Jul 29, 2026
977ded9
feat: render event schemas and sample payloads on the reference page …
ball42 Jul 29, 2026
ec39677
test: pin the reference page against a damaged event entry (J15)
ball42 Jul 29, 2026
499998f
fix: guard the reference page against wrong-typed catalog entries (J15)
ball42 Jul 29, 2026
dce7061
test: pin the reference index against a wrong-typed entry (J15)
ball42 Jul 29, 2026
a5ba3d1
feat: make Extras > Webhook Reference live, drop Notebooks (J14/J15)
ball42 Jul 29, 2026
781dae0
test: scope Extras menu tests to the dropdown, not the page
ball42 Jul 29, 2026
5a1b700
feat: give empty-state CTAs brand styling (J14)
ball42 Jul 29, 2026
ea1ef0d
test: scope empty-state assertions to the empty-state markup
ball42 Jul 29, 2026
089848f
feat: style the hero subtitle link with tokens, not an inline style (…
ball42 Jul 29, 2026
004a9a6
test: pin the hero subtitle link text and its escaping
ball42 Jul 31, 2026
2f2a1c1
feat: group the Jamf event dropdown by category from the catalog (J15)
ball42 Jul 31, 2026
f51f561
fix: degrade one bad event category instead of the whole dropdown
ball42 Jul 31, 2026
935654f
test: cover both mapping and string in miscategorised-event test
ball42 Jul 31, 2026
3a504fc
test: make the catalog-driven render assertions able to fail
ball42 Jul 31, 2026
35ad0d3
fix: apply the catalog shape rule to the reference pages too
ball42 Jul 31, 2026
716cac4
Merge J14+J15: dashboard/Extras polish + Webhook Reference page into …
ball42 Aug 4, 2026
6896531
chore: bump version strings to 3.2.0 (J9)
ball42 Aug 4, 2026
ba28430
chore: stop tracking runtime data/cron.json
ball42 Aug 4, 2026
8298d1f
fix: make every bundled template script runnable (J16, B6)
ball42 Aug 5, 2026
e2e0344
fix: correct template catalog metadata and the boolean-event crash (J…
ball42 Aug 5, 2026
fa07f68
fix: stop corrupting template config values on enable (J16, B13)
ball42 Aug 5, 2026
4fbf0a8
fix: substitute template tokens in one pass (J16, B13 review round 1)
ball42 Aug 5, 2026
14832ac
fix: reject imported template packages that cannot run (J16, B16)
ball42 Aug 5, 2026
d5ba4bc
feat: register templates as Jamf Pro automations and create them in J…
ball42 Aug 5, 2026
e4587af
fix: stop enable writing auth values that lock the webhook out (J16, …
ball42 Aug 6, 2026
e7a0582
test: guard the canonical API block against drift (J16)
ball42 Aug 7, 2026
5131f31
feat: offer Jamf Pro registration on the template import path (J16, B14)
ball42 Aug 10, 2026
869ea68
Merge v3.2.0 version bump + untrack data/cron.json into develop (J9)
ball42 Aug 10, 2026
154ea47
Merge J16: finish the bundled workflow templates into develop
ball42 Aug 10, 2026
3b1f6ba
fix: make the Resource Files page safe and consistent with the design…
ball42 Aug 11, 2026
c194a25
Merge the Resource Files page safety and design-system fix into develop
ball42 Aug 27, 2026
1781db4
fix: stop reflecting the webhook name into the 401 body (J11)
ball42 Aug 28, 2026
f0ffe5d
docs: rewrite the v3.2.0 release notes for the shipped payload (J9)
ball42 Aug 28, 2026
52c4e3d
fix: accept the activationcode shape a live Jamf Pro actually returns
ball42 Aug 28, 2026
8af3fb2
chore: bump requests and Werkzeug past their open advisories
ball42 Aug 28, 2026
56615c7
fix: hold requests at 2.32.x so Python 3.9 keeps resolving
ball42 Aug 28, 2026
5aedf02
docs: state the platform floor JAWA's dependencies actually require
ball42 Aug 28, 2026
87b3e70
fix: hide the credential fields a selected credential set supplies
ball42 Aug 28, 2026
9c3a68f
test: assert the credential precedence by whole line, not URL substring
ball42 Aug 28, 2026
888b74c
fix: close five release blockers found reviewing the v3.2.0 PR (J23-J27)
ball42 Aug 31, 2026
e9bde45
fix: refuse an unsupported Python before removing an existing install…
ball42 Aug 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: CI

on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
# Oldest supported and current — catches syntax drift both ways.
python-version: ["3.9", "3.14"]
steps:
- uses: actions/checkout@v4

- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
cache: pip

- name: Install dependencies
run: pip install -r requirements-dev.txt

- name: Lint (ruff)
run: ruff check .

- name: Test (pytest)
run: python -m pytest -v

installer-hygiene:
# Guards bin/installer.sh (curled off main, run as root) against invisible-unicode
# paste corruption. The only legitimate non-ASCII lives in the spinner charsets
# (`local spin=` assignments); anything else is a stray ZWSP/NBSP/BOM and fails.
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Bash syntax check
run: bash -n bin/installer.sh

- name: Reject stray non-ASCII (paste corruption)
run: |
stray=$(grep -nP '[^\x00-\x7F]' bin/installer.sh | grep -vE 'local spin=' || true)
if [ -n "$stray" ]; then
echo "::error::Non-ASCII characters found outside the spinner charsets in bin/installer.sh:"
echo "$stray"
echo "These are almost certainly invisible paste corruption (zero-width space, NBSP, BOM)."
exit 1
fi
echo "OK: no stray non-ASCII in bin/installer.sh"
12 changes: 12 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -151,3 +151,15 @@ dmypy.json

# ruff
.ruff_cache

# LLM generated files
.claude/
CLAUDE.md

# JAWA runtime data
data/server.json
data/credentials.json
data/cron.json
data/webhooks.json
resources/files/
scripts/
238 changes: 235 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
# Jamf Automation and Webhook Assistant ("JAWA") Version 3.1.1
# Jamf Automation and Webhook Assistant ("JAWA") Version 3.2

<p align="center"> <img src="https://github.com/jamf/JAWA/blob/main/static/img/jawa_icon.png" width="384"/> </p>


JAWA allows an IT Administrator to focus on providing the best end user experience through automation.

> **Prefer a hosted option?** JAWA is self-hosted — you run and maintain the server yourself. If you'd rather not operate infrastructure, **[Jamf Routines](https://learn.jamf.com/r/en-US/jamf-routines-documentation/jamf_workflow_automation)** is a Jamf-hosted, Jamf-supported automation service. You can run both — see [JAWA vs. Jamf Routines](#jawa-vs-jamf-routines) below.

***[!]** NOTE: Always test automations in a dev/eval environment before deploying to production.*

## What is JAWA?
Expand All @@ -16,15 +18,50 @@ JAWA, the Jamf Automation and Webhook Assistant, is a web server designed to str

*Read the [JAWA Admin Guide](https://github.com/jamf/JAWA/wiki) too!*

## JAWA vs. Jamf Routines

JAWA and [Jamf Routines](https://learn.jamf.com/r/en-US/jamf-routines-documentation/jamf_workflow_automation) both automate Jamf Pro workflows, in different ways.

| | **JAWA** | **Jamf Routines** |
|---|---|---|
| **Hosting** | Self-hosted (your server) | Jamf-hosted |
| **Maintenance** | You own the OS, TLS, updates, and uptime | Managed by Jamf |
| **Support** | Community / open source | Jamf-supported |
| **Automation model** | Your own scripts, triggered by webhooks or a schedule | Template-based workflows that connect tools to Jamf Pro |
| **Setup effort** | Provision a server, certificate, and DNS | Sign in and go |

**Choose JAWA** if you want full control, custom scripting, and don't mind running a server. **Choose Jamf Routines** if you'd rather not manage infrastructure and want a Jamf-supported, hosted experience. You can run both — they complement each other. For Jamf Routines availability and pricing, see the [Jamf Routines documentation](https://learn.jamf.com/r/en-US/jamf-routines-documentation/jamf_workflow_automation).

## Server Requirements

### General Server Requirements:

- Ubuntu 20.04+ or RHEL 8.x+
- Ubuntu 22.04+ or RHEL / Rocky 9.x+
- Minimum: 8GB RAM (16GB recommended)
- Minimum: 128GB Storage (512GB recommended)
- Minimum: 2 CPU Core (4 Cores recommended)
- Python 3.8+ (with pip)
- Python 3.9+ (with pip)

> The installer uses the distribution's default `python3` to build JAWA's virtual environment, so
> the OS version is what determines the Python version. Ubuntu 20.04 ships Python 3.8 and
> RHEL/Rocky 8 ships Python 3.6, neither of which satisfies JAWA's dependencies — use Ubuntu 22.04
> or later, or RHEL/Rocky 9 or later.
>
> The installer checks this **before it touches an existing install** and stops with the detected
> version if the host is below Python 3.9. The specific blocker is Werkzeug: its patched releases
> require Python 3.9 or later, and no patched Werkzeug exists for 3.8, so a 3.8 host cannot run
> JAWA without a known security advisory.
>
> If you accept that advisory and need to install on Python 3.8 anyway, set
> `JAWA_ALLOW_UNPATCHED_WERKZEUG=1`:
>
> ```bash
> sudo JAWA_ALLOW_UNPATCHED_WERKZEUG=1 bash ./installer.sh
> ```
>
> This holds Werkzeug at 3.0.6 — the newest release available to Python 3.8 — and prints the
> advisory you are accepting. It is a stopgap for hosts that cannot be upgraded yet, not a
> supported configuration.

### Network Requirements:

Expand Down Expand Up @@ -80,10 +117,205 @@ When scripting for webhooks, verify JSON structure sent from source:
1. [Jamf Pro Webhook Event Info](https://developer.jamf.com/developer-guide/docs/webhooks)
2. [Okta Webhook Event Info](https://developer.okta.com/docs/reference/api/event-types/?q=event-hook-eligible)

## Writing Automation Scripts

JAWA runs **your** scripts in response to Jamf Pro (or Okta/custom) webhooks and on a schedule. A script can be written in any language JAWA's host can execute; the examples here are Python. This section describes the contract JAWA uses to call your script.

### How JAWA calls a webhook script

When a webhook fires, JAWA executes your script and passes the **entire event payload as a single JSON string in the first command-line argument** (`sys.argv[1]`). It does not use stdin, environment variables, or a file. Your script's first job is to parse it:

```python
import json
import sys

event_data = json.loads(sys.argv[1]) # the whole webhook payload
```

A Jamf Pro webhook payload has two top-level keys:

- `event_data["webhook"]` — event metadata (`webhookEvent`, `eventTimestamp`, `id`)
- `event_data["event"]` — the event's own fields (for example `groupAddedDevicesIds`, `name`)

### Output and status (webhook automations)

- Anything your script prints (stdout and stderr) is captured line-by-line into the JAWA log under the automation's name. Use `print()` for progress and diagnostics.
- Exit `0` for success. A **non-zero exit code is recorded as a failure** in the log.

### Credentials

JAWA does not inject Jamf Pro or third-party credentials into your script. A script that calls the Jamf Pro API authenticates itself (for example, requesting its own OAuth token). Store secrets in your script's own configuration, not in JAWA.

### Scheduled (timed) automations

A timed automation runs your script on a schedule with **no webhook payload** — `sys.argv[1]` is not present. If one script serves both paths, guard for it:

```python
import json
import sys

event_data = json.loads(sys.argv[1]) if len(sys.argv) > 1 else {}
```

Timed automations run under the system's cron, so a script's output and exit status are handled by cron (for example, in the host's mail/syslog), not captured in the JAWA log.

### Complete example

This bundled script (`data/workflows/scripts/smart_group_slack.py`) posts to Slack when devices join a smart group:

```python
#!/usr/bin/env python3
"""Send Slack notification on smart group membership change.

Webhook event: SmartGroupComputerMembershipChange
"""

import json
import requests
import sys
from datetime import datetime

SLACK_WEBHOOK_URL = "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"


def main():
event_data = json.loads(sys.argv[1])
event = event_data["event"]

id_list = event.get("groupAddedDevicesIds", [])
if not id_list:
print("No devices entered the group.")
sys.exit(12)

group_name = event.get("name", "Unknown Group")

slack_data = {
"attachments": [
{
"title": f"Smart Group Update: {group_name}",
"text": f"{len(id_list)} device(s) added to {group_name}",
"footer": "JAWA Webhook Automation",
"ts": datetime.timestamp(datetime.now()),
}
]
}

resp = requests.post(
SLACK_WEBHOOK_URL,
data=json.dumps(slack_data),
headers={"Content-Type": "application/json"},
)
print(f"Slack notification: {resp.status_code}")


if __name__ == "__main__":
main()
```

### Common mistakes

- **The payload is `sys.argv[1]`, not stdin, not an environment variable, and not a file.**
- **It is a JSON *string*** — you must `json.loads()` it before use.
- **The event fields are nested** under `event_data["event"]`, not at the top level.
- **Don't assume JAWA provides a Jamf Pro token** — your script authenticates itself.
- **On the timed path there is no `sys.argv[1]`** — guard for it if a script serves both.

## Releases

Find JAWA releases [here.](https://github.com/jamf/JAWA/releases)

### JAWA v3.2.0 release

**Upgrade notes — please read before upgrading**

- **v3.2 is the last release that can migrate a JAWA v2 install.** The v2 upgrade path works in
this release and is unchanged. If you are still on v2, move to v3.2 before upgrading further.
- **Template webhooks you previously enabled will begin firing.** A bug meant enabled and
imported template webhooks silently never triggered. Template webhooks run **without
authentication by default** — anyone who knows the hook name can trigger one. Review your
enabled templates and add webhook authentication in the automation's edit screen if an
endpoint should be protected. Authenticated-by-default templates are planned for a future
release.
- **New webhook names are validated more strictly.** `#` and `%` are no longer accepted in a new
webhook name, because Jamf Pro cannot call a URL containing them. Existing automations are
unaffected.
- **Minimum platform is now Ubuntu 22.04 or RHEL/Rocky 9, and Python 3.9.** The installer builds
JAWA's virtual environment from the distribution's default `python3`, and JAWA's dependencies no
longer support Python 3.8. Ubuntu 20.04 (Python 3.8) and RHEL/Rocky 8 (Python 3.6) can no longer
run JAWA — RHEL/Rocky 8 in fact stopped being able to when JAWA moved to Flask 3, which the
stated requirements had not caught up with. Check `python3 --version` on the host before
upgrading. The installer now enforces this itself: it verifies the Python version **before**
backing up or removing anything, so an unsupported host is refused with its existing install
intact rather than left with a dead service. If you must install on Python 3.8 and accept an
unpatched Werkzeug, see `JAWA_ALLOW_UNPATCHED_WERKZEUG` under Server Requirements.
- **Content JAWA ships inside `data/` is not upgraded in place.** The installer preserves your
`data/` directory across an upgrade, which protects your automations and settings, but it also
means the bundled template scripts and the webhook event catalog stay at the version you first
installed. A fresh install gets the current copies.

- New features
- **Bundled templates now work as shipped.** Every bundled template runs when triggered; two
were incomplete sketches that failed immediately. Enabling a template also creates the
matching webhook in Jamf Pro for you and files the automation under Jamf Pro, so its
trigger event is visible and editable. Templates can be protected with Basic
authentication at enable time.
- **Importing a template package can create its webhook in Jamf Pro too** — a new *Create
webhook in Jamf Pro?* option, on by default. Clear it to install the script locally only.
Because the package name becomes part of the URL Jamf Pro calls, a name with spaces or
other URL-unsafe characters is refused on that path; fix the package file, or clear the box
to install locally under any name.
- **Webhook Reference page** documenting the Jamf Pro webhook events with sample payloads.
One event, `DeviceRateLimited`, is listed with its sample payload still pending.
- **Admin-configurable session timeout** in Setup: 15 minutes (default), 1 hour, 4 hours, or
8 hours, with hardened session cookies. The 15-minute default remains the most secure; the
longer options are convenient for workflow testing but leave an unattended signed-in
console exposed for longer. Choose deliberately.
- Smoke-test harness and CI (ruff + pytest) running on every push and pull request.
- Documentation for writing automation scripts.
- Script Preview and Download Script now show the real substitution tokens rather than
generic placeholder text, so a downloaded script is self-documenting.
- Resource Files listing gained Size and Type columns.
- Bugfixes
- Template webhooks now fire (see upgrade notes).
- Configuration values containing `&`, quotes, or angle brackets — Microsoft Teams and Power
Automate URLs, and some secrets — are no longer corrupted when written into a generated
script.
- Selecting a saved credential set on the template enable form now hides the server URL,
client ID and client secret fields it supplies, instead of showing empty fields alongside a
hint that claimed they would be auto-filled. A value typed into one of those fields was
previously discarded without warning, because the saved set takes precedence. Fields a
partial credential set cannot supply stay visible.
- Enabling a template no longer stores authentication values that locked the webhook out.
- Imported template packages are validated before installation: a `.jawa.json` whose script
is truncated or has a syntax error is rejected with the offending line number, instead of
installing a webhook that fails silently when it fires.
- Fixed a crash on templates whose trigger event was a boolean.
- The 401 response from an inbound webhook no longer echoes the requested hook name back.
- Rejected path traversal in template package import, and guarded the legacy redirect routes
against open redirects.
- Resource Files page: Download and Delete are no longer adjacent, identical buttons, Delete
routes through the shared confirmation screen, and hidden files no longer leak into the
listing.
- The success-page Back button no longer re-submits the action it just completed.
- Corrected dashboard links and removed dead Extras links.
- Uploads between 1 MB and 16 MB no longer fail with an opaque error; the server upload cap
is now set explicitly.
- Setup strips trailing slashes from Jamf Pro URLs, so generated webhook URLs no longer
contain double slashes.
- Script uploads with no `#!` shebang are rejected with a clear message instead of failing
cryptically at trigger time.
- The session-timeout warning now survives laptop sleep and backgrounded tabs.
- Fixed resource file deletion, added 403/405/500 error pages, and hardened receiver edge
cases including malformed form payloads.
- The "Setup Required" error page links directly to Setup.
- Removed
- The *Enrollment Pipeline* template, which shipped as an incomplete outline and needs a
device-assignment CSV contract that will be designed properly in a future release.
- Repository maintenance
- Removed dead code (legacy MongoEngine, stale stubs).
- `data/cron.json` is no longer tracked in git, so a checkout can no longer overwrite real
cron definitions with an empty seed file.

### JAWA v3.1.1 release
- Bugfix
- Resolved #49
Expand Down
Loading
Loading