@clear-capabilities/agentic-security-scanner 0.195.22 → 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 (36) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/dist/{c1e282247b06d51c8ffb.json → 01087ded263953062654.json} +2 -2
  3. package/dist/550.index.js +9 -8
  4. package/dist/5620.index.js +59 -10
  5. package/dist/5937.index.js +88 -16
  6. package/dist/7838.index.js +32 -5
  7. package/dist/9441.index.js +50 -7
  8. package/dist/9824.index.js +19 -13
  9. package/dist/agentic-security.mjs +2 -2
  10. package/dist/agentic-security.mjs.sha256 +1 -1
  11. package/package.json +2 -2
  12. package/src/discovery/detector-spec-synthesis.js +3 -3
  13. package/src/discovery/llm-invoke.js +3 -3
  14. package/src/egress/transport.js +148 -0
  15. package/src/llm-validator/index.js +4 -2
  16. package/src/llm-validator/local-endpoint.js +4 -1
  17. package/src/llm-validator/ollama-provider.js +8 -6
  18. package/src/posture/adversary-agent.js +27 -10
  19. package/src/posture/custom-rules.js +13 -1
  20. package/src/posture/fleet/cli.js +2 -4
  21. package/src/posture/fleet/fleet-manifest.js +4 -1
  22. package/src/posture/fleet/job-runner.js +28 -4
  23. package/src/posture/fleet/org-health.js +23 -0
  24. package/src/posture/fleet/scan-cache.js +1 -1
  25. package/src/posture/flow-narration.js +10 -8
  26. package/src/posture/llm-redteam.js +3 -1
  27. package/src/posture/provenance/coordinator.js +8 -0
  28. package/src/posture/provenance/origin-resolver.js +17 -4
  29. package/src/posture/rule-compiler/cli.js +56 -8
  30. package/src/posture/rule-compiler/corpus.js +10 -2
  31. package/src/posture/rule-compiler/ir.js +10 -1
  32. package/src/posture/rule-compiler/promotion.js +12 -2
  33. package/src/posture/rule-compiler/regression.js +1 -1
  34. package/src/posture/rule-compiler/store.js +27 -7
  35. package/src/posture/strategic-metrics.js +50 -7
  36. package/src/sca/llm-function-extract.js +17 -13
@@ -1 +1 @@
1
- af785a81038bee79b2d722220e74edbb0fae47162142a2b8b6f58ad56c278c86 agentic-security.mjs
1
+ c99c86a3f8172dcc3f6f1a23f6de0b955859ef9d8ea709e6d744e536f5676a15 agentic-security.mjs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clear-capabilities/agentic-security-scanner",
3
- "version": "0.195.22",
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",
@@ -16,6 +16,7 @@ import { resolveAssuranceConfig, featureStatus } from '../posture/assurance/conf
16
16
  import { guardedModelCall, readFileBounded } from '../posture/assurance/bounded-io.js';
17
17
  import { evaluateEgress } from '../egress/policy.js';
18
18
  import { recordEgressCall } from '../egress/audit.js';
19
+ import { modelFetch } from '../egress/transport.js';
19
20
  import { parseLastJsonObject } from '../llm-validator/index.js';
20
21
 
21
22
  export const FEATURE_ID = 'detector-spec-synthesis';
@@ -103,12 +104,11 @@ async function defaultTransport({ endpoint, text, signal, model, apiKey }) {
103
104
  // The operator's own API key goes to the operator's own endpoint as the bearer credential: that is what the key is for. The egress
104
105
  // policy has already allowed this endpoint (deny and local-only stop the call before this function runs), so the credentials-to-HTTP
105
106
  // flow the self-scan reports here is the intended one, suppressed with that reason rather than absorbed into the drift baseline.
106
- const res = await fetch(endpoint, { // agentic-security-ignore
107
+ const res = await modelFetch(endpoint, { // agentic-security-ignore
107
108
  method: 'POST',
108
109
  headers: { 'Content-Type': 'application/json', ...(apiKey ? { Authorization: `Bearer ${apiKey}` } : {}) },
109
110
  body: JSON.stringify({ model: model || 'gpt-4o-mini', messages: [{ role: 'system', content: 'You output a single JSON object. Nothing else.' }, { role: 'user', content: text }], temperature: 0, response_format: { type: 'json_object' } }),
110
- signal,
111
- });
111
+ }, { signal });
112
112
  if (!res.ok) throw new Error(`model endpoint returned ${res.status}`);
113
113
  const body = await res.json();
114
114
  return String(body?.choices?.[0]?.message?.content || body?.content || '');
@@ -6,6 +6,7 @@
6
6
  //
7
7
 
8
8
  import { evaluateEgress } from '../egress/policy.js';
9
+ import { modelFetch } from '../egress/transport.js';
9
10
  import { resolveProvider } from '../llm-validator/providers.js';
10
11
  import { callOllamaChat } from '../llm-validator/ollama-provider.js';
11
12
 
@@ -74,12 +75,11 @@ export async function defaultLlmInvoke(prompt, opts = {}) {
74
75
  // Absent, it falls back to the single configured endpoint — so the ordinary
75
76
  // single-model path is byte-identical to what it was before consensus existed.
76
77
  const endpoint = opts.endpoint || process.env.AGENTIC_SECURITY_LLM_ENDPOINT;
77
- const res = await fetch(endpoint, { // agentic-security-ignore: CWE-918
78
+ const res = await modelFetch(endpoint, {
78
79
  method: 'POST',
79
80
  headers: { 'content-type': 'application/json' },
80
81
  body: JSON.stringify({ prompt }),
81
- signal: AbortSignal.timeout(timeoutMs),
82
- });
82
+ }, { timeoutMs });
83
83
  if (!res.ok) throw new Error(`llm endpoint returned ${res.status}`);
84
84
  const body = await res.json();
85
85
  return typeof body === 'string' ? body : (body?.text ?? JSON.stringify(body));
@@ -0,0 +1,148 @@
1
+ // The one HTTP transport for model calls (GA-06.3).
2
+ //
3
+ // Every path that sends a prompt to a model endpoint evaluates the egress policy first (policy.js) and then sends the request through
4
+ // `modelFetch` here. The policy decides WHETHER a call goes out; this module decides HOW, and the "how" is the same for every caller:
5
+ //
6
+ // 1. no redirects. A loopback endpoint that answers 3xx to a remote host would carry the prompt off the machine; `redirect: 'error'`
7
+ // makes fetch reject instead. A redirect is reported as its own error code, never followed.
8
+ // 2. a per-call deadline. Every call is aborted after `timeoutMs` (default AGENTIC_SECURITY_LLM_TIMEOUT_MS, else 60 s); a caller's own
9
+ // signal is combined with it, so the stricter of the two wins. A connection that is accepted and never answers cannot hold a scan open.
10
+ // 3. a response cap. The body is read as a stream and the request is aborted once `maxResponseBytes` (default
11
+ // AGENTIC_SECURITY_LLM_MAX_RESPONSE_BYTES, else 4 MiB) have arrived, so a server cannot exhaust memory; the cap is applied while
12
+ // reading, not after `text()` has already buffered everything.
13
+ // 4. a process-wide wall budget. With AGENTIC_SECURITY_LLM_WALL_BUDGET_MS set, the total time spent inside model calls in this process
14
+ // is accounted and a call that would start past the budget is refused with `budget-exhausted`. Unset, there is no global limit and
15
+ // the per-call deadline is the only bound; the dollar ceiling (`posture/cost-ceiling.js`) is separate and applies to the validator.
16
+ // 5. no proxy. Node's fetch does not read HTTP_PROXY/HTTPS_PROXY, and this module installs no dispatcher, so a proxy variable in the
17
+ // environment never changes where a prompt goes. A loopback promise therefore stays a promise about the endpoint literal.
18
+ //
19
+ // The result mimics the part of a `Response` the callers use (`ok`, `status`, `statusText`, `headers`, `text()`, `json()`) over the
20
+ // already-capped body, so a call site changes one identifier. Failures are thrown as `ModelTransportError` with a `code` in
21
+ // `TRANSPORT_ERROR_CODES`, which the callers map to their own unavailable states; nothing here retries or falls back.
22
+
23
+ export const DEFAULT_TIMEOUT_MS = 60_000;
24
+ export const DEFAULT_MAX_RESPONSE_BYTES = 4 * 1024 * 1024;
25
+ export const TRANSPORT_ERROR_CODES = Object.freeze(['redirect', 'timeout', 'response-too-large', 'budget-exhausted', 'network']);
26
+
27
+ export class ModelTransportError extends Error {
28
+ constructor(code, message, extra = {}) {
29
+ super(message);
30
+ this.name = 'ModelTransportError';
31
+ this.code = code;
32
+ Object.assign(this, extra);
33
+ }
34
+ }
35
+
36
+ const budget = { spentMs: 0, calls: 0 };
37
+
38
+ /** Positive integer from the environment, else the fallback. */
39
+ function envInt(name, fallback, env = process.env) {
40
+ const n = Number(env[name]);
41
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : fallback;
42
+ }
43
+
44
+ /** The process-wide model wall budget: `{ limitMs|null, spentMs, calls }`. */
45
+ export function modelBudget(env = process.env) {
46
+ const raw = Number(env.AGENTIC_SECURITY_LLM_WALL_BUDGET_MS);
47
+ return { limitMs: Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : null, spentMs: budget.spentMs, calls: budget.calls };
48
+ }
49
+
50
+ /** Reset the accounted wall time (a new scan, or a test). */
51
+ export function resetModelBudget() { budget.spentMs = 0; budget.calls = 0; }
52
+
53
+ function isAbort(e) { return e && (e.name === 'AbortError' || e.name === 'TimeoutError'); }
54
+
55
+ /**
56
+ * Read a body stream, refusing more than `max` bytes. Aborts the request through `abort` so the server stops sending.
57
+ * @returns {Promise<string>}
58
+ */
59
+ async function readCapped(res, max, abort) {
60
+ if (!res.body || typeof res.body.getReader !== 'function') {
61
+ // a response without a readable stream (an injected fake in a test): read it whole, then apply the cap. The parsed form is
62
+ // preferred, because a fake's `json()` is the reply its author meant and its `text()` may be a different, raw string.
63
+ const text = typeof res.json === 'function' ? JSON.stringify(await res.json()) : await res.text();
64
+ if (Buffer.byteLength(text, 'utf8') > max) throw new ModelTransportError('response-too-large', `response is over the ${max} byte limit`, { limit: max });
65
+ return text;
66
+ }
67
+ const reader = res.body.getReader();
68
+ const chunks = [];
69
+ let total = 0;
70
+ for (;;) {
71
+ const { done, value } = await reader.read();
72
+ if (done) break;
73
+ total += value.byteLength;
74
+ if (total > max) {
75
+ try { await reader.cancel(); } catch { /* the request is being torn down */ }
76
+ abort();
77
+ throw new ModelTransportError('response-too-large', `response exceeded the ${max} byte limit`, { limit: max });
78
+ }
79
+ chunks.push(value);
80
+ }
81
+ return Buffer.concat(chunks.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength))).toString('utf8');
82
+ }
83
+
84
+ /**
85
+ * Send one model request. `init` is a fetch init without `redirect` or `signal` (both are set here);
86
+ * `opts`: `{ timeoutMs, maxResponseBytes, signal, env, fetchImpl }`.
87
+ *
88
+ * @returns {Promise<{ok:boolean,status:number,statusText:string,headers:Headers,bytes:number,text:()=>Promise<string>,json:()=>Promise<any>}>}
89
+ */
90
+ export async function modelFetch(endpoint, init = {}, opts = {}) {
91
+ const env = opts.env || process.env;
92
+ const timeoutMs = Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : envInt('AGENTIC_SECURITY_LLM_TIMEOUT_MS', DEFAULT_TIMEOUT_MS, env);
93
+ const maxBytes = Number.isFinite(opts.maxResponseBytes) && opts.maxResponseBytes > 0 ? opts.maxResponseBytes : envInt('AGENTIC_SECURITY_LLM_MAX_RESPONSE_BYTES', DEFAULT_MAX_RESPONSE_BYTES, env);
94
+ const b = modelBudget(env);
95
+ if (b.limitMs !== null && b.spentMs >= b.limitMs) {
96
+ throw new ModelTransportError('budget-exhausted', `the model wall budget of ${b.limitMs} ms is spent (${b.spentMs} ms over ${b.calls} call(s))`, { limitMs: b.limitMs, spentMs: b.spentMs });
97
+ }
98
+ const ac = new AbortController();
99
+ // the remaining budget, when one is set, caps this call too
100
+ const callMs = b.limitMs === null ? timeoutMs : Math.min(timeoutMs, Math.max(1, b.limitMs - b.spentMs));
101
+ const timer = setTimeout(() => ac.abort(new ModelTransportError('timeout', `no response within ${callMs} ms`)), callMs);
102
+ const onOuter = () => ac.abort(opts.signal?.reason);
103
+ if (opts.signal) {
104
+ if (opts.signal.aborted) onOuter();
105
+ else opts.signal.addEventListener('abort', onOuter, { once: true });
106
+ }
107
+ const started = Date.now();
108
+ const doFetch = opts.fetchImpl || fetch;
109
+ try {
110
+ const { redirect: _r, signal: _s, ...rest } = init;
111
+ let res;
112
+ try {
113
+ // The caller already evaluated the egress policy for this endpoint; this is the only place a model request leaves the process.
114
+ res = await doFetch(endpoint, { ...rest, redirect: 'error', signal: ac.signal }); // agentic-security-ignore: CWE-918
115
+ } catch (e) {
116
+ if (ac.signal.aborted && ac.signal.reason instanceof ModelTransportError) throw ac.signal.reason;
117
+ if (isAbort(e) || ac.signal.aborted) throw new ModelTransportError('timeout', `aborted after ${Date.now() - started} ms`, { cause: e });
118
+ const msg = String(e?.cause?.message || e?.message || e);
119
+ if (/redirect/i.test(msg)) throw new ModelTransportError('redirect', `the endpoint answered with a redirect, which is never followed: ${msg}`, { cause: e });
120
+ throw new ModelTransportError('network', msg, { cause: e });
121
+ }
122
+ if (res.status >= 300 && res.status < 400) {
123
+ // `redirect: 'error'` rejects before this point; a non-standard client or a 3xx without Location lands here
124
+ ac.abort();
125
+ throw new ModelTransportError('redirect', `the endpoint answered ${res.status}, which is never followed`, { status: res.status });
126
+ }
127
+ let body;
128
+ try {
129
+ body = await readCapped(res, maxBytes, () => ac.abort());
130
+ } catch (e) {
131
+ if (e instanceof ModelTransportError) throw e;
132
+ if (ac.signal.aborted && ac.signal.reason instanceof ModelTransportError) throw ac.signal.reason;
133
+ if (isAbort(e) || ac.signal.aborted) throw new ModelTransportError('timeout', `the response stalled after ${Date.now() - started} ms`, { cause: e });
134
+ throw new ModelTransportError('network', String(e?.message || e), { cause: e });
135
+ }
136
+ const bytes = Buffer.byteLength(body, 'utf8');
137
+ return {
138
+ ok: res.ok, status: res.status, statusText: res.statusText, headers: res.headers, bytes,
139
+ text: async () => body,
140
+ json: async () => JSON.parse(body),
141
+ };
142
+ } finally {
143
+ clearTimeout(timer);
144
+ if (opts.signal) opts.signal.removeEventListener('abort', onOuter);
145
+ budget.spentMs += Date.now() - started;
146
+ budget.calls += 1;
147
+ }
148
+ }
@@ -77,6 +77,7 @@ import { localEndpointConfig } from './local-endpoint.js';
77
77
  import { resolveProvider, buildProviderRequest, providerMatrix } from './providers.js';
78
78
  import { evaluateEgress } from '../egress/policy.js';
79
79
  import { recordEgressCall, payloadMetrics } from '../egress/audit.js';
80
+ import { modelFetch } from '../egress/transport.js';
80
81
  import { MODEL_STATUS, summarizeModelStatus } from './model-status.js';
81
82
  import { callOllamaChat } from './ollama-provider.js';
82
83
 
@@ -408,12 +409,13 @@ async function callEndpoint(endpoint, apiKey, model, prompt, preset = null, shap
408
409
  // A model that accepts the connection and never answers must not hold a scan open forever: every call is bounded.
409
410
  // The result is an UNAVAILABLE finding (kept, unvalidated), never a silent pass.
410
411
  const ms = Number(process.env.AGENTIC_SECURITY_LLM_TIMEOUT_MS) > 0 ? Number(process.env.AGENTIC_SECURITY_LLM_TIMEOUT_MS) : 60_000;
411
- const r = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body), signal: AbortSignal.timeout(ms) });
412
+ const r = await modelFetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) }, { timeoutMs: ms });
412
413
  if (!r.ok) return { ok: false, error: `HTTP ${r.status}` };
413
414
  const j = await r.json().catch(() => null);
414
415
  return { ok: true, text: String(extractText(j) || ''), usage: extractUsage ? extractUsage(j) : null };
415
416
  } catch (e) {
416
- return { ok: false, error: e && e.name === 'TimeoutError' ? 'timeout' : e.message };
417
+ // a redirect, an oversized body or a spent budget is an unavailable validator, named by its transport code, never a pass
418
+ return { ok: false, error: e && e.code === 'timeout' ? 'timeout' : (e && e.code ? `${e.code}: ${e.message}` : e.message) };
417
419
  }
418
420
  }
419
421
 
@@ -22,7 +22,10 @@
22
22
  // The connect timeout is short for the same reason — a dead port must not hang
23
23
  // a scan.
24
24
 
25
- const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1', '[::1]', '0.0.0.0']);
25
+ // `localhost` is the one name accepted; `[::1]` is how the URL parser spells the IPv6 loopback (a bare `::1` never reaches this set).
26
+ // `0.0.0.0` is NOT here (GA-06.3): it is the unspecified address, and a connect to it reaches a local interface only by an operating
27
+ // system convention, not by the address's meaning. The promise is "literal loopback", so it is refused like `[::]` and `http://0/`.
28
+ const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '[::1]']);
26
29
 
27
30
  export const DEFAULT_LOCAL_ENDPOINT = 'http://127.0.0.1:11434/v1/chat/completions';
28
31
  export const DEFAULT_LOCAL_TIMEOUT_MS = 20000;
@@ -32,6 +32,7 @@
32
32
  // never to this module's error path. See ollama-offline-egress.test.js.
33
33
 
34
34
  import { isLoopbackUrl } from './local-endpoint.js';
35
+ import { modelFetch } from '../egress/transport.js';
35
36
  import { recordOOMEvent } from './oom-feedback.js';
36
37
 
37
38
  export const DEFAULT_OLLAMA_HOST = 'http://127.0.0.1:11434';
@@ -199,16 +200,17 @@ export function parseOllamaChatResponse(json, model) {
199
200
  * "slow to generate" without a lower-level HTTP client.
200
201
  */
201
202
  async function _fetchOllama(url, init, { connectTimeoutMs, requestTimeoutMs }) {
202
- const controller = new AbortController();
203
- const totalTimer = setTimeout(() => controller.abort('total-timeout'), requestTimeoutMs);
203
+ void connectTimeoutMs; // see the note above: the client exposes no connect-only phase to bound separately
204
204
  try {
205
- const res = await fetch(url, { ...init, signal: controller.signal });
205
+ // the shared transport: no redirect is followed, the reply is capped while it is read, the deadline is the total timeout
206
+ const res = await modelFetch(url, init, { timeoutMs: requestTimeoutMs });
206
207
  return { ok: true, res };
207
208
  } catch (e) {
208
- if (controller.signal.aborted) return _err('ollama-timeout', `Ollama request timed out after ${requestTimeoutMs}ms.`);
209
+ if (e?.code === 'timeout') return _err('ollama-timeout', `Ollama request timed out after ${requestTimeoutMs}ms.`);
210
+ if (e?.code === 'redirect') return _err('ollama-unreachable', `Ollama answered with a redirect, which is never followed: ${e.message}`);
211
+ if (e?.code === 'response-too-large') return _err('ollama-malformed-response', e.message);
212
+ if (e?.code === 'budget-exhausted') return _err('ollama-timeout', e.message);
209
213
  return _err('ollama-unreachable', e?.message || String(e));
210
- } finally {
211
- clearTimeout(totalTimer);
212
214
  }
213
215
  }
214
216
 
@@ -26,6 +26,8 @@
26
26
 
27
27
  import * as crypto from 'node:crypto';
28
28
  import { evaluateEgress } from '../egress/policy.js';
29
+ import { modelFetch } from '../egress/transport.js';
30
+ import { resolveProvider } from '../llm-validator/providers.js';
29
31
 
30
32
  const MAX_CALLS_DEFAULT = 50;
31
33
  const MAX_WALL_MS_DEFAULT = 15 * 60 * 1000;
@@ -96,11 +98,21 @@ export function isExceeded(transcript, budget) {
96
98
  // and parses the response as a single tool-call request. The endpoint is
97
99
  // expected to follow an OpenAI-style chat-completions shape; we extract a
98
100
  // JSON object of `{ tool, args }` from the model's reply.
101
+ // GA-06.3: the provider is resolved like the validator's, so the `local` preset's loopback refusal applies here, and a vendor key is
102
+ // sent only with that vendor's own preset (a bare OPENAI_API_KEY is never attached to an arbitrary endpoint). Ollama's protocol is not
103
+ // implemented here, so that preset counts as not configured for this agent.
104
+ export function _agentProvider(env = process.env) {
105
+ const r = resolveProvider({ role: 'verify', env });
106
+ if (!r.ok || r.config.provider === 'ollama') return null;
107
+ return r.config;
108
+ }
109
+
99
110
  export async function defaultLlmInvoke(transcript) {
100
- const endpoint = process.env.AGENTIC_SECURITY_LLM_ENDPOINT;
101
- if (!endpoint) return null;
102
- const model = process.env.AGENTIC_SECURITY_LLM_MODEL || 'gpt-4o-mini';
103
- const apiKey = process.env.AGENTIC_SECURITY_LLM_API_KEY || process.env.OPENAI_API_KEY || '';
111
+ const provider = _agentProvider();
112
+ if (!provider) return null;
113
+ const { endpoint } = provider;
114
+ const model = provider.model || 'gpt-4o-mini';
115
+ const apiKey = provider.apiKey || '';
104
116
  const headers = { 'Content-Type': 'application/json' };
105
117
  if (apiKey) headers['Authorization'] = `Bearer ${apiKey}`;
106
118
  const tools = [...TOOL_ACL].map(t => ({ tool: t, description: TOOL_HELP[t] || '' }));
@@ -115,7 +127,7 @@ export async function defaultLlmInvoke(transcript) {
115
127
  };
116
128
  let res;
117
129
  try {
118
- res = await fetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
130
+ res = await modelFetch(endpoint, { method: 'POST', headers, body: JSON.stringify(body) });
119
131
  } catch (e) {
120
132
  return null;
121
133
  }
@@ -136,6 +148,9 @@ const TOOL_HELP = {
136
148
  'record_outcome': 'Terminate the run with a verdict (outcome: one of OUTCOMES)',
137
149
  };
138
150
 
151
+ /** Bounds for a tool probe against the target: a reply is kept to 4000 characters anyway, so 64 KiB and 30 s are generous. */
152
+ const PROBE_BOUNDS = Object.freeze({ timeoutMs: 30_000, maxResponseBytes: 64 * 1024 });
153
+
139
154
  // Default executeTool — wraps http.get / http.post against `transcript.target`.
140
155
  // Falls back to refusal when the tool is db.read_sandbox_copy (we do not ship
141
156
  // a sandboxed DB by default; the caller must supply one).
@@ -144,7 +159,8 @@ export async function defaultExecuteTool(call, transcript) {
144
159
  if (call.tool === 'http.get') {
145
160
  const url = (transcript?.target || '').replace(/\/$/, '') + (call.args?.path || '/');
146
161
  try {
147
- const r = await fetch(url, { method: 'GET', headers: call.args?.headers || {} });
162
+ // the probe goes through the shared transport too: no redirect off the target, a capped reply, a deadline
163
+ const r = await modelFetch(url, { method: 'GET', headers: call.args?.headers || {} }, PROBE_BOUNDS);
148
164
  const body = await r.text();
149
165
  return { status: r.status, headers: Object.fromEntries(r.headers), body: body.slice(0, 4000) };
150
166
  } catch (e) { return { error: String(e?.message || e) }; }
@@ -152,11 +168,11 @@ export async function defaultExecuteTool(call, transcript) {
152
168
  if (call.tool === 'http.post') {
153
169
  const url = (transcript?.target || '').replace(/\/$/, '') + (call.args?.path || '/');
154
170
  try {
155
- const r = await fetch(url, {
171
+ const r = await modelFetch(url, {
156
172
  method: 'POST',
157
173
  headers: { 'Content-Type': 'application/json', ...(call.args?.headers || {}) },
158
174
  body: typeof call.args?.body === 'string' ? call.args.body : JSON.stringify(call.args?.body || {}),
159
- });
175
+ }, PROBE_BOUNDS);
160
176
  const body = await r.text();
161
177
  return { status: r.status, headers: Object.fromEntries(r.headers), body: body.slice(0, 4000) };
162
178
  } catch (e) { return { error: String(e?.message || e) }; }
@@ -176,8 +192,9 @@ export async function runAgent(finding, opts = {}) {
176
192
  // called later in the loop below) never runs at all.
177
193
  let egressDecision = null;
178
194
  let llmInvoke = opts.llmInvoke || null;
179
- if (!llmInvoke && process.env.AGENTIC_SECURITY_LLM_ENDPOINT) {
180
- egressDecision = evaluateEgress({ scanRoot: opts.scanRoot, purpose: 'adversary-agent', endpoint: process.env.AGENTIC_SECURITY_LLM_ENDPOINT });
195
+ const provider = llmInvoke ? null : _agentProvider();
196
+ if (!llmInvoke && provider) {
197
+ egressDecision = evaluateEgress({ scanRoot: opts.scanRoot, purpose: 'adversary-agent', endpoint: provider.endpoint });
181
198
  if (egressDecision.allowed) llmInvoke = defaultLlmInvoke;
182
199
  }
183
200
  const executeTool = opts.executeTool || (transcript.target ? (call) => defaultExecuteTool(call, transcript) : null);
@@ -39,6 +39,7 @@ import * as path from 'node:path';
39
39
  import * as yaml from '../util/yaml.js';
40
40
  import { globFiles } from '../util/glob.js';
41
41
  import { loadTrustedKeys, verifyRulePack } from './rule-pack-signing.js';
42
+ import { isUnsafeRegex } from '../sast/redos-nfa.js';
42
43
 
43
44
  import { statePath, safeWriteState } from './state-dir.js';
44
45
  const LANG_EXTS = {
@@ -156,8 +157,19 @@ export function loadCustomRules(scanRoot) {
156
157
  // Catches (a+)+, (a*)*, (.+)+, (\w+)+, (a|b)+, (a+|b)* etc.
157
158
  // Not exhaustive but eliminates the vast majority of catastrophic patterns.
158
159
  const _REDOS_NESTED_RE = /\([^)]*[+*][^)]*\)[+*?{]/;
160
+ // The textual heuristic above misses an ambiguous alternation under a quantifier (`(a|a)*b`) and the polynomial shapes; since
161
+ // 0.195.23 (GA-07) the NFA-based detector the SAST layer already ships (`sast/redos-nfa.js`) is consulted as well, so a custom or
162
+ // compiled rule is held to the same bar as the code the scanner flags for CWE-1333.
159
163
  function _isSafePattern(p) {
160
- return !_REDOS_NESTED_RE.test(p);
164
+ if (_REDOS_NESTED_RE.test(p)) return false;
165
+ return !isUnsafeRegex(p).unsafe;
166
+ }
167
+ /** Why a pattern is refused, for the loader's message and the rule compiler's validation. Null when it is safe. */
168
+ export function unsafePatternReason(p) {
169
+ if (typeof p !== 'string') return 'not a string';
170
+ if (_REDOS_NESTED_RE.test(p)) return 'nested quantifiers';
171
+ const r = isUnsafeRegex(p);
172
+ return r.unsafe ? (r.reason || 'superlinear matching') : null;
161
173
  }
162
174
 
163
175
  /** The loader's own rule normalizer, exported so anything that scores or validates a rule uses the SAME one the scanner loads
@@ -15,7 +15,7 @@ import { rollupFleet, renderFleetSummary } from '../fleet.js';
15
15
  import { statePath, ensureStateDir, safeWriteState } from '../state-dir.js';
16
16
  import { createSqliteJobStore } from './job-store.js';
17
17
  import { planRun, runJobs, loadArtifacts, reclaimDeadWorkers, childScan } from './job-runner.js';
18
- import { computeOrgHealth, renderOrgHealth } from './org-health.js';
18
+ import { computeOrgHealth, renderOrgHealth, applyArtifactProblems } from './org-health.js';
19
19
  import { discoverEdges, splitDeclared } from './discover-edges.js';
20
20
  import { buildEnterpriseGraph, finalizeGraph, validateEnterpriseGraph, attackPaths, vulnerableSet, componentBlastRadius, renderBlastRadius, INTERNET } from './enterprise-graph.js';
21
21
 
@@ -90,9 +90,7 @@ function runOfJob(store, id) {
90
90
  async function finishRun({ args, root, store, runId, manifest, manifestFile, interrupted }) {
91
91
  const { artifacts, problems } = loadArtifacts({ store, runId, root });
92
92
  const jobs = store.listJobs(runId);
93
- const health = computeOrgHealth({ manifest, jobs });
94
- for (const p of problems) { const r = health.repositories.find((x) => x.id === p.repoId); if (r) { r.state = 'failed'; r.error = p.problem; } }
95
- 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(', ')}`); }
93
+ const health = applyArtifactProblems(computeOrgHealth({ manifest, jobs }), manifest, problems);
96
94
 
97
95
  let previous = null;
98
96
  const rollupFile = statePath(root, ROLLUP_FILE);
@@ -14,7 +14,7 @@ import fs from 'node:fs';
14
14
  import path from 'node:path';
15
15
  import { load as loadYaml } from '../../util/yaml.js';
16
16
 
17
- export const FLEET_MANIFEST_SCHEMA_KEYS = Object.freeze(['organization', 'repositories', 'workers', 'perRepoTimeout', 'retries', 'resources']);
17
+ export const FLEET_MANIFEST_SCHEMA_KEYS = Object.freeze(['organization', 'repositories', 'workers', 'perRepoTimeout', 'retries', 'resources', 'cacheMaxAge']);
18
18
  export const REPO_KEYS = Object.freeze(['id', 'path', 'required', 'kind', 'internetFacing', 'environment', 'dataClasses', 'trustZone', 'calls', 'produces', 'consumes', 'reads', 'writes', 'providers', 'aliases']);
19
19
  export const RESOURCE_KEYS = Object.freeze(['id', 'kind', 'dataClasses', 'trustZone', 'internetFacing', 'environment', 'aliases']);
20
20
  export const REPO_KINDS = Object.freeze(['service', 'library', 'frontend', 'worker', 'gateway']);
@@ -93,12 +93,15 @@ export function validateFleetManifest(doc, { baseDir = '.' } = {}) {
93
93
  if (doc.workers != null && !(Number.isInteger(doc.workers) && doc.workers >= 1 && doc.workers <= 32)) errors.push('workers must be an integer in 1..32');
94
94
  if (doc.retries != null && !(Number.isInteger(doc.retries) && doc.retries >= 0 && doc.retries <= 5)) errors.push('retries must be an integer in 0..5');
95
95
  if (doc.perRepoTimeout != null && parseDuration(doc.perRepoTimeout, null) == null) errors.push(`perRepoTimeout is not a valid duration: ${doc.perRepoTimeout}`);
96
+ if (doc.cacheMaxAge != null && parseDuration(doc.cacheMaxAge, null) == null) errors.push(`cacheMaxAge is not a valid duration: ${doc.cacheMaxAge}`);
96
97
  if (errors.length) return { ok: false, errors };
97
98
  return {
98
99
  ok: true, errors: [],
99
100
  manifest: {
100
101
  organization: doc.organization, repositories: repos, resources,
101
102
  workers: doc.workers ?? 4, retries: doc.retries ?? 1, perRepoTimeoutMs: parseDuration(doc.perRepoTimeout, 20 * 60000),
103
+ // 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
104
+ cacheMaxAgeMs: parseDuration(doc.cacheMaxAge, 7 * 24 * 3600000),
102
105
  },
103
106
  };
104
107
  }
@@ -69,6 +69,21 @@ 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 const DEFAULT_CACHE_MAX_AGE_MS = 7 * 24 * 3600000;
73
+
74
+ /**
75
+ * 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
76
+ * record older than `maxAgeMs` (the manifest's `cacheMaxAge`, 7 days by default) is stale. Returns `{ fresh, cacheAgeMs }`.
77
+ */
78
+ export function cacheFreshness(hit, maxAgeMs, nowMs) {
79
+ if (!hit) return { fresh: false, cacheAgeMs: null };
80
+ const t = typeof hit.cachedAt === 'string' ? Date.parse(hit.cachedAt) : NaN;
81
+ if (Number.isNaN(t)) return { fresh: false, cacheAgeMs: null };
82
+ const cacheAgeMs = Math.max(0, nowMs - t);
83
+ const max = Number.isFinite(maxAgeMs) && maxAgeMs > 0 ? maxAgeMs : DEFAULT_CACHE_MAX_AGE_MS;
84
+ return { fresh: cacheAgeMs <= max, cacheAgeMs };
85
+ }
86
+
72
87
  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
88
  const repoById = new Map(manifest.repositories.map((r) => [r.id, r]));
74
89
  const cacheD = cacheDir ?? statePath(root, 'fleet', 'cache');
@@ -78,7 +93,11 @@ export async function runJobs({ manifest, store, runId, root, scan = childScan,
78
93
  const writeArtifact = (job, payload) => {
79
94
  const text = JSON.stringify({ schema: 'agentic-security/fleet-scan-artifact', version: 1, runId, repoId: job.repoId, epoch: job.epoch, commit: job.commit, ...payload });
80
95
  const file = path.join(artD, `${job.repoId}.${job.epoch}.json`);
81
- fs.writeFileSync(file, text, { mode: 0o600 });
96
+ // 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
97
+ // stray temp file, and the job stays leased until the lease expires and another worker redoes it
98
+ const tmp = `${file}.${process.pid}.tmp`;
99
+ fs.writeFileSync(tmp, text, { mode: 0o600 });
100
+ fs.renameSync(tmp, file);
82
101
  return { file, sha256: sha(text) };
83
102
  };
84
103
 
@@ -100,11 +119,16 @@ export async function runJobs({ manifest, store, runId, root, scan = childScan,
100
119
  idleSince = null;
101
120
  const repo = repoById.get(job.repoId);
102
121
  onEvent({ type: 'start', repo: job.repoId, attempt: job.attempts });
103
- const cached = useCache ? readCache(cacheD, job.cacheKey) : null;
122
+ const hit = useCache ? readCache(cacheD, job.cacheKey) : null;
123
+ // 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
124
+ // manifest's `cacheMaxAge` is stale: it is scanned again, never folded into a green organization as if it were current.
125
+ const { fresh, cacheAgeMs } = cacheFreshness(hit, manifest.cacheMaxAgeMs, now());
126
+ const cached = fresh ? hit : null;
127
+ if (hit && !cached) onEvent({ type: 'cache-stale', repo: job.repoId, cacheAgeMs });
104
128
  if (cached) {
105
- const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, lineage: cached.lineage ?? null, cached: true });
129
+ const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, lineage: cached.lineage ?? null, cached: true, cachedAt: cached.cachedAt, cacheAgeMs });
106
130
  const status = cached.entry?.scanHealth && cached.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
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 });
131
+ if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: cached.entry?.total ?? null, cached: true, cacheAgeMs, artifact: path.basename(a.file) }, now: now() })) onEvent({ type: 'done', repo: job.repoId, cached: true, status });
108
132
  continue;
109
133
  }
110
134
  const ac = new AbortController();
@@ -45,6 +45,29 @@ export function computeOrgHealth({ manifest, jobs }) {
45
45
  return { counts, assurance, reasons, repositories: repos };
46
46
  }
47
47
 
48
+ /**
49
+ * Fold artifact problems (a result whose file is missing or does not match its recorded hash) into the health (GA-09). The repository
50
+ * becomes `failed`; a required one turns the organization red, an optional one makes it degraded (it used to stay green); the counts
51
+ * move from whatever state the repository had, not always from `complete`. Returns the same object, changed in place.
52
+ */
53
+ export function applyArtifactProblems(health, manifest, problems) {
54
+ if (!Array.isArray(problems) || !problems.length) return health;
55
+ const stateKey = { complete: 'complete', partial: 'partial', failed: 'failed', 'timed-out': 'timedOut', cancelled: 'cancelled', 'not-run': 'notRun' };
56
+ let requiredHit = false; let optionalHit = false;
57
+ for (const p of problems) {
58
+ const r = health.repositories.find((x) => x.id === p.repoId);
59
+ if (!r) continue;
60
+ if (r.state !== 'failed') { health.counts[stateKey[r.state]]--; health.counts.failed++; }
61
+ r.state = 'failed'; r.error = p.problem;
62
+ const required = manifest.repositories.some((m) => m.id === p.repoId && m.required);
63
+ if (required) requiredHit = true; else optionalHit = true;
64
+ }
65
+ if (requiredHit) health.assurance = 'red';
66
+ else if (optionalHit && health.assurance === 'green') health.assurance = 'degraded';
67
+ health.reasons.push(`${problems.length} result(s) could not be verified: ${problems.map((p) => `${p.repoId} (${p.problem})`).join(', ')}`);
68
+ return health;
69
+ }
70
+
48
71
  const n = (x) => x.toLocaleString('en-US');
49
72
  export function renderOrgHealth(h) {
50
73
  const c = h.counts;