diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..fff1545 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,16 @@ +name: CI + +on: + push: + pull_request: + +jobs: + test: + runs-on: macos-14 + steps: + - uses: actions/checkout@v4 + - name: Run tests + run: swift test + - name: Build app + run: bash scripts/build_app.sh + continue-on-error: true diff --git a/README.md b/README.md index 0cc145f..93abd26 100644 --- a/README.md +++ b/README.md @@ -53,21 +53,39 @@ without requiring enterprise EDR tooling or kernel entitlements. a restart instead of resetting to empty. - **System notifications** — a real macOS notification for matched events at or above a threshold you choose (off by default beyond critical-only). + Notifications carry "Show in Argus" and "Allowlist…" action buttons — the + allowlist action goes through the exact same Touch ID gate as the in-app path. Authorization is requested once on first launch. +- **Event export** — right-click an event to "Copy as JSON"; from the History + panel, export the full event history as JSON or CSV (RFC 4180-quoted so + command lines with embedded commas and quotes don't misalign spreadsheets). - **Settings** (gear icon in the header) — poll interval and risk-decay - half-life are tunable now rather than fixed constants, plus the - notification threshold. + half-life are tunable now rather than fixed constants, the notification + threshold, and a "Launch at login" toggle (macOS 13+, uses SMAppService). The app is dark-only by design, matching a monitoring-console identity — it does not follow the system light/dark appearance toggle. ## How detection works -`ProcessMonitor` samples `ps -axww -o pid,ppid,command` roughly every 1.2s +`ProcessMonitor` samples `ps -axww -o pid,ppid,user,command` roughly every 1.2s and diffs it against the previous sample to find newly-spawned processes. Each new process is checked against every active rule; a match becomes an event with severity, a MITRE technique label, and an explanation. +The sample includes the user column now, so rules can match `User` and +`ParentUser` fields. Parent context (image, command line, and user) is +resolved from a cross-tick cache retained for ~3 polling intervals, so a +short-lived parent that exits before the next poll still resolves for +parent-keyed rules — a common race in LOLBin chains where a spawned parent +like `sh -c` is gone from the process table by the next sample. + +The `ps` invocation has a 10-second hard timeout. After 3 consecutive +sampling failures, the app enters a visible "degraded" state: the menu bar +icon switches to a warning triangle, a warning row appears in the flyout, and +a MONITOR DEGRADED badge shows in the header. This prevents a failed sample +from being mistaken for "no processes running". Recovery is automatic. + This is **polling-based, not kernel-event-based** — deliberately. True exec()-level capture on macOS requires the `endpoint-security` entitlement, which Apple grants by application review, not something obtainable @@ -78,22 +96,87 @@ exits within the ~1.2s window can be missed as an individual event (though a parent shell invoking it inline is still visible, since the parent's full command line is what `ps` reports). +### Sequence and chain correlation + +When distinct techniques fire in the same process tree within a rolling +10-minute window, Argus emits a synthetic "Suspicious sequence" event. Its +severity is escalated one level above the members' maximum (capped at +critical), and the event lists the member processes and rules. This is the +signal the "Why this, specifically" section calls out: a single LOLBin invocation is often +unremarkable, but two or more different techniques firing inside the same +process tree is a much stronger indicator. Same-rule refires, same-pid +multi-rule matches, and process trees related only through launchd don't +qualify as chains. + +### Persistence-artifact watcher + +An independent, event-driven sensor watches standard macOS persistence +locations: `~/Library/LaunchAgents`, `/Library/LaunchAgents`, +`/Library/LaunchDaemons`, and `/etc/periodic`. Added or modified files are +reported with elevated severity; removals are also watched. Baseline state +at startup is silent, but the watcher catches persistence artifacts even +when the writing process was too short-lived for the polling monitor to ever +sample it. Allowlist filtering deliberately doesn't apply to these events — +a persistence artifact change is a different thing than an allowlisted +process. + +### Tamper evidence + +The app records HMAC-SHA256 MACs of `rules-state.json` and `allowlist.json` +on every authenticated write into a sidecar `integrity.json` file. The signing +key is stored in the login Keychain (service "Argus", account +"integrity-key") rather than on disk, so rewriting the +guarded files in place is not by itself enough to also fix up their MACs. At +launch, any mismatch between a file's current contents and its recorded MAC +is surfaced as a critical "Detection state modified outside Argus" event (T1562.001), +so the tamper itself becomes visible in the feed. This is evidence, not prevention — a same-user attacker can still rewrite the files, but no longer +silently. + +### Provenance attribution + +Every matched event's process ancestry is classified against a table of known supervisors: +AI coding agents (`claude`), container tooling (`docker`), package managers (`brew`), +terminals (`Terminal`/`iTerm2`/`tmux`), and IDEs (`VS Code`/`Cursor`). When a supervisor +is found in the ancestry, a dim "via claude"-style chip appears on the event row in the +feed and matches in the search field too. Rules can also match the raw ancestry via new +`AncestorImages` and `AncestorCommandLines` fields — semicolon-joined, nearest first — +for precise correlation. + +This is attribution for triage and visibility, not authorization. Ancestry is spoofable, +so tags never grant implicit trust. The only sanctioned uses are explicit, user-visible +mechanisms: the provenance-scoped allowlists (described below) and notification quieting, +both user-chosen. When an event's process ancestry indicates it ran under an attributed +supervisor and matches certain sensitive technique categories (persistence T1543/T1547/T1053, +credential access T1555/T1552/T1539, defense evasion T1562/T1553), an escalation rule fires +("AI agent session touched a sensitive technique") one severity level above the underlying +matches, capped at critical — because a prompt-injected agent session doing persistence is +exactly how that compromise presents. Routine agent-attributed events can have their +*notifications* quieted via a default-on Settings toggle; the feed, history, risk score, +and log remain unaffected, and a counter in Settings shows how many were quieted. + ## Rule format and management Rules are [Sigma](https://github.com/SigmaHQ/sigma) — the open, vendor- neutral YAML format the wider detection-engineering community actually publishes in, rather than a bespoke format invented for this app. A rule is -a `logsource` (we only match `category: process_creation`, `product: macos`), -a named `detection` block of field/modifier/value selections -(`CommandLine|contains`, `Image|endswith`, `ParentImage|contains`, `|re` -regex, `|all` for AND-of-list, etc.), and a `condition` string combining -those selections (`selection`, `1 of selection_*`, `all of selection_* and -not 1 of filter_*`, and so on). `Sources/Argus/Sigma/` is a real, if partial, -implementation of that spec: a hand-rolled YAML parser, a condition-language -parser/evaluator, and a field matcher — not a re-skin of the old pattern -list. - -**85 rules ship with the app**, sourced from three places: +a `logsource` (we only match `category: process_creation`, `product: macos` +or Linux), a named `detection` block of field/modifier/value selections, and +a `condition` string combining those selections. `Sources/Argus/Sigma/` is a +real, if partial, implementation of that spec: a hand-rolled YAML parser, +a condition-language parser/evaluator, and a field matcher — not a re-skin +of the old pattern list. + +The condition language now supports the general `N of selection_*` quantifier +(not just `1 of` and `all of`). The matcher supports `base64`, `base64offset` +(all three byte-alignment encodings, verified against SigmaHQ reference +vectors), and `cased` modifiers for case-sensitive comparison. Keyword +selections (those with no field name) match against all record fields per +spec, not just CommandLine. Rules whose `logsource` is incompatible +(something other than `process_creation`/`macos` or portable Linux techniques) +are skipped at load time; a count is shown in the rule browser +alongside the rule count in the header. + +**86 rules ship with the app**, sourced from three places: - **67 rules** imported verbatim from `SigmaHQ/sigma`'s `rules/macos/process_creation/` — a real, actively-maintained community ruleset (account/SIP/security-tool @@ -103,10 +186,15 @@ list. set — genuinely portable shell/interpreter techniques (netcat/perl/php/ python/ruby reverse shells, base64 pipe-to-shell) that apply unchanged on macOS, since it's the same shell tooling. -- **10 rules** authored for Argus, filling gaps neither imported set covered +- **11 rules** authored for Argus, filling gaps neither imported set covered (TCC.db tampering, browser cookie/session theft, pipe-to-interpreter fetch-and-execute, credential piping to `sudo -S`, AMFI/code-signing - tampering, and others). + tampering, and quarantine attribute removal). A new rule "Gatekeeper Bypass + via Quarantine Attribute Removal" matches only actual quarantine *removal* + via whitespace-bounded `-d` or `-c` flags, superseding an imported SigmaHQ + catch-all that also fired on Homebrew's safe `xattr -w` quarantine-adds. + Superseded imported rules are auto-disabled at launch and recorded persistently, + so a user's deliberate Touch ID re-enable is never overwritten on reload. See `Resources/Rules/NOTICE.md` for full attribution and license details (SigmaHQ's rules are DRL 1.1; imported files are unmodified). @@ -133,7 +221,13 @@ If a rule fires on something you know is your own legitimate automation, right-click the event in the feed and choose "Allow future … alerts from …". This suppresses that specific (rule, executable) pair going forward — allowlisting one automation's use of `osascript` won't blind Argus to a -*different* technique that happens to also involve `osascript`. Review or +*different* technique that happens to also involve `osascript`. The right-click +menu also now offers "Allow only when under