@clear-capabilities/agentic-security-scanner 0.132.0 → 0.134.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/bin/agentic-security.js +103 -1
  3. package/dist/113.index.js +3 -3
  4. package/dist/178.index.js +1 -1
  5. package/dist/384.index.js +1 -1
  6. package/dist/499.index.js +86 -0
  7. package/dist/526.index.js +3 -3
  8. package/dist/609.index.js +741 -0
  9. package/dist/637.index.js +1 -1
  10. package/dist/agentic-security.mjs +56 -56
  11. package/dist/agentic-security.mjs.sha256 +1 -1
  12. package/package.json +9 -4
  13. package/src/discovery/CLAUDE.md +38 -0
  14. package/src/discovery/confirm.js +47 -0
  15. package/src/discovery/disprove.js +79 -0
  16. package/src/discovery/hunter.js +116 -0
  17. package/src/discovery/index.js +159 -0
  18. package/src/discovery/judge.js +97 -0
  19. package/src/discovery/lenses.js +69 -0
  20. package/src/discovery/llm-invoke.js +31 -0
  21. package/src/discovery/partition.js +92 -0
  22. package/src/engine.js +151 -1
  23. package/src/llm-validator/cost-ceiling.js +199 -0
  24. package/src/llm-validator/index.js +254 -35
  25. package/src/llm-validator/local-endpoint.js +90 -0
  26. package/src/llm-validator/providers.js +227 -0
  27. package/src/posture/CLAUDE.md +76 -0
  28. package/src/posture/accuracy-scorecard.js +37 -6
  29. package/src/posture/autopilot.js +225 -0
  30. package/src/posture/comparison.js +181 -0
  31. package/src/posture/corpus-match.js +29 -14
  32. package/src/posture/execution-proof.js +25 -1
  33. package/src/posture/fleet.js +0 -0
  34. package/src/posture/integrity.js +42 -9
  35. package/src/posture/learning.js +8 -1
  36. package/src/posture/logic-claims.js +266 -0
  37. package/src/posture/model-routing.js +26 -0
  38. package/src/posture/model-trust.js +174 -0
  39. package/src/posture/poc-inprocess.js +567 -0
  40. package/src/posture/proof-artifact.js +101 -0
  41. package/src/posture/prove-findings.js +172 -0
  42. package/src/posture/rule-overrides.js +64 -3
  43. package/src/posture/state-dir.js +25 -0
  44. package/src/posture/vuln-archaeology.js +231 -0
  45. package/src/report/index.js +16 -0
  46. package/src/sandbox/CLAUDE.md +27 -5
  47. package/src/sandbox/backend-namespace.js +39 -11
  48. package/src/sandbox/backend-userspace.js +4 -0
  49. package/src/sast/CLAUDE.md +4 -0
  50. package/src/sast/crypto-specialist.js +247 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,233 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.134.0 — the loop closes, and the logic tier learns how to be wrong
4
+
5
+ The remaining PRD epics. Two of them are new capability; the other two are the
6
+ same idea applied twice — a claim nobody can disagree with is the weakest thing
7
+ this engine emits, so both new tiers ship with the machinery to refute
8
+ themselves.
9
+
10
+ ### Two more proof classes, and neither needed a running application
11
+
12
+ `sql-injection` and `path-traversal` join the three existing classes. The PRD
13
+ assumed both would wait on a running-app harness; they did not, because the
14
+ harness was never where the proof lived.
15
+
16
+ - **SQL injection is settled at the driver boundary.** Either the payload
17
+ arrives inside the query TEXT or it arrives as a bound parameter — the first
18
+ is the vulnerability by definition and the second is the fix by definition,
19
+ and no schema, rows or live server are needed to tell them apart. The PoC
20
+ stubs the driver with a recorder and writes the marker only when the payload
21
+ shows up inside something recognisably SQL. A parameterised query reaches
22
+ `proof-failed` by **execution**, not by reading the source.
23
+ - **Path traversal is settled by what comes back.** A sentinel is planted
24
+ outside the served directory and the marker is written only if its content —
25
+ or, for `sendFile`, its resolved path — comes back out of the handler. A
26
+ `basename` guard is refuted by running it.
27
+
28
+ Both directions are pinned by tests that execute in the real sandbox. Classes
29
+ still absent (IDOR, SSRF, XSS) are now documented as decisions with reasons
30
+ rather than gaps: a PoC built on invented application state proves something
31
+ about the invention.
32
+
33
+ Fixed along the way: the webhook PoC wrote its marker after a top-level `await`
34
+ guarded by an **unref'd** timer, so a handler that never replied let Node exit
35
+ with the promise pending and the marker check never ran. The test asserting
36
+ "no decision writes no marker" had been passing without reaching the line it
37
+ was testing. Timer is now ref'd and cleared, with a positive control asserting
38
+ the process reaches exit 0.
39
+
40
+ ### The autonomous loop, wired to real stages
41
+
42
+ `scripts/autopilot.mjs` connects the loop to a real scan, a real sandboxed
43
+ exploit, a deterministic-then-model fix, and the real gate. End-to-end tests run
44
+ the whole thing against a live HTTP endpoint, including the one that matters: a
45
+ patch that changes the file, reads like a fix, and would satisfy any "did the
46
+ scanner go quiet?" check is **refused and never written**, because the exploit
47
+ still fires against it.
48
+
49
+ The CLI refuses to start without a confinement backend (the verdict requires
50
+ executing something) and refuses a dirty git tree by default (the test leg
51
+ writes the candidate patch to disk and restores it in a `finally`; a clean tree
52
+ is what makes a crash recoverable). A verified fix reached with no test runner
53
+ detected is counted and reported separately — the exploit stopped firing, but
54
+ nothing checked the application still works.
55
+
56
+ ### The business-logic tier learns how to be wrong
57
+
58
+ The deterministic half already existed. The reviewing agent's half was prose:
59
+ it asserted that a handler lets one user act on another's resource, and nothing
60
+ in the finding gave a second party anything to disagree with. It was the only
61
+ tier in this engine with no way to be wrong.
62
+
63
+ `posture/logic-claims.js` adds three offline lenses that can refute one —
64
+ citation (the file exists and the line is inside it), quotation (the quoted
65
+ snippet is at the cited line), corroboration (a "no authentication" claim
66
+ against a handler that plainly authenticates). Verdicts go through the existing
67
+ producer/verifier separation, so a lens can never vote on a claim it produced;
68
+ that is why the lenses are deterministic code and not another prompt. Refuted
69
+ claims are quarantined, never deleted and never severity-touched.
70
+
71
+ ### A comparison harness that ships no opinion about who the competition is
72
+
73
+ `posture/comparison.js` + `scripts/comparison.mjs` score this engine
74
+ head-to-head against participants **the operator supplies**. The repository
75
+ ships the harness and the answer key and names no tool — a test enforces that.
76
+
77
+ Two properties are the entire module. Every rate is computed over the
78
+ **intersection** of corpus entries *all* participants completed, because a tool
79
+ that crashed on the forty hardest entries and was scored over the remaining
80
+ hundred and seventy looks like it beat one that completed everything, and the
81
+ difference is invisible in the output. And an entry a participant could not run
82
+ is **unscored**, never counted as a miss: counting a crash as a false negative
83
+ penalises a tool for a harness problem, counting it as a pass rewards it for
84
+ one. Matching is CWE-only so nobody is scored on this engine's vocabulary.
85
+
86
+ No comparison figures are published in this repository. Running other vendors'
87
+ tools and publishing the numbers is the operator's call, not the harness's.
88
+
89
+ ### The suppression pragma never worked
90
+
91
+ Found while suppressing a false positive in this release's own new code.
92
+ `// agentic-security-ignore: <rule-id>` is documented in `CLAUDE.md` and
93
+ `pr-comment.js` tells every reviewer to use it — and **nothing implemented it**.
94
+ It has been advertised and inert. A dead suppression mechanism is worse than an
95
+ absent one: the developer writes the pragma, sees the finding again, and
96
+ concludes the scanner is noisy rather than that the pragma is dead.
97
+
98
+ Now implemented in `engine.js`, applied after dedupe and after every cross-file
99
+ pass so it covers a finding whichever analysis produced it. Line-scoped, matched
100
+ against the finding's id / vuln / CWE / family, and **logged** to the same
101
+ ledger custom rules use so `--include-suppressed` can show it. Every test
102
+ carries a positive control — the same file without the pragma must still
103
+ produce the finding, or "0 findings" would prove the suppression works and
104
+ equally prove the detector stopped firing.
105
+
106
+ ## 0.133.0 — two ways findings could be silently deleted, both closed
107
+
108
+ Four rounds of adversarial premortem against this repository's own artifacts.
109
+ Two rounds found working exploits in shipped code; both are fixed and both are
110
+ pinned by tests that run the original attack. The rest is measurement honesty —
111
+ several gates turned out to prove less than they claimed, including one defect
112
+ this effort introduced and then caught.
113
+
114
+ ### Security
115
+
116
+ - **A signature anyone could forge could disable any detector.**
117
+ `verifyLastScan` accepted a second key derived as
118
+ `sha256(<constant salt> + ':' + hostname)`. The salt is a constant in
119
+ published, npm-shipped source and a hostname is not a secret. Because the
120
+ `disable:` list in `rules.yml` is gated on that verification, a signature
121
+ forged from public information alone switched off arbitrary detectors and the
122
+ scan reported clean. Demonstrated end to end: a command-injection finding went
123
+ from 1 reported to 0, and back to 1 after the fix.
124
+
125
+ This had been known and fixed once already. The 0.62.0 entry below introduced
126
+ the per-install key precisely because the old one was "hostname-derived and
127
+ publicly forgeable in CI / containers", and kept legacy verification "for one
128
+ release to migrate existing signed scans". It was still accepted **seventy
129
+ minor releases later**. A migration window nobody closes is not a migration
130
+ window; it is the vulnerability, kept. Verification now accepts exactly one
131
+ key. Signatures made under the legacy key stop verifying — intended, and it
132
+ fails closed.
133
+
134
+ - **The LLM validator cache was a finding-deletion primitive.** A cache hit
135
+ assigned a verdict directly, and a `reject` verdict drops a finding. The cache
136
+ was read with a bare `JSON.parse(readFileSync(...))`, so planting one file
137
+ under `.agentic-security/llm-cache/` deleted a critical finding with no model
138
+ call and no network. The key is derivable by anyone with repo access, and CI
139
+ restoring a cache directory between runs delivers it without a repo write at
140
+ all. Cache entries are now HMAC-signed with the same mechanism `last-scan.json`
141
+ already used; an unsigned, tampered or foreign-keyed entry is a MISS, never a
142
+ verdict.
143
+
144
+ - **A `reject` can no longer delete a strongly-provenanced finding.** The code
145
+ asserted that prompt-injecting the validator was harmless because "the worst
146
+ an attacker can produce is escalate". That was false: the challenge/nonce
147
+ cross-check defends against forged and replayed responses, not against a model
148
+ persuaded by source it legitimately read. Findings from real analysis
149
+ (taint-proven, multi-sink, execution-proven) are now demoted to `escalate`
150
+ rather than dropped, so the guarantee is structural instead of asserted.
151
+
152
+ - **Coverage reduction is now visible in the artifact.** A `disable:` that took
153
+ effect produced findings that were simply absent — indistinguishable from
154
+ clean code to whoever reads the report. `suppressedRules` now carries the
155
+ count, per-rule severity breakdown, example locations and the AUTHORITY the
156
+ suppression ran under, so a signed suppression reads differently from an
157
+ env-var opt-out. Authorised suppressions are reported too: a signature proves
158
+ who asked, not that the hidden findings stopped existing.
159
+
160
+ - **Signatures carry key provenance** (`env` / `per-install` / `ephemeral`).
161
+ `env` means whoever set the environment could have signed the run; `ephemeral`
162
+ means the key could not be persisted and the signature will never verify
163
+ again. Neither was inferable from the digest.
164
+
165
+ - **The suppression quorum has a floor of 2.** `AGENTIC_SECURITY_LEARN_QUORUM=1`
166
+ was honoured, so a single triage verdict could suppress a finding — and with
167
+ family+path matching, a whole family across a path. The root guidance warned
168
+ about exactly this; the code did not enforce it.
169
+
170
+ ### Measurement honesty
171
+
172
+ - **The corpus is fitted to the detectors it measures, and now says so.** 98% of
173
+ entries are self-authored synthetic fixtures and none come from the
174
+ disclosed-PoC tier. `npm run corpus:provenance` prints the composition on every
175
+ run and fails a commit that lands a detector together with the corpus entries
176
+ exercising it — caught against real history, including one such commit in this
177
+ very effort. This stops the loop tightening; it does not make the corpus
178
+ independent, and the docs no longer imply otherwise.
179
+
180
+ - **The precision gate covered 6% of the source.** It ran over `hooks/` and
181
+ `scripts/` — 22 files — while `scanner/src` (383 files, the entire product) sat
182
+ outside it. Now 240 files. The `scanner/src` count is published as a DRIFT
183
+ TRIPWIRE, explicitly not hand-reviewed and explicitly not a precision figure,
184
+ because a scanner's own source contains sink patterns as data.
185
+
186
+ - **The determinism gate exercised only the layer that cannot vary.** The
187
+ original fixture produced findings from regex and structural detectors alone. A
188
+ second fixture now drives the interprocedural taint engine and the Python
189
+ parser, digests are compared per fixture so a divergence names the layer, and
190
+ the comparator fails if the deep fixture degraded to the syntactic layer on any
191
+ machine.
192
+
193
+ - **The scorecard gate could not detect a stale scorecard.** It compared only the
194
+ engine version, so a document measured over 200 corpus entries passed while the
195
+ corpus held 210 — every published rate computed over a population that no
196
+ longer existed. It now compares the population too.
197
+
198
+ - **The independent-evaluation gate passed on a 4-sample smoke fixture** its own
199
+ README says must never be cited, because its thresholds had been calibrated to
200
+ pass. It now uses the README's own figures, treats exceeded calibration targets
201
+ as violations rather than notes, and refuses to emit a pass over the built-in
202
+ fixture however the thresholds are set. It fails today, correctly.
203
+
204
+ - **Claims re-scoped to what is measured.** The roadmap carries a "What the gates
205
+ do not prove" section, states outright that the false-positive-rate goal is not
206
+ met, and points at the two harnesses built for that gap — both of which need
207
+ data, not code. R13 is downgraded from "landed" to "mechanism landed, no
208
+ observation pipeline": nothing constructs its ledger, so it can never downgrade
209
+ anything.
210
+
211
+ ### Fixed along the way
212
+
213
+ - **The validator cache had never persisted a single entry.** `safeWriteState`
214
+ refused every directory nested under `.agentic-security/`, so `llm-cache/`,
215
+ `fix-history/` and `sbom-history/` were all unwritable while a
216
+ `validator-cache stats|gc` subcommand managed a cache that was always empty.
217
+ Found by a positive-control test asserting a legitimately written entry
218
+ round-trips.
219
+
220
+ - **Sandbox timeouts now use SIGKILL.** The kernel does not deliver
221
+ default-action signals to a PID namespace's pid 1 from outside it, so SIGTERM
222
+ was dropped and a payload ran to completion against a 1200 ms budget — measured
223
+ in CI at 30057 ms. Proof execution also gained its own aggregate wall-clock
224
+ budget, because a count cap bounds nothing in time.
225
+
226
+ - **Cost reporting stopped presenting an estimate as spend.** The endpoint's
227
+ usage report was discarded, so the ledger always booked the pre-call worst
228
+ case. Usage is now plumbed through, and any estimated component renders as
229
+ "at most $X" with the reason.
230
+
3
231
  ## 0.132.0 — a proven exploit becomes a permanent regression test
4
232
 
5
233
  The corpus stops being only a regression net and starts being fed by the engine
@@ -17,7 +17,7 @@ import { recordScan, formatStreakLine, formatGradeDelta } from '../src/posture/s
17
17
  import { ingestAndMerge } from '../src/sca/sarif-ingest.js';
18
18
  import { loadProfile, saveProfile, detectProfile, renderAttributionLine, ATTRIBUTION, ATTRIBUTION_URL } from '../src/posture/profile.js';
19
19
  import { applySuppressions, addSoftAcceptance, expiredSoftAcceptances } from '../src/posture/suppressions.js';
20
- import { applyOverrides, validateOverrides } from '../src/posture/rule-overrides.js';
20
+ import { applyOverrides, validateOverrides, suppressionReport, renderSuppressionSummary } from '../src/posture/rule-overrides.js';
21
21
  import { listPacks, loadPack, applyPacks } from '../src/posture/rule-packs.js';
22
22
  import { writeLockfile, verifyLockfile, makeDeterministic, isDeterministic } from '../src/posture/deterministic.js';
23
23
  import { enrichWithEPSS } from '../src/posture/epss.js';
@@ -512,6 +512,19 @@ async function cmdScan(args) {
512
512
  scan.findings = applyOverrides(scan.findings || [], targetAbs);
513
513
  scan.secrets = applyOverrides(scan.secrets || [], targetAbs);
514
514
  scan.logicVulns = applyOverrides(scan.logicVulns || [], targetAbs);
515
+ // Coverage reduction belongs in the ARTIFACT, not only in a log line. A
516
+ // `disable:` that takes effect otherwise produces findings that are simply
517
+ // absent, which is indistinguishable from clean code to whoever reads the
518
+ // report. Recorded whether the suppression was authorised or not — an
519
+ // authorised one still hides results.
520
+ try {
521
+ const _sup = suppressionReport(targetAbs);
522
+ if (_sup) {
523
+ scan.suppressedRules = _sup;
524
+ const _line = renderSuppressionSummary(_sup);
525
+ if (_line) process.stderr.write(`⚠️ agentic-security: ${_line}\n`);
526
+ }
527
+ } catch { /* reporting must never fail a scan */ }
515
528
 
516
529
  // Curated rule packs: --pack <name> (repeatable). Narrows findings to the
517
530
  // CWEs covered by the requested pack(s).
@@ -575,6 +588,7 @@ async function cmdScan(args) {
575
588
  // only — a failure here must never fail a scan.
576
589
  try {
577
590
  const { computeRunAttestation } = await import('../src/posture/attestation.js');
591
+ const { keyProvenance } = await import('../src/posture/integrity.js');
578
592
  const { effectiveVersion } = await import('../src/posture/ruleset-version.js');
579
593
  scan.attestation = computeRunAttestation({
580
594
  findings: normalizeFindings(scan),
@@ -584,6 +598,11 @@ async function cmdScan(args) {
584
598
  root: targetAbs,
585
599
  sign: true,
586
600
  });
601
+ // P1-3 — a signature is only as meaningful as the key behind it.
602
+ // `env` means whoever set the environment could have signed this;
603
+ // `ephemeral` means the key could not be persisted, so this signature will
604
+ // never verify on any later run. Neither is inferable from the digest.
605
+ if (scan.attestation) scan.attestation.keyProvenance = keyProvenance();
587
606
  } catch { /* attestation is metadata; never fail a scan over it */ }
588
607
 
589
608
  // R2: Always emit machine-readable artifacts to .agentic-security/.
@@ -1466,6 +1485,88 @@ async function cmdReset(args) {
1466
1485
  // FR-LEARN-6: read triage-feedback.json, group repeated FP verdicts by
1467
1486
  // (family, dir prefix), and propose a suppression YAML when ≥ threshold
1468
1487
  // (default 5) verdicts cluster. Writes to .agentic-security/rules-proposed/.
1488
+ // Languages the Layer-1 IR parses. Anything else cannot be partitioned into a
1489
+ // call-graph focus area, so feeding it to a hunter would spend tokens on files
1490
+ // the confirmation gate can never corroborate.
1491
+ const HUNT_EXTS = /\.(?:js|jsx|mjs|cjs|ts|tsx|py|java|cs|kt|go|php|rb)$/i;
1492
+ const HUNT_IGNORE = ['node_modules/**', '.git/**', 'dist/**', 'build/**', 'vendor/**', '**/.agentic-security/**'];
1493
+ const HUNT_MAX_FILES = 2000;
1494
+
1495
+ async function cmdHunt(args) {
1496
+ const scanRoot = path.resolve(args.flags.root || args._[0] || '.');
1497
+ const { listFiles } = await import('../src/util/glob.js');
1498
+ const { buildProjectIR } = await import('../src/ir/index.js');
1499
+ const { runDiscovery } = await import('../src/discovery/index.js');
1500
+ const { LENSES } = await import('../src/discovery/lenses.js');
1501
+
1502
+ const rels = (await listFiles(scanRoot, { ignore: HUNT_IGNORE })).filter(f => HUNT_EXTS.test(f));
1503
+ if (rels.length > HUNT_MAX_FILES) {
1504
+ console.error(`agentic-security: ${rels.length} source files exceeds the ${HUNT_MAX_FILES}-file hunt cap.`);
1505
+ console.error('Narrow the scope with --root <subdir>. Discovery is token-expensive and');
1506
+ console.error('unbounded fan-out on a large repository is the wrong default.');
1507
+ return 2;
1508
+ }
1509
+
1510
+ const fileContents = {};
1511
+ for (const rel of rels) {
1512
+ try { fileContents[rel] = fs.readFileSync(path.join(scanRoot, rel), 'utf8'); } catch { /* unreadable — skip */ }
1513
+ }
1514
+
1515
+ const { perFile, callGraph } = buildProjectIR(fileContents);
1516
+
1517
+ // Prior scan and triage verdicts feed the judge so a hunt does not re-report
1518
+ // what the rule engine already found or what a human already dismissed.
1519
+ let priorScan = null, triageFeedback = null;
1520
+ try { priorScan = JSON.parse(fs.readFileSync(path.join(scanRoot, '.agentic-security', 'last-scan.json'), 'utf8')); } catch {}
1521
+ try { triageFeedback = JSON.parse(fs.readFileSync(path.join(scanRoot, '.agentic-security', 'triage-feedback.json'), 'utf8')); } catch {}
1522
+
1523
+ const lenses = args.flags.lens ? String(args.flags.lens).split(',').map(s => s.trim()).filter(Boolean) : undefined;
1524
+ const report = await runDiscovery(
1525
+ { perFileIR: perFile, callGraph, fileContents, priorScan, triageFeedback },
1526
+ { lenses, maxAreas: args.flags['max-areas'] ? parseInt(args.flags['max-areas'], 10) : undefined },
1527
+ );
1528
+
1529
+ const c = report.coverage;
1530
+ console.log('');
1531
+ console.log(`Discovery — ${rels.length} file(s), ${c.areasPlanned} focus area(s), ${c.lensesPerArea} lens(es) each`);
1532
+ console.log(` areas hunted: ${c.areasHunted}/${c.areasPlanned} (fully: ${c.areasFullyHunted}) degraded runs: ${c.degradedRuns}/${report.runs.length}`);
1533
+ console.log(` confirmation: ${JSON.stringify(c.confirmedByTier)} panels: ${c.panelsRun} (undecided ${c.undecidedPanels})`);
1534
+ console.log('');
1535
+
1536
+ if (report.fresh.length === 0) {
1537
+ console.log('No new candidates survived confirmation and refutation.');
1538
+ } else {
1539
+ console.log(`${report.fresh.length} new candidate finding(s):`);
1540
+ for (const f of report.fresh) {
1541
+ console.log(` [${f.severity}] ${f.file}:${f.line} ${f.vuln}`);
1542
+ console.log(` lens=${f.discovery.lens} confirmation=${f.discovery.confirmation?.tier || 'unknown'}`);
1543
+ }
1544
+ }
1545
+ if (report.duplicates.length || report.suppressed.length || report.refutedCandidates.length) {
1546
+ console.log('');
1547
+ console.log(` (${report.duplicates.length} already known, ${report.suppressed.length} previously marked false positive, ` +
1548
+ `${report.refutedCandidates.length} refuted by the panel)`);
1549
+ }
1550
+
1551
+ // Degradation is part of the output. A half-failed hunt must never read as a
1552
+ // clean one, so every reason is printed rather than summarised away.
1553
+ if (c.reasons.length) {
1554
+ console.log('');
1555
+ console.log('Coverage gaps:');
1556
+ for (const r of c.reasons) console.log(` · ${r}`);
1557
+ }
1558
+ if (report.runs.length && c.degradedRuns === report.runs.length) {
1559
+ console.log('');
1560
+ console.log('EVERY hunter run degraded — this result says nothing about the code.');
1561
+ console.log('Set AGENTIC_SECURITY_LLM_ENDPOINT to enable discovery.');
1562
+ }
1563
+
1564
+ // Advisory by design: discovery never gates. Exit 0 unless the scope was bad.
1565
+ console.log('');
1566
+ console.log(`Lenses available: ${LENSES.map(l => l.key).join(', ')} (--lens a,b to narrow)`);
1567
+ return 0;
1568
+ }
1569
+
1469
1570
  async function cmdRuleSynth(args) {
1470
1571
  const scanRoot = path.resolve(args.flags.root || '.');
1471
1572
  const { synthesizeRules } = await import('../src/posture/rule-synthesis.js');
@@ -1758,6 +1859,7 @@ async function main() {
1758
1859
  case 'validator-cache': process.exit(await cmdValidatorCache(args));
1759
1860
  case 'verify': process.exit(await cmdVerify(args));
1760
1861
  case 'reset': process.exit(await cmdReset(args));
1862
+ case 'hunt': process.exit(await cmdHunt(args));
1761
1863
  case 'rule-synth': process.exit(await cmdRuleSynth(args));
1762
1864
  case 'digest': process.exit(await cmdDigest(args));
1763
1865
  case 'setup': process.exit(await cmdSetup(args));
package/dist/113.index.js CHANGED
@@ -406,8 +406,8 @@ var external_node_child_process_ = __webpack_require__(1421);
406
406
  var external_node_fs_ = __webpack_require__(3024);
407
407
  // EXTERNAL MODULE: external "node:path"
408
408
  var external_node_path_ = __webpack_require__(6760);
409
- // EXTERNAL MODULE: ./src/engine.js + 595 modules
410
- var engine = __webpack_require__(4660);
409
+ // EXTERNAL MODULE: ./src/engine.js + 501 modules
410
+ var engine = __webpack_require__(5695);
411
411
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
412
412
  // Deterministic honesty gates on fix / finding output (#7).
413
413
  //
@@ -946,7 +946,7 @@ async function verifyFix({
946
946
  let pocLeg = { status: 'not-requested', reason: null, tier: null };
947
947
  if (poc?.code) {
948
948
  try {
949
- const { proveFinding } = await __webpack_require__.e(/* import() */ 259).then(__webpack_require__.bind(__webpack_require__, 8259));
949
+ const { proveFinding } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1291));
950
950
  const proved = await proveFinding({ ...(poc.finding || {}), poc }, { files });
951
951
  const tier = proved.proofTier;
952
952
  pocLeg = tier === 'execution-proven'
package/dist/178.index.js CHANGED
@@ -13,7 +13,7 @@ export const modules = {
13
13
  /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
14
14
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
15
15
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
16
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(4660);
16
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(5695);
17
17
  // Time-travel + counterfactual scanning (v0.68).
18
18
  //
19
19
  // Two new modes that exploit the pure-input shape of runFullScan:
package/dist/384.index.js CHANGED
@@ -8,7 +8,7 @@ export const modules = {
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
9
  /* harmony export */ scanCredentials: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Sv)
10
10
  /* harmony export */ });
11
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(4660);
11
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(5695);
12
12
  // Secrets submodule view of the engine — credential + entropy + TODO scanning.
13
13
 
14
14
 
@@ -0,0 +1,86 @@
1
+ export const id = 499;
2
+ export const ids = [499];
3
+ export const modules = {
4
+
5
+ /***/ 3499:
6
+ /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
7
+
8
+ /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
+ /* harmony export */ H: () => (/* binding */ lensByKey),
10
+ /* harmony export */ LENSES: () => (/* binding */ LENSES),
11
+ /* harmony export */ j: () => (/* binding */ buildHunterPrompt)
12
+ /* harmony export */ });
13
+ //
14
+ // The seven hunting lenses. Each hunter run is one (focus area × lens) pair.
15
+ //
16
+ // WHY DIVERSE LENSES RATHER THAN N IDENTICAL HUNTERS: redundancy raises
17
+ // confidence in what was already found and adds nothing to coverage. A lens
18
+ // that is told to look only at authorization asks different questions of the
19
+ // same code than one told to look at crypto, so the union covers failure modes
20
+ // no single prompt reaches. `wildcard` exists because a fixed taxonomy is a
21
+ // ceiling, and the classes worth finding are the ones not on the list.
22
+ const LENSES = Object.freeze([
23
+ { key: 'injection', title: 'Injection', family: 'injection', cwe: 'CWE-74',
24
+ brief: 'Untrusted input reaching an interpreter: SQL, shell, template, XPath, LDAP, or deserialization. Follow the value, not the function name.' },
25
+ { key: 'authz', title: 'Authorization', family: 'access-control', cwe: 'CWE-285',
26
+ brief: 'Missing, partial, or bypassable authorization: object references not scoped to the caller, tier checks applied on one path but not another, checks performed after the effect.' },
27
+ { key: 'crypto', title: 'Cryptography', family: 'crypto', cwe: 'CWE-327',
28
+ brief: 'Misuse rather than choice of primitive: reused nonces, unauthenticated ciphertext, comparisons that are not constant time, keys derived from guessable material.' },
29
+ { key: 'business-logic', title: 'Business logic', family: 'business-logic', cwe: 'CWE-840',
30
+ brief: 'The code does what it says and what it says is wrong: state machines that accept out-of-order transitions, quantities that may be negative, refunds that exceed charges, limits enforced client side.' },
31
+ { key: 'feature-abuse', title: 'Feature abuse', family: 'abuse', cwe: 'CWE-799',
32
+ brief: 'A working feature used as a weapon: unbounded fan-out, expensive endpoints with no cost to the caller, invitations or exports that leak across tenants.' },
33
+ { key: 'chained', title: 'Chained', family: 'attack-chain', cwe: 'CWE-1173',
34
+ brief: 'Two behaviours that are each acceptable alone and unacceptable together. State the chain as an ordered sequence of steps with the attacker capability required at each.' },
35
+ { key: 'wildcard', title: 'Wildcard', family: 'other', cwe: 'CWE-710',
36
+ brief: 'Anything the other lenses do not cover. Prefer the surprising and specific over the generic; report nothing rather than something already obvious.' },
37
+ ]);
38
+
39
+ function lensByKey(key) {
40
+ if (typeof key !== 'string') return null;
41
+ return LENSES.find(l => l.key === key) || null;
42
+ }
43
+
44
+ const DEFAULT_MAX_CHARS = 60_000;
45
+
46
+ function buildHunterPrompt(focusArea, lens, ctx = {}) {
47
+ const maxChars = Number.isInteger(ctx.maxChars) && ctx.maxChars > 0 ? ctx.maxChars : DEFAULT_MAX_CHARS;
48
+ const contents = ctx.fileContents || {};
49
+ const files = (focusArea?.files || []).filter(f => typeof contents[f] === 'string');
50
+
51
+ let budget = maxChars;
52
+ const blocks = [];
53
+ for (const f of files) {
54
+ const src = contents[f];
55
+ const slice = src.length > budget ? src.slice(0, Math.max(0, budget)) : src;
56
+ const truncated = slice.length < src.length;
57
+ blocks.push(`--- ${f}${truncated ? ' (truncated)' : ''} ---\n${slice}`);
58
+ budget -= slice.length;
59
+ if (budget <= 0) break;
60
+ }
61
+ const omitted = files.length - blocks.length;
62
+
63
+ return [
64
+ `You are hunting for security vulnerabilities in one area of a codebase.`,
65
+ `Area: ${focusArea?.label ?? 'unknown'} (${files.length} files)`,
66
+ ``,
67
+ `Your lens is ${lens.title}. ${lens.brief}`,
68
+ `Report ONLY through this lens. Another hunter covers the others.`,
69
+ ``,
70
+ `Rules:`,
71
+ `- Report a candidate only if you can name the entry point an attacker controls and the effect they achieve.`,
72
+ `- Do not report defence-in-depth gaps, style, or "could be hardened". Those are not candidates.`,
73
+ `- Cite a real file and line from the source below. A candidate with no location is discarded.`,
74
+ ``,
75
+ `Return JSON: {"candidates":[{"title","file","line","rationale","entryPoint","sink"}]}`,
76
+ `Return {"candidates":[]} if you find nothing. An empty result is a valid and useful answer.`,
77
+ ``,
78
+ omitted > 0 ? `NOTE: ${omitted} file(s) omitted, prompt budget exhausted (truncated context).\n` : ``,
79
+ ...blocks,
80
+ ].join('\n');
81
+ }
82
+
83
+
84
+ /***/ })
85
+
86
+ };
package/dist/526.index.js CHANGED
@@ -234,8 +234,8 @@ var external_node_child_process_ = __webpack_require__(1421);
234
234
  var external_node_fs_ = __webpack_require__(3024);
235
235
  // EXTERNAL MODULE: external "node:path"
236
236
  var external_node_path_ = __webpack_require__(6760);
237
- // EXTERNAL MODULE: ./src/engine.js + 595 modules
238
- var engine = __webpack_require__(4660);
237
+ // EXTERNAL MODULE: ./src/engine.js + 501 modules
238
+ var engine = __webpack_require__(5695);
239
239
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
240
240
  // Deterministic honesty gates on fix / finding output (#7).
241
241
  //
@@ -774,7 +774,7 @@ async function verifyFix({
774
774
  let pocLeg = { status: 'not-requested', reason: null, tier: null };
775
775
  if (poc?.code) {
776
776
  try {
777
- const { proveFinding } = await __webpack_require__.e(/* import() */ 259).then(__webpack_require__.bind(__webpack_require__, 8259));
777
+ const { proveFinding } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 1291));
778
778
  const proved = await proveFinding({ ...(poc.finding || {}), poc }, { files });
779
779
  const tier = proved.proofTier;
780
780
  pocLeg = tier === 'execution-proven'