@clear-capabilities/agentic-security-scanner 0.176.0 → 0.177.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/bin/agentic-security.js +9 -2
- package/dist/1919.index.js +1 -1
- package/dist/2066.index.js +2 -1
- package/dist/2584.index.js +19 -1
- package/dist/{4978bec8fdf5c733dc85.json → 37d64df006576248d0d8.json} +5 -2
- package/dist/5456.index.js +291 -0
- package/dist/5620.index.js +147 -23
- package/dist/5937.index.js +672 -0
- package/dist/7838.index.js +70 -25
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/{df1e8683e7eda7b2b79c.js → b4289e728659450b5e19.js} +4 -1
- package/package.json +5 -2
- package/src/posture/custom-rules.js +54 -8
- package/src/posture/dnr/exploit-correlation.js +25 -0
- package/src/posture/evaluation/lab/dataset.js +1 -1
- package/src/posture/evaluation/lab/report.js +19 -1
- package/src/posture/fleet/cli.js +22 -10
- package/src/posture/fleet/enterprise-graph.js +44 -3
- package/src/posture/fleet/job-runner.js +7 -7
- package/src/posture/fleet/job-store.js +3 -1
- package/src/posture/fleet/lineage-summary.js +55 -0
- package/src/posture/fleet/scan-child.js +4 -1
- package/src/posture/proof-fabric/generate.js +2 -1
- package/src/posture/proof-fabric/runner.js +24 -2
- package/src/posture/rule-compiler/cli.js +40 -6
- package/src/posture/rule-compiler/corpus.js +3 -7
- package/src/posture/rule-compiler/ir.js +52 -9
- package/src/posture/rule-compiler/proposal.js +14 -9
- package/src/posture/rule-compiler/regression.js +95 -0
- package/src/posture/rule-compiler/store.js +29 -1
- package/src/posture/rule-pack-signing.js +4 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
6a72326303aab625b38a9efa798132a7652377aeaa8339131d043634fd8266ec agentic-security.mjs
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// controller): scanning a repository for the fleet must not change that repository.
|
|
9
9
|
import { runFleet } from '../fleet.js';
|
|
10
10
|
import { runScan } from '../../runScan.js';
|
|
11
|
+
import { summarizeLineage } from './lineage-summary.js';
|
|
11
12
|
|
|
12
13
|
export const BEGIN = '@@FLEET-SCAN-BEGIN@@';
|
|
13
14
|
export const END = '@@FLEET-SCAN-END@@';
|
|
@@ -21,7 +22,9 @@ async function main(repoPath) {
|
|
|
21
22
|
const entry = res.results?.[0];
|
|
22
23
|
if (!entry) throw new Error('the scan produced no entry');
|
|
23
24
|
const findings = (captured?.findings ?? []).slice(0, MAX_FINDINGS).map(slim);
|
|
24
|
-
|
|
25
|
+
// the Code Boundaries graph exists only when the controller asked for it (AGENTIC_SECURITY_LINEAGE_DEEP=1); it is summarized, never copied whole
|
|
26
|
+
const lineage = process.env.AGENTIC_SECURITY_LINEAGE_DEEP === '1' ? summarizeLineage(captured?.lineageGraph) : null;
|
|
27
|
+
process.stdout.write(`${BEGIN}${JSON.stringify({ entry, findings, findingsTruncated: (captured?.findings?.length ?? 0) > MAX_FINDINGS, lineage })}${END}`);
|
|
25
28
|
}
|
|
26
29
|
|
|
27
30
|
if (process.argv[1] && import.meta.url === new URL(`file://${process.argv[1]}`).href) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.177.0",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -85,7 +85,7 @@
|
|
|
85
85
|
"test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
|
|
86
86
|
"test:glob": "node --test test/glob-compat.test.js",
|
|
87
87
|
"test:sast": "node --test test/cpp-hardcoded-secret.test.js test/sibling-guard.test.js test/comment-blindness.test.js test/crypto-specialist.test.js test/llm.test.js test/llm-cost-advisor.test.js test/llm-owasp.test.js test/logic.test.js test/authz.test.js test/model-load.test.js test/prompt-template.test.js test/business-logic.test.js test/python-sinks.test.js test/phase1-detectors.test.js test/phase2-detectors.test.js test/phase3-v3.test.js test/phase7-extensions.test.js test/phase8-extensions.test.js test/new-cwe-detectors.test.js test/file-upload.test.js test/llmsecops-detectors.test.js test/db-taint.test.js test/dart-swift.test.js test/zip-slip-containment-guard.test.js test/zip-slip-go.test.js test/redos-anchoring.test.js test/path-guard-validator-call.test.js test/redos-nfa.test.js test/weak-randomness.test.js test/csharp-pipeline.test.js test/csharp-cross-file.test.js test/post-quantum-crypto.test.js test/web3-advanced.test.js test/cloud-iam-k8s.test.js test/crypto-protocol.test.js test/ml-supply-chain.test.js test/mcp-audit-scan.test.js test/wrong-context-sanitizer.test.js test/sanitizer-context.test.js test/frontend-hygiene.test.js test/csv-injection.test.js test/stored-taint.test.js test/tree-sitter-sinks.test.js test/kotlin-structural.test.js test/ruby-php-structural.test.js test/java-csharp-structural.test.js test/java-structural-cross-file.test.js test/guard-recognition.test.js test/guard-window-identifier-correlation.test.js test/js-python-framework-structural.test.js test/go-structural.test.js test/secret-concat.test.js test/secret-redaction.test.js test/xss-reflected-multilang.test.js test/code-injection-multilang.test.js test/xxe-multilang.test.js test/xpath-injection-multilang.test.js test/gapfill-batch13.test.js test/agent-untrusted-flow.test.js test/api-authz.test.js test/event-entrypoint.test.js test/iac-terraform.test.js test/cross-service.test.js test/rbac-consistency.test.js test/adversarial-robustness.test.js test/secret-entropy.test.js test/comment-strip.test.js test/juliet-path-filter-gate.test.js test/java-bench-shape-opt-in.test.js test/java-ast-folding.test.js test/java-bench-extras.test.js test/reachability-missing-callgraph-data.test.js test/rate-limit.test.js test/auth-signals.test.js test/convention-deviation.test.js test/resource-exhaustion.test.js test/resource-exhaustion-realworld.test.js test/redirect-toctou.test.js test/redirect-toctou-realworld.test.js test/codegen-sink.test.js test/ownership-authz.test.js test/ownership-authz-realworld.test.js test/t21-precision-audit.test.js test/secrets-coverage.test.js test/prompt-injection-payloads.test.js test/iac-cloud-templates.test.js test/ruby-path-join.test.js test/sast/manifest-line-tracking.test.js test/rust-sast.test.js test/java-deserialization-precision.test.js test/pipeline-env-secret.test.js test/mark-used-vuln-functions.test.js",
|
|
88
|
-
"test:posture": "node --test test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js test/load-factor.test.js test/fleet/job-store.test.js test/fleet/enterprise-graph.test.js test/fleet/discover-edges.test.js test/fleet/job-runner.test.js test/fleet/fleet-org-cli.test.js test/eval-lab/dataset-neutralize.test.js test/eval-lab/arms-score-stats.test.js test/eval-lab/run-report.test.js test/eval-lab/router.test.js test/eval-lab/arm-ollama.test.js test/cli/eval-lab-cli.test.js test/dnr/ir-and-outputs.test.js test/dnr/logs-and-hunt.test.js test/dnr/window-store-view.test.js test/cli/dnr-cli.test.js test/proof-fabric/core.test.js test/proof-fabric/container.test.js test/proof-fabric/generate.test.js test/proof-fabric/siblings.test.js test/proof-fabric/generate-python.test.js test/mcp-proof.test.js test/proof-fabric/fix-store-runner.test.js test/proof-fabric/fixtures.test.js test/cli/proof-fabric-cli.test.js test/rule-compiler/core.test.js test/rule-compiler/synthesis.test.js test/cli/rule-compiler-cli.test.js test/fleet/fleet-graph.test.js test/fleet/fleet-manifest.test.js test/fleet/fleet-cli.test.js test/artifact/safe-tar.test.js test/artifact/image-archive.test.js test/artifact/filesystem.test.js test/artifact/layer-secrets.test.js test/artifact/inventory.test.js test/artifact/vuln-correlate.test.js test/artifact/signature.test.js test/artifact/provenance-graph.test.js test/artifact/mismatch.test.js test/artifact/vex.test.js test/artifact/registry.test.js test/artifact/sources-cosign-evidence.test.js test/cli/artifact-sources-cli.test.js test/cli/artifact-provenance-cli.test.js",
|
|
88
|
+
"test:posture": "node --test test/rule-pack-signing.test.js test/threat-model.test.js test/git-hardening.test.js test/privacy-framework.test.js test/material-change.test.js test/drift.test.js test/scorecard.test.js test/accuracy-scorecard.test.js test/scorecard-gate.test.js test/predeploy-gate.test.js test/release-check.test.js test/pre-push-gate.test.js test/dependency-currency.test.js test/package-contents-check.test.js test/gate-verdict-cache.test.js test/run-unit-tests.test.js test/temp-leak-guard.test.js test/release-workflow.test.js test/release-workflow-structure.test.js test/independent-population.test.js test/why-missed.test.js test/independent-scoring.test.js test/evidence-bundle.test.js test/obligation-evidence-pack.test.js test/policy-bundle.test.js test/discovery-memory.test.js test/mttr.test.js test/license-policy.test.js test/aibom.test.js test/sbom.test.js test/api-inventory.test.js test/iam-policy.test.js test/container.test.js test/container-runtime.test.js test/image-packages.test.js test/kev.test.js test/dep-confusion.test.js test/sca-deprecated.test.js test/sca-batch.test.js test/composite-risk.test.js test/epss-enrich.test.js test/sca-coverage.test.js test/gradle-deps.test.js test/sca-route-reachable.test.js test/sca-policy.test.js test/sca-verdict.test.js test/install-script.test.js test/sca-linked-findings.test.js test/packs.test.js test/flow-narration.test.js test/regression-test-gen.test.js test/deterministic-fix.test.js test/falsification.test.js test/verification-separation.test.js test/logic-claims.test.js test/attestation.test.js test/verify-attestation-cli.test.js test/comparison.test.js test/determinism-cross-machine.test.js test/fix-honesty-gate.test.js test/model-routing.test.js test/root-cause-sweep.test.js test/entrypoint-inventory.test.js test/relevance.test.js test/untrusted.test.js test/agent-hardening.test.js test/rule-synthesis.test.js test/policy-gate.test.js test/agents-memory.test.js test/cve-lookup.test.js test/cve-alert-daemon.test.js test/fix-verify-loop.test.js test/fix-verify-tests.test.js test/fix-acceptance.test.js test/exploitability-probability.test.js test/history-scan.test.js test/viral-features.test.js test/viral-v074.test.js test/state-dir.test.js test/ignore-pragma.test.js test/license-graph.test.js test/secret-live-check.test.js test/attack-taxonomy.test.js test/triage-memory.test.js test/pr-augment.test.js test/chat-batch2.test.js test/chat-batch3.test.js test/chat-batch4.test.js test/chat-batch5.test.js test/chat-batch6.test.js test/risk-scenario-disclosure.test.js test/egress-policy.test.js test/egress-policy-completeness.test.js test/egress-policy-integration.test.js test/router.test.js test/legacy-alias-redirect.test.js test/cache-economics.test.js test/coverage-report.test.js test/corpus-status.test.js test/provenance.test.js test/secret-history.test.js test/execution-proof.test.js test/fix-metrics.test.js test/corpus-enroll.test.js test/prove-findings.test.js test/corpus-match.test.js test/corpus-provenance.test.js test/learning-quorum.test.js test/llm-cache-integrity.test.js test/proof-artifact.test.js test/providers.test.js test/fleet.test.js test/autopilot.test.js test/autopilot-cli.test.js test/verifier-independence.test.js test/integrity-legacy-key.test.js test/suppression-visibility.test.js test/cost-ceiling.test.js test/local-endpoint.test.js test/ollama-provider.test.js test/ollama-capabilities.test.js test/ollama-offline-egress.test.js test/ollama-fix-proposal.test.js test/ollama-explain-proposal.test.js test/ollama-poc-proposal.test.js test/ollama-model-probe.test.js test/ollama-agent-tools.test.js test/ollama-agent-loop.test.js test/ollama-e2e.test.js test/ollama-oom-feedback.test.js test/ollama-doc-drift.test.js test/model-trust.test.js test/vuln-archaeology.test.js test/scan-checkpoint.test.js test/llm-redact.test.js test/world-class-batch2.test.js test/world-class-modules.test.js test/privacy-ir-adapter.test.js test/privacy-taxonomy.test.js test/privacy-sink-policy.test.js test/privacy-governance.test.js test/privacy-inventory.test.js test/integration-end-to-end.test.js test/streak-cli-wiring.test.js test/stack-playbook.test.js test/machine-output-flag.test.js test/stdout-flush.test.js test/format-determinism.test.js test/only-flag-channels.test.js test/confidence.test.js test/security-trend.test.js test/calibration-drift.test.js test/validator-metrics.test.js test/clustering-ir-taint-line.test.js test/concurrency-cwe.test.js test/compliance-mapping-liveness.test.js test/module-artifact-liveness.test.js test/supply-chain-provenance-completeness.test.js test/compliance-severity-threshold.test.js test/compliance-severity-policy.test.js test/compliance-evidence-binding.test.js test/compliance-evidence-signing.test.js test/compliance-file-contains.test.js test/label-isolation.test.js test/evidence-grade-wording.test.js test/llm-model-status.test.js test/egress-policy-constraints.test.js test/egress-redact.test.js test/egress-audit.test.js test/egress-approved-providers.test.js test/family-registry.test.js test/coverage-strength.test.js test/framework-provenance.test.js test/framework-provenance-controlrefs.test.js test/third-party-verification.test.js test/proof-coverage.test.js test/calibration-holdout.test.js test/independent-population-gate.test.js test/catalog-freshness.test.js test/incremental-parity.test.js test/surface-smoke.test.js test/fix-coverage.test.js test/raw-source-carveout.test.js test/mlbom-conformance.test.js test/deploy-gate-replay.test.js test/sbom-conformance.test.js test/unscored-claims.test.js test/mcp-rug-pull.test.js test/agent-boundary-taint.test.js test/confinement-adversarial.test.js test/comment-strip-cost.test.js test/reachability-claim.test.js test/entrypoint-breadth.test.js test/dep-file-admission.test.js test/custom-rules-freshness.test.js test/posture/provenance-schema.test.js test/posture/provenance-cache.test.js test/helpers/build-git-fixture.test.js test/posture/provenance-git-evidence.test.js test/posture/provenance-dag-walk.test.js test/posture/provenance-confidence.test.js test/posture/provenance-predicate-replay.test.js test/posture/provenance-origin-resolver.test.js test/posture/provenance-ai-authorship.test.js test/posture/provenance-branch-entry.test.js test/posture/provenance-evidence-attribution.test.js test/posture/provenance-lifecycle.test.js test/posture/provenance-missing-control-resolver.test.js test/posture/provenance-missing-control-wiring.test.js test/posture/provenance-coordinator.test.js test/posture/provenance-sca-origin.test.js test/posture/provenance-transitive-sca.test.js test/posture/provenance-coordinator-sca.test.js test/posture/provenance-providers.test.js test/posture/provenance-evidence-bundle.test.js test/posture/provenance-repo-lineage.test.js test/posture/provenance-secrets-logic.test.js test/cli/provenance-flags.test.js test/cli/attest-provenance.test.js test/cli/attest-obligations.test.js test/cli/models.test.js test/cli/setup-llm.test.js test/cli/fix-retry.test.js test/cli/compliance-walkthrough-lineage.test.js test/cli/dataflow-export-privacy.test.js test/cli/dataflow-export-briefing.test.js test/cli/lineage-snapshot-persist.test.js test/cli/dataflow-diff.test.js test/cli/dataflow-watch.test.js test/cli/dataflow-recipients.test.js test/cli/dataflow-coverage.test.js test/security/provenance-safety.test.js test/security/provenance-injection.test.js test/posture/remediation-ledger.test.js test/posture/assurance-baseline.test.js test/posture/assurance-contracts.test.js test/posture/assurance-config.test.js test/evidence-issuer.test.js test/load-factor.test.js test/fleet/job-store.test.js test/fleet/enterprise-graph.test.js test/fleet/discover-edges.test.js test/fleet/code-boundaries.test.js test/fleet/job-runner.test.js test/fleet/fleet-org-cli.test.js test/eval-lab/dataset-neutralize.test.js test/eval-lab/arms-score-stats.test.js test/eval-lab/run-report.test.js test/eval-lab/router.test.js test/eval-lab/arm-ollama.test.js test/eval-lab/import-independent.test.js test/cli/eval-lab-cli.test.js test/dnr/ir-and-outputs.test.js test/dnr/logs-and-hunt.test.js test/dnr/window-store-view.test.js test/dnr/exploit-correlation.test.js test/cli/dnr-cli.test.js test/proof-fabric/core.test.js test/proof-fabric/container.test.js test/proof-fabric/generate.test.js test/proof-fabric/siblings.test.js test/proof-fabric/generate-python.test.js test/cli/proof-lifecycle-demo.test.js test/mcp-proof.test.js test/proof-fabric/fix-store-runner.test.js test/proof-fabric/fixtures.test.js test/cli/proof-fabric-cli.test.js test/rule-compiler/core.test.js test/rule-compiler/synthesis.test.js test/rule-compiler/structure.test.js test/cli/rule-compiler-cli.test.js test/fleet/fleet-graph.test.js test/fleet/fleet-manifest.test.js test/fleet/fleet-cli.test.js test/artifact/safe-tar.test.js test/artifact/image-archive.test.js test/artifact/filesystem.test.js test/artifact/layer-secrets.test.js test/artifact/inventory.test.js test/artifact/vuln-correlate.test.js test/artifact/signature.test.js test/artifact/provenance-graph.test.js test/artifact/mismatch.test.js test/artifact/vex.test.js test/artifact/registry.test.js test/artifact/sources-cosign-evidence.test.js test/cli/artifact-sources-cli.test.js test/cli/artifact-provenance-cli.test.js",
|
|
89
89
|
"test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-rust.test.js test/rust-taint-flow.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js test/dataflow-expr-taint-scaling.test.js test/evaluation/guard-dominance.test.js test/evaluation/engine-mechanisms.test.js test/rx-exec-receiver.test.js",
|
|
90
90
|
"test:mcp": "node --test test/mcp.test.js test/mcp-apply-fix-multifile.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js test/capabilities/tools.test.js",
|
|
91
91
|
"test:report": "node --test test/report-scan-invariants.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/scan-health.test.js test/finding-schema.test.js test/pipeline/finding-schema-provenance.test.js test/evidence-provenance.test.js test/report/provenance-output.test.js test/report/provenance-format-parity.test.js test/legacy-compat.test.js test/annotator-runner.test.js test/detector-runner.test.js test/detector-errors.test.js test/analyzer-supervisor.test.js test/cascade-worker-pool.test.js test/cascade-pool-wiring.test.js test/coverage-ledger.test.js test/coverage-ledger-completeness.test.js test/assurance-mode.test.js test/artifact-registry.test.js test/artifact-registry-completeness.test.js test/retention-policy.test.js test/state-lifecycle-report.test.js test/legal-hold.test.js test/production-feedback.test.js test/calibration-feedback.test.js test/encryption-provider.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js test/oscal-conformance.test.js test/cli-fix-apply.test.js test/apply-fix-service.test.js test/high-impact-approval-gate.test.js test/approver-registry.test.js test/suppression-exceptions.test.js test/fix-history.test.js test/producer-registry.test.js test/producer-collector.test.js test/enrichment-completion.test.js",
|
|
@@ -175,6 +175,9 @@
|
|
|
175
175
|
"check:bundle-source": "node ../scripts/check-bundle-source.mjs",
|
|
176
176
|
"release:prepare": "node ../scripts/release-prepare.mjs",
|
|
177
177
|
"ship": "node ../scripts/ship.mjs",
|
|
178
|
+
"eval:import-independent": "node ../scripts/eval-lab/import-independent.mjs",
|
|
179
|
+
"bench:proof-fabric": "node ../bench/proof-fabric/measure.mjs",
|
|
180
|
+
"bench:fleet-scale": "node ../bench/fleet-scale/run.mjs",
|
|
178
181
|
"bench:self-scan": "node ../bench/self-scan/measure.mjs",
|
|
179
182
|
"bench:self-scan:check": "node ../bench/self-scan/check.mjs",
|
|
180
183
|
"bench:nist-800-171-labels:check": "node ../bench/nist-800-171-labels/check.mjs",
|
|
@@ -23,6 +23,13 @@
|
|
|
23
23
|
// match.allOf: [regex…] — every regex must match somewhere in the file
|
|
24
24
|
// match.notMatch: regex — kill if this regex matches
|
|
25
25
|
// match.window: N — when allOf is set, all matches must fall within N lines
|
|
26
|
+
// match.fileRequires: [regex…]: file-level preconditions (e.g. a framework marker); every one must match somewhere in the file
|
|
27
|
+
// match.guardedBy: regex : a sanitizer or dominating guard: a hit is dropped when this matches within `guardWindow` lines BEFORE
|
|
28
|
+
// (or on) the hit's line. Unlike notMatch it is local to the hit, not the whole file.
|
|
29
|
+
// match.guardWindow: N : how far back guardedBy looks (default: the rule's window, else 5)
|
|
30
|
+
// match.excludeLine: [regex…]: a hit is dropped when its own line matches any of these
|
|
31
|
+
// (the last four are the structural vocabulary the rule compiler's IR compiles to; a rule that uses any of them is evaluated
|
|
32
|
+
// against EVERY anchor and reports the first that survives, not only the first anchor in the file)
|
|
26
33
|
//
|
|
27
34
|
// `agentic-security rule test <fixture-glob>` runs every rule against every
|
|
28
35
|
// file and prints PASS/FAIL.
|
|
@@ -153,6 +160,10 @@ function _isSafePattern(p) {
|
|
|
153
160
|
return !_REDOS_NESTED_RE.test(p);
|
|
154
161
|
}
|
|
155
162
|
|
|
163
|
+
/** The loader's own rule normalizer, exported so anything that scores or validates a rule uses the SAME one the scanner loads
|
|
164
|
+
* with (and so a rule the loader would reject is never reported as working). Returns null for a rule the loader refuses. */
|
|
165
|
+
export function normalizeCustomRule(r, fp = '(compiled)') { return normalizeRule(r, fp); }
|
|
166
|
+
|
|
156
167
|
function normalizeRule(r, fp) {
|
|
157
168
|
if (!r || !r.id || !r.match) return null;
|
|
158
169
|
const m = r.match;
|
|
@@ -177,6 +188,22 @@ function normalizeRule(r, fp) {
|
|
|
177
188
|
let notMatch = null;
|
|
178
189
|
if (m.notMatch) { try { notMatch = new RegExp(m.notMatch, 'm'); } catch {} }
|
|
179
190
|
|
|
191
|
+
// Structural vocabulary (rule compiler IR): every regex passes the same ReDoS guard as the main patterns, and a rule that
|
|
192
|
+
// cannot be compiled is rejected whole rather than loaded with a constraint silently missing.
|
|
193
|
+
const compileAll = (list) => {
|
|
194
|
+
const out = [];
|
|
195
|
+
for (const p of list) {
|
|
196
|
+
if (typeof p !== 'string' || !p || !_isSafePattern(p)) { console.error(`agentic-security: rejected unsafe or non-string structural regex in ${r.id}`); return null; }
|
|
197
|
+
try { out.push(new RegExp(p, 'm')); } catch (e) { console.error(`agentic-security: invalid structural regex in ${r.id}: ${e.message}`); return null; }
|
|
198
|
+
}
|
|
199
|
+
return out;
|
|
200
|
+
};
|
|
201
|
+
let fileRequires = null; let guardedBy = null; let excludeLine = null;
|
|
202
|
+
if (m.fileRequires != null) { if (!Array.isArray(m.fileRequires)) return null; fileRequires = compileAll(m.fileRequires); if (!fileRequires) return null; }
|
|
203
|
+
if (m.guardedBy != null) { const g = compileAll([m.guardedBy]); if (!g) return null; guardedBy = g[0]; }
|
|
204
|
+
if (m.excludeLine != null) { if (!Array.isArray(m.excludeLine)) return null; excludeLine = compileAll(m.excludeLine); if (!excludeLine) return null; }
|
|
205
|
+
const guardWindow = Number.isInteger(m.guardWindow) && m.guardWindow > 0 && m.guardWindow <= 200 ? m.guardWindow : (m.window || 5);
|
|
206
|
+
|
|
180
207
|
// SentQL extensions (Sentinel-parity FR-DSL-1):
|
|
181
208
|
// llm_validate: { prompt, min_confidence }
|
|
182
209
|
// path: { must_traverse: [...], must_not_traverse: [...] }
|
|
@@ -208,6 +235,8 @@ function normalizeRule(r, fp) {
|
|
|
208
235
|
notMatch,
|
|
209
236
|
requireAll: Array.isArray(m.allOf),
|
|
210
237
|
windowLines: m.window || 50,
|
|
238
|
+
fileRequires, guardedBy, guardWindow, excludeLine,
|
|
239
|
+
structural: !!(fileRequires || guardedBy || excludeLine),
|
|
211
240
|
sourceFile: fp,
|
|
212
241
|
llmValidate,
|
|
213
242
|
pathConstraints,
|
|
@@ -260,17 +289,34 @@ export function runRule(rule, file, content) {
|
|
|
260
289
|
});
|
|
261
290
|
|
|
262
291
|
const findings = [];
|
|
292
|
+
// structural preconditions and per-hit filters (see the header): file-level requirements first
|
|
293
|
+
if (rule.fileRequires && !rule.fileRequires.every((rx) => rx.test(content))) return [];
|
|
294
|
+
const lineText = (n) => lines[n - 1] ?? '';
|
|
295
|
+
const guardLines = [];
|
|
296
|
+
if (rule.guardedBy) {
|
|
297
|
+
const g = new RegExp(rule.guardedBy.source, 'gm');
|
|
298
|
+
let gm; const deadline = Date.now() + EXEC_TIMEOUT_MS;
|
|
299
|
+
while ((gm = g.exec(content)) !== null) { guardLines.push(offsetToLine(gm.index)); if (gm.index === g.lastIndex) g.lastIndex++; if (Date.now() > deadline) break; }
|
|
300
|
+
}
|
|
301
|
+
const survives = (hit) => {
|
|
302
|
+
if (rule.excludeLine && rule.excludeLine.some((rx) => rx.test(lineText(hit.line)))) return false;
|
|
303
|
+
if (rule.guardedBy && guardLines.some((gl) => gl <= hit.line && hit.line - gl <= rule.guardWindow)) return false;
|
|
304
|
+
return true;
|
|
305
|
+
};
|
|
263
306
|
if (rule.requireAll) {
|
|
264
307
|
if (matches.some(l => l.length === 0)) return [];
|
|
265
|
-
// Pick the earliest match in the first list, verify the others fall within window.
|
|
266
|
-
const
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
308
|
+
// Pick the earliest match in the first list, verify the others fall within window. A structural rule tries every anchor in order.
|
|
309
|
+
const anchors = rule.structural ? matches[0] : [matches[0][0]];
|
|
310
|
+
for (const anchor of anchors) {
|
|
311
|
+
const close = matches.slice(1).every(list =>
|
|
312
|
+
list.some(x => Math.abs(x.line - anchor.line) <= rule.windowLines)
|
|
313
|
+
);
|
|
314
|
+
if (!close || !survives(anchor)) continue;
|
|
315
|
+
findings.push(toFinding(rule, file, anchor));
|
|
316
|
+
break;
|
|
317
|
+
}
|
|
272
318
|
} else {
|
|
273
|
-
for (const m of matches[0]) findings.push(toFinding(rule, file, m));
|
|
319
|
+
for (const m of matches[0]) if (survives(m)) findings.push(toFinding(rule, file, m));
|
|
274
320
|
}
|
|
275
321
|
return findings;
|
|
276
322
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The finding-level form of the runtime twin's RUNTIME_EXPLOIT_CORRELATED (PRD 2): a finding is exploit-correlated when three independent
|
|
2
|
+
// pieces of evidence, each already recorded by its own subsystem, agree:
|
|
3
|
+
// 1. an execution-backed proof (EXPLOIT_CONFIRMED or beyond), so the flaw is real and exploitable;
|
|
4
|
+
// 2. a historical hunt that OBSERVED exploit-shaped requests in this installation's own logs;
|
|
5
|
+
// 3. (checked by the caller) a runtime trace showing the vulnerable function, line or route was exercised.
|
|
6
|
+
// The graph is destination-shaped and carries no finding locations, so this correlation lives on the FINDING, where all three records
|
|
7
|
+
// are keyed by the same id. A tampered record is never used, and a missing one is "not established", not "no".
|
|
8
|
+
import { listProofs } from '../proof-fabric/store.js';
|
|
9
|
+
import { stateRank } from '../proof-fabric/states.js';
|
|
10
|
+
import { listRecords } from './store.js';
|
|
11
|
+
|
|
12
|
+
export function exploitCorrelationOf(root, findingId) {
|
|
13
|
+
const proofs = listProofs(root, findingId);
|
|
14
|
+
const proof = proofs.find((p) => p.record && !p.tampered)?.record ?? null;
|
|
15
|
+
const confirmed = !!proof && stateRank(proof.state) >= stateRank('EXPLOIT_CONFIRMED');
|
|
16
|
+
const hunts = listRecords(root, findingId, 'hunt').filter((r) => r.record && !r.tampered).map((r) => r.record);
|
|
17
|
+
const observed = hunts.find((h) => h.verdict === 'OBSERVED') ?? null;
|
|
18
|
+
const correlated = confirmed && !!observed;
|
|
19
|
+
return {
|
|
20
|
+
correlated,
|
|
21
|
+
proof: proof ? { state: proof.state, confirmed } : null,
|
|
22
|
+
hunt: observed ? { verdict: 'OBSERVED', window: observed.window ?? null, matches: observed.matches?.count ?? null } : (hunts[0] ? { verdict: hunts[0].verdict } : null),
|
|
23
|
+
basis: correlated ? 'a confirmed exploit proof and exploit-shaped requests observed in historical logs' : (!proof ? 'no proof record' : !confirmed ? `the proof reached ${proof.state}, not a confirmed exploit` : !hunts.length ? 'no historical hunt has been run' : `the hunt verdict was ${hunts[0].verdict}, which is not an observation`),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -23,7 +23,7 @@ export const SPLITS = Object.freeze(['dev', 'validation', 'sealed']);
|
|
|
23
23
|
const ENTRY_KINDS = Object.freeze(['vulnerable', 'fixed', 'hard-negative']);
|
|
24
24
|
export const HARD_NEGATIVE_CATEGORIES = Object.freeze(['sanitization', 'authorization', 'parameterization', 'bounds-check', 'correct-crypto', 'safe-wrapper']);
|
|
25
25
|
const SOURCE_KINDS = Object.freeze(['directory', 'git-pairs']);
|
|
26
|
-
const PERMITTED_LICENSES = Object.freeze(['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MPL-2.0', '0BSD', 'Unlicense', 'CC0-1.0', 'GPL-2.0-only', 'GPL-2.0-or-later', 'GPL-3.0-only', 'GPL-3.0-or-later', 'LGPL-2.1-only', 'LGPL-2.1-or-later', 'LGPL-3.0-only', 'LGPL-3.0-or-later', 'AGPL-3.0-only', 'AGPL-3.0-or-later']);
|
|
26
|
+
export const PERMITTED_LICENSES = Object.freeze(['MIT', 'Apache-2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'ISC', 'MPL-2.0', '0BSD', 'Unlicense', 'CC0-1.0', 'GPL-2.0-only', 'GPL-2.0-or-later', 'GPL-3.0-only', 'GPL-3.0-or-later', 'LGPL-2.1-only', 'LGPL-2.1-or-later', 'LGPL-3.0-only', 'LGPL-3.0-or-later', 'AGPL-3.0-only', 'AGPL-3.0-or-later']);
|
|
27
27
|
const MAX_MANIFEST_BYTES = 5 * 1024 * 1024;
|
|
28
28
|
const MAX_ENTRIES = 20000;
|
|
29
29
|
const ID_RE = /^[a-z0-9][a-z0-9._-]{0,79}$/;
|
|
@@ -74,8 +74,22 @@ export function summarizeRun(run, { budgetUsd = 10 } = {}) {
|
|
|
74
74
|
const primary = Object.fromEntries(Object.entries(tasks).map(([t, m]) => [t, m[PRIMARY[t]]]));
|
|
75
75
|
return { id: arm.id, kind: arm.kind, model: arm.model, runs: rs.length, tasks, economics: econ, intelligencePerDollar: ipd, dollarsPer, primary };
|
|
76
76
|
});
|
|
77
|
+
// PRD 6 reliability: provider variance. The SAME model id reached through different providers is not necessarily the same behaviour
|
|
78
|
+
// (quantization, serving stack, sampling defaults), so when a run has such arms the spread between them is reported on its own.
|
|
79
|
+
const byModel = new Map();
|
|
80
|
+
for (const a of arms) if (a.kind === 'model' && a.model?.id) byModel.set(a.model.id, [...(byModel.get(a.model.id) ?? []), a]);
|
|
81
|
+
const providerVariance = [];
|
|
82
|
+
for (const [modelId, group] of byModel) {
|
|
83
|
+
if (new Set(group.map((g) => g.model.provider)).size < 2) continue;
|
|
84
|
+
for (const task of new Set(group.flatMap((g) => Object.keys(g.primary)))) {
|
|
85
|
+
const vals = group.map((g) => ({ arm: g.id, provider: g.model.provider, mean: g.primary[task]?.mean ?? null })).filter((v) => v.mean != null);
|
|
86
|
+
if (vals.length < 2) continue;
|
|
87
|
+
const means = vals.map((v) => v.mean);
|
|
88
|
+
providerVariance.push({ model: modelId, task, spread: Math.max(...means) - Math.min(...means), arms: vals, representative: group.every((g) => g.primary[task]?.representative) });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
77
91
|
return {
|
|
78
|
-
schema: REPORT_SCHEMA, version: 1, runId: run.runId, runDigest: run.digest,
|
|
92
|
+
schema: REPORT_SCHEMA, version: 1, runId: run.runId, runDigest: run.digest, providerVariance,
|
|
79
93
|
header: { scannerCommit: run.scanner?.commit ?? null, scannerVersion: run.scanner?.version ?? null, datasetId: run.dataset.id, datasetVersion: run.dataset.version, datasetHash: run.dataset.hash, split: run.dataset.split, models: run.arms.filter((a) => a.model).map((a) => `${a.model.provider}/${a.model.id}${a.model.local ? ' (local)' : ''}`), config: run.config, machine: run.machine, startedAt: run.startedAt, finishedAt: run.finishedAt },
|
|
80
94
|
leakageAudit: run.leakageAudit, complete: run.complete, incompleteReasons: run.incompleteReasons, arms,
|
|
81
95
|
notes: arms.some((a) => Object.values(a.primary).some((s) => s && !s.representative)) ? ['At least one figure rests on fewer than three runs and is marked not representative.'] : [],
|
|
@@ -111,6 +125,10 @@ export function toMarkdown(rep) {
|
|
|
111
125
|
}
|
|
112
126
|
L.push('## Cost, speed and reliability', '', '| arm | spend per run | median latency | p95 latency | timeouts | failures | unknown-cost attempts |', '|---|---|---|---|---|---|---|');
|
|
113
127
|
for (const a of rep.arms) L.push(`| ${a.id} | ${a.kind === 'deterministic' ? 'no model cost' : fmt(a.economics.usd, 'usd')} | ${fmt(a.economics.latencyMedianMs, 'ms')} | ${fmt(a.economics.latencyP95Ms, 'ms')} | ${a.economics.timeouts} | ${a.economics.failures} | ${a.economics.unknownCostAttempts} |`);
|
|
128
|
+
if (rep.providerVariance?.length) {
|
|
129
|
+
L.push('', '## Provider variance', '', 'The same model id reached through different providers:', '', '| model | task | spread | arms |', '|---|---|---|---|');
|
|
130
|
+
for (const v of rep.providerVariance) L.push(`| ${v.model} | ${v.task} | ${pct(v.spread)}${v.representative ? '' : ' (not representative)'} | ${v.arms.map((a) => `${a.arm} (${a.provider}) ${pct(a.mean)}`).join('; ')} |`);
|
|
131
|
+
}
|
|
114
132
|
L.push('', '## Intelligence per dollar', '', '| arm | confirmed vulnerabilities / $100 | verified fixes / $100 | true positives / $100 | recall at fixed budget |', '|---|---|---|---|---|');
|
|
115
133
|
for (const a of rep.arms) {
|
|
116
134
|
const i = a.intelligencePerDollar; const cell = (x) => (x?.value == null ? `n/a (${x?.reason ?? 'not measured'})` : x.value.toFixed(1));
|
package/src/posture/fleet/cli.js
CHANGED
|
@@ -25,7 +25,7 @@ const DB_FILE = path.join('fleet', 'fleet.db');
|
|
|
25
25
|
const ROLLUP_FILE = 'fleet-rollup.json';
|
|
26
26
|
const sha = (s) => createHash('sha256').update(s).digest('hex');
|
|
27
27
|
|
|
28
|
-
const usage = () => { console.error('usage: fleet init | edges [--undeclared] | scan <manifest.yaml> [--workers N] [--retries N] [--timeout-seconds N | --timeout-ms N] [--no-cache] | status [run-id] | resume <run-id|manifest.yaml> [--retry-failed] | query (--depends-on <id> | --blast-radius <component> [--version v] | --internet-to <data-class> [--involving-vulnerable auth|<repo>] | "<question>") | explore [--out file.html]'); return 4; };
|
|
28
|
+
const usage = () => { console.error('usage: fleet init | edges [--undeclared] | scan <manifest.yaml> [--workers N] [--retries N] [--timeout-seconds N | --timeout-ms N] [--no-cache] [--no-discover] [--lineage] | status [run-id] | resume <run-id|manifest.yaml> [--retry-failed] | query (--depends-on <id> | --blast-radius <component> [--version v] | --internet-to <data-class> [--involving-vulnerable auth|<repo>] | "<question>") | explore [--out file.html]'); return 4; };
|
|
29
29
|
const isManifestArg = (s) => /\.ya?ml$/i.test(String(s ?? ''));
|
|
30
30
|
const manifestArg = (args) => { const m = args._[2]; return m && !String(m).startsWith('--') ? m : 'fleet.yaml'; };
|
|
31
31
|
const dbPath = (root) => statePath(root, DB_FILE);
|
|
@@ -51,7 +51,7 @@ async function cmdEdges(args, root) {
|
|
|
51
51
|
if (args.flags.json) { console.log(JSON.stringify({ discovery: g.discovery ?? null, edges }, null, 2)); return 0; }
|
|
52
52
|
if (!edges.length) { console.log(args.flags.undeclared ? 'No edge was found in code that the manifest does not declare.' : 'The graph has no edges.'); }
|
|
53
53
|
for (const e of edges) {
|
|
54
|
-
const where = e.evidence.filter((x) => x.source === 'discovered-code-literal').map((x) => `${x.file}:${x.line}`);
|
|
54
|
+
const where = e.evidence.filter((x) => x.source === 'discovered-code-literal' || x.source === 'code-boundaries').map((x) => (x.file ? `${x.file}:${x.line}` : `code boundaries: ${x.host ?? x.topic ?? x.name}`));
|
|
55
55
|
console.log(`${e.from} -[${e.type}]-> ${e.to} ${e.discovered ? 'FOUND ONLY IN CODE' : where.length ? 'declared, and seen in code' : 'declared'}${where.length ? ` (${where.join(', ')})` : ''}`);
|
|
56
56
|
}
|
|
57
57
|
if (g.discovery?.limitations) console.log(`\nLimits: ${g.discovery.limitations.join(' ')}`);
|
|
@@ -81,6 +81,12 @@ function entriesOf(artifacts, manifest) {
|
|
|
81
81
|
return manifest.repositories.filter((r) => artifacts.has(r.id)).map((r) => ({ ...artifacts.get(r.id).entry, repo: r.id }));
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
/** The PRD names its commands `fleet status JOB-ID` and `fleet resume JOB-ID`; a run is a set of jobs, so a job id names the run it belongs to. */
|
|
85
|
+
function runOfJob(store, id) {
|
|
86
|
+
const job = store.getJob(String(id));
|
|
87
|
+
return job ? store.getRun(job.runId) : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
84
90
|
async function finishRun({ args, root, store, runId, manifest, manifestFile, interrupted }) {
|
|
85
91
|
const { artifacts, problems } = loadArtifacts({ store, runId, root });
|
|
86
92
|
const jobs = store.listJobs(runId);
|
|
@@ -100,14 +106,16 @@ async function finishRun({ args, root, store, runId, manifest, manifestFile, int
|
|
|
100
106
|
let discovered = null; let discoveryNote = args.flags['no-discover'] ? 'skipped (--no-discover)' : null;
|
|
101
107
|
if (!args.flags['no-discover']) { try { discovered = discoverEdges({ manifest }); } catch (e) { discoveryNote = `failed (${String(e.message).slice(0, 80)}); the graph holds declared edges only`; } }
|
|
102
108
|
const split = discovered ? splitDeclared(discovered, manifest) : null;
|
|
103
|
-
const
|
|
109
|
+
const lineageByRepo = new Map([...artifacts].filter(([, a]) => a.lineage).map(([id, a]) => [id, a.lineage]));
|
|
110
|
+
const graph = finalizeGraph(buildEnterpriseGraph({ manifest, dependencyGraph: dep, discovered, lineage: lineageByRepo.size ? lineageByRepo : null }));
|
|
104
111
|
const findingsByRepo = Object.fromEntries([...artifacts].map(([id, a]) => [id, a.findings ?? []]));
|
|
105
|
-
safeWriteState(statePath(root, ENTERPRISE_FILE), JSON.stringify({ ...graph, runId, manifestFile, findingsByRepo, discovery: discovered ? { scanned: discovered.scanned, limitations: discovered.limitations, undeclared: split.undeclared.length, confirmed: split.confirmed.length } : { note: discoveryNote }, builtAt: new Date().toISOString() }));
|
|
112
|
+
safeWriteState(statePath(root, ENTERPRISE_FILE), JSON.stringify({ ...graph, runId, manifestFile, findingsByRepo, codeBoundaries: lineageByRepo.size ? { repositories: lineageByRepo.size, undeclaredDataClasses: Object.fromEntries(graph.nodes.filter((n) => n.discoveredDataClasses?.length).map((n) => [n.id, n.discoveredDataClasses])) } : { note: args.flags.lineage ? 'no repository returned a Code Boundaries graph' : 'not requested (--lineage)' }, discovery: discovered ? { scanned: discovered.scanned, limitations: discovered.limitations, undeclared: split.undeclared.length, confirmed: split.confirmed.length } : { note: discoveryNote }, builtAt: new Date().toISOString() }));
|
|
106
113
|
|
|
107
114
|
if (args.flags.json) console.log(JSON.stringify({ runId, health, rollup }, null, 2));
|
|
108
115
|
else {
|
|
109
116
|
console.log(renderFleetSummary(rollup));
|
|
110
117
|
console.log(`\nRun: ${runId}\n${renderOrgHealth(health)}`);
|
|
118
|
+
if (lineageByRepo.size) { const und = graph.nodes.filter((n) => n.discoveredDataClasses?.length); console.log(`\nCode Boundaries: ${lineageByRepo.size} repositor${lineageByRepo.size === 1 ? 'y' : 'ies'} contributed a data flow graph.${und.length ? ` Data classes found in code but not declared: ${und.map((n) => `${n.id} (${n.discoveredDataClasses.join(', ')})`).join('; ')}.` : ' No undeclared data class was found.'}`); }
|
|
111
119
|
if (split) console.log(`\nEdges found in code: ${split.undeclared.length} not in the manifest, ${split.confirmed.length} matching a declared edge. ${split.undeclared.length ? 'List them with: agentic-security fleet edges --undeclared' : ''}`.trimEnd());
|
|
112
120
|
else if (discoveryNote) console.log(`\nEdge discovery ${discoveryNote}.`);
|
|
113
121
|
if (interrupted) console.log(`\nInterrupted. Continue with: agentic-security fleet resume ${runId}`);
|
|
@@ -119,7 +127,9 @@ async function finishRun({ args, root, store, runId, manifest, manifestFile, int
|
|
|
119
127
|
async function runAll({ args, root, store, runId, manifest, manifestFile }) {
|
|
120
128
|
const ac = new AbortController(); const onSig = () => ac.abort(); process.once('SIGINT', onSig); process.once('SIGTERM', onSig);
|
|
121
129
|
try {
|
|
122
|
-
|
|
130
|
+
// Code Boundaries are requested at scan time and remembered on the run, so a resume does the same thing the scan would have
|
|
131
|
+
const lineage = !!args.flags.lineage || !!store.getRun(runId)?.meta?.lineage;
|
|
132
|
+
await runJobs({ manifest, store, runId, root, scan: childScan, scanOptions: { lineage }, signal: ac.signal, useCache: !args.flags['no-cache'], onEvent: (e) => {
|
|
123
133
|
if (args.flags.json) return;
|
|
124
134
|
if (e.type === 'done') console.error(`[fleet] done: ${e.repo}${e.cached ? ' (cached)' : ''}${e.status === 'partial' ? ' (partial)' : ''}`);
|
|
125
135
|
else if (e.type === 'failed') console.error(`[fleet] ${e.timedOut ? 'TIMED OUT' : 'FAILED'}: ${e.repo} (${e.error})${e.next === 'queued' ? ', will retry' : ''}`);
|
|
@@ -141,7 +151,7 @@ async function cmdScan(args, root) {
|
|
|
141
151
|
try {
|
|
142
152
|
const manifestHash = sha(fs.readFileSync(manifestFile));
|
|
143
153
|
console.error(`[fleet] planning ${manifest.repositories.length} repository(ies) for ${manifest.organization} (${manifest.workers} worker(s), ${manifest.retries} retr${manifest.retries === 1 ? 'y' : 'ies'}, ${Math.round(manifest.perRepoTimeoutMs / 1000)}s per repository)...`);
|
|
144
|
-
const plan = planRun({ manifest, manifestHash, store, meta: { manifestFile } });
|
|
154
|
+
const plan = planRun({ manifest, manifestHash, store, config: args.flags.lineage ? { lineage: true } : {}, meta: { manifestFile, lineage: !!args.flags.lineage } });
|
|
145
155
|
return await runAll({ args, root, store, runId: plan.runId, manifest, manifestFile });
|
|
146
156
|
} finally { store.close(); }
|
|
147
157
|
}
|
|
@@ -158,7 +168,7 @@ async function cmdResume(args, root) {
|
|
|
158
168
|
const hash = sha(fs.readFileSync(path.resolve(root, target)));
|
|
159
169
|
run = store.listRuns().find((r) => r.manifestHash === hash && store.listJobs(r.runId).some((j) => ['queued', 'leased'].includes(j.status)));
|
|
160
170
|
if (!run) { console.error('Nothing to resume for that manifest: no run of it has unfinished work. Use `fleet resume <run-id> --retry-failed` to retry failed repositories.'); return 4; }
|
|
161
|
-
} else run = store.getRun(target);
|
|
171
|
+
} else run = store.getRun(target) ?? runOfJob(store, target);
|
|
162
172
|
if (!run) { console.error(`No run ${target}. Runs: ${store.listRuns().slice(0, 5).map((r) => r.runId).join(', ') || 'none'}`); return 4; }
|
|
163
173
|
const full = store.getRun(run.runId);
|
|
164
174
|
const manifestFile = path.resolve(root, args.flags.manifest ?? full.meta?.manifestFile ?? (isManifestArg(target) ? target : 'fleet.yaml'));
|
|
@@ -190,14 +200,15 @@ async function cmdStatus(args, root) {
|
|
|
190
200
|
const store = await createSqliteJobStore({ file: dbFile });
|
|
191
201
|
try {
|
|
192
202
|
const arg = args._[2];
|
|
193
|
-
const run = arg && !isManifestArg(arg) ? store.getRun(String(arg)) : store.listRuns()[0] && store.getRun(store.listRuns()[0].runId);
|
|
194
|
-
if (!run) { console.error(arg && !isManifestArg(arg) ? `No run ${arg}.` : 'No fleet run exists yet.'); return 4; }
|
|
203
|
+
const run = arg && !isManifestArg(arg) ? (store.getRun(String(arg)) ?? runOfJob(store, String(arg))) : store.listRuns()[0] && store.getRun(store.listRuns()[0].runId);
|
|
204
|
+
if (!run) { console.error(arg && !isManifestArg(arg) ? `No run or job ${arg}.` : 'No fleet run exists yet.'); return 4; }
|
|
205
|
+
if (arg && !isManifestArg(arg) && String(arg) !== run.runId) console.error(`Job ${arg} belongs to run ${run.runId}.`);
|
|
195
206
|
const meta = run.meta ?? {};
|
|
196
207
|
const manifest = loadFleetManifest(path.resolve(root, meta.manifestFile ?? 'fleet.yaml'));
|
|
197
208
|
const jobs = store.listJobs(run.runId);
|
|
198
209
|
const m = manifest.ok ? manifest.manifest : { repositories: jobs.map((j) => ({ id: j.repoId, required: j.required })) };
|
|
199
210
|
const health = computeOrgHealth({ manifest: m, jobs });
|
|
200
|
-
if (args.flags.json) { console.log(JSON.stringify({ runId: run.runId, health, jobs: jobs.map((j) => ({ repo: j.repoId, status: j.status, attempts: j.attempts, worker: j.worker, leaseExpiresAt: j.leaseExpiresAt, error: j.error })) }, null, 2)); return 0; }
|
|
211
|
+
if (args.flags.json) { console.log(JSON.stringify({ runId: run.runId, health, jobs: jobs.map((j) => ({ jobId: j.jobId, repo: j.repoId, status: j.status, attempts: j.attempts, worker: j.worker, leaseExpiresAt: j.leaseExpiresAt, error: j.error })) }, null, 2)); return 0; }
|
|
201
212
|
console.log(`Run: ${run.runId} (${run.organization})\n${renderOrgHealth(health)}`);
|
|
202
213
|
const live = jobs.filter((j) => j.status === 'leased');
|
|
203
214
|
if (live.length) console.log(`\nIn progress: ${live.map((j) => `${j.repoId} (attempt ${j.attempts}, lease until ${new Date(j.leaseExpiresAt).toISOString()})`).join(', ')}`);
|
|
@@ -266,6 +277,7 @@ async function cmdQuery(args, root) {
|
|
|
266
277
|
for (const p of res.paths) {
|
|
267
278
|
console.log(p.nodes.map((n) => (n === INTERNET ? 'Internet' : n)).join(' -> '));
|
|
268
279
|
if (p.discoveredEdges?.length) console.log(` rests on edge(s) found only in code, not declared: ${p.discoveredEdges.join(', ')}`);
|
|
280
|
+
if (p.discoveredDataClassNodes?.length) console.log(` reaches ${toClass} data only because the code shows it, not the manifest: ${p.discoveredDataClassNodes.join(', ')}`);
|
|
269
281
|
if (p.involving.length) console.log(` involves vulnerable code: ${p.involving.map((n) => `${n} (${involving.why.get(n)})`).join(', ')}`);
|
|
270
282
|
}
|
|
271
283
|
if (res.paths.length) console.log(`\n${res.paths.length} path(s). Reach edges are as declared in the fleet manifest, plus any marked above as found only in code.${res.truncated ? ' The search bound was reached; more may exist.' : ''}`);
|
|
@@ -22,7 +22,7 @@ const AUTH_CWES = new Set(['CWE-287', 'CWE-306', 'CWE-285', 'CWE-862', 'CWE-863'
|
|
|
22
22
|
const edgeId = (type, from, to) => `edge:${type}:${from}:${to}`;
|
|
23
23
|
const hash = (v) => createHash('sha256').update(JSON.stringify(v)).digest('hex');
|
|
24
24
|
|
|
25
|
-
export function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = null }) {
|
|
25
|
+
export function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = null, lineage = null }) {
|
|
26
26
|
const nodes = new Map();
|
|
27
27
|
const edges = new Map();
|
|
28
28
|
const addNode = (n) => nodes.set(n.id, n);
|
|
@@ -50,7 +50,48 @@ export function buildEnterpriseGraph({ manifest, dependencyGraph, discovered = n
|
|
|
50
50
|
// Edges found in the code. They share an id with a declared edge of the same type and endpoints, so the evidence list shows both; an
|
|
51
51
|
// edge with ONLY discovered evidence is marked `discovered: true` so a query can say it rests on a code literal, not on a declaration.
|
|
52
52
|
for (const d of discovered?.edges ?? []) for (const evidence of d.evidence) addEdge(d.type, d.from, d.to, evidence, true);
|
|
53
|
-
|
|
53
|
+
// Code Boundaries (each repository's own data flow graph, summarized): data classes the code handles, and destinations it sends to.
|
|
54
|
+
// These add to what the manifest declared and never replace it; a class the manifest did not declare is listed on the node so a path
|
|
55
|
+
// that exists only because of it can say so.
|
|
56
|
+
const aliasesOf = new Map([...manifest.repositories, ...(manifest.resources ?? [])].map((n) => [n.id, [n.id, ...(n.aliases ?? [])].map((a) => String(a).toLowerCase())]));
|
|
57
|
+
for (const [repoId, sum] of lineage ?? []) {
|
|
58
|
+
const node = nodes.get(repoId);
|
|
59
|
+
if (!node || !sum) continue;
|
|
60
|
+
const declared = new Set(node.dataClasses);
|
|
61
|
+
const found = (sum.dataClasses ?? []).filter((c) => !declared.has(c));
|
|
62
|
+
node.dataClasses = [...declared, ...found];
|
|
63
|
+
node.discoveredDataClasses = found;
|
|
64
|
+
const cb = (extra) => ({ source: 'code-boundaries', repo: repoId, declared: false, file: null, ...extra });
|
|
65
|
+
for (const d of sum.externalDestinations ?? []) {
|
|
66
|
+
if (!d.host) continue;
|
|
67
|
+
const first = d.host.split('.')[0];
|
|
68
|
+
for (const [target, names] of aliasesOf) {
|
|
69
|
+
if (target === repoId || !(names.includes(d.host) || names.includes(first))) continue;
|
|
70
|
+
const tn = nodes.get(target);
|
|
71
|
+
addEdge(tn?.type === 'external-provider' ? 'uses-provider' : 'calls', repoId, target, cb({ host: d.host, nodeKind: 'external' }), true);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
for (const q of sum.queues ?? []) {
|
|
75
|
+
for (const [target, names] of aliasesOf) {
|
|
76
|
+
const tn = nodes.get(target);
|
|
77
|
+
if (tn?.type !== 'queue' || !names.includes(String(q.topic).toLowerCase())) continue;
|
|
78
|
+
if (q.operation === 'consume') addEdge('consumes', target, repoId, cb({ topic: q.topic }), true); else addEdge('produces', repoId, target, cb({ topic: q.topic }), true);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
for (const st of sum.stores ?? []) {
|
|
82
|
+
for (const key of [st.host, st.database]) {
|
|
83
|
+
if (!key) continue;
|
|
84
|
+
const k = String(key).toLowerCase();
|
|
85
|
+
for (const [target, names] of aliasesOf) {
|
|
86
|
+
const tn = nodes.get(target);
|
|
87
|
+
if (tn?.type !== 'database' || !(names.includes(k) || names.includes(k.split('.')[0]))) continue;
|
|
88
|
+
addEdge('connects-to', repoId, target, cb({ store: st.subtype, name: key }), true);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const FOUND_IN_CODE = new Set(['discovered-code-literal', 'code-boundaries']);
|
|
94
|
+
for (const e of edges.values()) e.discovered = e.evidence.every((x) => FOUND_IN_CODE.has(x.source));
|
|
54
95
|
return { schema: ENTERPRISE_GRAPH_SCHEMA, version: 1, organization: manifest.organization, nodes: [...nodes.values()], edges: [...edges.values()], digest: null };
|
|
55
96
|
}
|
|
56
97
|
|
|
@@ -97,7 +138,7 @@ export function attackPaths({ graph, toDataClass, from = INTERNET, involving = n
|
|
|
97
138
|
if (results.length >= maxPaths) { truncated = true; return; }
|
|
98
139
|
if (targets.has(id) && nodePath.length > 1) {
|
|
99
140
|
const inv = involving ? nodePath.filter((n) => involving.all.has(n)) : [];
|
|
100
|
-
if (!involving || inv.length) results.push({ nodes: [...nodePath], edges: edgePath.map((e) => e.id), involving: inv, discoveredEdges: edgePath.filter((e) => e.discovered).map((e) => e.id), evidence: edgePath.flatMap((e) => e.evidence.map((x) => ({ edge: e.id, ...x }))) });
|
|
141
|
+
if (!involving || inv.length) results.push({ nodes: [...nodePath], edges: edgePath.map((e) => e.id), involving: inv, discoveredEdges: edgePath.filter((e) => e.discovered).map((e) => e.id), discoveredDataClassNodes: nodePath.filter((n) => (nodeOf(graph, n)?.discoveredDataClasses ?? []).includes(toDataClass)), evidence: edgePath.flatMap((e) => e.evidence.map((x) => ({ edge: e.id, ...x }))) });
|
|
101
142
|
}
|
|
102
143
|
if (nodePath.length > maxDepth) { truncated = true; return; }
|
|
103
144
|
for (const e of out.get(id) ?? []) if (!nodePath.includes(e.to)) walk(e.to, [...nodePath, e.to], [...edgePath, e]);
|
|
@@ -25,8 +25,8 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
25
25
|
export const DEFAULTS = Object.freeze({ leaseMs: 30_000, heartbeatMs: 10_000, retryDelayMs: 2_000, pollMs: 100 });
|
|
26
26
|
|
|
27
27
|
/** The default scan: one supervised child process per repository. Throws `{code: 'TIMEOUT'|'ERROR', message}`. */
|
|
28
|
-
export async function childScan(repoPath, { timeoutMs, signal }) {
|
|
29
|
-
const r = await superviseSpawn(process.execPath, [SCAN_CHILD, repoPath], { cwd: repoPath, env: { ...process.env, AGENTIC_SECURITY_NO_STATE: '1' }, timeoutMs, signal, maxOutputBytes: 96 * 1024 * 1024 });
|
|
28
|
+
export async function childScan(repoPath, { timeoutMs, signal, lineage = false }) {
|
|
29
|
+
const r = await superviseSpawn(process.execPath, [SCAN_CHILD, repoPath], { cwd: repoPath, env: { ...process.env, AGENTIC_SECURITY_NO_STATE: '1', ...(lineage ? { AGENTIC_SECURITY_LINEAGE_DEEP: '1' } : {}) }, timeoutMs, signal, maxOutputBytes: 96 * 1024 * 1024 });
|
|
30
30
|
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
31
31
|
if (r.timedOut) throw fail('TIMEOUT', `no result within ${Math.round(timeoutMs / 1000)}s`);
|
|
32
32
|
if (r.cancelled) throw fail('CANCELLED', 'cancelled');
|
|
@@ -69,7 +69,7 @@ export function planRun({ manifest, manifestHash, store, runId = null, config =
|
|
|
69
69
|
* Run (or continue) a planned run until no job is queued or leased by a live worker. Returns when the work is done, the signal
|
|
70
70
|
* aborts, or `maxWaitMs` passes with only other workers' leases outstanding.
|
|
71
71
|
*/
|
|
72
|
-
export async function runJobs({ manifest, store, runId, root, scan = childScan, workers = manifest.workers, leaseMs = DEFAULTS.leaseMs, heartbeatMs = DEFAULTS.heartbeatMs, retryDelayMs = DEFAULTS.retryDelayMs, pollMs = DEFAULTS.pollMs, maxWaitMs = null, signal, now = () => Date.now(), onEvent = () => {}, cacheDir = null, artifactDir = null, useCache = true }) {
|
|
72
|
+
export async function runJobs({ manifest, store, runId, root, scan = childScan, scanOptions = {}, workers = manifest.workers, leaseMs = DEFAULTS.leaseMs, heartbeatMs = DEFAULTS.heartbeatMs, retryDelayMs = DEFAULTS.retryDelayMs, pollMs = DEFAULTS.pollMs, maxWaitMs = null, signal, now = () => Date.now(), onEvent = () => {}, cacheDir = null, artifactDir = null, useCache = true }) {
|
|
73
73
|
const repoById = new Map(manifest.repositories.map((r) => [r.id, r]));
|
|
74
74
|
const cacheD = cacheDir ?? statePath(root, 'fleet', 'cache');
|
|
75
75
|
const artD = artifactDir ?? statePath(root, 'fleet', 'artifacts', runId);
|
|
@@ -102,7 +102,7 @@ export async function runJobs({ manifest, store, runId, root, scan = childScan,
|
|
|
102
102
|
onEvent({ type: 'start', repo: job.repoId, attempt: job.attempts });
|
|
103
103
|
const cached = useCache ? readCache(cacheD, job.cacheKey) : null;
|
|
104
104
|
if (cached) {
|
|
105
|
-
const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, cached: true });
|
|
105
|
+
const a = writeArtifact(job, { entry: cached.entry, findings: cached.findings, findingsTruncated: !!cached.findingsTruncated, lineage: cached.lineage ?? null, cached: true });
|
|
106
106
|
const status = cached.entry?.scanHealth && cached.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
|
|
107
107
|
if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: cached.entry?.total ?? null, cached: true, artifact: path.basename(a.file) }, now: now() })) onEvent({ type: 'done', repo: job.repoId, cached: true, status });
|
|
108
108
|
continue;
|
|
@@ -113,15 +113,15 @@ export async function runJobs({ manifest, store, runId, root, scan = childScan,
|
|
|
113
113
|
let lost = false;
|
|
114
114
|
const hb = setInterval(() => { if (!store.heartbeat(job.jobId, job.epoch, { now: now(), leaseMs })) { lost = true; ac.abort(); } }, heartbeatMs);
|
|
115
115
|
try {
|
|
116
|
-
const out = await scan(repo.path, { timeoutMs: job.timeoutMs, signal: ac.signal });
|
|
116
|
+
const out = await scan(repo.path, { timeoutMs: job.timeoutMs, signal: ac.signal, ...scanOptions });
|
|
117
117
|
clearInterval(hb);
|
|
118
118
|
if (lost) { onEvent({ type: 'fenced', repo: job.repoId }); continue; }
|
|
119
119
|
if (!out?.entry) throw Object.assign(new Error('the scan returned no entry'), { code: 'ERROR' });
|
|
120
120
|
if (out.entry.ok === false) throw Object.assign(new Error(out.entry.error || 'the scan failed'), { code: 'ERROR' });
|
|
121
|
-
const a = writeArtifact(job, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated });
|
|
121
|
+
const a = writeArtifact(job, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated, lineage: out.lineage ?? null });
|
|
122
122
|
const status = out.entry.scanHealth && out.entry.scanHealth.status !== 'complete' ? 'partial' : 'succeeded';
|
|
123
123
|
if (store.complete(job.jobId, job.epoch, { status, artifactSha256: a.sha256, summary: { total: out.entry.total ?? null, cached: false, artifact: path.basename(a.file) }, now: now() })) {
|
|
124
|
-
if (useCache && status === 'succeeded') writeCache(cacheD, job.cacheKey, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated });
|
|
124
|
+
if (useCache && status === 'succeeded') writeCache(cacheD, job.cacheKey, { entry: out.entry, findings: out.findings ?? [], findingsTruncated: !!out.findingsTruncated, lineage: out.lineage ?? null });
|
|
125
125
|
onEvent({ type: 'done', repo: job.repoId, cached: false, status });
|
|
126
126
|
} else onEvent({ type: 'fenced', repo: job.repoId });
|
|
127
127
|
} catch (e) {
|
|
@@ -89,7 +89,9 @@ export async function createSqliteJobStore({ file }) {
|
|
|
89
89
|
const { DatabaseSync } = await import('node:sqlite');
|
|
90
90
|
if (file !== ':memory:') fs.mkdirSync(path.dirname(file), { recursive: true, mode: 0o700 });
|
|
91
91
|
const db = new DatabaseSync(file);
|
|
92
|
-
|
|
92
|
+
// the busy timeout comes FIRST: the very next statements (switching the journal mode, creating tables) take locks, and another process
|
|
93
|
+
// opening the same file at the same instant must wait for them instead of failing with "database is locked"
|
|
94
|
+
db.exec('PRAGMA busy_timeout = 5000; PRAGMA journal_mode = WAL; PRAGMA foreign_keys = ON;');
|
|
93
95
|
db.exec(SCHEMA);
|
|
94
96
|
const tx = (fn) => { db.exec('BEGIN IMMEDIATE'); try { const r = fn(); db.exec('COMMIT'); return r; } catch (e) { try { db.exec('ROLLBACK'); } catch { /* already rolled back */ } throw e; } };
|
|
95
97
|
const q = (sql, ...p) => db.prepare(sql).all(...p);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// A bounded summary of one repository's Code Boundaries graph (the Data Flow Explorer's `DataFlowGraph v1`), for the enterprise graph
|
|
2
|
+
// (PRD 4: "federate Code Boundaries across repositories"). The whole graph is not copied: a fleet of hundreds of repositories does not
|
|
3
|
+
// need each node, only what crosses a repository boundary or changes what the organization knows about it:
|
|
4
|
+
// * the data classes that appear in the code (so a repository holding payment data is known to, even if the manifest forgot to say);
|
|
5
|
+
// * the external destinations the code sends data to, by host;
|
|
6
|
+
// * the queue topics it publishes to or consumes from;
|
|
7
|
+
// * the stores it touches, by host/database/table name;
|
|
8
|
+
// * counts, including flows that carry sensitive classes across a trust boundary.
|
|
9
|
+
// Pure; hostile input (the graph came out of a scan of someone's code), so every string is bounded and shape-checked.
|
|
10
|
+
const MAX_ITEMS = 64;
|
|
11
|
+
const SENSITIVE = new Set(['pii', 'phi', 'pci', 'fin', 'credentials']);
|
|
12
|
+
const arr = (v) => (Array.isArray(v) ? v : []);
|
|
13
|
+
const clean = (v, max = 120) => (typeof v === 'string' && v.length > 0 && v.length <= max && !/[\s\u0000-\u001f]/.test(v) ? v : null);
|
|
14
|
+
|
|
15
|
+
function hostOf(literal) {
|
|
16
|
+
if (typeof literal !== 'string') return null;
|
|
17
|
+
try { const h = new URL(literal).hostname.toLowerCase(); return clean(h, 253); } catch { /* not a URL */ }
|
|
18
|
+
const bare = literal.toLowerCase().replace(/:\d+$/, '');
|
|
19
|
+
return /^[a-z0-9][a-z0-9.-]{0,252}$/.test(bare) ? bare : null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function summarizeLineage(graph) {
|
|
23
|
+
if (!graph || typeof graph !== 'object') return null;
|
|
24
|
+
const classes = new Set();
|
|
25
|
+
for (const d of arr(graph.dataElements)) for (const c of arr(d?.dataClasses)) { if (typeof c !== 'string') continue; const l = clean(c.toLowerCase(), 31); if (l && /^[a-z][a-z0-9-]*$/.test(l)) classes.add(l); }
|
|
26
|
+
const ext = new Map(); const queues = new Map(); const stores = new Map();
|
|
27
|
+
for (const n of arr(graph.nodes)) {
|
|
28
|
+
if (!n || typeof n !== 'object') continue;
|
|
29
|
+
if (n.kind === 'external') {
|
|
30
|
+
const host = hostOf(n.destination?.literalValue);
|
|
31
|
+
const key = `${n.subtype ?? 'external'}|${host ?? ''}`;
|
|
32
|
+
if (!ext.has(key) && ext.size < MAX_ITEMS) ext.set(key, { subtype: clean(n.subtype, 60) ?? 'external', host });
|
|
33
|
+
} else if (n.kind === 'queue') {
|
|
34
|
+
const topic = clean(n.queueDetail?.topic, 120); const op = n.queueDetail?.operation === 'consume' || n.queueDetail?.operation === 'receive' ? 'consume' : 'publish';
|
|
35
|
+
if (topic && queues.size < MAX_ITEMS) queues.set(`${topic}|${op}`, { topic, operation: op });
|
|
36
|
+
} else if (n.kind === 'store') {
|
|
37
|
+
const d = n.storeDetail ?? {};
|
|
38
|
+
const item = { subtype: clean(n.subtype, 60) ?? 'store', host: hostOf(d.host), database: clean(d.database, 120), table: clean(d.table, 120) };
|
|
39
|
+
const key = JSON.stringify(item);
|
|
40
|
+
if (stores.size < MAX_ITEMS) stores.set(key, item);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const edges = arr(graph.edges);
|
|
44
|
+
const crossing = new Set(edges.filter((e) => arr(e?.boundaryCrossings).length).map((e) => e.id));
|
|
45
|
+
const dataById = new Map(arr(graph.dataElements).map((d) => [d?.id, d]));
|
|
46
|
+
const sensitiveCrossing = arr(graph.flows).filter((f) => arr(f?.edgeIds).some((id) => crossing.has(id)) && arr(f?.dataElementIds).some((id) => arr(dataById.get(id)?.dataClasses).some((c) => SENSITIVE.has(String(c).toLowerCase())))).length;
|
|
47
|
+
return {
|
|
48
|
+
schema: 'agentic-security/fleet-lineage-summary', version: 1,
|
|
49
|
+
dataClasses: [...classes].sort().slice(0, MAX_ITEMS),
|
|
50
|
+
externalDestinations: [...ext.values()],
|
|
51
|
+
queues: [...queues.values()],
|
|
52
|
+
stores: [...stores.values()],
|
|
53
|
+
counts: { nodes: arr(graph.nodes).length, edges: edges.length, flows: arr(graph.flows).length, boundaryCrossingEdges: crossing.size, sensitiveFlowsCrossingBoundary: sensitiveCrossing },
|
|
54
|
+
};
|
|
55
|
+
}
|