Skip to content

Detection robustness and agent-activity attribution - #2

Merged
MarkRWatts merged 28 commits into
mainfrom
feature/detection-robustness-extensions
Aug 21, 2026
Merged

MarkRWatts merged 28 commits into
mainfrom
feature/detection-robustness-extensions

Conversation

@MarkRWatts

Copy link
Copy Markdown
Owner

Two batches of work extending Argus's detection power, robustness, and handling of supervised automation (AI agents, package managers, containers).

Detection & robustness extensions

  • Sigma engine spec coverage — general N of quantifier; base64/base64offset/cased modifiers (offset encodings verified against the reference algorithm); keyword selections match all record fields; incompatible-logsource rules skipped at load with a visible count.
  • Sequence/chain correlation — distinct techniques firing in one process tree within 10 minutes produce an escalated "Suspicious sequence" event.
  • Persistence-artifact watcher — event-driven second sensor on LaunchAgents/LaunchDaemons/periodic; catches artifacts even when the writing process outruns the poll.
  • ProcessMonitor robustness — cross-tick parent-context cache, 10s ps timeout with a visible degraded state (never fails dark), User/ParentUser fields.
  • Tamper evidence — HMAC-SHA256 MACs of 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).
  • Lifecycle — monitoring starts at app launch (not window appearance); SMAppService launch-at-login toggle; stable-identity code signing preferred over ad-hoc so the Keychain ACL survives rebuilds.
  • Export & actionable notifications — copy-as-JSON, RFC 4180 CSV/JSON history export, notification actions routed through the same Touch ID gates.
  • Tooling — GitHub Actions CI (test + build), manual SigmaHQ rule re-sync script.

Agent-activity attribution

  • Provenance attribution — event ancestry classified against known supervisors (claude/docker/brew/terminals/IDEs), shown as violet "via claude" chips; AncestorImages/AncestorCommandLines fields for rules. Attribution for triage, never implicit trust.
  • Provenance-scoped allowlisting — "Allow only when under claude": suppress a (rule, executable) pair only inside a supervisor's tree.
  • AI-agent trust domain — agent-attributed events hitting persistence/credential/defense-evasion techniques get escalated one severity level (capped critical); routine agent events can have notifications-only quieted (default-on toggle, visible counter).
  • Xattr precision — new Argus-authored quarantine-removal rule supersedes the imported catch-all that false-positived on Homebrew's safe 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.json edit fired the expected critical tamper event; a real xattr -r -d com.apple.quarantine from a Claude Code session was attributed via claude, matched the new precision rule, and picked up the trust-domain escalation to critical, while a loop of safe -w quarantine adds stayed silent.

🤖 Generated with Claude Code

Mark Watts and others added 28 commits August 21, 2026 11:38
- .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]>
…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]>
@MarkRWatts
MarkRWatts merged commit b493813 into main Aug 21, 2026
2 checks passed
@MarkRWatts
MarkRWatts deleted the feature/detection-robustness-extensions branch August 21, 2026 14:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant