A macOS-only Go CLI for the Iru (formerly Kandji) Endpoint Management API.
jellyfish vulns list- vulnerability detections across the fleet, one row per device-CVE pair; filter by device ID or serial.jellyfish vulns summary- per-CVE rollup: status, severity, CVSS, KEV score, affected software, device count.jellyfish user show <id-or-email>- a user, their devices, and the active detections on each.jellyfish overview- org-widesec_scoreper user with Best-5 / Most-Dangerous-5 leaderboards and a ranked roster.jellyfish users send-email- bulk per-user vulnerability reports from a CSV or email list.jellyfish configure- store tenant, region, API token and Gmail credentials (secrets go in the macOS Keychain).
Requires Go 1.25+ on macOS.
go install github.com/bawdo/jellyfish@latestThis installs to $GOBIN (or $GOPATH/bin, default ~/go/bin). Add it to your PATH:
echo 'export PATH="$HOME/go/bin:$PATH"' >> ~/.zshrc && source ~/.zshrcFor a local build without installing, make build produces ./bin/jellyfish. Confirm the install with jellyfish version.
jellyfish ships Cobra's completion script. For Zsh (the macOS default):
mkdir -p ~/.zsh/completions
jellyfish completion zsh > ~/.zsh/completions/_jellyfish
exec zshIf ~/.zsh/completions is not on fpath, add it in ~/.zshrc. Re-run the command after an upgrade to pick up new flags. Other shells: jellyfish completion {bash,fish,powershell} --help.
Every command accepts --help, -h, or the bareword help - jellyfish overview help and jellyfish overview --help are equivalent.
jellyfish configurePrompts for the tenant subdomain (the bit before .api.kandji.io - the hostname is still kandji.io even though the product was renamed Iru), region (us or eu), and API token. Re-running is safe: each prompt shows the current value, and Enter keeps it.
Subdomain and region are written to ~/.config/jellyfish/config.yml (mode 0600); the token goes to the macOS Keychain under service jellyfish.secrets, account default:
security find-generic-password -s jellyfish.secrets -a default
security delete-generic-password -s jellyfish.secrets -a defaultjellyfish configure emailPrompts for From, default To, a Gmail service-account JSON path, header background colour, a logo PNG, and an optional List-Id domain. Non-secret values go to the email: block of config.yml; the Gmail JSON goes to the Keychain; the logo PNG (validated, under 512 KB) is copied into ~/.config/jellyfish/logos/. Enter keeps a value; a literal - clears it.
list_id_domain sets the List-Id header on sent mail. Left unset, it falls back to jellyfish. plus the domain of email.from - so a from of ops@example.com yields List-Id: <jellyfish.example.com>. You can also edit config.yml directly.
The Gmail service-account JSON is only needed if you want --send-email to deliver mail directly. If you would rather not set that up, skip it: every reporting command can still produce email with -o email, which writes a ready-to-send .eml file you can open or send from your own mail client (see Email output).
--send-email delivers through the Gmail API using a GCP service account with domain-wide delegation - it does not use your personal Google login. Setting it up is a one-off admin task and needs both a Google Cloud project and Google Workspace super-admin access:
- In a Google Cloud project, enable the Gmail API.
- Create a service account and generate a JSON key - this is the file
jellyfish configure emailasks for. - Note the service account's OAuth client ID (a numeric ID on its details page).
- In the Google Workspace Admin console, grant that client ID domain-wide delegation for exactly one scope:
https://www.googleapis.com/auth/gmail.send.
The From address you configure must be a real mailbox in that Workspace domain - the service account impersonates it to send. Google's official guide: Using OAuth 2.0 for server-to-server applications.
jellyfish vulns list # everything
jellyfish vulns list --device-id d-123 # one device by ID
jellyfish vulns list --serial C02XL0RKDV4 # one device by serial
jellyfish vulns list --limit 50 # single page
jellyfish vulns list -o json # JSON for jq
jellyfish vulns list --no-cache # always fetch freshEvery detection returned is active by definition - Iru drops a detection once the CVE is patched, so there is no "active only" filter. --limit is clamped to Iru's server-side maximum (300).
vulns list and user show walk Iru's detections endpoint in full, because Iru has no per-device server filter. The first call takes 30-90 seconds on a large tenant; results are cached for 15 minutes under ~/Library/Caches/jellyfish/. Pass --no-cache to force a fresh fetch. Change the TTL with cache_ttl_minutes in config.yml or via jellyfish configure cache.
A per-CVE rollup across the fleet - one row per CVE with status, severity, CVSS, KEV score, affected software, and device count.
jellyfish vulns summary # severity-sorted
jellyfish vulns summary --status active # currently-affecting only
jellyfish vulns summary --severity critical # critical only
jellyfish vulns summary --sort devices --limit 20 # top 20 by exposure
jellyfish vulns summary --sort kev # sort by KEVSort keys: severity (default), cvss, kev, devices, cve. The kev_score field reflects whether a CVE is in CISA's Known Exploited Vulnerabilities Catalog - bugs seen exploited in the wild, often a stronger patch-priority signal than CVSS alone.
jellyfish user show keith@example.com # by email
jellyfish user show 1f5b...e4 # by user ID
jellyfish user show keith@example.com -o jsonEmail lookup returns every user whose address matches, not just the first; bucketing detections per device triggers the detection walk (see Detection cache).
If two or more Iru users share the email address you passed, user show lists them and asks which to display when stdin is a terminal. In non-interactive runs (CI, pipes) the command exits non-zero with every matching user's ID and a jellyfish user show <id> re-run hint.
-o accepts table (default), json, yaml, csv, email. user show -o csv flattens to one row per detection, with these columns:
user_id, user_email, user_name, device_id, device_name, serial_number,
cve_id, package_name, package_version, severity, cvss_score,
detection_datetime
-o email writes an RFC 5322 .eml to stdout - styled HTML plus a plain-text alternative, with clickable NVD/MITRE CVE links. Open it in Mail, pipe it onward, or use --send-email to send it via Gmail.
jellyfish vulns summary --severity critical -o email > critical.eml
jellyfish vulns summary -o email | open -f -a MailFilter large reports with --severity, --status, or --limit first - Gmail clips long messages.
Recipient, sender, and subject default from the email: block of config.yml; flags override:
| Flag | Config key | Default |
|---|---|---|
--email-to |
email.default_to |
empty |
--email-from |
email.from |
git config user.email |
--email-subject |
email.subject_template |
per-command default |
--email-header-bg |
email.header_bg |
#2b3a55 (slate blue) |
--email-logo |
email.logo_path |
empty (no logo) |
--message / --message-file |
- | unset |
The logo renders at 56px height (width scales to its aspect ratio); supply a PNG under 512 KB, ideally at 2x height for retina displays. email.subject_template is a Go template with {{.Date}} and {{.Time}}. CVE link targets (cve_link_primary, cve_link_secondary) are config-overridable; the {cve} token is substituted literally.
Adds a short note to the top of an email. Supported by vulns summary and user show when producing email output.
--messageopens$VISUAL/$EDITOR/vion a scratch file;#lines are dropped on save, and an empty result aborts.--message-file <path>reads the note verbatim (use-for stdin;#lines are kept).- The two are mutually exclusive and require email output. URLs in the note become clickable links.
jellyfish vulns summary --severity critical --send-email --message
jellyfish user show alice@example.com --send-email --message-file note.txt
echo "Patching window moved to Saturday." | jellyfish user show alice@example.com --send-email --message-file ---send-email renders the .eml and sends it via the Gmail API instead of writing it to stdout.
jellyfish vulns summary --severity critical --send-email --email-to secops@example.com
jellyfish user show keith@example.com --send-emailFor user show, the recipient is --email-to, else email.default_to, else the user's own address. vulns summary has no user fallback - pass --email-to or set email.default_to. On success, stderr prints sent: to=<addr> from=<addr> gmail-id=<id>.
The Gmail path uses a Workspace service account with domain-wide delegation; the service-account JSON lives in the Keychain under service jellyfish.secrets, account gmail_default (set it via jellyfish configure email).
Every sent message carries a List-Id header. Create a Gmail filter with Has the words: list:<your-domain> to label all Jellyfish mail in one rule. The X-Jellyfish-Report header (vulns-summary, user-show, users-send, overview) distinguishes commands; X-Jellyfish-Tenant and X-Jellyfish-Version are also set for audit.
Mails per-user vulnerability reports to a list of addresses in one run. Each recipient gets a report for their own devices; users with no devices or no vulnerabilities are skipped.
jellyfish users send-email --csv fleet.csv # auto-detects email column
jellyfish users send-email --emails alice@example.com,bob@example.com
jellyfish users send-email --csv fleet.csv --dry-run # preview, no mail sent
jellyfish users send-email --csv fleet.csv --email-to me@example.com # redirect all (test mode)--csv and --emails are mutually exclusive. The detection walk runs once and is reused, so a 50-user run costs about one user show plus the per-user sends. The command prompts before sending; --yes skips the prompt. --csv-email-column overrides CSV header auto-detection (email, user_email, e-mail).
Stderr emits one line per recipient and a final summary:
sent input=alice@example.com to=alice@example.com gmail-id=msg-abc
skip input=bob@example.com reason=no-devices
error input=dave@example.com reason=user-not-found
summary: sent=1 skipped=1 errors=1
reason= values: no-devices, no-vulnerabilities, user-not-found, no-recipient. Dry-run lines use would-send. Unlike user show, this command ignores email.default_to - set --email-to explicitly to redirect.
When one input email matches more than one Iru user, each user is processed independently and the stderr lines gain a user=<id> segment (e.g. sent input=keith@example.com user=u-abc to=keith@example.com gmail-id=...). The summary counters reflect user-level outcomes, not input rows.
Computes a sec_score per user (the sum of CVSS scores across their active detections) and rolls those into org totals, averages, a Best-5 and Most-Dangerous-5 leaderboard, and a ranked roster. The roster is sorted by sec_score ascending, so rank 1 is the most secure user. Users with no devices are excluded.
jellyfish overview # table to stdout
jellyfish overview -o csv > scores.csv
jellyfish overview --send-email --email-to security@example.com # admin report
jellyfish overview --send-email --per-user # personalised fanout
jellyfish overview --emails alice@example.com,bob@example.com # roster subset--per-user requires --send-email and sends each user a copy with a "Your standing" callout and a highlighted roster row. --send-email without --per-user requires --email-to. --csv / --emails narrow the roster - and the totals, leaderboards, and fanout - to a named subset.
The roster is keyed by Iru user ID, so when two users share an email both appear in the ranking, and --per-user sends each of them their own copy.
Roster rows are coloured by tier:
| Tier | SecScore | Colour |
|---|---|---|
| critical | >= 100 | red |
| high | 30 - 99.9 | orange |
| medium | 5 - 29.9 | yellow |
| good | < 5 | green |
| Flag | Purpose |
|---|---|
--csv <path> |
User emails to include, from a CSV. Mutually exclusive with --emails. |
--emails <list> |
Comma-separated user emails. Mutually exclusive with --csv. |
--csv-email-column <name> |
Override CSV header auto-detection. |
--send-email |
Send via Gmail: admin report, or per-user fanout with --per-user. |
--per-user |
One personalised copy per user (requires --send-email). |
--email-to <addr> |
Admin recipients; with --per-user, redirects every copy here (test mode). |
--email-from / --email-subject |
Override the From and Subject headers. |
--email-header-bg / --email-logo |
Override the header colour and logo. |
--message / --message-file |
Add a shared message above the body. |
--dry-run |
Render but do not send. |
--yes |
Skip the confirmation prompt. |
--no-cache |
Bypass the detection cache. |
Stderr emits one line per recipient (sent / would-send / skip / error) and a trailing summary: line. With --per-user --email-to, lines gain a for=<user-email> field for traceability.
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | User error (bad flags, missing config) |
| 2 | Authentication or permissions failure |
| 3 | User, device or detection not found |
| 4 | Upstream error (5xx, network) |
make test # go test ./...
make lint # golangci-lint run
make pre-ci # nine-check local build validator
make build # ./bin/jellyfish with version ldflagsmake pre-ci (scripts/pre-ci-check.sh) runs the Go version check, go mod download, gofmt -s, go test -race, golangci-lint, coverage tracking, a versioned build, govulncheck, and a CLI smoke test; logs land in coverage/. make pre-ci-fix auto-fixes gofmt issues first.
Real-Keychain integration tests:
JELLYFISH_KEYCHAIN_TESTS=1 go test ./internal/keychain/... -count=1The first run pops a macOS dialog asking to allow the test binary to read your Keychain; approve it and re-run.
- Env-var fallback for the token (
JELLYFISH_API_TOKEN) for CI environments with no Keychain. - A
-vvextra-verbose mode that logs response bodies with token + PII redaction.