@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.
- package/CHANGELOG.md +13 -0
- package/dist/{c1e282247b06d51c8ffb.json → 01087ded263953062654.json} +2 -2
- package/dist/550.index.js +9 -8
- package/dist/5620.index.js +59 -10
- package/dist/5937.index.js +88 -16
- package/dist/7838.index.js +32 -5
- package/dist/9441.index.js +50 -7
- package/dist/9824.index.js +19 -13
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +2 -2
- package/src/discovery/detector-spec-synthesis.js +3 -3
- package/src/discovery/llm-invoke.js +3 -3
- package/src/egress/transport.js +148 -0
- package/src/llm-validator/index.js +4 -2
- package/src/llm-validator/local-endpoint.js +4 -1
- package/src/llm-validator/ollama-provider.js +8 -6
- package/src/posture/adversary-agent.js +27 -10
- package/src/posture/custom-rules.js +13 -1
- package/src/posture/fleet/cli.js +2 -4
- package/src/posture/fleet/fleet-manifest.js +4 -1
- package/src/posture/fleet/job-runner.js +28 -4
- package/src/posture/fleet/org-health.js +23 -0
- package/src/posture/fleet/scan-cache.js +1 -1
- package/src/posture/flow-narration.js +10 -8
- package/src/posture/llm-redteam.js +3 -1
- package/src/posture/provenance/coordinator.js +8 -0
- package/src/posture/provenance/origin-resolver.js +17 -4
- package/src/posture/rule-compiler/cli.js +56 -8
- package/src/posture/rule-compiler/corpus.js +10 -2
- package/src/posture/rule-compiler/ir.js +10 -1
- package/src/posture/rule-compiler/promotion.js +12 -2
- package/src/posture/rule-compiler/regression.js +1 -1
- package/src/posture/rule-compiler/store.js +27 -7
- package/src/posture/strategic-metrics.js +50 -7
- package/src/sca/llm-function-extract.js +17 -13
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
78
|
+
const res = await modelFetch(endpoint, {
|
|
78
79
|
method: 'POST',
|
|
79
80
|
headers: { 'content-type': 'application/json' },
|
|
80
81
|
body: JSON.stringify({ prompt }),
|
|
81
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
101
|
-
if (!
|
|
102
|
-
const
|
|
103
|
-
const
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
180
|
-
|
|
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
|
-
|
|
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
|
package/src/posture/fleet/cli.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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;
|