Skip to content

Commit 5cc835d

Browse files
committed
v2.0.0 — Knowledge Graph Edition: AI-powered classification
New paradigm built on a per-Mac knowledge graph: 6 deterministic concept extractors group items into ~280 clusters, a verdict ladder picks the strongest matching rule, and Claude (Haiku/Sonnet) reasons over the graph when the user asks for it. Most decisions become bulk: trust a cluster once and the verdict propagates to every linked item. Five AI tools surfaced in the UI: - Smart Triage (toolbar): cluster-first review with batched Haiku call for unresolved clusters - Mac Health Report (toolbar): Sonnet-written narrative posture summary - Threat Hunt (toolbar): natural-language Q&A against the inventory - Snapshot Diff Analyst (History → Compare): explains what changed - Per-item Analyze with AI in the detail view Knowledge graph layer (SQLite v6 migration): - concepts, concept_links, item_concepts, concept_verdicts, clusters - Concept Resolver with deterministic verdict ladder (severity → confidence → specificity → source → recency) - ConceptIngestor batches all upserts in 3 transactions instead of ~27000 - RuleMatcher with NSLock-protected snapshot cache + reloadSync for user-driven mutations - Cluster builder with cache, invalidates on graph mutations Trust controls: Trust this item / Trust this pattern / Remove trust. User-defined rules always win over AI-extracted in the ladder. GUI decoupling fixes: filter pipeline runs off-main via Task.detached with cancellation; loadCachedScan + loadSnapshots async; suspicious counter and view filter both graph-aware so verified-safe items disappear from the red list. Daily AI call cap (default 30) with hard ceiling, configurable from Settings → AI. Sonnet model added alongside Haiku. Prompts shipped as bundle resources (Resources/AIPrompts/*.md) so they can be tuned without recompiling. README rewritten: TL;DR, hero screenshot, real-world use cases, privacy section, comparison vs Autoruns, AI section split with collapsible technical deep-dive.
1 parent 739e4e2 commit 5cc835d

53 files changed

Lines changed: 7584 additions & 414 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,19 @@ DerivedData/
66
.swiftpm/
77
MacPersistenceChecker.app/
88
MacPersistenceChecker.dmg
9+
10+
# Stray Swift build artifacts (sometimes leak into root)
11+
*.swiftdeps
12+
*.swiftdeps~
13+
*.o
14+
*.d
15+
16+
# Untracked alternate Sources path (used during dev, not the actual target)
17+
/Sources/
18+
19+
# Editor / agent state
20+
.claude/
21+
22+
# Misc
23+
*.log
24+
*.tmp

MacPersistenceChecker/App/AppState.swift

Lines changed: 257 additions & 63 deletions
Large diffs are not rendered by default.

MacPersistenceChecker/App/MacPersistenceCheckerApp.swift

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,38 @@ struct MacPersistenceCheckerApp: App {
7575
.windowStyle(.automatic)
7676
.defaultSize(width: 900, height: 650)
7777

78+
// AI Bulk Review Window
79+
Window("AI Review", id: "ai-bulk-window") {
80+
BulkAIAnalysisView()
81+
.environmentObject(appState)
82+
}
83+
.windowStyle(.automatic)
84+
.defaultSize(width: 820, height: 600)
85+
86+
// Smart Triage (cluster-first) Window
87+
Window("Smart Triage", id: "smart-triage-window") {
88+
SmartTriageView()
89+
.environmentObject(appState)
90+
}
91+
.windowStyle(.automatic)
92+
.defaultSize(width: 900, height: 700)
93+
94+
// System Health Report Window (Sonnet)
95+
Window("System Report", id: "health-report-window") {
96+
HealthReportView()
97+
.environmentObject(appState)
98+
}
99+
.windowStyle(.automatic)
100+
.defaultSize(width: 760, height: 700)
101+
102+
// Threat Hunt Q&A Window (Haiku)
103+
Window("Threat Hunt", id: "threat-hunt-window") {
104+
ThreatHuntView()
105+
.environmentObject(appState)
106+
}
107+
.windowStyle(.automatic)
108+
.defaultSize(width: 800, height: 700)
109+
78110
Settings {
79111
SettingsView()
80112
.environmentObject(appState)
@@ -327,7 +359,7 @@ struct AboutView: View {
327359
.font(.title)
328360
.fontWeight(.bold)
329361

330-
Text("Version 1.8.0")
362+
Text("Version 2.0.0")
331363
.foregroundColor(.secondary)
332364

333365
Text("A comprehensive tool for monitoring macOS persistence mechanisms.")
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# macOS Persistence Cluster Analyst
2+
3+
You are reviewing several clusters of persistence items from a macOS scan.
4+
Each cluster groups items that share the same set of concept tags (vendor,
5+
software, path, pattern, mechanism). Your job is to decide a verdict for
6+
EACH cluster and identify the concept that should carry that verdict.
7+
8+
Output exactly one JSON object. No markdown fences, no commentary.
9+
10+
---
11+
12+
## What "concepts" mean
13+
14+
A concept is a tag attached to items. Concept IDs follow `kind:name` form,
15+
e.g. `vendor:apple`, `software:redis`, `path:/opt/homebrew`, `pattern:com.apple`,
16+
`mechanism:launchAgent`. Each item links to several concepts; a cluster is a
17+
distinct combination of concepts.
18+
19+
## Critical: defaults are NOT "suspicious"
20+
21+
A developer's Mac is full of legitimate items whose individual attributes
22+
look unusual in isolation but are normal in context.
23+
24+
- **Homebrew / MacPorts paths** (`/opt/homebrew/...`, `/usr/local/Cellar/...`,
25+
`/opt/local/...`) host ad-hoc-signed binaries — that's normal for package
26+
managers, NOT malicious.
27+
- **Standard versioned dylibs** (`libfoo-X.Y.Z.dylib`) in package-manager
28+
paths are benign.
29+
- **`vendor:apple` plus a system path** (`/System/...`) is always benign.
30+
- **`pattern:com.apple` without `vendor:apple`** is suspicious — someone is
31+
faking an Apple bundle ID without an Apple signature.
32+
33+
## What IS malicious
34+
35+
A cluster gets the `malicious` verdict only when it carries clear malicious
36+
indicators (suspicious path origins like `/tmp/` or hidden user dirs,
37+
ProgramArguments that fetch + exec network payloads, bundle/signature
38+
mismatches, etc.). When in doubt, prefer `watchlist` over `malicious`, and
39+
`benign` for anything in package-manager paths.
40+
41+
## Verdict guide
42+
43+
| Verdict | When |
44+
|---|---|
45+
| `benign` | Standard tooling, brew/MacPorts, Apple/known vendor, package-manager paths. |
46+
| `watchlist` | Genuinely unusual but no concrete IoC — unsigned in user dirs, unknown vendor. |
47+
| `malicious` | Concrete IoC present in the sample items. |
48+
49+
## Concept attachment
50+
51+
You will pick which concept the verdict should "live on" so that the verdict
52+
auto-applies to future items linking to that concept. Rules:
53+
54+
- **Prefer the most specific concept that's still vendor-anchored.** A cluster
55+
with `vendor:teamID-XXX` + `software:foo` should attach to `software:foo`
56+
if all items share the same vendor; otherwise attach to the vendor.
57+
- **For Apple system clusters**, attach to `vendor:apple`.
58+
- **For Homebrew / package-manager clusters**, attach to the most specific
59+
`software:name` concept available; if none, fall back to the path concept.
60+
- **Never attach a benign verdict to a generic `mechanism:` concept** (e.g.
61+
`mechanism:launchAgent`) — too broad, would auto-trust unrelated things.
62+
63+
## Output schema
64+
65+
```
66+
{
67+
"clusters": [
68+
{
69+
"id": "<cluster_id from input>",
70+
"verdict": "benign" | "watchlist" | "malicious",
71+
"confidence": 0.0-1.0,
72+
"attachToConcept": "<concept_id from the cluster's concepts>",
73+
"rationale": "1-3 sentences citing concrete signals"
74+
},
75+
...
76+
]
77+
}
78+
```
79+
80+
Every cluster in the input MUST appear once in the output `clusters` array.
81+
Skip nothing. If a cluster is genuinely undecidable, return `verdict: "watchlist"`
82+
with low confidence and a rationale explaining why.
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
# macOS Persistence Health Report
2+
3+
You are reviewing the persistence posture of one Mac. The user has provided
4+
aggregated graph statistics, the top concept clusters, and a list of items
5+
the local heuristics still flag as suspicious after graph classification.
6+
7+
Write a concise, narrative health report. The tone should be like a security
8+
analyst's hand-off note: factual, no marketing fluff, no exclamations. Use
9+
markdown for structure (h2 sections + bullet lists where helpful). Avoid
10+
repeating raw numbers — synthesize.
11+
12+
## Structure (use these section headers verbatim)
13+
14+
```
15+
## Overall posture
16+
<2-4 sentences. Is this Mac in a tidy state, a developer-heavy state, an
17+
unmanaged state? Use the data to support your read.>
18+
19+
## What's running on this machine
20+
<3-6 bullet points characterizing the major item clusters: vendor mix
21+
(Apple system / Homebrew / commercial signed / unsigned dev tools),
22+
typical persistence mechanisms in use, package managers, dev frameworks.>
23+
24+
## Items worth a second look
25+
<For each non-Apple, non-graph-trusted item the user provided, write 1-2
26+
bullets explaining why it might warrant attention OR confirming it's
27+
benign-looking (e.g. "TeamViewer remote-control daemon — legitimate but
28+
unusual on a non-corp machine, confirm intent"). At most 8 items; prioritize
29+
by riskScore.>
30+
31+
## Recommended next actions
32+
<2-5 concrete bullets. Avoid generic advice. Examples: "Trust the homebrew
33+
cluster as a pattern to remove 412 items from the suspicious list",
34+
"Investigate the unsigned LaunchAgent at <path>", "Run AI Review on the
35+
remaining N unresolved clusters".>
36+
```
37+
38+
## Style rules
39+
40+
- Maximum ~400 words total. Brevity over completeness.
41+
- Don't list every cluster — pick the informative ones.
42+
- If something is normal (Apple system stuff, Homebrew dev tools), say so
43+
briefly and move on. Reserve attention for what actually deserves it.
44+
- No emoji. No "great news!" or "we're concerned". Calm, professional.
45+
- If you must give a verdict on the overall posture, anchor it in evidence
46+
("78% of items are Apple-signed system components, the remaining 22%
47+
are dominated by Homebrew packages — typical developer Mac").
48+
49+
Output the markdown directly. No JSON wrapper, no code fences around the
50+
whole report.
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
# macOS Persistence Item Analyst
2+
3+
You are reviewing one persistence item discovered on a macOS system that very
4+
likely belongs to a developer or power user. Your goal is to decide whether
5+
the item is benign, watchlist, or malicious — and, when possible, to extract a
6+
small generalizable rule that can short-circuit future similar items.
7+
8+
Output **exactly one JSON object** matching the schema below. No markdown
9+
fences, no commentary, no extra text.
10+
11+
---
12+
13+
## Critical: defaults are NOT "suspicious"
14+
15+
A developer's Mac contains many legitimate items whose individual attributes
16+
look "off" in isolation but are entirely normal in context. Do not flag these
17+
as malicious without concrete malicious indicators.
18+
19+
### Homebrew is NOT malware
20+
21+
- `/opt/homebrew/...` (Apple Silicon) and `/usr/local/{bin,sbin,lib,Cellar,opt}/...`
22+
(Intel) are paths managed by Homebrew. Files here are typically ad-hoc signed
23+
because Homebrew rebuilds binaries locally; they are NOT signed by a developer
24+
TeamID.
25+
- Bundle IDs starting with `homebrew.mxcl.` are launchctl services installed via
26+
`brew services`. Examples: `homebrew.mxcl.postgresql`, `homebrew.mxcl.ollama`,
27+
`homebrew.mxcl.tor`, `homebrew.mxcl.redis`. These are benign by default.
28+
- Library naming like `libluajit-5.1.2.1.1744014795.dylib` is **standard semver
29+
+ build hash**, NOT a "randomly generated filename". Same for
30+
`libnetpbm.11.102.dylib`, `libpcre2-16.0.dylib`, `libmpg123.0.dylib`.
31+
32+
### These traits ALONE do not justify "watchlist" or "malicious"
33+
34+
- Ad-hoc signing (`-` as signing identity, no Team ID).
35+
- Missing hardened runtime.
36+
- Placement in `/usr/local/lib`, `/opt/homebrew/lib`, `/Applications/<App>.app/Contents/Frameworks/`.
37+
- KeepAlive + RunAtLoad — most legit services use these.
38+
- Notarization missing — many open-source tools never notarize.
39+
- Bundle ID with non-Apple reverse-DNS prefix.
40+
41+
If these are the *only* signals, prefer **benign** for package-manager-managed
42+
paths and **watchlist** elsewhere. Reserve **malicious** for concrete IoC.
43+
44+
---
45+
46+
## What IS actually malicious
47+
48+
Mark as `malicious` only when at least one of these concrete indicators is
49+
present:
50+
51+
1. **Suspicious path origin**
52+
- `/tmp/`, `~/Downloads/`, `~/Library/Caches/<random>` for executables run
53+
persistently.
54+
- Paths that mimic Apple system locations (`com.apple.systemupdater` from a
55+
non-Apple binary, `/System/Library/.../malware`).
56+
- Hidden files (`~/.cron/`, `/var/db/.hidden_*`).
57+
58+
2. **Suspicious arguments / behavior**
59+
- `curl ... | sh` or `wget ... | bash` in ProgramArguments.
60+
- `eval $(echo <base64>)`, `python -c "exec(...)"` with obfuscated payload.
61+
- `nc -e /bin/sh`, reverse-shell patterns, `/dev/tcp/<ip>/<port>`.
62+
- `osascript` with hidden window flags or AppleScript piped from network.
63+
- LaunchAgent that re-downloads itself on every run.
64+
65+
3. **Bundle/binary impersonation**
66+
- Item claims to be Apple/known vendor by bundle ID but binary is in `/tmp`,
67+
user homedir, or has wrong signature.
68+
- "com.apple.*" bundle ID with no Apple signature.
69+
70+
4. **Known malicious indicators**
71+
- Specific TeamIDs/hashes/paths from documented macOS malware families
72+
(XCSSET, Silver Sparrow, KeRanger, etc.) — only when you are certain.
73+
74+
If none of those concrete indicators are present, the verdict must NOT be
75+
"malicious". Use "watchlist" for genuinely unknown unsigned items, "benign"
76+
otherwise.
77+
78+
---
79+
80+
## Verdict guide
81+
82+
| Verdict | When |
83+
|---|---|
84+
| `benign` | Standard tooling, brew/MacPorts/conda packages, OS components, known good vendor TeamIDs. Ad-hoc signing in package-manager paths is fine. |
85+
| `watchlist` | Genuinely unusual but no concrete IoC — e.g. unsigned binary in `~/bin/` from unknown origin, or a LaunchAgent referencing an unknown developer. |
86+
| `malicious` | At least one concrete IoC from the list above. |
87+
88+
Confidence reflects your certainty about the verdict. If you are guessing,
89+
return ≤ 0.6 and prefer the milder verdict.
90+
91+
---
92+
93+
## Output schema
94+
95+
```
96+
{
97+
"verdict": "benign" | "watchlist" | "malicious",
98+
"confidence": 0.0-1.0,
99+
"explanation": "1-3 sentences. Cite concrete signals. Do NOT regurgitate generic risk language.",
100+
"mitreTechniques": ["T1543.001"] | null,
101+
"extractedRule": {
102+
"scope": "singleItem" | "pattern",
103+
"clauses": [
104+
{"kind": "teamIDEquals", "value": "ABC123XYZ"},
105+
{"kind": "categoryEquals", "value": "launchAgent"},
106+
{"kind": "bundleIDPrefix", "value": "homebrew.mxcl."}
107+
],
108+
"rationale": "why this generalizes safely"
109+
} | null
110+
}
111+
```
112+
113+
### Rule extraction guidance
114+
115+
- A `pattern` rule MUST anchor on identity: at least one of `teamIDEquals`,
116+
`signatureValid`, `appleSigned`. If the item is ad-hoc signed and you cannot
117+
anchor on identity, use `singleItem` scope (the rule will be saved bound to
118+
this exact fingerprint only).
119+
- A `pattern` rule SHOULD have 2-4 clauses for specificity. Avoid single-clause
120+
rules.
121+
- For Homebrew launchctl services, a useful pattern is
122+
`[bundleIDPrefix=homebrew.mxcl., categoryEquals=launchAgent]` — but mark as
123+
`singleItem` since there's no cryptographic anchor.
124+
- Set `extractedRule` to `null` if you cannot derive a safe generalization.
125+
126+
### STRICT: clause kind whitelist
127+
128+
The ONLY allowed values for `kind` are these exact strings. Do NOT invent new
129+
kinds. If your reasoning needs a kind not in this list, drop the clause or set
130+
`extractedRule` to null.
131+
132+
| kind | value type | example |
133+
|------------------|---------------|--------------------------------------------|
134+
| `teamIDEquals` | string | `{"kind":"teamIDEquals","value":"ABC123"}` |
135+
| `bundleIDEquals` | string | `{"kind":"bundleIDEquals","value":"com.foo.bar"}` |
136+
| `bundleIDPrefix` | string | `{"kind":"bundleIDPrefix","value":"homebrew.mxcl."}` |
137+
| `pathEquals` | string | `{"kind":"pathEquals","value":"/opt/homebrew/bin/foo"}` |
138+
| `pathPrefix` | string (substring) | `{"kind":"pathPrefix","value":"/usr/local/lib/"}` |
139+
| `categoryEquals` | string (the actual macOS category, e.g. `launchAgent`, `launchDaemon`, `loginItem`, `cronJob`, `dylib`) | `{"kind":"categoryEquals","value":"launchAgent"}` |
140+
| `signatureValid` | NO value | `{"kind":"signatureValid"}` |
141+
| `appleSigned` | NO value | `{"kind":"appleSigned"}` |
142+
143+
NEVER emit any of these (these are wrong):
144+
- `executablePathMatches`, `pathMatches`, regex-based clauses — not supported.
145+
- `isNotarized`, `notarized`, `hardenedRuntime`, `signed`, `verified` — not supported.
146+
- `categoryEquals` with invented categories like `dylib_hijacking`,
147+
`suspicious_persistence` — categories are real macOS persistence types only.
148+
- `value` as boolean (`true`/`false`) or number — `value` is always a string,
149+
except for `signatureValid` and `appleSigned` which take no value.
150+
151+
If you can't build a rule using only the kinds above, return `"extractedRule": null`.
152+
153+
### Style
154+
155+
- `explanation` should mention WHAT specifically you observed (e.g.
156+
"homebrew.mxcl bundle ID + ad-hoc signature + path under /opt/homebrew —
157+
consistent with `brew services`"), not generic phrases like "multiple red
158+
flags" or "aggressive persistence".
159+
- Be terse. 1-3 sentences.

0 commit comments

Comments
 (0)