Skip to content

Repository files navigation

Argus

A native macOS menu-bar app that watches your Mac's process table in real time for living-off-the-land (LOLBin) attack patterns — malicious use of Apple's own signed, trusted command-line tools (osascript, curl, xattr, launchctl, security, dscl, csrutil, and others) in the sequences and argument shapes real adversary tradecraft actually uses. Everything runs locally; nothing is ever sent anywhere.

See the build plan for the full design rationale.

Argus dashboard — orbit view, risk gauge, activity sparkline, and the event feed

Why this, specifically

Modern macOS intrusions increasingly avoid dropping custom malware binaries — Gatekeeper and notarization make that expensive to get past. Instead they chain together binaries Apple already ships and already trusts: an osascript privilege-escalation prompt, a curl | bash fetch-and-execute, an xattr -d com.apple.quarantine to dodge Gatekeeper, a launchctl load for persistence. No single line looks like a virus. The signal is in the sequence and the arguments — exactly the class of activity a one-shot file scanner can't see, because there's no file to scan. Argus gives an individual user that visibility with an interface actually worth glancing at, without requiring enterprise EDR tooling or kernel entitlements.

What it looks like

  • Orbit view — a live radial visualization: every newly-spawned process launches from a central node and drifts into orbit; color and size encode severity; a parent process still on screen gets an arc drawn to its child, so a whole attack chain visually clusters instead of reading as scattered log lines.
  • Risk gauge — an arced meter for the aggregate threat score, which decays over time so it reflects recent posture, not a lifetime tally.
  • Event feed — reverse-chronological, monospaced, severity-striped, tagged with the technique it matched; click a row to expand the full command line and explanation.
  • Activity sparkline — rolling 5-minute histogram of matched events.
  • Menu bar icon — tints calm teal → amber → red with current posture, so you get the gist without opening the window. Argus runs as a menu-bar-only app (no Dock icon, no Cmd+Tab entry); closing the main window just hides it — reopen it any time via "Open Argus" in the menu bar dropdown.
  • Search & severity filters — a search field (matches executable, command, or technique) plus toggleable severity chips above the feed.
  • Session drill-down — click the pid on any event to focus the feed on every event sharing that process's parent, so a multi-step chain reads as one group instead of scattered rows. "Clear" returns to the full feed.
  • Activity history — a day-by-day heatmap (last ~12 weeks) and a top-techniques chart, from the "HISTORY" stat in the header. Matched events persist to disk now, so this — and the event feed itself — survives 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. Busy process trees (e.g. an AI-agent session doing extensive automation) no longer produce unbounded notification spam: per process-tree root, the first 3 sub-critical notifications in a rolling 5-minute window deliver individually; after that they collapse into one continuously-updated digest notification ("15 alerts, techniques: privilege escalation, persistence… since 14:02"). Critical alerts always deliver individually and never count against the budget.
  • 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, 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,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 unattended in one evening. Polling trades sub-second precision for zero entitlements and zero permission prompts, which fits a personal visibility tool better than an enterprise agent. The tradeoff: a process that starts and 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.

Docker container activity

Containers run inside Docker's Linux VM, invisible to the host's ps. A docker events subscriber surfaces container lifecycle activity as synthetic process events tagged with provenance "docker": container starts (info level, T1610 Deploy Container) and execs into running containers (watch level, T1609 Container Administration Command — a common post-compromise lateral step). The watcher is resilient to the Docker daemon restarting (60-second retry loop) and inert if Docker CLI is not installed. Note the scope limitation: only container operations the daemon reports; processes inside containers remain invisible to host-level detection — in-container detection is a different tool's job.

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 — 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 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 discovery, LaunchAgent and cron persistence, TCC and keychain access, known macOS malware families like XCSSET, and much more).
  • 8 rules imported verbatim from SigmaHQ/sigma's Linux process_creation 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.
  • 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 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).

Managing rules: the gear-adjacent "RULES LOADED" stat in the header opens a rule browser — search, filter by source, expand a rule to read its description and raw YAML, and toggle any rule off without deleting it (persisted, survives restarts). Adding your own: drop a .yml file (standard Sigma process_creation/macos format) into the folder the "Rules folder" button reveals in Finder, then hit "Reload" — no rebuild required. A rule you write there shows up tagged "Your rules" in the browser, exactly like the bundled ones.

Toggling a rule requires Touch ID (or your account password as fallback) — enabling/disabling detections is exactly the kind of thing a LOLBin-style attacker with local execution would want to do silently before running the technique it catches, so it isn't a free, unaudited click. Every attempt — granted or denied, either direction — is written to ~/Library/Logs/Argus/argus.log.

Managing false positives

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. The right-click menu also now offers "Allow only when under " for each supervisor attributed to the event's ancestry — e.g. suppressing a rule for zsh only when the tree runs under claude, so the same rule still fires for any other zsh. Scoped entries show their scope in the allowlist panel. Unconditional allowlist entries subsume scoped ones, so an unscoped allowlist for a pair will shadow any scoped entries for that same (rule, executable). Review or revoke allowlist entries from the "ALLOWLISTED" counter in the header. Nothing is suppressed silently: the header also shows a running count of events an allowlist rule has hidden.

Adding or revoking an allowlist entry requires Touch ID/password too, for the same reason as toggling a rule — it's a way to blind a detection, so it isn't a free, unaudited click. Same log, same attempt-either-way guarantee.

Running it

The built app isn't committed (see .gitignore) — only the source and a build script are. To build and launch:

./scripts/build_app.sh
open build/Argus.app

Requires Xcode command line tools (xcode-select -p) and macOS 13+. No network access, no special permissions, no entitlements to approve.

The build signs with the first codesigning identity in your keychain (or $ARGUS_SIGN_IDENTITY), falling back to an ad-hoc signature when there is none, as on CI. A stable identity matters beyond cosmetics: the tamper- evidence key lives in the Keychain, and its ACL matches the app by signing identity — ad-hoc signatures change every rebuild, so every rebuild would re-prompt for Keychain access, while a real identity keeps the ACL matching across rebuilds (and, for Apple Development certificates, across renewals).

A rolling diagnostic log is written to ~/Library/Logs/Argus/argus.log independent of the UI:

tail -f ~/Library/Logs/Argus/argus.log

Allowlist decisions persist to ~/Library/Application Support/Argus/allowlist.json. Matched events persist to ~/Library/Application Support/Argus/events.jsonl (newline-delimited JSON, capped at the most recent 5000 — trimmed periodically rather than on every write, so a live tail will occasionally see the file shrink back down). Disabled-rule state persists to ~/Library/Application Support/Argus/rules-state.json, and your own rule files live in ~/Library/Application Support/Argus/rules/. An integrity.json sidecar records HMAC-SHA256 MACs of the security-relevant JSON files (allowlist.json and rules-state.json) to detect out-of-band edits.

Testing

swift test

210 tests. The Sigma engine is validated two ways: SigmaEngineTests checks the YAML parser, condition evaluator, and matcher against real rule text fetched from SigmaHQ (including the N of selection_* quantifier, base64/ base64offset/cased modifiers, and keyword matching), and BundledRulesTests loads all 86 shipped rule files, asserts structural invariants, and — for the 11 Argus-authored rules — verifies they match what they claim to. Also covered: RuleStoreTests (enable/disable persistence, user-rule loading), ProcessMonitorTests (parent-context caching, sampling health tracking), AllowlistTests, EventStoreTests, EventFilterTests, HistoryStatsTests, AppSettingsTests, EventExportTests (JSON and CSV serialization), ChainCorrelatorTests (sequence detection), PersistenceWatcherTests (artifact diffing and event generation), IntegrityGuardTests (MAC recording and verification), ProvenanceClassifierTests (ancestry classification), AgentActivityPolicyTests (agent-attributed event escalation and notification quieting), NotificationRollupTests (digest throttling and event batching), and DockerWatcherTests (container lifecycle event synthesis).

Project layout

Package.swift                  Swift Package Manager manifest (macOS 13+)
Sources/Argus/
  App.swift                    App entry point — main window + MenuBarExtra
  Models.swift                 Severity, RawProcess, ProcessEvent, OrbitNode (Codable)
  Sigma/
    YAMLValue.swift               minimal YAML document tree
    YAMLParser.swift              hand-rolled block-YAML parser
    SigmaRule.swift                rule model + YAML→model mapping
    SigmaCondition.swift           condition-language parser/evaluator (N of, all of)
    SigmaMatcher.swift             field/selection matching (base64, base64offset, cased)
    RuleStore.swift                loads bundled + user rules, enable/disable, skip count
  ProcessMonitor.swift          ps polling, diffing, risk-score decay, Sigma matching,
                                  cross-tick parent cache, sampling watchdog
  ProvenanceClassifier.swift     classifies process ancestry against supervisor table
  AgentActivityPolicy.swift      escalation and notification quieting for agent-attributed events
  ChainCorrelator.swift         correlates techniques in same process tree (10-min window)
  PersistenceWatcher.swift      event-driven monitoring of LaunchAgents/Daemons/periodic
  DockerWatcher.swift           docker events subscriber for container lifecycle events
  IntegrityGuard.swift          HMAC verification of rules-state.json and allowlist.json
  AllowlistStore.swift          persisted (rule, executable, provenance scope) suppression
  EventStore.swift              persisted event history (events.jsonl)
  EventFilter.swift             search/severity/session filter logic
  EventExport.swift             JSON and RFC 4180-quoted CSV serialization
  HistoryStats.swift            day-bucketing + technique-frequency aggregation
  AppSettings.swift             tunable poll interval, decay, notification threshold
  NotificationManager.swift     UNUserNotificationCenter wrapper
  NotificationRollup.swift      per-tree-root notification throttling and digesting
  NotificationResponder.swift   notification actions (Show in Argus, Allowlist)
  DiagnosticsLog.swift          on-disk activity log
  Theme.swift                   color/type tokens
  OrbitView.swift               Canvas-based radial visualization
  GaugeView.swift               arced risk meter
  SparklineView.swift           activity histogram
  DashboardView.swift           main window, event feed, filters, all panels
  MenuBarPanel.swift            compact menu-bar popover
Tests/ArgusTests/
  SigmaEngineTests.swift         parser/condition/matcher vs. real SigmaHQ rule text
  BundledRulesTests.swift        all 86 shipped rules load + custom-rule fixtures
  RuleStoreTests.swift           enable/disable persistence, user-rule loading
  AllowlistTests.swift           filter logic + persistence round-trip
  EventStoreTests.swift          history persistence + trimming
  EventFilterTests.swift         search/severity/session filter logic
  EventExportTests.swift         JSON/CSV serialization round-trip
  HistoryStatsTests.swift        day-bucketing + technique-frequency aggregation
  AppSettingsTests.swift         settings persistence/clamping + notification-threshold logic
  ProcessMonitorTests.swift      parent-context caching, health tracking
  ProvenanceClassifierTests.swift ancestry classification against supervisor table
  AgentActivityPolicyTests.swift  escalation rules and notification quieting logic
  ChainCorrelatorTests.swift     sequence detection in process trees
  PersistenceWatcherTests.swift  artifact diffing and event generation
  DockerWatcherTests.swift       container lifecycle event synthesis and resilience
  NotificationRollupTests.swift  per-tree throttling, digest batching, budget enforcement
  IntegrityGuardTests.swift      MAC recording and verification
Resources/
  Info.plist                   app bundle metadata
  icon_gen.swift                generates the app icon programmatically
  Rules/
    NOTICE.md                     attribution — what's imported vs. authored, and under what license
    SIGMA_LICENSE.txt             SigmaHQ/sigma's license file
    imported/                     67 rules verbatim from SigmaHQ macOS process_creation
    imported-portable/            8 rules verbatim from SigmaHQ Linux process_creation (portable shell techniques)
    custom/                       11 rules authored for Argus, filling gaps in the imported sets
scripts/
  build_app.sh                  release build → signed .app bundle (bundles Resources/Rules)
  sync_sigma_rules.sh           manual dev tool to refresh bundled SigmaHQ rules from upstream
.github/workflows/
  ci.yml                        runs swift test + app build on every push/PR

Non-goals

Not a System Extension, not a blocker/firewall, not a cloud product. It observes and visualizes; it takes no automated action on your processes, and nothing about your machine ever leaves it.

License

Argus's own code and the rules under Resources/Rules/custom/ are MIT — see LICENSE. The rules imported verbatim from SigmaHQ under Resources/Rules/imported/ and imported-portable/ remain under the Detection Rule License (DRL) 1.1 — see Resources/Rules/NOTICE.md.

About

Real-time macOS living-off-the-land (LOLBin) activity radar, built on the Sigma detection rule format.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages