@clear-capabilities/agentic-security-scanner 0.195.21 → 0.195.23

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 (46) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/bin/agentic-security.js +5 -2
  3. package/dist/{7feb08356355c30181a1.json → 01087ded263953062654.json} +3 -3
  4. package/dist/550.index.js +9 -8
  5. package/dist/5620.index.js +59 -10
  6. package/dist/5937.index.js +88 -16
  7. package/dist/7838.index.js +32 -5
  8. package/dist/9441.index.js +50 -7
  9. package/dist/9824.index.js +19 -13
  10. package/dist/agentic-security.mjs +2 -2
  11. package/dist/agentic-security.mjs.sha256 +1 -1
  12. package/package.json +3 -3
  13. package/src/discovery/CLAUDE.md +4 -0
  14. package/src/discovery/detector-spec-synthesis.js +203 -0
  15. package/src/discovery/llm-invoke.js +3 -3
  16. package/src/egress/transport.js +148 -0
  17. package/src/engine.js +9 -0
  18. package/src/integrations/index.js +18 -9
  19. package/src/llm-validator/index.js +4 -2
  20. package/src/llm-validator/local-endpoint.js +4 -1
  21. package/src/llm-validator/ollama-provider.js +8 -6
  22. package/src/pipeline/scan-health.js +15 -2
  23. package/src/posture/CLAUDE.md +1 -0
  24. package/src/posture/adversary-agent.js +27 -10
  25. package/src/posture/assurance/config.js +2 -0
  26. package/src/posture/assurance/decision.js +45 -0
  27. package/src/posture/assurance/rollout.js +3 -0
  28. package/src/posture/custom-rules.js +13 -1
  29. package/src/posture/fleet/cli.js +2 -4
  30. package/src/posture/fleet/fleet-manifest.js +4 -1
  31. package/src/posture/fleet/job-runner.js +28 -4
  32. package/src/posture/fleet/org-health.js +23 -0
  33. package/src/posture/fleet/scan-cache.js +1 -1
  34. package/src/posture/flow-narration.js +10 -8
  35. package/src/posture/llm-redteam.js +3 -1
  36. package/src/posture/provenance/coordinator.js +8 -0
  37. package/src/posture/provenance/origin-resolver.js +17 -4
  38. package/src/posture/rule-compiler/cli.js +56 -8
  39. package/src/posture/rule-compiler/corpus.js +10 -2
  40. package/src/posture/rule-compiler/ir.js +10 -1
  41. package/src/posture/rule-compiler/promotion.js +12 -2
  42. package/src/posture/rule-compiler/regression.js +1 -1
  43. package/src/posture/rule-compiler/store.js +27 -7
  44. package/src/posture/strategic-metrics.js +50 -7
  45. package/src/report/index.js +7 -6
  46. package/src/sca/llm-function-extract.js +17 -13
package/CHANGELOG.md CHANGED
@@ -9,6 +9,32 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.195.23 - GA programme, release C: one model transport, reviewed rule promotion, metrics with a scope, provenance that proves absence, fleet results that age
13
+
14
+ **Measured, 0.195.22:** `TIMINGS preflight=3s push=6s pr=4s checks=398s merge=5s verify=6s tag=2s release=161s npm=206s total=791s release-clock=174s`, shipped in 14 min 56 s on the second attempt (the first was stopped by the background gate's `ci-parity` check: a new deep-mode pin skipped under the hosted-CI switches until it set `DEEP_IN_CI` like the existing helper).
15
+
16
+ The next tranche of the GA plan that is code and tests: GA-06.3 (the model transport), GA-07.1 to 7.3 (the rule compiler's gates), GA-11.3 and 11.4 (strategic metrics), GA-05.2 and 5.4 (provenance parent boundaries) and GA-09.5 (fleet accounting). The lineage corpus was re-run on today's source for GA-03.1 and is still 17 of 24, with the same seven capability-tier failures the promotion record names (four source categories with no JavaScript catalog entry, an alias hop, an interprocedural return, an unsupported sink); that engine work is recorded as open, not done.
17
+
18
+ - **One transport for every model call.** `scanner/src/egress/transport.js` (`modelFetch`): no redirect is followed (a loopback endpoint answering 3xx to a remote host would have carried the prompt off the machine; eight call sites followed redirects by default), the reply is capped while it is read (every site read `res.json()` unbounded; 4 MiB by default, `AGENTIC_SECURITY_LLM_MAX_RESPONSE_BYTES`), every call has a deadline (four sites had none and could hold a scan open on a stalled connection), an optional process-wide wall budget (`AGENTIC_SECURITY_LLM_WALL_BUDGET_MS`), and proxy variables are documented as ignored. The validator, the Ollama provider, discovery, synthesis, the dependency function extractor, flow narration, the red-team sender, the adversary agent (its model call and its probes of the target) and the autopilot use it. The GA ledger check `egress-all-model-paths` now also fails on a model site that sends through a bare `fetch`, and a file that uses only the transport is still a candidate.
19
+ - **The `local` preset holds on every caller.** Flow narration, the dependency function extractor and the adversary agent read the raw endpoint variable and called it regardless of the preset; each resolves its provider as the validator does, so a non-loopback endpoint under `local` is refused there too, and a bare `OPENAI_API_KEY` is no longer attached to an arbitrary endpoint by the adversary agent. `0.0.0.0`, `[::]` and `http://0/` are refused as loopback (the unspecified address reaches a local interface by operating-system convention, not by its meaning); the contract page's table says so.
20
+ - **The rule compiler promotes by review.** `rules promote` needs `--reviewed-by <person>` (a model or agent name is not a reviewer) and records the reviewer and `--scope` in the ledger and the provenance. Promotion needs independent examples (`independentVulnerableCode`, `independentCleanCode`: outside the finding, never mutated; every vulnerable one detected, no safe one fired on), refuses a rule edited after its evaluation, an originating commit not in the repository's history, a hand-written rule file under the same id, and a promoted rule with the identical match. The lifecycle ledger is signed with the per-install key and never replaced by an empty one on a failed check; an unsigned pre-0.195.23 ledger is accepted once with `AGENTIC_SECURITY_RULES_UNSIGNED=1`. Every custom and compiled rule pattern now passes the SAST layer's superlinear-regex detector, not only the nested-quantifier heuristic (`(a|a)*b` was accepted). The CI templates run `rules regress` after the scan. Found on the way: the rule digest passed the top-level key list as the JSON replacer, which also filters nested objects, so `match.pattern` and every structural regex were digested as `{}`; the digest covers the whole rule now.
21
+ - **Strategic metrics state their scope.** The report carries `scope` (per-scan, the scan id and start time, the attempt window, and that it is not a trend), one status vocabulary on every metric (`measured`, `partial`, `unmeasured`; the learning rate was `not-measured`), and the remediation rate counts one attempt per finding (the latest; earlier ones reported as `repeated`), with `recorded`, `failed` and `window` beside the numerator and denominator. Five retries of one fix were five attempts before.
22
+ - **Provenance proves absence or says it cannot.** A parent commit whose replay failed counted as verified absence (`present: false` for any reason); it is now `partial` with reason `parent-not-inspectable`, and a `complete` result names its evidence of absence (`stableId-not-reproduced`, or `no-files-at-commit` for a file new in that commit).
23
+ - **Fleet results age, and a bad artifact is never green.** A cached repository result records when it was cached and is reused only inside the manifest's `cacheMaxAge` (7 days by default; a record with no time is never reused). An artifact that fails its hash check for an optional repository makes the organization degraded (it stayed green), the counts move from the state the repository had (a partial result's problem no longer decremented `complete`), and artifacts are written whole or not at all.
24
+
25
+ ## 0.195.22 - GA programme, release B: every model path through the egress policy, one assurance decision on every surface, and unknown is not fresh
26
+
27
+ **Measured, 0.195.21:** `TIMINGS preflight=3s push=5s pr=4s checks=393s merge=5s verify=6s tag=2s release=223s npm=145s total=786s release-clock=236s`, shipped in 14 min 56 s on the third attempt (the first stopped at verify on the new expected-jobs rule because the check-runs query had never been paginated; the second stopped at the warm gate because the CodeQL summary check is not a workflow job). The npm check was re-run once with `--resume`.
28
+
29
+ The safety and semantics items of the GA plan that are code (GA-07.4, GA-07.5, GA-11.7, its day 5 and day 6, and the two contract failures its review found), built on the 0.195.21 ledger.
30
+
31
+ - **Detector-spec synthesis goes through the shared bounded model call.** `scripts/synthesize-detector.mjs` posted its prompt with a bare `fetch`: no egress policy, no redaction, no deadline, a regular-expression JSON extractor that could not parse the nested array it required, and an output path written anywhere. It is now a thin front over `scanner/src/discovery/detector-spec-synthesis.js`: the `detector-spec-synthesis` assurance feature (off by default), the egress policy evaluated BEFORE any sample file is read (a denied call reads nothing), bounded reads, a CSPRNG nonce, `guardedModelCall` (redaction, request cap, deadline with bounded retries, audit entry, output cap), the string-aware JSON parser the validator uses, and a closed-world validation of the proposal (identifiers only, so no field can carry a pattern; unknown fields, a wrong nonce and a bad argument index are refused with reasons, never with the raw reply). A proposal is written only inside the working directory, never under `scanner/src`, never over a file without `--force`; nothing installs it. Exit codes 0, 2, 3, 4. Eleven pins with an injected transport, and the `model-egress-policy` contract test now covers this path.
32
+ - **The autopilot's fix proposals too.** `scripts/autopilot.mjs` was the second model path the 0.195.21 egress check found: its whole-file fix prompt now goes through the egress policy (a denial leaves the finding NEEDS_REVIEW), the redactor and the audit log. The `model-egress-policy:EGRESS-ALL-PATHS` ledger record moves from `failed` to `passed`.
33
+ - **One assurance decision on every surface.** `posture/assurance/decision.js`: `deployable`, `needs_review`, `not_deployable`, or `unknown` when no scan health was recorded. The CLI ship verdict, the Slack, Discord and Teams digests and the pull-request comment all derive their wording from it; `digest` reads the scan health the last scan recorded. A findings file with no scan health now reads "cannot confirm", never "safe to deploy", and a scan object without scan health in the tests was re-pinned to that. The default wording of the three existing states is unchanged (the bounded wording stays the `portfolio-assurance` opt-in).
34
+ - **Unknown feed freshness is a condition.** `freshness.<feed>.stale` is tri-state and documented so; `null` for a feed the scan consulted (`consulted` items for the KEV catalog, `cvesChecked` for EPSS) because it was unavailable, never loaded or cached without a timestamp is a condition like staleness, the result is `partial` and strict mode fails closed on it. A feed the scan had nothing to look up in is not a condition, and neither is `offline-skipped`, the operator's own declared configuration. The catalog metadata is reset per scan so one scan cannot inherit another's. The test that asserted a boolean, and the offline clean-scan pin, now assert the tri-state and the feed-only conditions.
35
+ - **A suppression pragma now reaches privacy-flow findings.** The self-scan of the new synthesis module showed that `agentic-security-ignore` on a credential-to-HTTP line was inert: the privacy flows (`dataflow/privacy-taint.js`) are appended after both pragma passes, against the documented rule that a stage appending findings runs the filter after itself. A third pass runs after the privacy stage; pinned both ways with a deep-mode fixture, and the suppression is logged like every other.
36
+ - **The `proof doctor` exit code is documented.** 0 when an execution environment is usable, 1 when none is (the report is still printed), `--json` always 0. The contract test that demanded 0 on every host, and so failed on one without a confinement backend, now pins the documented contract; the CLI's behaviour is unchanged.
37
+
12
38
  ## 0.195.21 - The GA programme ledger: every shipped surface owned, every acceptance record machine-checked, every expected hosted job required
13
39
 
14
40
  **Measured, 0.195.20:** `TIMINGS preflight=3s push=6s pr=1s checks=363s merge=5s verify=5s tag=2s release=192s npm=267s total=844s release-clock=204s`, shipped in 15 min 53 s on the second attempt (the first stopped in the hosted checks: the version bump had regenerated the scorecard before the support registry was re-measured, so the two carried different measurement dates; the scorecard was regenerated on the clean tree). The npm check was re-run once with `--resume`.
@@ -3578,14 +3578,17 @@ async function cmdDigest(args) {
3578
3578
  const summary = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
3579
3579
  for (const f of (last.findings || [])) summary[f.severity || 'medium']++;
3580
3580
  const project = args.flags.project || path.basename(target);
3581
+ // The digest states the shared assurance decision, which needs the scan health the last scan recorded; a findings file alone says "cannot confirm".
3582
+ let scanHealth = null;
3583
+ try { const ls = statePath(target, 'last-scan.json'); if (fs.existsSync(ls)) scanHealth = JSON.parse(await fsp.readFile(ls, 'utf8')).scanHealth || null; } catch { scanHealth = null; }
3581
3584
  if (args.flags.slack) {
3582
- const payload = buildSlackDigest(findings, summary, { project });
3585
+ const payload = buildSlackDigest(findings, summary, { project, scanHealth });
3583
3586
  const r = await postWebhook(args.flags.slack, payload);
3584
3587
  console.log(r.ok ? `✓ Slack digest sent` : `✗ Slack failed: ${r.reason || r.status}`);
3585
3588
  return r.ok ? 0 : 4;
3586
3589
  }
3587
3590
  if (args.flags.discord) {
3588
- const payload = buildDiscordDigest(findings, summary, { project });
3591
+ const payload = buildDiscordDigest(findings, summary, { project, scanHealth });
3589
3592
  const r = await postWebhook(args.flags.discord, payload);
3590
3593
  console.log(r.ok ? `✓ Discord digest sent` : `✗ Discord failed: ${r.reason || r.status}`);
3591
3594
  return r.ok ? 0 : 4;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.195.21",
3
+ "version": "0.195.23",
4
4
  "description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -86,7 +86,7 @@
86
86
  "test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
87
87
  "test:glob": "node --test test/glob-compat.test.js",
88
88
  "test:sast": "node --test test/dag-precision.test.js test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.test.js test/todo-marker-words.test.js test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/zip-slip-containment-guard.test.js test/zip-slip-go.test.js test/redos-anchoring.test.js test/path-guard-validator-call.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/csharp-cross-file.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/mcp-audit-scan.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/java-structural-cross-file.test.js test/guard-recognition.test.js test/guard-window-identifier-correlation.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/secret-redaction.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js test/adversarial-robustness.test.js test/secret-entropy.test.js test/comment-strip.test.js test/juliet-path-filter-gate.test.js test/java-bench-shape-opt-in.test.js test/vendor-java-parser-current.test.js test/java-ast-folding.test.js test/java-bench-extras.test.js test/reachability-missing-callgraph-data.test.js test/rate-limit.test.js test/auth-signals.test.js test/convention-deviation.test.js test/resource-exhaustion.test.js test/resource-exhaustion-realworld.test.js test/redirect-toctou.test.js test/redirect-toctou-realworld.test.js test/codegen-sink.test.js test/ownership-authz.test.js test/ownership-authz-realworld.test.js test/t21-precision-audit.test.js test/secrets-coverage.test.js test/prompt-injection-payloads.test.js test/iac-cloud-templates.test.js test/ruby-path-join.test.js test/sast/manifest-line-tracking.test.js test/rust-sast.test.js test/java-deserialization-precision.test.js test/pipeline-env-secret.test.js test/mark-used-vuln-functions.test.js",
89
- "test:posture": "node --test test/contracts/root-cause-sweep.contract.test.js test/contracts/model-validation.contract.test.js test/contracts/agentic-discovery.contract.test.js test/contracts/local-models.contract.test.js test/contracts/dependency-finding.contract.test.js test/contracts/taint-flow.contract.test.js test/contracts/runtime-corroboration.contract.test.js test/compliance-coverage.test.js test/contracts/blast-radius.contract.test.js test/contracts/federation.contract.test.js test/contracts/eval-lab-report.contract.test.js test/contracts/fleet.contract.test.js test/contracts/rule-compiler.contract.test.js test/contracts/release-assurance.contract.test.js test/contracts/dnr.contract.test.js test/contracts/strategic-metrics.contract.test.js test/contracts/exploit-proof.contract.test.js test/contracts/artifact-provenance.contract.test.js test/dnr/artifact-join.test.js test/rule-pack-signing.test.js test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/api-contract-scope.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js test/load-factor.test.js test/fleet/job-store.test.js test/fleet/enterprise-graph.test.js test/fleet/discover-edges.test.js test/fleet/code-boundaries.test.js test/fleet/job-runner.test.js test/fleet/fleet-org-cli.test.js test/eval-lab/dataset-neutralize.test.js test/eval-lab/arms-score-stats.test.js test/eval-lab/run-report.test.js test/eval-lab/router.test.js test/eval-lab/arm-ollama.test.js test/eval-lab/import-independent.test.js test/cli/eval-lab-cli.test.js test/dnr/ir-and-outputs.test.js test/dnr/logs-and-hunt.test.js test/dnr/window-store-view.test.js test/dnr/exploit-correlation.test.js test/cli/dnr-cli.test.js test/proof-fabric/core.test.js test/proof-fabric/container.test.js test/proof-fabric/generate.test.js test/proof-fabric/siblings.test.js test/proof-fabric/generate-python.test.js test/proof-fabric/generate-haskell.test.js test/proof-fabric/generate-haskell-widened.test.js test/cli/proof-lifecycle-demo.test.js test/mcp-proof.test.js test/proof-fabric/fix-store-runner.test.js test/proof-fabric/fixtures.test.js test/cli/proof-fabric-cli.test.js test/rule-compiler/core.test.js test/rule-compiler/synthesis.test.js test/rule-compiler/structure.test.js test/cli/rule-compiler-cli.test.js test/fleet/fleet-graph.test.js test/fleet/fleet-manifest.test.js test/fleet/fleet-cli.test.js test/artifact/safe-tar.test.js test/artifact/image-archive.test.js test/artifact/filesystem.test.js test/artifact/layer-secrets.test.js test/artifact/inventory.test.js test/artifact/vuln-correlate.test.js test/artifact/signature.test.js test/artifact/provenance-graph.test.js test/artifact/mismatch.test.js test/artifact/vex.test.js test/artifact/registry.test.js test/artifact/sources-cosign-evidence.test.js test/cli/artifact-sources-cli.test.js test/cli/artifact-provenance-cli.test.js test/witness-bundle.test.js",
89
+ "test:posture": "node --test test/contracts/root-cause-sweep.contract.test.js test/contracts/model-validation.contract.test.js test/contracts/agentic-discovery.contract.test.js test/contracts/local-models.contract.test.js test/contracts/dependency-finding.contract.test.js test/contracts/taint-flow.contract.test.js test/contracts/runtime-corroboration.contract.test.js test/compliance-coverage.test.js test/contracts/blast-radius.contract.test.js test/contracts/federation.contract.test.js test/contracts/eval-lab-report.contract.test.js test/contracts/fleet.contract.test.js test/contracts/rule-compiler.contract.test.js test/contracts/release-assurance.contract.test.js test/contracts/dnr.contract.test.js test/contracts/strategic-metrics.contract.test.js test/contracts/exploit-proof.contract.test.js test/contracts/artifact-provenance.contract.test.js test/dnr/artifact-join.test.js test/rule-pack-signing.test.js test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-transport.test.js test/local-preset-callers.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/api-contract-scope.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js test/load-factor.test.js test/fleet/job-store.test.js test/fleet/enterprise-graph.test.js test/fleet/discover-edges.test.js test/fleet/code-boundaries.test.js test/fleet/job-runner.test.js test/fleet/accounting.test.js test/fleet/fleet-org-cli.test.js test/eval-lab/dataset-neutralize.test.js test/eval-lab/arms-score-stats.test.js test/eval-lab/run-report.test.js test/eval-lab/router.test.js test/eval-lab/arm-ollama.test.js test/eval-lab/import-independent.test.js test/cli/eval-lab-cli.test.js test/dnr/ir-and-outputs.test.js test/dnr/logs-and-hunt.test.js test/dnr/window-store-view.test.js test/dnr/exploit-correlation.test.js test/cli/dnr-cli.test.js test/proof-fabric/core.test.js test/proof-fabric/container.test.js test/proof-fabric/generate.test.js test/proof-fabric/siblings.test.js test/proof-fabric/generate-python.test.js test/proof-fabric/generate-haskell.test.js test/proof-fabric/generate-haskell-widened.test.js test/cli/proof-lifecycle-demo.test.js test/mcp-proof.test.js test/proof-fabric/fix-store-runner.test.js test/proof-fabric/fixtures.test.js test/cli/proof-fabric-cli.test.js test/rule-compiler/core.test.js test/rule-compiler/gates.test.js test/rule-compiler/synthesis.test.js test/rule-compiler/structure.test.js test/cli/rule-compiler-cli.test.js test/fleet/fleet-graph.test.js test/fleet/fleet-manifest.test.js test/fleet/fleet-cli.test.js test/artifact/safe-tar.test.js test/artifact/image-archive.test.js test/artifact/filesystem.test.js test/artifact/layer-secrets.test.js test/artifact/inventory.test.js test/artifact/vuln-correlate.test.js test/artifact/signature.test.js test/artifact/provenance-graph.test.js test/artifact/mismatch.test.js test/artifact/vex.test.js test/artifact/registry.test.js test/artifact/sources-cosign-evidence.test.js test/cli/artifact-sources-cli.test.js test/cli/artifact-provenance-cli.test.js test/witness-bundle.test.js",
90
90
  "test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-rust.test.js test/rust-taint-flow.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js test/dataflow-expr-taint-scaling.test.js test/evaluation/guard-dominance.test.js test/evaluation/engine-mechanisms.test.js test/rx-exec-receiver.test.js test/incremental-dependency.test.js test/incremental-integrity.test.js test/demand-query.test.js test/ir-cache.test.js test/canonicalize-parity.test.js test/dataflow-source-attribution.test.js",
91
91
  "test:mcp": "node --test test/mcp.test.js test/mcp-apply-fix-multifile.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js test/capabilities/tools.test.js",
92
92
  "test:report": "node --test test/report-scan-invariants.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/git-history-batch.test.js test/default-scan-drift.test.js test/bundle-optional-deps.test.js test/scan-health.test.js test/finding-schema.test.js test/pipeline/finding-schema-provenance.test.js test/evidence-provenance.test.js test/report/provenance-output.test.js test/report/provenance-format-parity.test.js test/legacy-compat.test.js test/annotator-runner.test.js test/detector-runner.test.js test/detector-errors.test.js test/analyzer-supervisor.test.js test/cascade-worker-pool.test.js test/cascade-pool-wiring.test.js test/coverage-ledger.test.js test/coverage-ledger-completeness.test.js test/assurance-mode.test.js test/artifact-registry.test.js test/artifact-registry-completeness.test.js test/retention-policy.test.js test/state-lifecycle-report.test.js test/legal-hold.test.js test/production-feedback.test.js test/calibration-feedback.test.js test/encryption-provider.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js test/oscal-conformance.test.js test/cli-fix-apply.test.js test/apply-fix-service.test.js test/high-impact-approval-gate.test.js test/approver-registry.test.js test/suppression-exceptions.test.js test/fix-history.test.js test/producer-registry.test.js test/producer-collector.test.js test/enrichment-completion.test.js",
@@ -133,7 +133,7 @@
133
133
  "verification:replay-example": "node ../scripts/verification-replay-example.mjs",
134
134
  "test:lifecycle": "node --test test/ci-hang-protection.test.js test/gate-select.test.js test/ci-test-shards.test.js test/pre-bash-guard.test.js test/lsp-protocol-smoke.test.js test/ide-surfaces.test.js test/detector-liveness.test.js test/bench-watchdog.test.js test/ci-parity.test.js test/dead-code.test.js test/no-dead-modules.test.js test/no-orphan-scripts.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js test/trust-boundary.test.js test/ci-templates.test.js test/agent-mcp-tools.test.js test/triage-command.test.js test/no-orphan-tests.test.js test/supply-command.test.js test/posture-command.test.js test/labs-command.test.js test/compliance-command.test.js test/check-doc-drift.test.js test/gate-warm.test.js test/docs-positioning.test.js test/dist-chunks-tracked.test.js test/ga-program.test.js test/workflow-confinement.test.js test/check-bundle-source.test.js test/ship.test.js test/release-prepare.test.js && node ../scripts/lint-command-descriptions.mjs",
135
135
  "test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
136
- "test:discovery": "node --test test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js test/context-intelligence/build-js.test.js test/context-intelligence/build-python-haskell.test.js test/context-intelligence/hie-adapter.test.js test/context-intelligence/store.test.js test/context-intelligence/review-queue.test.js test/context-intelligence/packer-egress.test.js test/context-intelligence/evaluation.test.js test/context-intelligence/queue-diff.test.js test/context-intelligence/cli.test.js test/context-intelligence/resilience.test.js test/context-intelligence/frameworks.test.js test/context-intelligence/skew-git-rls.test.js test/context-intelligence/premortem.test.js test/context-intelligence/git-diff-candidates.test.js",
136
+ "test:discovery": "node --test test/discovery-spec-synthesis.test.js test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js test/context-intelligence/build-js.test.js test/context-intelligence/build-python-haskell.test.js test/context-intelligence/hie-adapter.test.js test/context-intelligence/store.test.js test/context-intelligence/review-queue.test.js test/context-intelligence/packer-egress.test.js test/context-intelligence/evaluation.test.js test/context-intelligence/queue-diff.test.js test/context-intelligence/cli.test.js test/context-intelligence/resilience.test.js test/context-intelligence/frameworks.test.js test/context-intelligence/skew-git-rls.test.js test/context-intelligence/premortem.test.js test/context-intelligence/git-diff-candidates.test.js",
137
137
  "eval:independent": "node ../bench/independent-eval/runner.mjs",
138
138
  "eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
139
139
  "smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
package/dist/550.index.js CHANGED
@@ -287,8 +287,9 @@ function judgeCandidates(candidates, priorScan, triageFeedback) {
287
287
  /* harmony export */ });
288
288
  /* unused harmony exports defaultLlmInvoke, parseEndpoints, consensusOf, resolveLlmInvoke */
289
289
  /* harmony import */ var _egress_policy_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(45712);
290
- /* harmony import */ var _llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(38947);
291
- /* harmony import */ var _llm_validator_ollama_provider_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(23837);
290
+ /* harmony import */ var _egress_transport_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73785);
291
+ /* harmony import */ var _llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(38947);
292
+ /* harmony import */ var _llm_validator_ollama_provider_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(23837);
292
293
  //
293
294
  // Shared LLM endpoint caller. Both the hunter and the refutation panel need
294
295
  // the same default endpoint caller when tests don't inject a mock. Two copies
@@ -300,6 +301,7 @@ function judgeCandidates(candidates, priorScan, triageFeedback) {
300
301
 
301
302
 
302
303
 
304
+
303
305
  const DEFAULT_TIMEOUT_MS = 60000;
304
306
 
305
307
  // agentic-security-ollama-offline-prd.md §32 — hunt is one of the highest-
@@ -326,7 +328,7 @@ async function defaultLlmInvoke(prompt, opts = {}) {
326
328
  const timeoutMs = Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : DEFAULT_TIMEOUT_MS;
327
329
 
328
330
  if (!opts.endpoint) {
329
- const resolved = (0,_llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_1__.resolveProvider)({ role: opts.role || 'hunt' });
331
+ const resolved = (0,_llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_2__.resolveProvider)({ role: opts.role || 'hunt' });
330
332
  // A REFUSAL (a preset was explicitly configured and declined — e.g. a
331
333
  // non-loopback Ollama host, or `local`'s own non-loopback refusal) must
332
334
  // propagate here, not silently fall through to the legacy raw-endpoint
@@ -338,7 +340,7 @@ async function defaultLlmInvoke(prompt, opts = {}) {
338
340
  if (!resolved.ok && resolved.reason) throw new Error(resolved.reason);
339
341
  if (resolved.ok && resolved.config.provider === 'ollama') {
340
342
  const oc = resolved.config.ollama;
341
- const r = await (0,_llm_validator_ollama_provider_js__WEBPACK_IMPORTED_MODULE_2__/* .callOllamaChat */ .L5)({
343
+ const r = await (0,_llm_validator_ollama_provider_js__WEBPACK_IMPORTED_MODULE_3__/* .callOllamaChat */ .L5)({
342
344
  host: resolved.config.endpoint,
343
345
  model: resolved.config.model,
344
346
  messages: [{ role: 'user', content: prompt }],
@@ -365,12 +367,11 @@ async function defaultLlmInvoke(prompt, opts = {}) {
365
367
  // Absent, it falls back to the single configured endpoint — so the ordinary
366
368
  // single-model path is byte-identical to what it was before consensus existed.
367
369
  const endpoint = opts.endpoint || process.env.AGENTIC_SECURITY_LLM_ENDPOINT;
368
- const res = await fetch(endpoint, { // agentic-security-ignore: CWE-918
370
+ const res = await (0,_egress_transport_js__WEBPACK_IMPORTED_MODULE_1__/* .modelFetch */ .Ye)(endpoint, {
369
371
  method: 'POST',
370
372
  headers: { 'content-type': 'application/json' },
371
373
  body: JSON.stringify({ prompt }),
372
- signal: AbortSignal.timeout(timeoutMs),
373
- });
374
+ }, { timeoutMs });
374
375
  if (!res.ok) throw new Error(`llm endpoint returned ${res.status}`);
375
376
  const body = await res.json();
376
377
  return typeof body === 'string' ? body : (body?.text ?? JSON.stringify(body));
@@ -513,7 +514,7 @@ function resolveLlmInvokeWithDecision(opts = {}) {
513
514
  // exist, per the block comment above). Checked BEFORE the legacy
514
515
  // raw-endpoint fallback below, matching providers.js's own precedence
515
516
  // (ollama/local checked before a bare BYO endpoint).
516
- const resolved = (0,_llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_1__.resolveProvider)({ role: opts.role || 'hunt' });
517
+ const resolved = (0,_llm_validator_providers_js__WEBPACK_IMPORTED_MODULE_2__.resolveProvider)({ role: opts.role || 'hunt' });
517
518
  if (!resolved.ok && resolved.reason) {
518
519
  // A REFUSAL (non-loopback ollama/local, explicitly configured and
519
520
  // declined) is itself a policy decision — same shape as an egress
@@ -674,9 +674,7 @@ function runOfJob(store, id) {
674
674
  async function finishRun({ args, root, store, runId, manifest, manifestFile, interrupted }) {
675
675
  const { artifacts, problems } = (0,_job_runner_js__WEBPACK_IMPORTED_MODULE_8__/* .loadArtifacts */ .kZ)({ store, runId, root });
676
676
  const jobs = store.listJobs(runId);
677
- const health = (0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .computeOrgHealth */ .WB)({ manifest, jobs });
678
- for (const p of problems) { const r = health.repositories.find((x) => x.id === p.repoId); if (r) { r.state = 'failed'; r.error = p.problem; } }
679
- if (problems.length) { health.counts.complete -= problems.filter((p) => health.repositories.find((x) => x.id === p.repoId)).length; health.counts.failed += problems.length; if (health.assurance !== 'red' && manifest.repositories.some((r) => r.required && problems.some((p) => p.repoId === r.id))) health.assurance = 'red'; health.reasons.push(`${problems.length} result(s) could not be verified: ${problems.map((p) => `${p.repoId} (${p.problem})`).join(', ')}`); }
677
+ const health = (0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .applyArtifactProblems */ .Dh)((0,_org_health_js__WEBPACK_IMPORTED_MODULE_9__/* .computeOrgHealth */ .WB)({ manifest, jobs }), manifest, problems);
680
678
 
681
679
  let previous = null;
682
680
  const rollupFile = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, ROLLUP_FILE);
@@ -1433,7 +1431,7 @@ function blastRadius(graph, repoId) {
1433
1431
 
1434
1432
 
1435
1433
 
1436
- const FLEET_MANIFEST_SCHEMA_KEYS = Object.freeze(['organization', 'repositories', 'workers', 'perRepoTimeout', 'retries', 'resources']);
1434
+ const FLEET_MANIFEST_SCHEMA_KEYS = Object.freeze(['organization', 'repositories', 'workers', 'perRepoTimeout', 'retries', 'resources', 'cacheMaxAge']);
1437
1435
  const REPO_KEYS = Object.freeze(['id', 'path', 'required', 'kind', 'internetFacing', 'environment', 'dataClasses', 'trustZone', 'calls', 'produces', 'consumes', 'reads', 'writes', 'providers', 'aliases']);
1438
1436
  const RESOURCE_KEYS = Object.freeze(['id', 'kind', 'dataClasses', 'trustZone', 'internetFacing', 'environment', 'aliases']);
1439
1437
  const REPO_KINDS = Object.freeze(['service', 'library', 'frontend', 'worker', 'gateway']);
@@ -1512,12 +1510,15 @@ function validateFleetManifest(doc, { baseDir = '.' } = {}) {
1512
1510
  if (doc.workers != null && !(Number.isInteger(doc.workers) && doc.workers >= 1 && doc.workers <= 32)) errors.push('workers must be an integer in 1..32');
1513
1511
  if (doc.retries != null && !(Number.isInteger(doc.retries) && doc.retries >= 0 && doc.retries <= 5)) errors.push('retries must be an integer in 0..5');
1514
1512
  if (doc.perRepoTimeout != null && parseDuration(doc.perRepoTimeout, null) == null) errors.push(`perRepoTimeout is not a valid duration: ${doc.perRepoTimeout}`);
1513
+ if (doc.cacheMaxAge != null && parseDuration(doc.cacheMaxAge, null) == null) errors.push(`cacheMaxAge is not a valid duration: ${doc.cacheMaxAge}`);
1515
1514
  if (errors.length) return { ok: false, errors };
1516
1515
  return {
1517
1516
  ok: true, errors: [],
1518
1517
  manifest: {
1519
1518
  organization: doc.organization, repositories: repos, resources,
1520
1519
  workers: doc.workers ?? 4, retries: doc.retries ?? 1, perRepoTimeoutMs: parseDuration(doc.perRepoTimeout, 20 * 60000),
1520
+ // GA-09: a cached result older than this is stale and is not reused (the feeds it was enriched with have moved on); 7 days by default
1521
+ cacheMaxAgeMs: parseDuration(doc.cacheMaxAge, 7 * 24 * 3600000),
1521
1522
  },
1522
1523
  };
1523
1524
  }
@@ -1554,7 +1555,7 @@ __webpack_require__.a(__webpack_module__, async (__webpack_handle_async_dependen
1554
1555
  /* harmony export */ nY: () => (/* binding */ planRun),
1555
1556
  /* harmony export */ zD: () => (/* binding */ reclaimDeadWorkers)
1556
1557
  /* harmony export */ });
1557
- /* unused harmony export DEFAULTS */
1558
+ /* unused harmony exports DEFAULTS, DEFAULT_CACHE_MAX_AGE_MS, cacheFreshness */
1558
1559
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
1559
1560
  /* harmony import */ var node_os__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(48161);
1560
1561
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
@@ -1637,6 +1638,21 @@ function planRun({ manifest, manifestHash, store, runId = null, config = {}, now
1637
1638
  * Run (or continue) a planned run until no job is queued or leased by a live worker. Returns when the work is done, the signal
1638
1639
  * aborts, or `maxWaitMs` passes with only other workers' leases outstanding.
1639
1640
  */
1641
+ const DEFAULT_CACHE_MAX_AGE_MS = 7 * 24 * 3600000;
1642
+
1643
+ /**
1644
+ * Is a cache hit fresh enough to reuse (GA-09)? A record with no readable `cachedAt` (written before 0.195.23) is never fresh, and a
1645
+ * record older than `maxAgeMs` (the manifest's `cacheMaxAge`, 7 days by default) is stale. Returns `{ fresh, cacheAgeMs }`.
1646
+ */
1647
+ function cacheFreshness(hit, maxAgeMs, nowMs) {
1648
+ if (!hit) return { fresh: false, cacheAgeMs: null };
1649
+ const t = typeof hit.cachedAt === 'string' ? Date.parse(hit.cachedAt) : NaN;
1650
+ if (Number.isNaN(t)) return { fresh: false, cacheAgeMs: null };
1651
+ const cacheAgeMs = Math.max(0, nowMs - t);
1652
+ const max = Number.isFinite(maxAgeMs) && maxAgeMs > 0 ? maxAgeMs : DEFAULT_CACHE_MAX_AGE_MS;
1653
+ return { fresh: cacheAgeMs <= max, cacheAgeMs };
1654
+ }
1655
+
1640
1656
  async function runJobs({ manifest, store, runId, root, scan = childScan, scanOptions = {}, workers = manifest.workers, leaseMs = DEFAULTS.leaseMs, heartbeatMs = DEFAULTS.heartbeatMs, retryDelayMs = DEFAULTS.retryDelayMs, pollMs = DEFAULTS.pollMs, maxWaitMs = null, signal, now = () => Date.now(), onEvent = () => {}, cacheDir = null, artifactDir = null, useCache = true }) {
1641
1657
  const repoById = new Map(manifest.repositories.map((r) => [r.id, r]));
1642
1658
  const cacheD = cacheDir ?? (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_6__.statePath)(root, 'fleet', 'cache');
@@ -1646,7 +1662,11 @@ async function runJobs({ manifest, store, runId, root, scan = childScan, scanOpt
1646
1662
  const writeArtifact = (job, payload) => {
1647
1663
  const text = JSON.stringify({ schema: 'agentic-security/fleet-scan-artifact', version: 1, runId, repoId: job.repoId, epoch: job.epoch, commit: job.commit, ...payload });
1648
1664
  const file = node_path__WEBPACK_IMPORTED_MODULE_2__.join(artD, `${job.repoId}.${job.epoch}.json`);
1649
- node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(file, text, { mode: 0o600 });
1665
+ // written whole or not at all (GA-09): a worker killed mid-write leaves no half artifact for the hash check to find, only a
1666
+ // stray temp file, and the job stays leased until the lease expires and another worker redoes it
1667
+ const tmp = `${file}.${process.pid}.tmp`;
1668
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(tmp, text, { mode: 0o600 });
1669
+ node_fs__WEBPACK_IMPORTED_MODULE_0__.renameSync(tmp, file);
1650
1670
  return { file, sha256: sha(text) };
1651
1671
  };
1652
1672
 
@@ -1668,11 +1688,16 @@ async function runJobs({ manifest, store, runId, root, scan = childScan, scanOpt
1668
1688
  idleSince = null;
1669
1689
  const repo = repoById.get(job.repoId);
1670
1690
  onEvent({ type: 'start', repo: job.repoId, attempt: job.attempts });
1671
- const cached = useCache ? (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .readCache */ .xe)(cacheD, job.cacheKey) : null;
1691
+ const hit = useCache ? (0,_scan_cache_js__WEBPACK_IMPORTED_MODULE_7__/* .readCache */ .xe)(cacheD, job.cacheKey) : null;
1692
+ // GA-09: a hit is reused only while it is fresh. A result with no recorded time (written before 0.195.23) or older than the
1693
+ // manifest's `cacheMaxAge` is stale: it is scanned again, never folded into a green organization as if it were current.
1694
+ const { fresh, cacheAgeMs } = cacheFreshness(hit, manifest.cacheMaxAgeMs, now());
1695
+ const cached = fresh ? hit : null;
1696
+ if (hit && !cached) onEvent({ type: 'cache-stale', repo: job.repoId, cacheAgeMs });
1672
1697
  if (cached) {
1673
- const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, lineage: cached.lineage ?? null, cached: true });
1698
+ const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, lineage: cached.lineage ?? null, cached: true, cachedAt: cached.cachedAt, cacheAgeMs });
1674
1699
  const status = cached.entry?.scanHealth && cached.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
1675
- if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: cached.entry?.total ?? null, cached: true, artifact: node_path__WEBPACK_IMPORTED_MODULE_2__.basename(a.file) }, now: now() })) onEvent({ type: 'done', repo: job.repoId, cached: true, status });
1700
+ if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: cached.entry?.total ?? null, cached: true, cacheAgeMs, artifact: node_path__WEBPACK_IMPORTED_MODULE_2__.basename(a.file) }, now: now() })) onEvent({ type: 'done', repo: job.repoId, cached: true, status });
1676
1701
  continue;
1677
1702
  }
1678
1703
  const ac = new AbortController();
@@ -1946,6 +1971,7 @@ function summarizeLineage(graph) {
1946
1971
  /***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
1947
1972
 
1948
1973
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
1974
+ /* harmony export */ Dh: () => (/* binding */ applyArtifactProblems),
1949
1975
  /* harmony export */ Vp: () => (/* binding */ renderOrgHealth),
1950
1976
  /* harmony export */ WB: () => (/* binding */ computeOrgHealth)
1951
1977
  /* harmony export */ });
@@ -1998,6 +2024,29 @@ function computeOrgHealth({ manifest, jobs }) {
1998
2024
  return { counts, assurance, reasons, repositories: repos };
1999
2025
  }
2000
2026
 
2027
+ /**
2028
+ * Fold artifact problems (a result whose file is missing or does not match its recorded hash) into the health (GA-09). The repository
2029
+ * becomes `failed`; a required one turns the organization red, an optional one makes it degraded (it used to stay green); the counts
2030
+ * move from whatever state the repository had, not always from `complete`. Returns the same object, changed in place.
2031
+ */
2032
+ function applyArtifactProblems(health, manifest, problems) {
2033
+ if (!Array.isArray(problems) || !problems.length) return health;
2034
+ const stateKey = { complete: 'complete', partial: 'partial', failed: 'failed', 'timed-out': 'timedOut', cancelled: 'cancelled', 'not-run': 'notRun' };
2035
+ let requiredHit = false; let optionalHit = false;
2036
+ for (const p of problems) {
2037
+ const r = health.repositories.find((x) => x.id === p.repoId);
2038
+ if (!r) continue;
2039
+ if (r.state !== 'failed') { health.counts[stateKey[r.state]]--; health.counts.failed++; }
2040
+ r.state = 'failed'; r.error = p.problem;
2041
+ const required = manifest.repositories.some((m) => m.id === p.repoId && m.required);
2042
+ if (required) requiredHit = true; else optionalHit = true;
2043
+ }
2044
+ if (requiredHit) health.assurance = 'red';
2045
+ else if (optionalHit && health.assurance === 'green') health.assurance = 'degraded';
2046
+ health.reasons.push(`${problems.length} result(s) could not be verified: ${problems.map((p) => `${p.repoId} (${p.problem})`).join(', ')}`);
2047
+ return health;
2048
+ }
2049
+
2001
2050
  const n = (x) => x.toLocaleString('en-US');
2002
2051
  function renderOrgHealth(h) {
2003
2052
  const c = h.counts;
@@ -2131,7 +2180,7 @@ function writeCache(dir, key, payload) {
2131
2180
  if (!key || !/^[0-9a-f]{64}$/.test(key)) return false;
2132
2181
  try {
2133
2182
  node_fs__WEBPACK_IMPORTED_MODULE_0__.mkdirSync(dir, { recursive: true, mode: 0o700 });
2134
- const body = { schema: 'agentic-security/fleet-scan-cache', version: 1, key, ...payload };
2183
+ const body = { schema: 'agentic-security/fleet-scan-cache', version: 1, key, cachedAt: new Date().toISOString(), ...payload };
2135
2184
  const doc = { ...body, digest: sha(JSON.stringify(body)) };
2136
2185
  const tmp = node_path__WEBPACK_IMPORTED_MODULE_1__.join(dir, `.${key}.${process.pid}.tmp`);
2137
2186
  node_fs__WEBPACK_IMPORTED_MODULE_0__.writeFileSync(tmp, JSON.stringify(doc), { mode: 0o600 });
@@ -82,10 +82,12 @@ __webpack_require__.d(__webpack_exports__, {
82
82
 
83
83
  // EXTERNAL MODULE: external "node:fs"
84
84
  var external_node_fs_ = __webpack_require__(73024);
85
- // EXTERNAL MODULE: ./src/posture/state-dir.js
86
- var state_dir = __webpack_require__(31174);
87
85
  // EXTERNAL MODULE: external "node:path"
88
86
  var external_node_path_ = __webpack_require__(76760);
87
+ // EXTERNAL MODULE: external "node:child_process"
88
+ var external_node_child_process_ = __webpack_require__(31421);
89
+ // EXTERNAL MODULE: ./src/posture/state-dir.js
90
+ var state_dir = __webpack_require__(31174);
89
91
  // EXTERNAL MODULE: external "node:crypto"
90
92
  var external_node_crypto_ = __webpack_require__(77598);
91
93
  // EXTERNAL MODULE: ./src/posture/rule-compiler/ir.js
@@ -149,6 +151,8 @@ function verifyLedger(ledger) {
149
151
  function historyFor(ledger, ruleId) { return ledger.entries.filter((e) => e.ruleId === ruleId); }
150
152
  function stateOf(ledger, ruleId) { return currentState(ledger, ruleId); }
151
153
 
154
+ // EXTERNAL MODULE: ./src/posture/integrity.js
155
+ var integrity = __webpack_require__(71130);
152
156
  // EXTERNAL MODULE: ./src/posture/rule-pack-signing.js
153
157
  var rule_pack_signing = __webpack_require__(99527);
154
158
  ;// CONCATENATED MODULE: ./src/posture/rule-compiler/store.js
@@ -164,25 +168,45 @@ var rule_pack_signing = __webpack_require__(99527);
164
168
 
165
169
 
166
170
 
171
+
167
172
  const DIR = 'rule-compiler';
168
173
  const EXT = { javascript: 'js', typescript: 'ts', python: 'py', go: 'go', ruby: 'rb', java: 'java', rust: 'rs', csharp: 'cs', php: 'php', any: 'txt' };
169
174
 
170
175
  function ruleDir(scanRoot, id) { return (0,state_dir.statePath)(scanRoot, DIR, id); }
171
176
 
172
- function loadLedger(scanRoot) {
177
+ /**
178
+ * The lifecycle ledger is hash-chained (lifecycle.js) AND signed with the per-install HMAC key (the same key as last-scan.json),
179
+ * since 0.195.23 (GA-07): the chain alone could be recomputed by anyone who could edit the file. A ledger that fails either check,
180
+ * or that cannot be parsed, is returned WITH `tampered: true` and its reason, and `saveLedger` refuses to write it, so a bad ledger
181
+ * is never quietly replaced by a fresh one and its history lost. An unsigned ledger with entries (written before 0.195.23) is
182
+ * accepted only with AGENTIC_SECURITY_RULES_UNSIGNED=1, the same switch that accepts an unsigned rules override, and is signed on
183
+ * its next save.
184
+ */
185
+ function loadLedger(scanRoot, env = process.env) {
173
186
  const f = (0,state_dir.statePath)(scanRoot, DIR, 'ledger.json');
174
187
  if (!external_node_fs_.existsSync(f)) return newLedger();
175
- try {
176
- const ledger = JSON.parse(external_node_fs_.readFileSync(f, 'utf8'));
177
- const v = verifyLedger(ledger);
178
- return v.ok ? ledger : { ...newLedger(), tampered: true, tamperReason: v.reason };
179
- } catch { return newLedger(); }
188
+ let text;
189
+ try { text = external_node_fs_.readFileSync(f, 'utf8'); } catch (e) { return { ...newLedger(), tampered: true, tamperReason: `cannot read the ledger: ${e.message}` }; }
190
+ let ledger;
191
+ try { ledger = JSON.parse(text); } catch (e) { return { ...newLedger(), tampered: true, tamperReason: `the ledger is not valid JSON: ${e.message}` }; }
192
+ if (!ledger || !Array.isArray(ledger.entries)) return { ...newLedger(), tampered: true, tamperReason: 'the ledger has no entries array' };
193
+ const v = verifyLedger(ledger);
194
+ if (!v.ok) return { ...ledger, tampered: true, tamperReason: v.reason };
195
+ const sig = (0,integrity/* verifyLastScan */.Ef)(text, `${f}.sig`);
196
+ if (sig === false) return { ...ledger, tampered: true, tamperReason: 'the ledger signature does not verify under this install key' };
197
+ if (sig === null && ledger.entries.length && env.AGENTIC_SECURITY_RULES_UNSIGNED !== '1') {
198
+ return { ...ledger, tampered: true, tamperReason: 'the ledger is unsigned (written before 0.195.23 or by another tool); set AGENTIC_SECURITY_RULES_UNSIGNED=1 once to accept and re-sign it' };
199
+ }
200
+ return ledger;
180
201
  }
181
202
 
182
203
  function saveLedger(scanRoot, ledger) {
204
+ if (ledger?.tampered) return { ok: false, reason: `refusing to overwrite a ledger that failed its integrity check (${ledger.tamperReason})` };
183
205
  if (!(0,state_dir.ensureStateDir)(scanRoot)) return { ok: false, reason: 'no safe state directory for this project' };
184
206
  const f = (0,state_dir.statePath)(scanRoot, DIR, 'ledger.json');
185
- return { ok: (0,state_dir.safeWriteState)(f, JSON.stringify(ledger, null, 2)) };
207
+ const text = JSON.stringify(ledger, null, 2);
208
+ const ok = (0,state_dir.safeWriteState)(f, text) && (0,state_dir.safeWriteState)(`${f}.sig`, (0,integrity/* signLastScan */.lU)(text));
209
+ return { ok };
186
210
  }
187
211
 
188
212
  function saveIR(scanRoot, ir) {
@@ -455,6 +479,8 @@ var reproducer = __webpack_require__(37055);
455
479
 
456
480
 
457
481
 
482
+
483
+
458
484
  function loadFinding(root, id) {
459
485
  const p = (0,state_dir.statePath)(root, 'last-scan.json');
460
486
  if (!external_node_fs_.existsSync(p)) return null;
@@ -469,6 +495,12 @@ function readJsonArg(pathOrInline) {
469
495
 
470
496
  async function cmdRules(args, root) {
471
497
  const sub = args._[1];
498
+ // GA-07: a ledger that failed its integrity check (broken chain, bad or missing signature) refuses EVERY command, not only the
499
+ // ones that change state; nothing below can read a tampered history as fact or write over it.
500
+ if (['synthesize', 'evaluate', 'promote', 'status', 'rollback', 'cost', 'regress'].includes(sub)) {
501
+ const guard = loadLedger(root);
502
+ if (guard.tampered) { console.error(`Refusing: rule lifecycle ledger failed its integrity check (${guard.tamperReason}).`); return 4; }
503
+ }
472
504
  if (sub === 'synthesize') return cmdSynthesize(args, root);
473
505
  if (sub === 'evaluate') return cmdEvaluate(args, root);
474
506
  if (sub === 'promote') return cmdPromote(args, root);
@@ -476,7 +508,7 @@ async function cmdRules(args, root) {
476
508
  if (sub === 'rollback') return cmdRollback(args, root);
477
509
  if (sub === 'cost') return cmdCost(args, root);
478
510
  if (sub === 'regress') return cmdRegress(args, root);
479
- console.error('usage: rules synthesize <finding-id> --spec <file.json> [--auto] [--max-attempts N] | evaluate <rule-id> | promote <rule-id> | status [rule-id] | rollback <rule-id> | cost <rule-id>');
511
+ console.error('usage: rules synthesize <finding-id> --spec <file.json> [--auto] [--max-attempts N] | evaluate <rule-id> | promote <rule-id> --reviewed-by <person> [--scope <text>] | status [rule-id] | rollback <rule-id> | cost <rule-id> | regress [rule-id]');
480
512
  return 4;
481
513
  }
482
514
 
@@ -495,7 +527,7 @@ async function cmdSynthesize(args, root) {
495
527
  // The candidate pattern is the model's or the person's, when they gave one. Without one, or with --auto, the synthesis loop proposes
496
528
  // from the vulnerable and patched code, scores each attempt on the real corpus and its mutations, and revises (see proposal.js).
497
529
  let finalSpec = spec; let synthesis = null;
498
- const corpusSpec = { originalCode: spec.originalCode, patchedCode: spec.patchedCode ?? null, nearbyCleanCode: spec.nearbyCleanCode ?? [], protectedVariantsCode: spec.protectedVariantsCode ?? [] };
530
+ const corpusSpec = { originalCode: spec.originalCode, patchedCode: spec.patchedCode ?? null, nearbyCleanCode: spec.nearbyCleanCode ?? [], protectedVariantsCode: spec.protectedVariantsCode ?? [], independentVulnerableCode: spec.independentVulnerableCode ?? [], independentCleanCode: spec.independentCleanCode ?? [] };
499
531
  const leak = (0,promotion/* checkLeakage */.r_)(JSON.stringify(corpusSpec), { benchmarkNames: args.flags['benchmark-names'] ? String(args.flags['benchmark-names']).split(',') : [], fileNames: spec.fileNames ?? [] });
500
532
  if (!leak.ok) { console.error(`Refusing: the material for this rule shows benchmark or answer-key signals (${leak.offendingTerms.join('; ')}). Nothing was written.`); return 4; }
501
533
  if (args.flags.auto || (!spec.match && !spec.sink)) {
@@ -554,7 +586,7 @@ async function cmdEvaluate(args, root) {
554
586
  originatingFinding: ir.provenance.originatingFindingId, originatingCommit: ir.provenance.originatingCommit, evidenceState: ir.provenance.evidenceState,
555
587
  generatingModel: ir.provenance.model, promptConfigVersion: ir.provenance.promptConfigVersion,
556
588
  positiveCorpusHash: corpusHash(corpus.positives), negativeCorpusHash: corpusHash(corpus.negatives),
557
- benchmarkResults: { precision: gate.evaluation.score.precision, recall: gate.evaluation.score.recall, tp: gate.evaluation.score.tp, fp: gate.evaluation.score.fp, fn: gate.evaluation.score.fn, tn: gate.evaluation.score.tn, protectedFailures: gate.evaluation.score.protectedFailures, thresholds: gate.evaluation.thresholds, gate: gate.pass ? 'passed' : 'failed', reasons: gate.reasons },
589
+ benchmarkResults: { precision: gate.evaluation.score.precision, recall: gate.evaluation.score.recall, tp: gate.evaluation.score.tp, fp: gate.evaluation.score.fp, fn: gate.evaluation.score.fn, tn: gate.evaluation.score.tn, protectedFailures: gate.evaluation.score.protectedFailures, independent: gate.independent, thresholds: gate.evaluation.thresholds, gate: gate.pass ? 'passed' : 'failed', reasons: gate.reasons },
558
590
  leakageCheck: leakage, mutatorCoverage: (0,mutation/* mutatorCoverage */.xv)(corpusSpec.originalCode), discoveryCost: corpusSpec.discoveryCost ?? prev?.discoveryCost ?? null,
559
591
  promotionEvent: prev?.promotionEvent ?? null, evaluatedAt: new Date().toISOString(),
560
592
  });
@@ -570,23 +602,63 @@ async function cmdEvaluate(args, root) {
570
602
  function isStr(v) { return typeof v === 'string' && v.length > 0; }
571
603
  const fmtPct = (n) => (n == null ? 'unmeasured' : `${(n * 100).toFixed(1)}%`);
572
604
 
605
+ /** Does this commit exist in the project's history? `true`, `false`, or `null` when the project is not a git repository. */
606
+ function commitExists(root, sha) {
607
+ if (!external_node_fs_.existsSync(external_node_path_.join(root, '.git'))) return null;
608
+ try { (0,external_node_child_process_.execFileSync)('git', ['cat-file', '-e', `${sha}^{commit}`], { cwd: root, stdio: 'ignore' }); return true; } catch { return false; }
609
+ }
610
+
611
+ const NOT_A_REVIEWER = /^(?:agent|model|auto|bot|llm|ai)$/i;
612
+
613
+ // Promotion is a reviewed, human decision (GA-07). It needs the reviewer's name, a rule that is exactly the one that was evaluated, a
614
+ // provenance that checks out, and no conflict with a rule that already exists; a model's recommendation on its own promotes nothing.
573
615
  async function cmdPromote(args, root) {
574
616
  const id = args._[2];
575
- if (!id) { console.error('usage: rules promote <rule-id>'); return 4; }
617
+ if (!id) { console.error('usage: rules promote <rule-id> --reviewed-by <person> [--scope <text>] [--reason <text>]'); return 4; }
618
+ const reviewer = args.flags['reviewed-by'];
619
+ if (!isStr(reviewer) || NOT_A_REVIEWER.test(reviewer.trim())) {
620
+ console.error('usage: rules promote <rule-id> --reviewed-by <person>. A promotion records the person who reviewed the rule and its scope; a model or agent recommendation is not a review, so nothing is promoted without a named reviewer.');
621
+ return 4;
622
+ }
576
623
  const ledger = loadLedger(root);
577
624
  if (ledger.tampered) { console.error(`Refusing: rule lifecycle ledger failed its integrity check (${ledger.tamperReason}).`); return 4; }
578
625
  if (stateOf(ledger, id) !== 'VALIDATED') { console.error(`Rule ${id} is not VALIDATED (run rules evaluate first); refusing to promote.`); return 4; }
579
626
  const ir = loadIR(root, id);
580
627
  if (!ir) { console.error(`No rule IR found for ${id}.`); return 4; }
628
+ // the rule promoted must be the rule evaluated: an IR edited after `rules evaluate` has no evidence
629
+ const prov = loadProvenance(root, id);
630
+ if (!prov) { console.error(`Promotion refused: no evaluation record for ${id}; run rules evaluate first.`); return 4; }
631
+ if (prov.ruleHash !== (0,rule_compiler_ir/* ruleIRDigest */.GE)(ir)) { console.error(`Promotion refused: the rule ${id} changed after it was evaluated (evaluated ${String(prov.ruleHash).slice(0, 12)}, now ${(0,rule_compiler_ir/* ruleIRDigest */.GE)(ir).slice(0, 12)}); run rules evaluate again.`); return 4; }
632
+ // a named originating commit must be real history, not a typed-in string
633
+ const commit = ir.provenance?.originatingCommit;
634
+ if (commit) {
635
+ const exists = commitExists(root, commit);
636
+ if (exists === false) { console.error(`Promotion refused: originating commit ${commit} is not in this repository's history (invalid provenance).`); return 4; }
637
+ if (exists === null) { console.error(`Promotion refused: originating commit ${commit} cannot be verified because this project is not a git repository.`); return 4; }
638
+ }
639
+ // conflicts: a hand-written rule file with the same id, or a promoted rule with the identical match
640
+ const live = (0,state_dir.statePath)(root, 'rules', `${id}.yml`);
641
+ let firstLine = null; // null: no file there (or unreadable, which is treated as absent below only when the read fails with ENOENT)
642
+ try { firstLine = external_node_fs_.readFileSync(live, 'utf8').split('\n')[0]; } catch (e) { if (e.code !== 'ENOENT') firstLine = ''; }
643
+ if (firstLine !== null && !firstLine.startsWith('# Compiled by the rule compiler')) { console.error(`Promotion refused: a rule file that the compiler did not write already exists at ${live}; choose another rule id or remove that file deliberately.`); return 4; }
644
+ for (const other of promotedRuleIds(ledger)) {
645
+ if (other === id) continue;
646
+ const oir = loadIR(root, other);
647
+ if (oir && JSON.stringify(oir.match) === JSON.stringify(ir.match) && JSON.stringify(oir.languages) === JSON.stringify(ir.languages)) {
648
+ console.error(`Promotion refused: ${id} duplicates the promoted rule ${other} (identical match on the same languages); roll one back or narrow this one.`);
649
+ return 4;
650
+ }
651
+ }
581
652
  // other rules may have been promoted since this one was evaluated: hold it to the false-positive corpus as it is NOW
582
653
  const fpNow = falsePositiveHits(ir, sharedFalsePositiveCorpus(root, ledger, id));
583
654
  if (fpNow.length) { console.error(`Promotion refused: ${id} fires on ${fpNow.length} item(s) of the shared false-positive corpus (${fpNow.slice(0, 3).map((h) => h.label).join(', ')}). Re-evaluate or narrow the rule.`); return 1; }
584
655
  const pub = publishCustomRule(root, ir);
585
656
  if (!pub.ok) { console.error(`Promotion refused: ${pub.reason}`); return 4; }
586
- const t = recordTransition(ledger, { ruleId: id, from: 'VALIDATED', to: 'PROMOTED', reason: args.flags.reason || 'promoted', actor: args.flags.actor || 'operator' });
657
+ const scope = isStr(args.flags.scope) ? args.flags.scope : null;
658
+ const review = { reviewer: reviewer.trim(), scope, independent: prov.benchmarkResults?.independent ?? null };
659
+ const t = recordTransition(ledger, { ruleId: id, from: 'VALIDATED', to: 'PROMOTED', reason: args.flags.reason || 'promoted', evidence: review, actor: args.flags.actor || reviewer.trim() });
587
660
  saveLedger(root, ledger);
588
- const prov = loadProvenance(root, id);
589
- if (prov) saveProvenance(root, id, { ...prov, promotionEvent: { at: new Date().toISOString(), actor: args.flags.actor || 'operator', reason: args.flags.reason || 'promoted', ruleHash: (0,rule_compiler_ir/* ruleIRDigest */.GE)(ir) } });
661
+ saveProvenance(root, id, { ...prov, promotionEvent: { at: new Date().toISOString(), actor: args.flags.actor || reviewer.trim(), reviewer: reviewer.trim(), scope, reason: args.flags.reason || 'promoted', ruleHash: (0,rule_compiler_ir/* ruleIRDigest */.GE)(ir) } });
590
662
  console.log(`Rule ${id}: VALIDATED -> PROMOTED. Published to ${pub.file}.`);
591
663
  // Publishing is not trusting. Rule packs are signature-checked so a rule dropped into a repository cannot run in someone's CI; sign this
592
664
  // one with the operator's key when there is one, and say plainly whether the scanner will load it.