Detection robustness and agent-activity attribution - #2
Merged
Merged
Conversation
- .github/workflows/ci.yml: GitHub Actions workflow for testing and building on macOS 14 - scripts/sync_sigma_rules.sh: Developer tool to sync bundled Sigma rules from upstream Co-Authored-By: Claude Fable 5 <[email protected]>
…ord scope, logsource filtering Generalizes "1 of x*" to any "N of x*", adds base64/base64offset and cased field modifiers with values precomputed at load time, makes keyword selections search every record field instead of just CommandLine, and skips rules at load whose logsource isn't macOS/Linux process_creation (surfaced in the rule browser and covered by new RuleStoreTests/SigmaEngineTests). Co-Authored-By: Claude Fable 5 <[email protected]>
…rd scope, logsource filtering
…rocessMonitor Resolve ParentImage/ParentCommandLine/ParentUser from a pid-keyed cache retained across a few ticks so an already-exited parent still supplies context for Sigma matching. Bound each ps invocation with a hard timeout and surface repeated sampling failures via a new isDegraded published property instead of silently treating them as "no processes". Add the process owner (User/ParentUser) to the Sigma match record. Extracts parsePSOutput, ParentContextCache, and SamplingHealthTracker as pure, independently testable units and adds ProcessMonitorTests covering them. Co-Authored-By: Claude Fable 5 <[email protected]>
The reference algorithm drops 3 chars when the final group holds 1 dangling byte and 2 when it holds 2; the cases were inverted, producing encodings one char too long or too short at two of the three offsets. Verified against reference vectors, now asserted directly in tests. Co-Authored-By: Claude Fable 5 <[email protected]>
Monitoring and notification-permission wiring now happens in ArgusApp.init() instead of the dashboard window's onAppear, so Argus watches processes even if the window is never opened. Adds an SMAppService-backed "Launch at login" toggle to Settings (source of truth stays in SMAppService, not AppSettings). Surfaces ProcessMonitor.isDegraded in the menu bar icon, the menu bar flyout, and the dashboard header. Replaces the flyout dismissal's fragile window-title check with NSWindow.identifier, verified empirically to reliably match the Window scene's id across order-out/reopen cycles. Co-Authored-By: Claude Fable 5 <[email protected]>
…odic ProcessMonitor polls `ps` every ~1.2s, which can miss a process that spawns, writes a persistence artifact, and exits within a single tick. This adds an independent, event-driven watcher on the standard macOS persistence locations (~/Library/LaunchAgents, /Library/LaunchAgents, /Library/LaunchDaemons, /etc/periodic) so the artifact left behind is caught even when the writing process itself was never sampled. Detected changes feed into ProcessMonitor via a new ingestExternal(_:) entry point that mirrors processSample's matched-event handling, minus orbit-node/allowlist handling which don't apply to synthetic, process-less events. Co-Authored-By: Claude Fable 5 <[email protected]>
Touch ID gates rule toggling and allowlist edits in the UI, but rules-state.json and allowlist.json are still plain, same-user-writable JSON that any local process can rewrite directly to blind a detection silently. IntegrityGuard records an HMAC-SHA256 (keyed by a Keychain-held key) of each file after every authenticated write and re-verifies it at launch, reporting a mismatch as a critical synthetic event in the feed. This is evidence, not prevention — true prevention needs privilege separation, which is out of scope here. Co-Authored-By: Claude Fable 5 <[email protected]>
Stores previously defaulted to IntegrityGuard.shared, so any test or tool constructing a store against a temp file silently created a real Keychain key and wrote MACs into the user's live integrity.json — and because the sidecar was keyed by filename, a temp rules-state.json clobbered the real file's recorded MAC, priming a false tamper alarm on the next app launch. The guard is now nil unless injected (the app passes .shared explicitly) and the sidecar keys on full paths so same-named files can never collide. Removed the sidecar entries the test runs had already polluted. Co-Authored-By: Claude Fable 5 <[email protected]>
Extends ParentContextCache with ppid tracking and an ancestry walk, and adds ChainCorrelator: a pure, testable class that links matched processes sharing a process-tree lineage within a rolling window when they trip distinct techniques, escalating severity one level and emitting a synthetic "chain" event via ingestExternal. Realizes the README's thesis that LOLBin signal lives in technique sequence, not just individual process scores. Co-Authored-By: Claude Fable 5 <[email protected]>
Sharing a detected event previously meant hand-copying from events.jsonl.
Adds "Copy as JSON" to the event feed's context menu, JSON/CSV export of
the full history from the History panel, and notification actions
("Show in Argus" / "Allowlist…") so a caught event can be acted on
without opening the dashboard first.
Co-Authored-By: Claude Fable 5 <[email protected]>
Updated README to cover: Sigma engine spec improvements (N of quantifier, base64/base64offset/cased modifiers, keyword field matching, logsource filtering); richer match records with User/ParentUser fields and cross-tick parent-context cache; sampling watchdog with 10s timeout and degraded-state visibility; sequence/chain correlation in 10-min windows; persistence-artifact watcher on LaunchAgents/Daemons/periodic; tamper evidence via integrity.json MACs; lifecycle change (monitoring starts in app init); export/notification actions; and tooling (ci.yml, sync_sigma_rules.sh). Test count updated from 45 to 119, with new suites documented. Project layout tree expanded with new source and test files. Co-Authored-By: Claude Fable 5 <[email protected]>
ParentContextCache/SamplingHealthTracker are types inside ProcessMonitor.swift, not files; the tree listed them as files. Also reworded the self-referential thesis mention, the Keychain rationale, and a 'silently skipped'/'count is shown' contradiction. Co-Authored-By: Claude Fable 5 <[email protected]>
Fetching the HMAC key can present a Keychain consent prompt (the item's ACL doesn't cover a rebuilt ad-hoc-signed binary), and SecItem calls block until answered — verifying in the stores' inits froze the whole app behind that dialog, and every verify/record call re-prompted after a denial. IntegrityGuard now funnels all work through its own serial queue with a cached one-shot key attempt (at most one prompt per launch, never on the main thread); stores verify on request via an async verifyIntegrity() the app calls after wiring; DiagnosticsLog appends are serialized now that they arrive from multiple queues. Co-Authored-By: Claude Fable 5 <[email protected]>
An ad-hoc signature changes on every rebuild, so the Keychain ACL on the IntegrityGuard key stopped matching after each rebuild and macOS re-prompted for access. The build now signs with $ARGUS_SIGN_IDENTITY or the first valid codesigning identity in the keychain (an Apple Development certificate here), keeping the designated requirement stable across rebuilds; CI has no identities and still lands on the ad-hoc fallback. Verified live: a rebuilt binary read the key with no prompt, and an out-of-band allowlist.json edit made across the restart was reported as the expected critical T1562.001 tamper event. Co-Authored-By: Claude Fable 5 <[email protected]>
Introduces ProvenanceClassifier, a data-driven table that tags a matched process with the supervisors found in its ancestry (Claude Code, Docker, Homebrew, terminals, IDEs) so alerts from supervised automation are distinguishable from standalone activity in the feed. ParentContextCache gains ancestorRecords(of:maxDepth:) — image/command per ancestor pid, nearest first — which ancestry(of:) now derives from, and which processSample reuses both to populate new AncestorImages/AncestorCommandLines Sigma fields and to feed the chain correlator, avoiding a duplicate walk. ProcessEvent.provenance is decoded with decodeIfPresent so existing events.jsonl history without the key still loads. The event feed shows a dim "via claude"-style chip, and search now matches provenance labels too. This is attribution for triage only — ancestry is spoofable and a tag must never be treated as a trust signal. Co-Authored-By: Claude Fable 5 <[email protected]>
The bundled SigmaHQ rule (imported/proc_creation_macos_xattr_gatekeeper_bypass.yml,
id f5141b6d-9f42-41c6-a7bf-2a780678b29b) requires only a bare '-d' substring
alongside 'com.apple.quarantine', which can false-positive on Homebrew's safe
"xattr -w com.apple.quarantine ..." (add-quarantine) direction when a '-d'
lands inside the quarantine value's UUID by chance. Since imported rules stay
verbatim, add an Argus-authored replacement that requires '-d'/'-c' as a
whitespace-bounded flag, correctly telling removal apart from addition, and
have RuleStore auto-disable the superseded rule by default via a new
supersededBundledRuleIDs map. The disabled-state file migrates from a bare
Set<String> to {disabled, supersessionsApplied} so a user who deliberately
re-enables the superseded rule isn't overridden again on the next launch.
Co-Authored-By: Claude Fable 5 <[email protected]>
Allowlisting (rule, executable) globally blinds the rule everywhere that executable runs — a real problem for e.g. zsh under a Claude Code session. AllowlistEntry gains an optional requiredProvenance label so an entry can suppress alerts only when the matching event's provenance (from ProvenanceClassifier) contains that label, leaving the unscoped default behavior untouched. Provenance is now classified before allowlist filtering in ProcessMonitor so scoped entries can see it, the dashboard's event context menu offers a scoped "Allow only when under <label>" alongside the existing unconditional action, and the allowlist panel shows an entry's scope. Co-Authored-By: Claude Fable 5 <[email protected]>
An AI-agent session (Claude Code, etc.) is a distinct trust domain, not benign background noise — a prompt-injected agent doing persistence or credential access is a high-value signal. AgentActivityPolicy classifies each event's provenance tags against matched rules: agent-attributed activity touching a persistence/credential-access/defense-evasion technique gets a synthetic escalation rule (severity bumped one level, capped at critical); routine agent-attributed activity is unaffected in the feed/history/risk score but, per the new default-on quietAgentNotifications setting, skips the system notification it would otherwise have earned (with a visible per-session counter in Settings). Chain correlation still keys off each event's original rule names so the synthetic escalation rule can't chain unrelated agent events together. Reconciles ProvenanceTag's doc comment: a tag must never be an implicit trust signal, but explicit user-visible mechanisms — allowlist entries, the quieting toggle, and this escalation — are the sanctioned uses. Co-Authored-By: Claude Fable 5 <[email protected]>
…tion, scoped allowlisting, agent activity policy, and xattr rule precision Updates test count (170), rule counts (86 total, 11 authored), and project layout tree with ProvenanceClassifier, AgentActivityPolicy, and their test suites. Co-Authored-By: Claude Fable 5 <[email protected]>
The 'via claude' badge and the allowlist scope chip were rendered in Theme.dim on a subtle surface — easy to miss, which defeats attribution as a triage aid. Both now use a dedicated Theme.provenance violet (foreground + tinted capsule), a hue the severity palette never uses, so the chips stand out at a glance without ever reading as a severity signal. Co-Authored-By: Claude Fable 5 <[email protected]>
The CI runner's Swift compiler doesn't infer MainActor isolation for this private View method the way the local toolchain does, so its call to the MainActor-isolated EventStore.loadAll() failed to compile there. The explicit annotation is also simply correct: the method drives NSSavePanel, which is main-actor-bound anyway. Co-Authored-By: Claude Fable 5 <[email protected]>
…ains Follow-up to the exportHistory annotation: the CI toolchain's SDK isolates only View.body, not the whole conformance, so every non-body member touching main-actor state (stores, monitor, save panels) needs the isolation stated explicitly. Annotating the structs rather than chasing individual members keeps both toolchains compiling identically. Co-Authored-By: Claude Fable 5 <[email protected]>
The scripted annotation inserted @mainactor between 'private' and 'struct', which doesn't parse — and the verification pipeline masked the failure (its exit status came from tail, not the build), so the broken commit was pushed. Amends the sweep with the attribute in front of the access modifier where it belongs. Co-Authored-By: Claude Fable 5 <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two batches of work extending Argus's detection power, robustness, and handling of supervised automation (AI agents, package managers, containers).
Detection & robustness extensions
N ofquantifier;base64/base64offset/casedmodifiers (offset encodings verified against the reference algorithm); keyword selections match all record fields; incompatible-logsourcerules skipped at load with a visible count.pstimeout with a visible degraded state (never fails dark),User/ParentUserfields.rules-state.json/allowlist.json(key in the login Keychain), out-of-band edits surfaced as critical T1562.001 events; all Keychain work off the launch path on a serial queue (a consent prompt can never freeze startup).Agent-activity attribution
AncestorImages/AncestorCommandLinesfields for rules. Attribution for triage, never implicit trust.xattr -w; supersession is persistent and respects deliberate re-enables.Verification
Test suite grew 45 → 170, all green. Live-verified on this machine: stable signing survives rebuilds without Keychain prompts; an out-of-band
allowlist.jsonedit fired the expected critical tamper event; a realxattr -r -d com.apple.quarantinefrom a Claude Code session was attributedvia claude, matched the new precision rule, and picked up the trust-domain escalation to critical, while a loop of safe-wquarantine adds stayed silent.🤖 Generated with Claude Code