@clear-capabilities/agentic-security-scanner 0.176.0 → 0.177.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 (33) hide show
  1. package/CHANGELOG.md +9 -0
  2. package/bin/agentic-security.js +9 -2
  3. package/dist/1919.index.js +1 -1
  4. package/dist/2066.index.js +2 -1
  5. package/dist/2584.index.js +19 -1
  6. package/dist/{4978bec8fdf5c733dc85.json → 37d64df006576248d0d8.json} +5 -2
  7. package/dist/5456.index.js +291 -0
  8. package/dist/5620.index.js +147 -23
  9. package/dist/5937.index.js +672 -0
  10. package/dist/7838.index.js +70 -25
  11. package/dist/agentic-security.mjs +2 -2
  12. package/dist/agentic-security.mjs.sha256 +1 -1
  13. package/dist/{df1e8683e7eda7b2b79c.js → b4289e728659450b5e19.js} +4 -1
  14. package/package.json +5 -2
  15. package/src/posture/custom-rules.js +54 -8
  16. package/src/posture/dnr/exploit-correlation.js +25 -0
  17. package/src/posture/evaluation/lab/dataset.js +1 -1
  18. package/src/posture/evaluation/lab/report.js +19 -1
  19. package/src/posture/fleet/cli.js +22 -10
  20. package/src/posture/fleet/enterprise-graph.js +44 -3
  21. package/src/posture/fleet/job-runner.js +7 -7
  22. package/src/posture/fleet/job-store.js +3 -1
  23. package/src/posture/fleet/lineage-summary.js +55 -0
  24. package/src/posture/fleet/scan-child.js +4 -1
  25. package/src/posture/proof-fabric/generate.js +2 -1
  26. package/src/posture/proof-fabric/runner.js +24 -2
  27. package/src/posture/rule-compiler/cli.js +40 -6
  28. package/src/posture/rule-compiler/corpus.js +3 -7
  29. package/src/posture/rule-compiler/ir.js +52 -9
  30. package/src/posture/rule-compiler/proposal.js +14 -9
  31. package/src/posture/rule-compiler/regression.js +95 -0
  32. package/src/posture/rule-compiler/store.js +29 -1
  33. package/src/posture/rule-pack-signing.js +4 -2
@@ -1 +1 @@
1
- ccbccaa1ca1f3c6b8f1332e15cb9ee82fa2c4712cc68d0eb2c47051aaa1f7989 agentic-security.mjs
1
+ 6a72326303aab625b38a9efa798132a7652377aeaa8339131d043634fd8266ec agentic-security.mjs
@@ -8,6 +8,7 @@
8
8
  // controller): scanning a repository for the fleet must not change that repository.
9
9
  import { runFleet } from '../fleet.js';
10
10
  import { runScan } from '../../runScan.js';
11
+ import { summarizeLineage } from './lineage-summary.js';
11
12
 
12
13
  export const BEGIN = '@@FLEET-SCAN-BEGIN@@';
13
14
  export const END = '@@FLEET-SCAN-END@@';
@@ -21,7 +22,9 @@ async function main(repoPath) {
21
22
  const entry = res.results?.[0];
22
23
  if (!entry) throw new Error('the scan produced no entry');
23
24
  const findings = (captured?.findings ?? []).slice(0, MAX_FINDINGS).map(slim);
24
- process.stdout.write(`${BEGIN}${JSON.stringify({ entry, findings, findingsTruncated: (captured?.findings?.length ?? 0) > MAX_FINDINGS })}${END}`);
25
+ // the Code Boundaries graph exists only when the controller asked for it (AGENTIC_SECURITY_LINEAGE_DEEP=1); it is summarized, never copied whole
26
+ const lineage = process.env.AGENTIC_SECURITY_LINEAGE_DEEP === '1' ? summarizeLineage(captured?.lineageGraph) : null;
27
+ process.stdout.write(`${BEGIN}${JSON.stringify({ entry, findings, findingsTruncated: (captured?.findings?.length ?? 0) > MAX_FINDINGS, lineage })}${END}`);
25
28
  }
26
29
 
27
30
  if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.176.0",
3
+ "version": "0.177.0",
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",
@@ -85,7 +85,7 @@
85
85
  "test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
86
86
  "test:glob": "node --test test/glob-compat.test.js",
87
87
  "test:sast": "node --test test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.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/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",
88
- "test:posture": "node --test 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/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/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/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/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/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/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",
88
+ "test:posture": "node --test 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/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/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",
89
89
  "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",
90
90
  "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",
91
91
  "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/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",
@@ -175,6 +175,9 @@
175
175
  "check:bundle-source": "node ../scripts/check-bundle-source.mjs",
176
176
  "release:prepare": "node ../scripts/release-prepare.mjs",
177
177
  "ship": "node ../scripts/ship.mjs",
178
+ "eval:import-independent": "node ../scripts/eval-lab/import-independent.mjs",
179
+ "bench:proof-fabric": "node ../bench/proof-fabric/measure.mjs",
180
+ "bench:fleet-scale": "node ../bench/fleet-scale/run.mjs",
178
181
  "bench:self-scan": "node ../bench/self-scan/measure.mjs",
179
182
  "bench:self-scan:check": "node ../bench/self-scan/check.mjs",
180
183
  "bench:nist-800-171-labels:check": "node ../bench/nist-800-171-labels/check.mjs",
@@ -23,6 +23,13 @@
23
23
  // match.allOf: [regex…] — every regex must match somewhere in the file
24
24
  // match.notMatch: regex — kill if this regex matches
25
25
  // match.window: N — when allOf is set, all matches must fall within N lines
26
+ // match.fileRequires: [regex…]: file-level preconditions (e.g. a framework marker); every one must match somewhere in the file
27
+ // match.guardedBy: regex : a sanitizer or dominating guard: a hit is dropped when this matches within `guardWindow` lines BEFORE
28
+ // (or on) the hit's line. Unlike notMatch it is local to the hit, not the whole file.
29
+ // match.guardWindow: N : how far back guardedBy looks (default: the rule's window, else 5)
30
+ // match.excludeLine: [regex…]: a hit is dropped when its own line matches any of these
31
+ // (the last four are the structural vocabulary the rule compiler's IR compiles to; a rule that uses any of them is evaluated
32
+ // against EVERY anchor and reports the first that survives, not only the first anchor in the file)
26
33
  //
27
34
  // `agentic-security rule test <fixture-glob>` runs every rule against every
28
35
  // file and prints PASS/FAIL.
@@ -153,6 +160,10 @@ function _isSafePattern(p) {
153
160
  return !_REDOS_NESTED_RE.test(p);
154
161
  }
155
162
 
163
+ /** The loader's own rule normalizer, exported so anything that scores or validates a rule uses the SAME one the scanner loads
164
+ * with (and so a rule the loader would reject is never reported as working). Returns null for a rule the loader refuses. */
165
+ export function normalizeCustomRule(r, fp = '(compiled)') { return normalizeRule(r, fp); }
166
+
156
167
  function normalizeRule(r, fp) {
157
168
  if (!r || !r.id || !r.match) return null;
158
169
  const m = r.match;
@@ -177,6 +188,22 @@ function normalizeRule(r, fp) {
177
188
  let notMatch = null;
178
189
  if (m.notMatch) { try { notMatch = new RegExp(m.notMatch, 'm'); } catch {} }
179
190
 
191
+ // Structural vocabulary (rule compiler IR): every regex passes the same ReDoS guard as the main patterns, and a rule that
192
+ // cannot be compiled is rejected whole rather than loaded with a constraint silently missing.
193
+ const compileAll = (list) => {
194
+ const out = [];
195
+ for (const p of list) {
196
+ if (typeof p !== 'string' || !p || !_isSafePattern(p)) { console.error(`agentic-security: rejected unsafe or non-string structural regex in ${r.id}`); return null; }
197
+ try { out.push(new RegExp(p, 'm')); } catch (e) { console.error(`agentic-security: invalid structural regex in ${r.id}: ${e.message}`); return null; }
198
+ }
199
+ return out;
200
+ };
201
+ let fileRequires = null; let guardedBy = null; let excludeLine = null;
202
+ if (m.fileRequires != null) { if (!Array.isArray(m.fileRequires)) return null; fileRequires = compileAll(m.fileRequires); if (!fileRequires) return null; }
203
+ if (m.guardedBy != null) { const g = compileAll([m.guardedBy]); if (!g) return null; guardedBy = g[0]; }
204
+ if (m.excludeLine != null) { if (!Array.isArray(m.excludeLine)) return null; excludeLine = compileAll(m.excludeLine); if (!excludeLine) return null; }
205
+ const guardWindow = Number.isInteger(m.guardWindow) && m.guardWindow > 0 && m.guardWindow <= 200 ? m.guardWindow : (m.window || 5);
206
+
180
207
  // SentQL extensions (Sentinel-parity FR-DSL-1):
181
208
  // llm_validate: { prompt, min_confidence }
182
209
  // path: { must_traverse: [...], must_not_traverse: [...] }
@@ -208,6 +235,8 @@ function normalizeRule(r, fp) {
208
235
  notMatch,
209
236
  requireAll: Array.isArray(m.allOf),
210
237
  windowLines: m.window || 50,
238
+ fileRequires, guardedBy, guardWindow, excludeLine,
239
+ structural: !!(fileRequires || guardedBy || excludeLine),
211
240
  sourceFile: fp,
212
241
  llmValidate,
213
242
  pathConstraints,
@@ -260,17 +289,34 @@ export function runRule(rule, file, content) {
260
289
  });
261
290
 
262
291
  const findings = [];
292
+ // structural preconditions and per-hit filters (see the header): file-level requirements first
293
+ if (rule.fileRequires && !rule.fileRequires.every((rx) => rx.test(content))) return [];
294
+ const lineText = (n) => lines[n - 1] ?? '';
295
+ const guardLines = [];
296
+ if (rule.guardedBy) {
297
+ const g = new RegExp(rule.guardedBy.source, 'gm');
298
+ let gm; const deadline = Date.now() + EXEC_TIMEOUT_MS;
299
+ while ((gm = g.exec(content)) !== null) { guardLines.push(offsetToLine(gm.index)); if (gm.index === g.lastIndex) g.lastIndex++; if (Date.now() > deadline) break; }
300
+ }
301
+ const survives = (hit) => {
302
+ if (rule.excludeLine && rule.excludeLine.some((rx) => rx.test(lineText(hit.line)))) return false;
303
+ if (rule.guardedBy && guardLines.some((gl) => gl <= hit.line && hit.line - gl <= rule.guardWindow)) return false;
304
+ return true;
305
+ };
263
306
  if (rule.requireAll) {
264
307
  if (matches.some(l => l.length === 0)) return [];
265
- // Pick the earliest match in the first list, verify the others fall within window.
266
- const anchor = matches[0][0];
267
- const close = matches.slice(1).every(list =>
268
- list.some(x => Math.abs(x.line - anchor.line) <= rule.windowLines)
269
- );
270
- if (!close) return [];
271
- findings.push(toFinding(rule, file, anchor));
308
+ // Pick the earliest match in the first list, verify the others fall within window. A structural rule tries every anchor in order.
309
+ const anchors = rule.structural ? matches[0] : [matches[0][0]];
310
+ for (const anchor of anchors) {
311
+ const close = matches.slice(1).every(list =>
312
+ list.some(x => Math.abs(x.line - anchor.line) <= rule.windowLines)
313
+ );
314
+ if (!close || !survives(anchor)) continue;
315
+ findings.push(toFinding(rule, file, anchor));
316
+ break;
317
+ }
272
318
  } else {
273
- for (const m of matches[0]) findings.push(toFinding(rule, file, m));
319
+ for (const m of matches[0]) if (survives(m)) findings.push(toFinding(rule, file, m));
274
320
  }
275
321
  return findings;
276
322
  }
@@ -0,0 +1,25 @@
1
+ // The finding-level form of the runtime twin's RUNTIME_EXPLOIT_CORRELATED (PRD 2): a finding is exploit-correlated when three independent
2
+ // pieces of evidence, each already recorded by its own subsystem, agree:
3
+ // 1. an execution-backed proof (EXPLOIT_CONFIRMED or beyond), so the flaw is real and exploitable;
4
+ // 2. a historical hunt that OBSERVED exploit-shaped requests in this installation's own logs;
5
+ // 3. (checked by the caller) a runtime trace showing the vulnerable function, line or route was exercised.
6
+ // The graph is destination-shaped and carries no finding locations, so this correlation lives on the FINDING, where all three records
7
+ // are keyed by the same id. A tampered record is never used, and a missing one is "not established", not "no".
8
+ import { listProofs } from '../proof-fabric/store.js';
9
+ import { stateRank } from '../proof-fabric/states.js';
10
+ import { listRecords } from './store.js';
11
+
12
+ export function exploitCorrelationOf(root, findingId) {
13
+ const proofs = listProofs(root, findingId);
14
+ const proof = proofs.find((p) => p.record && !p.tampered)?.record ?? null;
15
+ const confirmed = !!proof && stateRank(proof.state) >= stateRank('EXPLOIT_CONFIRMED');
16
+ const hunts = listRecords(root, findingId, 'hunt').filter((r) => r.record && !r.tampered).map((r) => r.record);
17
+ const observed = hunts.find((h) => h.verdict === 'OBSERVED') ?? null;
18
+ const correlated = confirmed && !!observed;
19
+ return {
20
+ correlated,
21
+ proof: proof ? { state: proof.state, confirmed } : null,
22
+ hunt: observed ? { verdict: 'OBSERVED', window: observed.window ?? null, matches: observed.matches?.count ?? null } : (hunts[0] ? { verdict: hunts[0].verdict } : null),
23
+ basis: correlated ? 'a confirmed exploit proof and exploit-shaped requests observed in historical logs' : (!proof ? 'no proof record' : !confirmed ? `the proof reached ${proof.state}, not a confirmed exploit` : !hunts.length ? 'no historical hunt has been run' : `the hunt verdict was ${hunts[0].verdict}, which is not an observation`),
24
+ };
25
+ }
@@ -23,7 +23,7 @@ export const SPLITS = Object.freeze(['dev', 'validation', 'sealed']);
23
23
  const ENTRY_KINDS = Object.freeze(['vulnerable', 'fixed', 'hard-negative']);
24
24
  export const HARD_NEGATIVE_CATEGORIES = Object.freeze(['sanitization', 'authorization', 'parameterization', 'bounds-check', 'correct-crypto', 'safe-wrapper']);
25
25
  const SOURCE_KINDS = Object.freeze(['directory', 'git-pairs']);
26
- const PERMITTED_LICENSES = Object.freeze(['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MPL-2.0', '0BSD', 'Unlicense', 'CC0-1.0', 'GPL-2.0-only', 'GPL-2.0-or-later', 'GPL-3.0-only', 'GPL-3.0-or-later', 'LGPL-2.1-only', 'LGPL-2.1-or-later', 'LGPL-3.0-only', 'LGPL-3.0-or-later', 'AGPL-3.0-only', 'AGPL-3.0-or-later']);
26
+ export const PERMITTED_LICENSES = Object.freeze(['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MPL-2.0', '0BSD', 'Unlicense', 'CC0-1.0', 'GPL-2.0-only', 'GPL-2.0-or-later', 'GPL-3.0-only', 'GPL-3.0-or-later', 'LGPL-2.1-only', 'LGPL-2.1-or-later', 'LGPL-3.0-only', 'LGPL-3.0-or-later', 'AGPL-3.0-only', 'AGPL-3.0-or-later']);
27
27
  const MAX_MANIFEST_BYTES = 5 * 1024 * 1024;
28
28
  const MAX_ENTRIES = 20000;
29
29
  const ID_RE = /^[a-z0-9][a-z0-9._-]{0,79}$/;
@@ -74,8 +74,22 @@ export function summarizeRun(run, { budgetUsd = 10 } = {}) {
74
74
  const primary = Object.fromEntries(Object.entries(tasks).map(([t, m]) => [t, m[PRIMARY[t]]]));
75
75
  return { id: arm.id, kind: arm.kind, model: arm.model, runs: rs.length, tasks, economics: econ, intelligencePerDollar: ipd, dollarsPer, primary };
76
76
  });
77
+ // PRD 6 reliability: provider variance. The SAME model id reached through different providers is not necessarily the same behaviour
78
+ // (quantization, serving stack, sampling defaults), so when a run has such arms the spread between them is reported on its own.
79
+ const byModel = new Map();
80
+ for (const a of arms) if (a.kind === 'model' && a.model?.id) byModel.set(a.model.id, [...(byModel.get(a.model.id) ?? []), a]);
81
+ const providerVariance = [];
82
+ for (const [modelId, group] of byModel) {
83
+ if (new Set(group.map((g) => g.model.provider)).size < 2) continue;
84
+ for (const task of new Set(group.flatMap((g) => Object.keys(g.primary)))) {
85
+ const vals = group.map((g) => ({ arm: g.id, provider: g.model.provider, mean: g.primary[task]?.mean ?? null })).filter((v) => v.mean != null);
86
+ if (vals.length < 2) continue;
87
+ const means = vals.map((v) => v.mean);
88
+ providerVariance.push({ model: modelId, task, spread: Math.max(...means) - Math.min(...means), arms: vals, representative: group.every((g) => g.primary[task]?.representative) });
89
+ }
90
+ }
77
91
  return {
78
- schema: REPORT_SCHEMA, version: 1, runId: run.runId, runDigest: run.digest,
92
+ schema: REPORT_SCHEMA, version: 1, runId: run.runId, runDigest: run.digest, providerVariance,
79
93
  header: { scannerCommit: run.scanner?.commit ?? null, scannerVersion: run.scanner?.version ?? null, datasetId: run.dataset.id, datasetVersion: run.dataset.version, datasetHash: run.dataset.hash, split: run.dataset.split, models: run.arms.filter((a) => a.model).map((a) => `${a.model.provider}/${a.model.id}${a.model.local ? ' (local)' : ''}`), config: run.config, machine: run.machine, startedAt: run.startedAt, finishedAt: run.finishedAt },
80
94
  leakageAudit: run.leakageAudit, complete: run.complete, incompleteReasons: run.incompleteReasons, arms,
81
95
  notes: arms.some((a) => Object.values(a.primary).some((s) => s && !s.representative)) ? ['At least one figure rests on fewer than three runs and is marked not representative.'] : [],
@@ -111,6 +125,10 @@ export function toMarkdown(rep) {
111
125
  }
112
126
  L.push('## Cost, speed and reliability', '', '| arm | spend per run | median latency | p95 latency | timeouts | failures | unknown-cost attempts |', '|---|---|---|---|---|---|---|');
113
127
  for (const a of rep.arms) L.push(`| ${a.id} | ${a.kind === 'deterministic' ? 'no model cost' : fmt(a.economics.usd, 'usd')} | ${fmt(a.economics.latencyMedianMs, 'ms')} | ${fmt(a.economics.latencyP95Ms, 'ms')} | ${a.economics.timeouts} | ${a.economics.failures} | ${a.economics.unknownCostAttempts} |`);
128
+ if (rep.providerVariance?.length) {
129
+ L.push('', '## Provider variance', '', 'The same model id reached through different providers:', '', '| model | task | spread | arms |', '|---|---|---|---|');
130
+ for (const v of rep.providerVariance) L.push(`| ${v.model} | ${v.task} | ${pct(v.spread)}${v.representative ? '' : ' (not representative)'} | ${v.arms.map((a) => `${a.arm} (${a.provider}) ${pct(a.mean)}`).join('; ')} |`);
131
+ }
114
132
  L.push('', '## Intelligence per dollar', '', '| arm | confirmed vulnerabilities / $100 | verified fixes / $100 | true positives / $100 | recall at fixed budget |', '|---|---|---|---|---|');
115
133
  for (const a of rep.arms) {
116
134
  const i = a.intelligencePerDollar; const cell = (x) => (x?.value == null ? `n/a (${x?.reason ?? 'not measured'})` : x.value.toFixed(1));
@@ -25,7 +25,7 @@ const DB_FILE = path.join('fleet', 'fleet.db');
25
25
  const ROLLUP_FILE = 'fleet-rollup.json';
26
26
  const sha = (s) => createHash('sha256').update(s).digest('hex');
27
27
 
28
- const usage = () => { console.error('usage: fleet init | edges [--undeclared] | scan <manifest.yaml> [--workers N] [--retries N] [--timeout-seconds N | --timeout-ms N] [--no-cache] | status [run-id] | resume <run-id|manifest.yaml> [--retry-failed] | query (--depends-on <id> | --blast-radius <component> [--version v] | --internet-to <data-class> [--involving-vulnerable auth|<repo>] | "<question>") | explore [--out file.html]'); return 4; };
28
+ const usage = () => { console.error('usage: fleet init | edges [--undeclared] | scan <manifest.yaml> [--workers N] [--retries N] [--timeout-seconds N | --timeout-ms N] [--no-cache] [--no-discover] [--lineage] | status [run-id] | resume <run-id|manifest.yaml> [--retry-failed] | query (--depends-on <id> | --blast-radius <component> [--version v] | --internet-to <data-class> [--involving-vulnerable auth|<repo>] | "<question>") | explore [--out file.html]'); return 4; };
29
29
  const isManifestArg = (s) => /\.ya?ml$/i.test(String(s ?? ''));
30
30
  const manifestArg = (args) => { const m = args._[2]; return m && !String(m).startsWith('--') ? m : 'fleet.yaml'; };
31
31
  const dbPath = (root) => statePath(root, DB_FILE);
@@ -51,7 +51,7 @@ async function cmdEdges(args, root) {
51
51
  if (args.flags.json) { console.log(JSON.stringify({ discovery: g.discovery ?? null, edges }, null, 2)); return 0; }
52
52
  if (!edges.length) { console.log(args.flags.undeclared ? 'No edge was found in code that the manifest does not declare.' : 'The graph has no edges.'); }
53
53
  for (const e of edges) {
54
- const where = e.evidence.filter((x) => x.source === 'discovered-code-literal').map((x) => `${x.file}:${x.line}`);
54
+ const where = e.evidence.filter((x) => x.source === 'discovered-code-literal' || x.source === 'code-boundaries').map((x) => (x.file ? `${x.file}:${x.line}` : `code boundaries: ${x.host ?? x.topic ?? x.name}`));
55
55
  console.log(`${e.from} -[${e.type}]-> ${e.to} ${e.discovered ? 'FOUND ONLY IN CODE' : where.length ? 'declared, and seen in code' : 'declared'}${where.length ? ` (${where.join(', ')})` : ''}`);
56
56
  }
57
57
  if (g.discovery?.limitations) console.log(`\nLimits: ${g.discovery.limitations.join(' ')}`);
@@ -81,6 +81,12 @@ function entriesOf(artifacts, manifest) {
81
81
  return manifest.repositories.filter((r) => artifacts.has(r.id)).map((r) => ({ ...artifacts.get(r.id).entry, repo: r.id }));
82
82
  }
83
83
 
84
+ /** The PRD names its commands `fleet status JOB-ID` and `fleet resume JOB-ID`; a run is a set of jobs, so a job id names the run it belongs to. */
85
+ function runOfJob(store, id) {
86
+ const job = store.getJob(String(id));
87
+ return job ? store.getRun(job.runId) : null;
88
+ }
89
+
84
90
  async function finishRun({ args, root, store, runId, manifest, manifestFile, interrupted }) {
85
91
  const { artifacts, problems } = loadArtifacts({ store, runId, root });
86
92
  const jobs = store.listJobs(runId);
@@ -100,14 +106,16 @@ async function finishRun({ args, root, store, runId, manifest, manifestFile, int
100
106
  let discovered = null; let discoveryNote = args.flags['no-discover'] ? 'skipped (--no-discover)' : null;
101
107
  if (!args.flags['no-discover']) { try { discovered = discoverEdges({ manifest }); } catch (e) { discoveryNote = `failed (${String(e.message).slice(0, 80)}); the graph holds declared edges only`; } }
102
108
  const split = discovered ? splitDeclared(discovered, manifest) : null;
103
- const graph = finalizeGraph(buildEnterpriseGraph({ manifest, dependencyGraph: dep, discovered }));
109
+ const lineageByRepo = new Map([...artifacts].filter(([, a]) => a.lineage).map(([id, a]) => [id, a.lineage]));
110
+ const graph = finalizeGraph(buildEnterpriseGraph({ manifest, dependencyGraph: dep, discovered, lineage: lineageByRepo.size ? lineageByRepo : null }));
104
111
  const findingsByRepo = Object.fromEntries([...artifacts].map(([id, a]) => [id, a.findings ?? []]));
105
- safeWriteState(statePath(root, ENTERPRISE_FILE), JSON.stringify({ ...graph, runId, manifestFile, findingsByRepo, discovery: discovered ? { scanned: discovered.scanned, limitations: discovered.limitations, undeclared: split.undeclared.length, confirmed: split.confirmed.length } : { note: discoveryNote }, builtAt: new Date().toISOString() }));
112
+ safeWriteState(statePath(root, ENTERPRISE_FILE), JSON.stringify({ ...graph, runId, manifestFile, findingsByRepo, codeBoundaries: lineageByRepo.size ? { repositories: lineageByRepo.size, undeclaredDataClasses: Object.fromEntries(graph.nodes.filter((n) => n.discoveredDataClasses?.length).map((n) => [n.id, n.discoveredDataClasses])) } : { note: args.flags.lineage ? 'no repository returned a Code Boundaries graph' : 'not requested (--lineage)' }, discovery: discovered ? { scanned: discovered.scanned, limitations: discovered.limitations, undeclared: split.undeclared.length, confirmed: split.confirmed.length } : { note: discoveryNote }, builtAt: new Date().toISOString() }));
106
113
 
107
114
  if (args.flags.json) console.log(JSON.stringify({ runId, health, rollup }, null, 2));
108
115
  else {
109
116
  console.log(renderFleetSummary(rollup));
110
117
  console.log(`\nRun: ${runId}\n${renderOrgHealth(health)}`);
118
+ if (lineageByRepo.size) { const und = graph.nodes.filter((n) => n.discoveredDataClasses?.length); console.log(`\nCode Boundaries: ${lineageByRepo.size} repositor${lineageByRepo.size === 1 ? 'y' : 'ies'} contributed a data flow graph.${und.length ? ` Data classes found in code but not declared: ${und.map((n) => `${n.id} (${n.discoveredDataClasses.join(', ')})`).join('; ')}.` : ' No undeclared data class was found.'}`); }
111
119
  if (split) console.log(`\nEdges found in code: ${split.undeclared.length} not in the manifest, ${split.confirmed.length} matching a declared edge. ${split.undeclared.length ? 'List them with: agentic-security fleet edges --undeclared' : ''}`.trimEnd());
112
120
  else if (discoveryNote) console.log(`\nEdge discovery ${discoveryNote}.`);
113
121
  if (interrupted) console.log(`\nInterrupted. Continue with: agentic-security fleet resume ${runId}`);
@@ -119,7 +127,9 @@ async function finishRun({ args, root, store, runId, manifest, manifestFile, int
119
127
  async function runAll({ args, root, store, runId, manifest, manifestFile }) {
120
128
  const ac = new AbortController(); const onSig = () => ac.abort(); process.once('SIGINT', onSig); process.once('SIGTERM', onSig);
121
129
  try {
122
- await runJobs({ manifest, store, runId, root, scan: childScan, signal: ac.signal, useCache: !args.flags['no-cache'], onEvent: (e) => {
130
+ // Code Boundaries are requested at scan time and remembered on the run, so a resume does the same thing the scan would have
131
+ const lineage = !!args.flags.lineage || !!store.getRun(runId)?.meta?.lineage;
132
+ await runJobs({ manifest, store, runId, root, scan: childScan, scanOptions: { lineage }, signal: ac.signal, useCache: !args.flags['no-cache'], onEvent: (e) => {
123
133
  if (args.flags.json) return;
124
134
  if (e.type === 'done') console.error(`[fleet] done: ${e.repo}${e.cached ? ' (cached)' : ''}${e.status === 'partial' ? ' (partial)' : ''}`);
125
135
  else if (e.type === 'failed') console.error(`[fleet] ${e.timedOut ? 'TIMED OUT' : 'FAILED'}: ${e.repo} (${e.error})${e.next === 'queued' ? ', will retry' : ''}`);
@@ -141,7 +151,7 @@ async function cmdScan(args, root) {
141
151
  try {
142
152
  const manifestHash = sha(fs.readFileSync(manifestFile));
143
153
  console.error(`[fleet] planning ${manifest.repositories.length} repository(ies) for ${manifest.organization} (${manifest.workers} worker(s), ${manifest.retries} retr${manifest.retries === 1 ? 'y' : 'ies'}, ${Math.round(manifest.perRepoTimeoutMs / 1000)}s per repository)...`);
144
- const plan = planRun({ manifest, manifestHash, store, meta: { manifestFile } });
154
+ const plan = planRun({ manifest, manifestHash, store, config: args.flags.lineage ? { lineage: true } : {}, meta: { manifestFile, lineage: !!args.flags.lineage } });
145
155
  return await runAll({ args, root, store, runId: plan.runId, manifest, manifestFile });
146
156
  } finally { store.close(); }
147
157
  }
@@ -158,7 +168,7 @@ async function cmdResume(args, root) {
158
168
  const hash = sha(fs.readFileSync(path.resolve(root, target)));
159
169
  run = store.listRuns().find((r) => r.manifestHash === hash && store.listJobs(r.runId).some((j) => ['queued', 'leased'].includes(j.status)));
160
170
  if (!run) { console.error('Nothing to resume for that manifest: no run of it has unfinished work. Use `fleet resume <run-id> --retry-failed` to retry failed repositories.'); return 4; }
161
- } else run = store.getRun(target);
171
+ } else run = store.getRun(target) ?? runOfJob(store, target);
162
172
  if (!run) { console.error(`No run ${target}. Runs: ${store.listRuns().slice(0, 5).map((r) => r.runId).join(', ') || 'none'}`); return 4; }
163
173
  const full = store.getRun(run.runId);
164
174
  const manifestFile = path.resolve(root, args.flags.manifest ?? full.meta?.manifestFile ?? (isManifestArg(target) ? target : 'fleet.yaml'));
@@ -190,14 +200,15 @@ async function cmdStatus(args, root) {
190
200
  const store = await createSqliteJobStore({ file: dbFile });
191
201
  try {
192
202
  const arg = args._[2];
193
- const run = arg && !isManifestArg(arg) ? store.getRun(String(arg)) : store.listRuns()[0] && store.getRun(store.listRuns()[0].runId);
194
- if (!run) { console.error(arg && !isManifestArg(arg) ? `No run ${arg}.` : 'No fleet run exists yet.'); return 4; }
203
+ const run = arg && !isManifestArg(arg) ? (store.getRun(String(arg)) ?? runOfJob(store, String(arg))) : store.listRuns()[0] && store.getRun(store.listRuns()[0].runId);
204
+ if (!run) { console.error(arg && !isManifestArg(arg) ? `No run or job ${arg}.` : 'No fleet run exists yet.'); return 4; }
205
+ if (arg && !isManifestArg(arg) && String(arg) !== run.runId) console.error(`Job ${arg} belongs to run ${run.runId}.`);
195
206
  const meta = run.meta ?? {};
196
207
  const manifest = loadFleetManifest(path.resolve(root, meta.manifestFile ?? 'fleet.yaml'));
197
208
  const jobs = store.listJobs(run.runId);
198
209
  const m = manifest.ok ? manifest.manifest : { repositories: jobs.map((j) => ({ id: j.repoId, required: j.required })) };
199
210
  const health = computeOrgHealth({ manifest: m, jobs });
200
- if (args.flags.json) { console.log(JSON.stringify({ runId: run.runId, health, jobs: jobs.map((j) => ({ repo: j.repoId, status: j.status, attempts: j.attempts, worker: j.worker, leaseExpiresAt: j.leaseExpiresAt, error: j.error })) }, null, 2)); return 0; }
211
+ if (args.flags.json) { console.log(JSON.stringify({ runId: run.runId, health, jobs: jobs.map((j) => ({ jobId: j.jobId, repo: j.repoId, status: j.status, attempts: j.attempts, worker: j.worker, leaseExpiresAt: j.leaseExpiresAt, error: j.error })) }, null, 2)); return 0; }
201
212
  console.log(`Run: ${run.runId} (${run.organization})\n${renderOrgHealth(health)}`);
202
213
  const live = jobs.filter((j) => j.status === 'leased');
203
214
  if (live.length) console.log(`\nIn progress: ${live.map((j) => `${j.repoId} (attempt ${j.attempts}, lease until ${new Date(j.leaseExpiresAt).toISOString()})`).join(', ')}`);
@@ -266,6 +277,7 @@ async function cmdQuery(args, root) {
266
277
  for (const p of res.paths) {
267
278
  console.log(p.nodes.map((n) => (n === INTERNET ? 'Internet' : n)).join(' -> '));
268
279
  if (p.discoveredEdges?.length) console.log(` rests on edge(s) found only in code, not declared: ${p.discoveredEdges.join(', ')}`);
280
+ if (p.discoveredDataClassNodes?.length) console.log(` reaches ${toClass} data only because the code shows it, not the manifest: ${p.discoveredDataClassNodes.join(', ')}`);
269
281
  if (p.involving.length) console.log(` involves vulnerable code: ${p.involving.map((n) => `${n} (${involving.why.get(n)})`).join(', ')}`);
270
282
  }
271
283
  if (res.paths.length) console.log(`\n${res.paths.length} path(s). Reach edges are as declared in the fleet manifest, plus any marked above as found only in code.${res.truncated ? ' The search bound was reached; more may exist.' : ''}`);
@@ -22,7 +22,7 @@ const AUTH_CWES = new Set(['CWE-287', 'CWE-306', 'CWE-285', 'CWE-862', 'CWE-863'
22
22
  const edgeId = (type, from, to) => `edge:${type}:${from}:${to}`;
23
23
  const hash = (v) => createHash('sha256').update(JSON.stringify(v)).digest('hex');
24
24
 
25
- export function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = null }) {
25
+ export function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = null, lineage = null }) {
26
26
  const nodes = new Map();
27
27
  const edges = new Map();
28
28
  const addNode = (n) => nodes.set(n.id, n);
@@ -50,7 +50,48 @@ export function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = n
50
50
  // Edges found in the code. They share an id with a declared edge of the same type and endpoints, so the evidence list shows both; an
51
51
  // edge with ONLY discovered evidence is marked `discovered: true` so a query can say it rests on a code literal, not on a declaration.
52
52
  for (const d of discovered?.edges ?? []) for (const evidence of d.evidence) addEdge(d.type, d.from, d.to, evidence, true);
53
- for (const e of edges.values()) e.discovered = e.evidence.every((x) => x.source === 'discovered-code-literal');
53
+ // Code Boundaries (each repository's own data flow graph, summarized): data classes the code handles, and destinations it sends to.
54
+ // These add to what the manifest declared and never replace it; a class the manifest did not declare is listed on the node so a path
55
+ // that exists only because of it can say so.
56
+ const aliasesOf = new Map([...manifest.repositories, ...(manifest.resources ?? [])].map((n) => [n.id, [n.id, ...(n.aliases ?? [])].map((a) => String(a).toLowerCase())]));
57
+ for (const [repoId, sum] of lineage ?? []) {
58
+ const node = nodes.get(repoId);
59
+ if (!node || !sum) continue;
60
+ const declared = new Set(node.dataClasses);
61
+ const found = (sum.dataClasses ?? []).filter((c) => !declared.has(c));
62
+ node.dataClasses = [...declared, ...found];
63
+ node.discoveredDataClasses = found;
64
+ const cb = (extra) => ({ source: 'code-boundaries', repo: repoId, declared: false, file: null, ...extra });
65
+ for (const d of sum.externalDestinations ?? []) {
66
+ if (!d.host) continue;
67
+ const first = d.host.split('.')[0];
68
+ for (const [target, names] of aliasesOf) {
69
+ if (target === repoId || !(names.includes(d.host) || names.includes(first))) continue;
70
+ const tn = nodes.get(target);
71
+ addEdge(tn?.type === 'external-provider' ? 'uses-provider' : 'calls', repoId, target, cb({ host: d.host, nodeKind: 'external' }), true);
72
+ }
73
+ }
74
+ for (const q of sum.queues ?? []) {
75
+ for (const [target, names] of aliasesOf) {
76
+ const tn = nodes.get(target);
77
+ if (tn?.type !== 'queue' || !names.includes(String(q.topic).toLowerCase())) continue;
78
+ if (q.operation === 'consume') addEdge('consumes', target, repoId, cb({ topic: q.topic }), true); else addEdge('produces', repoId, target, cb({ topic: q.topic }), true);
79
+ }
80
+ }
81
+ for (const st of sum.stores ?? []) {
82
+ for (const key of [st.host, st.database]) {
83
+ if (!key) continue;
84
+ const k = String(key).toLowerCase();
85
+ for (const [target, names] of aliasesOf) {
86
+ const tn = nodes.get(target);
87
+ if (tn?.type !== 'database' || !(names.includes(k) || names.includes(k.split('.')[0]))) continue;
88
+ addEdge('connects-to', repoId, target, cb({ store: st.subtype, name: key }), true);
89
+ }
90
+ }
91
+ }
92
+ }
93
+ const FOUND_IN_CODE = new Set(['discovered-code-literal', 'code-boundaries']);
94
+ for (const e of edges.values()) e.discovered = e.evidence.every((x) => FOUND_IN_CODE.has(x.source));
54
95
  return { schema: ENTERPRISE_GRAPH_SCHEMA, version: 1, organization: manifest.organization, nodes: [...nodes.values()], edges: [...edges.values()], digest: null };
55
96
  }
56
97
 
@@ -97,7 +138,7 @@ export function attackPaths({ graph, toDataClass, from = INTERNET, involving = n
97
138
  if (results.length >= maxPaths) { truncated = true; return; }
98
139
  if (targets.has(id) && nodePath.length > 1) {
99
140
  const inv = involving ? nodePath.filter((n) => involving.all.has(n)) : [];
100
- if (!involving || inv.length) results.push({ nodes: [...nodePath], edges: edgePath.map((e) => e.id), involving: inv, discoveredEdges: edgePath.filter((e) => e.discovered).map((e) => e.id), evidence: edgePath.flatMap((e) => e.evidence.map((x) => ({ edge: e.id, ...x }))) });
141
+ if (!involving || inv.length) results.push({ nodes: [...nodePath], edges: edgePath.map((e) => e.id), involving: inv, discoveredEdges: edgePath.filter((e) => e.discovered).map((e) => e.id), discoveredDataClassNodes: nodePath.filter((n) => (nodeOf(graph, n)?.discoveredDataClasses ?? []).includes(toDataClass)), evidence: edgePath.flatMap((e) => e.evidence.map((x) => ({ edge: e.id, ...x }))) });
101
142
  }
102
143
  if (nodePath.length > maxDepth) { truncated = true; return; }
103
144
  for (const e of out.get(id) ?? []) if (!nodePath.includes(e.to)) walk(e.to, [...nodePath, e.to], [...edgePath, e]);
@@ -25,8 +25,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
25
25
  export const DEFAULTS = Object.freeze({ leaseMs: 30_000, heartbeatMs: 10_000, retryDelayMs: 2_000, pollMs: 100 });
26
26
 
27
27
  /** The default scan: one supervised child process per repository. Throws `{code: 'TIMEOUT'|'ERROR', message}`. */
28
- export async function childScan(repoPath, { timeoutMs, signal }) {
29
- const r = await superviseSpawn(process.execPath, [SCAN_CHILD, repoPath], { cwd: repoPath, env: { ...process.env, AGENTIC_SECURITY_NO_STATE: '1' }, timeoutMs, signal, maxOutputBytes: 96 * 1024 * 1024 });
28
+ export async function childScan(repoPath, { timeoutMs, signal, lineage = false }) {
29
+ const r = await superviseSpawn(process.execPath, [SCAN_CHILD, repoPath], { cwd: repoPath, env: { ...process.env, AGENTIC_SECURITY_NO_STATE: '1', ...(lineage ? { AGENTIC_SECURITY_LINEAGE_DEEP: '1' } : {}) }, timeoutMs, signal, maxOutputBytes: 96 * 1024 * 1024 });
30
30
  const fail = (code, message) => Object.assign(new Error(message), { code });
31
31
  if (r.timedOut) throw fail('TIMEOUT', `no result within ${Math.round(timeoutMs / 1000)}s`);
32
32
  if (r.cancelled) throw fail('CANCELLED', 'cancelled');
@@ -69,7 +69,7 @@ export function planRun({ manifest, manifestHash, store, runId = null, config =
69
69
  * 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
70
70
  * aborts, or `maxWaitMs` passes with only other workers' leases outstanding.
71
71
  */
72
- export async function runJobs({ manifest, store, runId, root, scan = childScan, 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 }) {
72
+ export 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 }) {
73
73
  const repoById = new Map(manifest.repositories.map((r) => [r.id, r]));
74
74
  const cacheD = cacheDir ?? statePath(root, 'fleet', 'cache');
75
75
  const artD = artifactDir ?? statePath(root, 'fleet', 'artifacts', runId);
@@ -102,7 +102,7 @@ export async function runJobs({ manifest, store, runId, root, scan = childScan,
102
102
  onEvent({ type: 'start', repo: job.repoId, attempt: job.attempts });
103
103
  const cached = useCache ? readCache(cacheD, job.cacheKey) : null;
104
104
  if (cached) {
105
- const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, cached: true });
105
+ const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, lineage: cached.lineage ?? null, cached: true });
106
106
  const status = cached.entry?.scanHealth && cached.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
107
107
  if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: cached.entry?.total ?? null, cached: true, artifact: path.basename(a.file) }, now: now() })) onEvent({ type: 'done', repo: job.repoId, cached: true, status });
108
108
  continue;
@@ -113,15 +113,15 @@ export async function runJobs({ manifest, store, runId, root, scan = childScan,
113
113
  let lost = false;
114
114
  const hb = setInterval(() => { if (!store.heartbeat(job.jobId, job.epoch, { now: now(), leaseMs })) { lost = true; ac.abort(); } }, heartbeatMs);
115
115
  try {
116
- const out = await scan(repo.path, { timeoutMs: job.timeoutMs, signal: ac.signal });
116
+ const out = await scan(repo.path, { timeoutMs: job.timeoutMs, signal: ac.signal, ...scanOptions });
117
117
  clearInterval(hb);
118
118
  if (lost) { onEvent({ type: 'fenced', repo: job.repoId }); continue; }
119
119
  if (!out?.entry) throw Object.assign(new Error('the scan returned no entry'), { code: 'ERROR' });
120
120
  if (out.entry.ok === false) throw Object.assign(new Error(out.entry.error || 'the scan failed'), { code: 'ERROR' });
121
- const a = writeArtifact(job, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated });
121
+ const a = writeArtifact(job, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated, lineage: out.lineage ?? null });
122
122
  const status = out.entry.scanHealth && out.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
123
123
  if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: out.entry.total ?? null, cached: false, artifact: path.basename(a.file) }, now: now() })) {
124
- if (useCache && status === 'succeeded') writeCache(cacheD, job.cacheKey, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated });
124
+ if (useCache && status === 'succeeded') writeCache(cacheD, job.cacheKey, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated, lineage: out.lineage ?? null });
125
125
  onEvent({ type: 'done', repo: job.repoId, cached: false, status });
126
126
  } else onEvent({ type: 'fenced', repo: job.repoId });
127
127
  } catch (e) {
@@ -89,7 +89,9 @@ export async function createSqliteJobStore({ file }) {
89
89
  const { DatabaseSync } = await import('node:sqlite');
90
90
  if (file !== ':memory:') fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
91
91
  const db = new DatabaseSync(file);
92
- db.exec('PRAGMA journal_mode = WAL; PRAGMA busy_timeout = 5000; PRAGMA foreign_keys = ON;');
92
+ // the busy timeout comes FIRST: the very next statements (switching the journal mode, creating tables) take locks, and another process
93
+ // opening the same file at the same instant must wait for them instead of failing with "database is locked"
94
+ db.exec('PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;');
93
95
  db.exec(SCHEMA);
94
96
  const tx = (fn) => { db.exec('BEGIN IMMEDIATE'); try { const r = fn(); db.exec('COMMIT'); return r; } catch (e) { try { db.exec('ROLLBACK'); } catch { /* already rolled back */ } throw e; } };
95
97
  const q = (sql, ...p) => db.prepare(sql).all(...p);
@@ -0,0 +1,55 @@
1
+ // A bounded summary of one repository's Code Boundaries graph (the Data Flow Explorer's `DataFlowGraph v1`), for the enterprise graph
2
+ // (PRD 4: "federate Code Boundaries across repositories"). The whole graph is not copied: a fleet of hundreds of repositories does not
3
+ // need each node, only what crosses a repository boundary or changes what the organization knows about it:
4
+ // * the data classes that appear in the code (so a repository holding payment data is known to, even if the manifest forgot to say);
5
+ // * the external destinations the code sends data to, by host;
6
+ // * the queue topics it publishes to or consumes from;
7
+ // * the stores it touches, by host/database/table name;
8
+ // * counts, including flows that carry sensitive classes across a trust boundary.
9
+ // Pure; hostile input (the graph came out of a scan of someone's code), so every string is bounded and shape-checked.
10
+ const MAX_ITEMS = 64;
11
+ const SENSITIVE = new Set(['pii', 'phi', 'pci', 'fin', 'credentials']);
12
+ const arr = (v) => (Array.isArray(v) ? v : []);
13
+ const clean = (v, max = 120) => (typeof v === 'string' && v.length > 0 && v.length <= max && !/[\s\u0000-\u001f]/.test(v) ? v : null);
14
+
15
+ function hostOf(literal) {
16
+ if (typeof literal !== 'string') return null;
17
+ try { const h = new URL(literal).hostname.toLowerCase(); return clean(h, 253); } catch { /* not a URL */ }
18
+ const bare = literal.toLowerCase().replace(/:\d+$/, '');
19
+ return /^[a-z0-9][a-z0-9.-]{0,252}$/.test(bare) ? bare : null;
20
+ }
21
+
22
+ export function summarizeLineage(graph) {
23
+ if (!graph || typeof graph !== 'object') return null;
24
+ const classes = new Set();
25
+ for (const d of arr(graph.dataElements)) for (const c of arr(d?.dataClasses)) { if (typeof c !== 'string') continue; const l = clean(c.toLowerCase(), 31); if (l && /^[a-z][a-z0-9-]*$/.test(l)) classes.add(l); }
26
+ const ext = new Map(); const queues = new Map(); const stores = new Map();
27
+ for (const n of arr(graph.nodes)) {
28
+ if (!n || typeof n !== 'object') continue;
29
+ if (n.kind === 'external') {
30
+ const host = hostOf(n.destination?.literalValue);
31
+ const key = `${n.subtype ?? 'external'}|${host ?? ''}`;
32
+ if (!ext.has(key) && ext.size < MAX_ITEMS) ext.set(key, { subtype: clean(n.subtype, 60) ?? 'external', host });
33
+ } else if (n.kind === 'queue') {
34
+ const topic = clean(n.queueDetail?.topic, 120); const op = n.queueDetail?.operation === 'consume' || n.queueDetail?.operation === 'receive' ? 'consume' : 'publish';
35
+ if (topic && queues.size < MAX_ITEMS) queues.set(`${topic}|${op}`, { topic, operation: op });
36
+ } else if (n.kind === 'store') {
37
+ const d = n.storeDetail ?? {};
38
+ const item = { subtype: clean(n.subtype, 60) ?? 'store', host: hostOf(d.host), database: clean(d.database, 120), table: clean(d.table, 120) };
39
+ const key = JSON.stringify(item);
40
+ if (stores.size < MAX_ITEMS) stores.set(key, item);
41
+ }
42
+ }
43
+ const edges = arr(graph.edges);
44
+ const crossing = new Set(edges.filter((e) => arr(e?.boundaryCrossings).length).map((e) => e.id));
45
+ const dataById = new Map(arr(graph.dataElements).map((d) => [d?.id, d]));
46
+ const sensitiveCrossing = arr(graph.flows).filter((f) => arr(f?.edgeIds).some((id) => crossing.has(id)) && arr(f?.dataElementIds).some((id) => arr(dataById.get(id)?.dataClasses).some((c) => SENSITIVE.has(String(c).toLowerCase())))).length;
47
+ return {
48
+ schema: 'agentic-security/fleet-lineage-summary', version: 1,
49
+ dataClasses: [...classes].sort().slice(0, MAX_ITEMS),
50
+ externalDestinations: [...ext.values()],
51
+ queues: [...queues.values()],
52
+ stores: [...stores.values()],
53
+ counts: { nodes: arr(graph.nodes).length, edges: edges.length, flows: arr(graph.flows).length, boundaryCrossingEdges: crossing.size, sensitiveFlowsCrossingBoundary: sensitiveCrossing },
54
+ };
55
+ }