@clear-capabilities/agentic-security-scanner 0.181.0 → 0.183.1
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 +25 -0
- package/bin/agentic-security.js +19 -2
- package/dist/2946.index.js +6 -3
- package/dist/{703e82c3705f455c16cc.json → 2a8456e0800aeee871b6.json} +5 -2
- package/dist/3360.index.js +744 -0
- package/dist/5616.index.js +51 -8
- package/dist/5620.index.js +1 -1
- package/dist/9441.index.js +180 -0
- package/dist/9614.index.js +275 -0
- package/dist/agentic-security.mjs +12 -12
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +5 -2
- package/src/artifact/cosign.js +6 -3
- package/src/posture/accuracy-scorecard.js +22 -0
- package/src/posture/assurance/release-package-cli.js +114 -0
- package/src/posture/dnr/cli.js +13 -4
- package/src/posture/dnr/exposure-window.js +39 -6
- package/src/posture/fleet/fleet-manifest.js +5 -5
- package/src/posture/proof-fabric/container.js +4 -1
- package/src/posture/proof-fabric/runner.js +5 -1
- package/src/posture/strategic-metrics-cli.js +25 -0
- package/src/posture/strategic-metrics.js +122 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
8af5ae6f4aca6a22d9fd577f074d74d1fd352d4b5ee48e7a2e9be0df29b0bc98 agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.183.1",
|
|
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/contracts/root-cause-sweep.contract.test.js test/contracts/blast-radius.contract.test.js test/contracts/federation.contract.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/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",
|
|
88
|
+
"test:posture": "node --test test/contracts/root-cause-sweep.contract.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/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",
|
|
@@ -199,6 +199,9 @@
|
|
|
199
199
|
"bench:sibling-sweep:check": "node ../bench/sibling-sweep/runner.mjs --check",
|
|
200
200
|
"bench:sibling-sweep:update-baseline": "node ../bench/sibling-sweep/runner.mjs --update-baseline",
|
|
201
201
|
"bench:sca-replay": "node ../bench/sca-replay/runner.mjs",
|
|
202
|
+
"check:artifact-real": "node ../scripts/artifact-real-check.mjs",
|
|
203
|
+
"check:local-model": "node ../scripts/local-model-check.mjs",
|
|
204
|
+
"bench:fix-proof-gate:check": "node ../bench/fix-verification/proof-gate.mjs --check",
|
|
202
205
|
"bench:sca-replay:check": "node ../bench/sca-replay/check.mjs",
|
|
203
206
|
"bench:sca-replay:reachability": "node ../bench/sca-replay/reachability.mjs",
|
|
204
207
|
"bench:sca-replay:fetch": "node ../bench/sca-replay/fetch.mjs",
|
package/src/artifact/cosign.js
CHANGED
|
@@ -28,7 +28,7 @@ function findTool(name, pathEnv = process.env.PATH || '') {
|
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
/** @param {{repository: string, digest: string, publicKeyPem?: string, certificateIdentity?: string, certificateOidcIssuer?: string}} o */
|
|
31
|
-
export async function verifyWithCosign({ repository, digest, publicKeyPem = null, certificateIdentity = null, certificateOidcIssuer = null, timeoutMs = 60_000, signal, pathEnv = process.env.PATH } = {}) {
|
|
31
|
+
export async function verifyWithCosign({ repository, digest, publicKeyPem = null, certificateIdentity = null, certificateOidcIssuer = null, ignoreTlog = false, timeoutMs = 60_000, signal, pathEnv = process.env.PATH } = {}) {
|
|
32
32
|
const na = (reason) => ({ state: 'unavailable', via: 'cosign', reason });
|
|
33
33
|
if (!DIGEST_RE.test(String(digest))) return na('there is no sha256 image digest to verify a signature against');
|
|
34
34
|
if (!REPO_RE.test(String(repository ?? '')) || String(repository).includes('..') || String(repository).startsWith('-')) return na('the repository name is not valid');
|
|
@@ -41,6 +41,9 @@ export async function verifyWithCosign({ repository, digest, publicKeyPem = null
|
|
|
41
41
|
try {
|
|
42
42
|
const args = ['verify'];
|
|
43
43
|
if (publicKeyPem) { const kf = path.join(dir, 'key.pub'); fs.writeFileSync(kf, publicKeyPem, { mode: 0o600 }); args.push('--key', kf); } else args.push('--certificate-identity', certificateIdentity, '--certificate-oidc-issuer', certificateOidcIssuer);
|
|
44
|
+
// cosign requires a transparency-log entry for a key-signed image unless told otherwise. A private registry or an air-gapped build has none, so
|
|
45
|
+
// this is an explicit opt-in, never the default, and a result that skipped the log says so. Keyless verification always needs the log.
|
|
46
|
+
if (ignoreTlog && publicKeyPem) args.push('--insecure-ignore-tlog=true');
|
|
44
47
|
args.push(`${repository}@${digest}`);
|
|
45
48
|
const r = await superviseSpawn(bin, args, { cwd: dir, env: scrubEnv(process.env), timeoutMs, signal, maxOutputBytes: 2 * 1024 * 1024 });
|
|
46
49
|
if (r.spawnError) return na(`could not start cosign: ${r.spawnError}`);
|
|
@@ -51,11 +54,11 @@ export async function verifyWithCosign({ repository, digest, publicKeyPem = null
|
|
|
51
54
|
let payloads = [];
|
|
52
55
|
try { const j = JSON.parse(r.stdout); payloads = Array.isArray(j) ? j : [j]; } catch { /* handled below */ }
|
|
53
56
|
const matching = payloads.filter((p) => p?.critical?.image?.['docker-manifest-digest'] === digest);
|
|
54
|
-
if (matching.length) return { state: 'verified', via: 'cosign', mode: keyless ? 'keyless' : 'key', signatures: matching.length, identity: keyless ? certificateIdentity : null, reason: null };
|
|
57
|
+
if (matching.length) return { state: 'verified', via: 'cosign', mode: keyless ? 'keyless' : 'key', signatures: matching.length, identity: keyless ? certificateIdentity : null, reason: null, ...(ignoreTlog && !keyless ? { transparencyLog: 'not-checked' } : {}) };
|
|
55
58
|
return { state: 'invalid', via: 'cosign', reason: 'cosign reported success but none of the verified signatures names this image digest' };
|
|
56
59
|
}
|
|
57
|
-
if (/no signatures found|no matching signatures|no signatures associated|manifest unknown/.test(err)) return { state: 'missing', via: 'cosign', reason: 'no signature exists for this digest' };
|
|
58
60
|
if (/invalid signature|signature verification failed|crypto\/ecdsa|verification failed|certificate identity|none of the expected identities/.test(err)) return { state: 'invalid', via: 'cosign', reason: 'cosign rejected the signature or the identity' };
|
|
61
|
+
if (/no signatures found|no matching signatures|no signatures associated|manifest unknown/.test(err)) return { state: 'missing', via: 'cosign', reason: 'no signature exists for this digest' };
|
|
59
62
|
return na(`cosign could not complete the check (exit ${r.exitCode}): ${String(r.stderr).trim().split('\n').pop()?.slice(0, 160) ?? ''}`);
|
|
60
63
|
} finally { try { fs.rmSync(dir, { recursive: true, force: true }); } catch { /* best effort */ } }
|
|
61
64
|
}
|
|
@@ -393,6 +393,9 @@ export function buildScorecard(inputs) {
|
|
|
393
393
|
// frameworks" from "this scorecard predates the metric" the same way
|
|
394
394
|
// every other section here distinguishes absence from zero.
|
|
395
395
|
complianceMappingCoverage: inputs.complianceMappingCoverage || [],
|
|
396
|
+
// Strategic metrics (GA promotion Wave 3): computed by the driver from a scan of a reference project and the recorded fix attempts, relayed here
|
|
397
|
+
// unchanged. `null` (never omitted) when the caller supplies nothing, so a scorecard that predates the metric reads as such.
|
|
398
|
+
strategicMetrics: inputs.strategicMetrics || null,
|
|
396
399
|
// QA-001/QA-003: the real-code gates registered in the evaluation protocol. Relayed, never computed here.
|
|
397
400
|
evaluationGates: summarizeEvaluationGates(inputs.evaluation),
|
|
398
401
|
// QA-008.AC01: what this scorecard measured, stated once. `clean` is what lets the commit and bundle identify the measured tree.
|
|
@@ -668,6 +671,25 @@ export function renderScorecardMarkdown(m) {
|
|
|
668
671
|
L.push('not a channel this measurement structurally cannot yet cover.');
|
|
669
672
|
L.push('');
|
|
670
673
|
}
|
|
674
|
+
if (m.strategicMetrics && m.strategicMetrics.metrics) {
|
|
675
|
+
const sm = m.strategicMetrics; const mt = sm.metrics;
|
|
676
|
+
const cell = (r) => (r && r.percent !== null && r.percent !== undefined ? `${r.numerator}/${r.denominator} (${r.percent}%)` : `${r ? r.numerator : 0}/${r ? r.denominator : 0} (not measurable)`);
|
|
677
|
+
L.push('## Strategic metrics');
|
|
678
|
+
L.push('');
|
|
679
|
+
L.push(`Counted on ${sm.source}. These describe one reference project and its recorded fix attempts, not field averages; every figure carries its numerator and denominator. Contract: \`docs/reference/strategic-metrics.md\`.`);
|
|
680
|
+
L.push('');
|
|
681
|
+
L.push('| Metric | Figure | Basis |');
|
|
682
|
+
L.push('| --- | --- | --- |');
|
|
683
|
+
const ac = mt.assuranceCoverage;
|
|
684
|
+
L.push(`| Assurance Coverage | ${ac.percent === null ? 'not measurable' : `${ac.percent}%`}${ac.files ? ` (files ${cell(ac.files)}, analyzers ${ac.analyzers ? cell(ac.analyzers) : 'not reported'})` : ''} | the lower of files completed and analyzers completed |`);
|
|
685
|
+
L.push(`| Evidence Conversion | ${cell(mt.evidenceConversion)} | findings corroborated or stronger, over all findings |`);
|
|
686
|
+
L.push(`| Verified Remediation Rate | ${cell(mt.verifiedRemediationRate)} | ${sm.remediationSource}; a lower bound where a class has no attack replay |`);
|
|
687
|
+
L.push('| Security Learning Rate | not measured | nothing measures it independently of the rule compiler\'s own corpus |');
|
|
688
|
+
L.push('');
|
|
689
|
+
const st = mt.evidenceConversion.byState;
|
|
690
|
+
L.push(`Evidence states reached: ${Object.keys(st).map((k) => `${k} ${st[k]}`).join(', ')}.`);
|
|
691
|
+
L.push('');
|
|
692
|
+
}
|
|
671
693
|
if (Array.isArray(m.complianceMappingCoverage) && m.complianceMappingCoverage.length) {
|
|
672
694
|
L.push('## Compliance mapping coverage');
|
|
673
695
|
L.push('');
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// `agentic-security release-package build|verify`: assembles a signed release assurance package from the last scan and checks one offline.
|
|
2
|
+
//
|
|
3
|
+
// release-package build [path] --private-key <pem> [--output <file>] [--fix-records <file>] [--public-key-out <file>]
|
|
4
|
+
// release-package verify <package.json> --public-key <pem> [--require kind,kind]
|
|
5
|
+
//
|
|
6
|
+
// `io.git(root, ...args)` answers a read-only git query (or null); the caller supplies it so this layer never spawns a process itself.
|
|
7
|
+
// Exit codes: 0 built or verified; 1 the scan state failed its integrity check, or a package was rejected; 2 a usage problem or unreadable input.
|
|
8
|
+
//
|
|
9
|
+
// What goes in: the scan's health record and one record per finding (and, with --fix-records, one verification record per fix claimed). The proof tier of a
|
|
10
|
+
// finding is whatever the scan recorded; a finding that recorded none is `taint-proven` when the taint engine found its path and `unproven` otherwise. An
|
|
11
|
+
// executed counterexample is never inferred here. The verdict is computed from the package's own contents and recorded as a policy decision, so the stated
|
|
12
|
+
// verdict can always be recomputed by anyone holding the package.
|
|
13
|
+
import * as fs from 'node:fs';
|
|
14
|
+
import * as path from 'node:path';
|
|
15
|
+
import { statePath } from '../state-dir.js';
|
|
16
|
+
import { verifyLastScan } from '../integrity.js';
|
|
17
|
+
import { buildSnapshotRef } from './evidence-registry.js';
|
|
18
|
+
import { buildReleasePackage, signReleasePackage } from './release-package.js';
|
|
19
|
+
import { releaseVerdict, verifyStatedVerdict } from './release-verdict.js';
|
|
20
|
+
import { verifyReleasePackage } from './release-package-verify.js';
|
|
21
|
+
|
|
22
|
+
const USAGE = 'Usage: agentic-security release-package build [path] --private-key <pem> [--output <file>] [--fix-records <file>] [--public-key-out <file>]\n agentic-security release-package verify <package.json> --public-key <pem> [--require kind,kind]';
|
|
23
|
+
const DEFAULT_OUT = 'release-package.json';
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
function proofTierOf(f) {
|
|
27
|
+
if (typeof f.proofTier === 'string') return f.proofTier;
|
|
28
|
+
return f.parser === 'IR-TAINT' ? 'taint-proven' : 'unproven';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function findingPayload(f) {
|
|
32
|
+
return {
|
|
33
|
+
id: f.id, stableId: f.stableId ?? null, severity: f.severity, vuln: f.vuln, file: f.file, line: f.line ?? null,
|
|
34
|
+
parser: f.parser, family: f.family, cwe: f.cwe ?? null, proofTier: proofTierOf(f),
|
|
35
|
+
...(f.triage ? { triage: f.triage } : {}),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function build(args, say, git) {
|
|
40
|
+
const root = path.resolve(args._[2] || '.');
|
|
41
|
+
const keyFile = args.flags['private-key'];
|
|
42
|
+
if (!keyFile) { say.err(USAGE); return 2; }
|
|
43
|
+
let privateKey;
|
|
44
|
+
try { privateKey = fs.readFileSync(path.resolve(String(keyFile)), 'utf8'); } catch { say.err(`Could not read the private key at ${keyFile}.`); return 2; }
|
|
45
|
+
|
|
46
|
+
const file = statePath(root, 'last-scan.json');
|
|
47
|
+
let body;
|
|
48
|
+
try { body = fs.readFileSync(file, 'utf8'); } catch { say.err(`No scan found at ${file}. Run a scan first.`); return 2; }
|
|
49
|
+
const sig = verifyLastScan(body, file + '.sig');
|
|
50
|
+
if (sig !== true) { say.err(sig === null ? 'The last scan has no signature, so it is not used as evidence. Run a scan again.' : 'The last scan failed its integrity check, so it is not used as evidence.'); return 1; }
|
|
51
|
+
let scan;
|
|
52
|
+
try { scan = JSON.parse(body); } catch { say.err('The last scan is not valid JSON.'); return 1; }
|
|
53
|
+
if (!scan.scanHealth || typeof scan.scanHealth !== 'object') { say.err('The last scan carries no scan health record, which a release package requires.'); return 1; }
|
|
54
|
+
|
|
55
|
+
let fixes = [];
|
|
56
|
+
if (args.flags['fix-records']) {
|
|
57
|
+
try { fixes = JSON.parse(fs.readFileSync(path.resolve(String(args.flags['fix-records'])), 'utf8')); } catch (e) { say.err(`Could not read the fix records: ${e.message}`); return 2; }
|
|
58
|
+
if (!Array.isArray(fixes)) { say.err('The fix records file must hold a list.'); return 2; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const findings = (scan.findings || []).filter(f => f && typeof f === 'object');
|
|
62
|
+
const evidence = [{ kind: 'scan-health', payload: scan.scanHealth }];
|
|
63
|
+
for (const f of findings) evidence.push({ kind: 'finding', payload: findingPayload(f) });
|
|
64
|
+
for (const rec of fixes) {
|
|
65
|
+
const at = findings.findIndex(f => f.id === rec.findingId || (rec.findingStableId && f.stableId === rec.findingStableId));
|
|
66
|
+
evidence.push({ kind: 'fix-verification', payload: rec, ...(at >= 0 ? { restsOn: [at + 1] } : {}) });
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const commit = git(root, 'rev-parse', 'HEAD');
|
|
70
|
+
const dirty = commit ? (git(root, 'status', '--porcelain') || '').length > 0 : false;
|
|
71
|
+
const snapshot = buildSnapshotRef({ commit, dirty, engineVersion: scan.engineVersion || null });
|
|
72
|
+
let pkg;
|
|
73
|
+
try {
|
|
74
|
+
const draft = buildReleasePackage({ snapshot, evidence });
|
|
75
|
+
const v = releaseVerdict(draft);
|
|
76
|
+
pkg = signReleasePackage(buildReleasePackage({ snapshot, evidence, policyDecisions: [{ id: 'release-verdict', outcome: v.verdict, reason: v.statement, policyVersion: '1' }] }), privateKey);
|
|
77
|
+
} catch (e) { say.err(`Could not build the package: ${e.message}`); return 1; }
|
|
78
|
+
|
|
79
|
+
const out = path.resolve(String(args.flags.output || path.join(root, DEFAULT_OUT)));
|
|
80
|
+
fs.writeFileSync(out, JSON.stringify(pkg, null, 2) + '\n');
|
|
81
|
+
if (args.flags['public-key-out']) {
|
|
82
|
+
const { createPublicKey } = await import('node:crypto');
|
|
83
|
+
fs.writeFileSync(path.resolve(String(args.flags['public-key-out'])), createPublicKey(privateKey).export({ type: 'spki', format: 'pem' }));
|
|
84
|
+
}
|
|
85
|
+
const v = releaseVerdict(pkg);
|
|
86
|
+
say.out(`release package written to ${out}: ${pkg.evidence.length} evidence records, ${pkg.findings.length} findings, scan ${pkg.completeness.scanStatus}`);
|
|
87
|
+
say.out(`verdict: ${v.verdict}. ${v.statement}`);
|
|
88
|
+
if (!snapshot.immutable) say.out('The snapshot is not immutable: the working tree has uncommitted changes or there is no commit, and the package says so.');
|
|
89
|
+
return 0;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function verify(args, say) {
|
|
93
|
+
const file = args._[2];
|
|
94
|
+
const keyFile = args.flags['public-key'];
|
|
95
|
+
if (!file || !keyFile) { say.err(USAGE); return 2; }
|
|
96
|
+
let pkg; let key;
|
|
97
|
+
try { pkg = JSON.parse(fs.readFileSync(path.resolve(file), 'utf8')); key = fs.readFileSync(path.resolve(String(keyFile)), 'utf8'); } catch (e) { say.err(`Could not read the input: ${e.message}`); return 2; }
|
|
98
|
+
const r = verifyReleasePackage(pkg, key, { require: String(args.flags.require || '').split(',').filter(Boolean) });
|
|
99
|
+
const stated = r.ok ? verifyStatedVerdict(pkg) : null;
|
|
100
|
+
if (r.ok && stated && stated.ok) { say.out(`release package verified: scan ${r.completeness}, ${pkg.evidence.length} records, ${pkg.findings.length} findings`); return 0; }
|
|
101
|
+
for (const e of (r.errors || [])) say.err(`${e.code}: ${e.message}`);
|
|
102
|
+
if (r.ok && stated && stated.ok === false) say.err(`verdict-mismatch: ${stated.message}`);
|
|
103
|
+
say.err('release package REJECTED');
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function cmdReleasePackage(args, io = {}) {
|
|
108
|
+
const say = { out: io.out || ((s) => console.log(s)), err: io.err || ((s) => console.error(s)) };
|
|
109
|
+
const sub = args._[1];
|
|
110
|
+
if (sub === 'build') return build(args, say, io.git || (() => null));
|
|
111
|
+
if (sub === 'verify') return verify(args, say);
|
|
112
|
+
say.err(USAGE);
|
|
113
|
+
return 2;
|
|
114
|
+
}
|
package/src/posture/dnr/cli.js
CHANGED
|
@@ -12,11 +12,11 @@ import { buildDetectionIR, validateDetectionIR } from './detection-ir.js';
|
|
|
12
12
|
import { renderDetection, renderAll, FORMATS } from './outputs.js';
|
|
13
13
|
import { validateDetection } from './validate.js';
|
|
14
14
|
import { huntLogs } from './hunt.js';
|
|
15
|
-
import { computeExposureWindow, loadDeployments, formatExposureWindow } from './exposure-window.js';
|
|
15
|
+
import { computeExposureWindow, loadDeployments, formatExposureWindow, artifactCommitMap } from './exposure-window.js';
|
|
16
16
|
import { saveRecord, latestRecord, listRecords } from './store.js';
|
|
17
17
|
import { buildLifecycleView, formatLifecycleView, loadFindingFromLastScan } from './view.js';
|
|
18
18
|
|
|
19
|
-
const HUNT_EXIT = Object.freeze({ NOT_OBSERVED: 0, OBSERVED: 10, INCONCLUSIVE: 11, TELEMETRY_UNAVAILABLE: 12 });
|
|
19
|
+
export const HUNT_EXIT = Object.freeze({ NOT_OBSERVED: 0, OBSERVED: 10, INCONCLUSIVE: 11, TELEMETRY_UNAVAILABLE: 12 });
|
|
20
20
|
const EXT = { sigma: 'yml', http: 'json', waf: 'conf', 'log-query': 'json', yara: 'yar', spl: 'spl', kql: 'kql', cloudwatch: 'txt', elastic: 'json', yaral: 'yaral', datadog: 'json' };
|
|
21
21
|
|
|
22
22
|
const usage = () => { console.error('usage: dnr generate <finding-id> [--format sigma|http|waf|log-query|yara|spl|kql|cloudwatch|elastic|yaral|datadog|all] [--out dir] [--allow-unproven]\n dnr validate <finding-id>\n dnr hunt <finding-id> --logs <file-or-dir> [--since ISO] [--until ISO]\n dnr window <finding-id> --deployments <file> [--fix-commit sha] [--environment name]\n dnr show <finding-id>\n dnr status <finding-id>'); return 4; };
|
|
@@ -109,10 +109,19 @@ export async function cmdDnr(args, root) {
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
if (sub === 'window') {
|
|
112
|
-
if (!flags.deployments) { console.error('dnr window needs --deployments <file> (a JSON list of {commit, deployedAt, environment?}). Nothing was computed.'); return 4; }
|
|
112
|
+
if (!flags.deployments) { console.error('dnr window needs --deployments <file> (a JSON list of {commit, deployedAt, environment?}; with --artifact-graph, {artifactDigest, deployedAt} is enough). Nothing was computed.'); return 4; }
|
|
113
113
|
const finding = loadFindingFromLastScan(root, id);
|
|
114
114
|
if (!finding) { console.error(`Finding ${id} not found in the last scan.`); return 4; }
|
|
115
|
-
|
|
115
|
+
let artifactCommits = null; const graphProblems = [];
|
|
116
|
+
if (flags['artifact-graph']) {
|
|
117
|
+
const docs = [];
|
|
118
|
+
for (const f of String(flags['artifact-graph']).split(',').filter(Boolean)) {
|
|
119
|
+
try { docs.push(JSON.parse(fs.readFileSync(path.resolve(f), 'utf8'))); } catch (e) { graphProblems.push(`artifact graph ${f}: could not be read (${e.code || 'not JSON'})`); }
|
|
120
|
+
}
|
|
121
|
+
const map = artifactCommitMap(docs); artifactCommits = map.commits; graphProblems.push(...map.problems);
|
|
122
|
+
}
|
|
123
|
+
const loaded = loadDeployments(path.resolve(String(flags.deployments)), { environment: flags.environment ?? null, artifactCommits });
|
|
124
|
+
const deployments = loaded.deployments; const problems = [...loaded.problems, ...graphProblems];
|
|
116
125
|
const hunt = latestRecord(root, id, 'hunt')?.record ?? null;
|
|
117
126
|
const telemetry = flags['telemetry-start'] && flags['telemetry-end'] ? { start: flags['telemetry-start'], end: flags['telemetry-end'] } : hunt?.window ?? null;
|
|
118
127
|
const w = computeExposureWindow({ finding, repo: root, deployments, deploymentProblems: problems, fixCommit: flags['fix-commit'] ?? null, telemetry });
|
|
@@ -13,8 +13,34 @@ const SHA_RE = /^[0-9a-f]{7,64}$/i;
|
|
|
13
13
|
const ts = (v) => { if (v == null) return null; const t = Date.parse(v); return Number.isNaN(t) ? null : t; };
|
|
14
14
|
const iso = (t) => (t == null ? null : new Date(t).toISOString());
|
|
15
15
|
|
|
16
|
-
/**
|
|
17
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Which source commit each artifact digest was built from, read from artifact provenance graphs (`provenance trace` output, or the
|
|
18
|
+
* graph alone). The commit is whatever the graph's build label declared: a graph whose commit is unknown contributes nothing, and a
|
|
19
|
+
* digest named by two graphs that disagree is listed as a conflict and resolves to neither.
|
|
20
|
+
* Returns { commits: Map<digest, sha>, problems }.
|
|
21
|
+
*/
|
|
22
|
+
export function artifactCommitMap(graphs) {
|
|
23
|
+
const commits = new Map(); const conflicts = new Set(); const problems = [];
|
|
24
|
+
(graphs || []).forEach((doc, i) => {
|
|
25
|
+
const g = doc?.artifactProvenanceGraph ?? doc;
|
|
26
|
+
if (!g || !Array.isArray(g.nodes) || !Array.isArray(g.edges)) { problems.push(`artifact graph ${i}: not an artifact provenance graph`); return; }
|
|
27
|
+
const digest = g.nodes.find((n) => n && n.kind === 'ociDigest' && typeof n.digest === 'string')?.digest;
|
|
28
|
+
const commit = g.nodes.find((n) => n && n.kind === 'gitCommit' && n.known === true && typeof n.sha === 'string' && SHA_RE.test(n.sha))?.sha?.toLowerCase();
|
|
29
|
+
if (!digest) { problems.push(`artifact graph ${i}: no image digest`); return; }
|
|
30
|
+
if (!commit) { problems.push(`artifact graph ${i}: the image declares no source commit, so ${digest.slice(0, 19)} cannot be placed in history`); return; }
|
|
31
|
+
if (commits.has(digest) && commits.get(digest) !== commit) { conflicts.add(digest); return; }
|
|
32
|
+
commits.set(digest, commit);
|
|
33
|
+
});
|
|
34
|
+
for (const d of conflicts) { commits.delete(d); problems.push(`artifact graphs disagree about the source commit of ${d.slice(0, 19)}; it resolves to neither`); }
|
|
35
|
+
return { commits, problems };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parse a deployment-history file. Returns {deployments, problems}. Unusable entries are listed, never silently dropped.
|
|
40
|
+
* With `artifactCommits` (from artifactCommitMap), a deployment that names only an artifact digest is placed at the commit the image
|
|
41
|
+
* declares, and one that names both is refused when they disagree: the record cannot be trusted over the build label or the reverse.
|
|
42
|
+
*/
|
|
43
|
+
export function loadDeployments(file, { environment = null, artifactCommits = null } = {}) {
|
|
18
44
|
const problems = [];
|
|
19
45
|
let raw;
|
|
20
46
|
try { raw = JSON.parse(fs.readFileSync(file, 'utf8')); } catch (e) { return { deployments: [], problems: [`could not read deployment history: ${e.code || e.message}`] }; }
|
|
@@ -24,10 +50,17 @@ export function loadDeployments(file, { environment = null } = {}) {
|
|
|
24
50
|
const deployments = [];
|
|
25
51
|
list.slice(0, MAX_DEPLOYMENTS).forEach((d, i) => {
|
|
26
52
|
if (!d || typeof d !== 'object') { problems.push(`deployment ${i}: not an object`); return; }
|
|
27
|
-
|
|
53
|
+
const digest = typeof d.artifactDigest === 'string' ? d.artifactDigest.slice(0, 80) : null;
|
|
54
|
+
const fromGraph = digest && artifactCommits ? artifactCommits.get(digest) ?? null : null;
|
|
55
|
+
let commit = typeof d.commit === 'string' && SHA_RE.test(d.commit) ? d.commit.toLowerCase() : null;
|
|
56
|
+
let commitSource = 'deployment-record';
|
|
57
|
+
if (!commit && d.commit !== undefined && d.commit !== null) { problems.push(`deployment ${i}: commit must be a hex sha`); return; }
|
|
58
|
+
if (commit && fromGraph && !(fromGraph.startsWith(commit) || commit.startsWith(fromGraph))) { problems.push(`deployment ${i}: the record says commit ${commit.slice(0, 12)} but the image ${digest.slice(0, 19)} declares ${fromGraph.slice(0, 12)}; it was not used`); return; }
|
|
59
|
+
if (!commit && fromGraph) { commit = fromGraph; commitSource = 'artifact-provenance-graph'; }
|
|
60
|
+
if (!commit) { problems.push(digest && artifactCommits ? `deployment ${i}: commit must be a hex sha, and image ${digest.slice(0, 19)} is not in the artifact graphs` : `deployment ${i}: commit must be a hex sha`); return; }
|
|
28
61
|
if (ts(d.deployedAt) == null) { problems.push(`deployment ${i}: deployedAt is not a date`); return; }
|
|
29
62
|
if (environment && d.environment !== environment) return;
|
|
30
|
-
deployments.push({ commit
|
|
63
|
+
deployments.push({ commit, commitSource, deployedAt: iso(ts(d.deployedAt)), environment: typeof d.environment === 'string' ? d.environment.slice(0, 40) : null, artifactDigest: digest });
|
|
31
64
|
});
|
|
32
65
|
return { deployments, problems };
|
|
33
66
|
}
|
|
@@ -107,9 +140,9 @@ export function computeExposureWindow({ finding, repo, deployments: rawDeploymen
|
|
|
107
140
|
return {
|
|
108
141
|
schema: 'agentic-security/exposure-window', version: 1, findingId: finding?.stableId ?? finding?.id ?? null, status,
|
|
109
142
|
vulnerabilityIntroduced: introducedAt ? { at: introducedAt, commit: introCommit, source: 'finding-provenance' } : null,
|
|
110
|
-
firstVulnerableDeployment: firstVulnerable ? { at: firstVulnerable.deployedAt, commit: firstVulnerable.commit, environment: firstVulnerable.environment, artifactDigest: firstVulnerable.artifactDigest } : null,
|
|
143
|
+
firstVulnerableDeployment: firstVulnerable ? { at: firstVulnerable.deployedAt, commit: firstVulnerable.commit, commitSource: firstVulnerable.commitSource ?? 'deployment-record', environment: firstVulnerable.environment, artifactDigest: firstVulnerable.artifactDigest } : null,
|
|
111
144
|
fixCommitted: fixAt ? { at: fixAt, commit: fixCommit } : null,
|
|
112
|
-
fixDeployed: fixDeployedAt ? { at: fixDeployedAt.deployedAt, commit: fixDeployedAt.commit, environment: fixDeployedAt.environment, artifactDigest: fixDeployedAt.artifactDigest } : null,
|
|
145
|
+
fixDeployed: fixDeployedAt ? { at: fixDeployedAt.deployedAt, commit: fixDeployedAt.commit, commitSource: fixDeployedAt.commitSource ?? 'deployment-record', environment: fixDeployedAt.environment, artifactDigest: fixDeployedAt.artifactDigest } : null,
|
|
113
146
|
stillExposed: stillExposed(deployments, vulnerable),
|
|
114
147
|
telemetryWindow: telStart != null && telEnd != null ? { start: iso(telStart), end: iso(telEnd) } : null,
|
|
115
148
|
observableWindow: observable, unobservableBefore, unresolvedDeployments: unresolved, deploymentProblems, unknowns,
|
|
@@ -14,11 +14,11 @@ 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
|
-
const FLEET_MANIFEST_SCHEMA_KEYS = Object.freeze(['organization', 'repositories', 'workers', 'perRepoTimeout', 'retries', 'resources']);
|
|
18
|
-
const REPO_KEYS = Object.freeze(['id', 'path', 'required', 'kind', 'internetFacing', 'environment', 'dataClasses', 'trustZone', 'calls', 'produces', 'consumes', 'reads', 'writes', 'providers', 'aliases']);
|
|
19
|
-
const RESOURCE_KEYS = Object.freeze(['id', 'kind', 'dataClasses', 'trustZone', 'internetFacing', 'environment', 'aliases']);
|
|
20
|
-
const REPO_KINDS = Object.freeze(['service', 'library', 'frontend', 'worker', 'gateway']);
|
|
21
|
-
const RESOURCE_KINDS = Object.freeze(['queue', 'database', 'external-provider', 'gateway']);
|
|
17
|
+
export const FLEET_MANIFEST_SCHEMA_KEYS = Object.freeze(['organization', 'repositories', 'workers', 'perRepoTimeout', 'retries', 'resources']);
|
|
18
|
+
export const REPO_KEYS = Object.freeze(['id', 'path', 'required', 'kind', 'internetFacing', 'environment', 'dataClasses', 'trustZone', 'calls', 'produces', 'consumes', 'reads', 'writes', 'providers', 'aliases']);
|
|
19
|
+
export const RESOURCE_KEYS = Object.freeze(['id', 'kind', 'dataClasses', 'trustZone', 'internetFacing', 'environment', 'aliases']);
|
|
20
|
+
export const REPO_KINDS = Object.freeze(['service', 'library', 'frontend', 'worker', 'gateway']);
|
|
21
|
+
export const RESOURCE_KINDS = Object.freeze(['queue', 'database', 'external-provider', 'gateway']);
|
|
22
22
|
export const ENVIRONMENTS = Object.freeze(['production', 'staging', 'development']);
|
|
23
23
|
const NAME_RE = /^[a-z][a-z0-9-]{0,63}$/;
|
|
24
24
|
const DURATION_RE = /^(\d+)(s|m|h)$/;
|
|
@@ -18,6 +18,9 @@ import { superviseSpawn } from '../../sandbox/supervise.js';
|
|
|
18
18
|
import { scrubEnv } from '../../sandbox/trust-domains.js';
|
|
19
19
|
|
|
20
20
|
export const CONTAINER_ROOT = '/work';
|
|
21
|
+
// the image's own toolchain directories matter: the Temurin image puts the JDK at /opt/java/openjdk/bin and sets it through ENV, which a
|
|
22
|
+
// bare PATH override throws away (found by the first run against a real engine: `javac: not found`)
|
|
23
|
+
const CONTAINER_PATH = '/opt/java/openjdk/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin';
|
|
21
24
|
// the interpreter names inside the image; the host paths the runner uses locally mean nothing in there
|
|
22
25
|
export const CONTAINER_TOOLS = Object.freeze({ node: 'node', python: 'python3', java: 'java', javac: 'javac', cc: 'gcc', dotnet: 'dotnet' });
|
|
23
26
|
|
|
@@ -30,7 +33,7 @@ export function containerArgs({ image, name, hostRoot, argv, env = {}, limits, u
|
|
|
30
33
|
'--pids-limit', String(limits.pids), '--memory', `${limits.memoryMb}m`, '--memory-swap', `${limits.memoryMb}m`, '--cpus', String(limits.cpus),
|
|
31
34
|
'--tmpfs', `/tmp:rw,nosuid,size=${limits.diskMb}m`, '--user', uid,
|
|
32
35
|
'-v', `${hostRoot}:${CONTAINER_ROOT}:rw`, '--workdir', CONTAINER_ROOT,
|
|
33
|
-
'-e', 'HOME=/work', '-e',
|
|
36
|
+
'-e', 'HOME=/work', '-e', `PATH=${CONTAINER_PATH}`, '-e', 'CI=1',
|
|
34
37
|
];
|
|
35
38
|
for (const [k, v] of Object.entries(env)) args.push('-e', `${k}=${v}`);
|
|
36
39
|
args.push(image, ...argv);
|
|
@@ -97,7 +97,11 @@ function localEnv(force) {
|
|
|
97
97
|
}
|
|
98
98
|
|
|
99
99
|
function defaultDockerProbe(bin, image) {
|
|
100
|
-
|
|
100
|
+
// a daemon that is busy (a runner just finished pulling images) can take longer than a few seconds to answer; one slow
|
|
101
|
+
// answer must not read as "no daemon", so the first probe is patient and a failed one is tried once more
|
|
102
|
+
const ask = () => spawnSync(bin, ['info', '--format', '{{.ServerVersion}}'], { encoding: 'utf8', timeout: 15000, killSignal: 'SIGKILL' });
|
|
103
|
+
let info = ask();
|
|
104
|
+
if (info.status !== 0) info = ask();
|
|
101
105
|
if (info.status !== 0) return { ok: false, reason: 'container daemon not responding' };
|
|
102
106
|
const img = spawnSync(bin, ['image', 'inspect', '--format', '{{.Id}}', image], { encoding: 'utf8', timeout: 5000, killSignal: 'SIGKILL' });
|
|
103
107
|
// the image id is the environment's identity: two proofs with the same id ran on the same base
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// `agentic-security metrics [path] [--format text|json]`: the strategic metrics for the last scan and the recorded fix attempts.
|
|
2
|
+
// Exit codes: 0 reported; 1 the last scan failed its integrity check; 2 no scan to report on, or a usage problem.
|
|
3
|
+
import * as fs from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import { statePath } from './state-dir.js';
|
|
6
|
+
import { verifyLastScan } from './integrity.js';
|
|
7
|
+
import { loadFixAttempts } from './fix-metrics.js';
|
|
8
|
+
import { computeStrategicMetrics, renderStrategicMetrics } from './strategic-metrics.js';
|
|
9
|
+
|
|
10
|
+
export async function cmdMetrics(args, io = {}) {
|
|
11
|
+
const out = io.out || ((s) => process.stdout.write(s));
|
|
12
|
+
const err = io.err || ((s) => console.error(s));
|
|
13
|
+
const root = path.resolve(args._[1] || '.');
|
|
14
|
+
const format = String(args.flags.format || 'text');
|
|
15
|
+
if (!['text', 'json'].includes(format)) { err('Unknown --format. Use text or json.'); return 2; }
|
|
16
|
+
const file = statePath(root, 'last-scan.json');
|
|
17
|
+
let body;
|
|
18
|
+
try { body = fs.readFileSync(file, 'utf8'); } catch { err(`No scan found at ${file}. Run a scan first.`); return 2; }
|
|
19
|
+
if (verifyLastScan(body, file + '.sig') === false) { err('The last scan failed its integrity check, so no metric is reported from it.'); return 1; }
|
|
20
|
+
let scan;
|
|
21
|
+
try { scan = JSON.parse(body); } catch { err('The last scan is not valid JSON.'); return 1; }
|
|
22
|
+
const m = computeStrategicMetrics({ scan, fixAttempts: loadFixAttempts(root) });
|
|
23
|
+
out(format === 'json' ? JSON.stringify(m, null, 2) + '\n' : renderStrategicMetrics(m) + '\n');
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Strategic metrics: three numbers a scan, its findings and its fix attempts already carry the inputs for, each reported with its numerator
|
|
2
|
+
// and denominator and the basis it was counted on. Pure: no fs, no clock.
|
|
3
|
+
//
|
|
4
|
+
// Assurance Coverage of the analysis the scan was asked to do, how much finished (scanHealth files and analyzers)
|
|
5
|
+
// Evidence Conversion of the findings, how many reached an evidence state stronger than one analyzer's say-so
|
|
6
|
+
// Verified Remediation of the fix attempts, how many reached the strongest verification tier
|
|
7
|
+
//
|
|
8
|
+
// A fourth, the Security Learning Rate, is reported as not measured: nothing measures it independently of the rule compiler's own corpus.
|
|
9
|
+
//
|
|
10
|
+
// The rules that keep these honest:
|
|
11
|
+
// - a figure never appears without its numerator and denominator, and an empty denominator is `null`, not 0% or 100%;
|
|
12
|
+
// - a quantity the scan did not report (`expected: null`) makes that component `unmeasured`, never zero;
|
|
13
|
+
// - the headline of Assurance Coverage is the LOWER of its two components, so a complete analyzer set cannot hide unread files;
|
|
14
|
+
// - Evidence Conversion counts a state only when its evidence is present on the finding, and names the state each finding reached;
|
|
15
|
+
// - Verified Remediation is a lower bound where the strongest tier does not apply to a class, and says so.
|
|
16
|
+
|
|
17
|
+
export const STRATEGIC_METRICS_SCHEMA = 'agentic-security/strategic-metrics';
|
|
18
|
+
export const STRATEGIC_METRICS_VERSION = 1;
|
|
19
|
+
|
|
20
|
+
/** Evidence states, weakest first. A finding is counted at the strongest state it has evidence for. */
|
|
21
|
+
export const EVIDENCE_STATES = Object.freeze(['single-signal', 'corroborated', 'taint-confirmed', 'execution-proven']);
|
|
22
|
+
|
|
23
|
+
const rate = (n, d) => ({ numerator: n, denominator: d, percent: d > 0 ? Math.round((n / d) * 1000) / 10 : null });
|
|
24
|
+
const isNum = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
|
25
|
+
|
|
26
|
+
/** Of the analysis requested, how much finished. `scanHealth` is the scan's own record. */
|
|
27
|
+
export function assuranceCoverage(scanHealth) {
|
|
28
|
+
if (!scanHealth || typeof scanHealth !== 'object') {
|
|
29
|
+
return { status: 'unmeasured', reason: 'the scan carries no scan health record', files: null, analyzers: null, percent: null, scanStatus: null };
|
|
30
|
+
}
|
|
31
|
+
const f = scanHealth.files || {};
|
|
32
|
+
const a = scanHealth.analyzers || {};
|
|
33
|
+
const files = isNum(f.expected) && isNum(f.scanned)
|
|
34
|
+
? { ...rate(Math.max(0, Math.min(f.scanned, f.expected) - (isNum(f.timedOut) ? f.timedOut : 0)), f.expected), skipped: isNum(f.skipped) ? f.skipped : 0, unanalysed: isNum(f.unanalysed) ? f.unanalysed : 0, timedOut: isNum(f.timedOut) ? f.timedOut : 0 }
|
|
35
|
+
: null;
|
|
36
|
+
const analyzers = isNum(a.expected) && isNum(a.completed)
|
|
37
|
+
? { ...rate(Math.min(a.completed, a.expected), a.expected), failed: isNum(a.failed) ? a.failed : 0, timedOut: isNum(a.timedOut) ? a.timedOut : 0, skippedByPolicy: isNum(a.skippedByPolicy) ? a.skippedByPolicy : 0 }
|
|
38
|
+
: null;
|
|
39
|
+
const parts = [files, analyzers].filter((p) => p && p.percent !== null);
|
|
40
|
+
const percent = parts.length ? Math.min(...parts.map((p) => p.percent)) : null;
|
|
41
|
+
const missing = [!files && 'files', !analyzers && 'analyzers'].filter(Boolean);
|
|
42
|
+
return {
|
|
43
|
+
status: parts.length === 2 ? 'measured' : parts.length === 1 ? 'partial' : 'unmeasured',
|
|
44
|
+
...(missing.length ? { reason: `the scan did not report the expected count for: ${missing.join(', ')}` } : {}),
|
|
45
|
+
files, analyzers, percent, scanStatus: typeof scanHealth.status === 'string' ? scanHealth.status : null,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** The strongest evidence state a finding has evidence for. */
|
|
50
|
+
export function evidenceStateOf(f) {
|
|
51
|
+
if (!f || typeof f !== 'object') return 'single-signal';
|
|
52
|
+
const proof = f.proof && typeof f.proof.verdict === 'string' ? f.proof.verdict : null;
|
|
53
|
+
if (f.proofTier === 'execution-proven' || (proof && /^(proven|confirmed|exploit-confirmed|still-exploitable)$/i.test(proof))) return 'execution-proven';
|
|
54
|
+
if (f.parser === 'IR-TAINT' || f.proofTier === 'taint-proven' || f.proofTier === 'taint-confirmed') return 'taint-confirmed';
|
|
55
|
+
const by = f.corroboration && Array.isArray(f.corroboration.by) ? new Set(f.corroboration.by).size : (Number(f.corroborationCount) || 0);
|
|
56
|
+
if (by >= 2) return 'corroborated';
|
|
57
|
+
return 'single-signal';
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Of the findings, how many reached a state stronger than one analyzer's say-so. */
|
|
61
|
+
export function evidenceConversion(findings) {
|
|
62
|
+
const list = Array.isArray(findings) ? findings.filter((f) => f && typeof f === 'object') : [];
|
|
63
|
+
const byState = Object.fromEntries(EVIDENCE_STATES.map((s) => [s, 0]));
|
|
64
|
+
for (const f of list) byState[evidenceStateOf(f)]++;
|
|
65
|
+
const converted = list.length - byState['single-signal'];
|
|
66
|
+
return { ...rate(converted, list.length), byState, basis: 'findings that reached corroborated or stronger, over all findings' };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const strongest = (a) => {
|
|
70
|
+
if (typeof a.verifiedFull === 'boolean') return a.verifiedFull;
|
|
71
|
+
const disappeared = a.rescanOk === true || (a.ok === true && a.rescanOk !== false);
|
|
72
|
+
return disappeared && a.testsRan === true && a.testsOk !== false && a.pocOk === true;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** Of the fix attempts, how many reached the strongest verification tier. A lower bound: some classes have no independent proof to run. */
|
|
76
|
+
export function verifiedRemediationRate(attempts) {
|
|
77
|
+
const list = Array.isArray(attempts) ? attempts.filter((a) => a && typeof a === 'object') : [];
|
|
78
|
+
const full = list.filter(strongest).length;
|
|
79
|
+
const disappearedOnly = list.filter((a) => typeof a.verifiedFull !== 'boolean' && (a.rescanOk === true || (a.ok === true && a.rescanOk !== false)) && !(a.testsRan === true && a.testsOk !== false) && a.pocOk !== true).length;
|
|
80
|
+
return {
|
|
81
|
+
...rate(full, list.length),
|
|
82
|
+
disappearedOnly,
|
|
83
|
+
basis: 'attempts that passed the re-scan, the project tests and the independent attack replay, over all attempts. A lower bound where a class has no replay',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Nothing measures it independently of the rule compiler's own corpus, so it is reported as not measured. */
|
|
88
|
+
export function securityLearningRate() {
|
|
89
|
+
return { status: 'not-measured', reason: 'the rule compiler grades its rules against a corpus built from the same finding; no independent measurement of novel weaknesses converted into reusable knowledge exists' };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* @param {object} o
|
|
94
|
+
* @param {object} [o.scan] a scan result (`scanHealth`, `findings`)
|
|
95
|
+
* @param {object[]} [o.fixAttempts] fix verification attempts
|
|
96
|
+
*/
|
|
97
|
+
export function computeStrategicMetrics({ scan = null, fixAttempts = [] } = {}) {
|
|
98
|
+
return {
|
|
99
|
+
schema: STRATEGIC_METRICS_SCHEMA, version: STRATEGIC_METRICS_VERSION,
|
|
100
|
+
assuranceCoverage: assuranceCoverage(scan && scan.scanHealth),
|
|
101
|
+
evidenceConversion: evidenceConversion(scan && scan.findings),
|
|
102
|
+
verifiedRemediationRate: verifiedRemediationRate(fixAttempts),
|
|
103
|
+
securityLearningRate: securityLearningRate(),
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const pc = (r) => (r && r.percent !== null && r.percent !== undefined ? `${r.percent}%` : 'not measurable');
|
|
108
|
+
|
|
109
|
+
export function renderStrategicMetrics(m) {
|
|
110
|
+
const ac = m.assuranceCoverage; const ec = m.evidenceConversion; const vr = m.verifiedRemediationRate;
|
|
111
|
+
const L = [];
|
|
112
|
+
L.push(`Assurance Coverage: ${ac.percent === null ? 'not measurable' : `${ac.percent}% (the lower of the two components)`}${ac.scanStatus ? `; scan ${ac.scanStatus}` : ''}`);
|
|
113
|
+
if (ac.files) L.push(` files: ${ac.files.numerator} of ${ac.files.denominator} completed (${pc(ac.files)}); ${ac.files.skipped} skipped, ${ac.files.unanalysed} not analysed, ${ac.files.timedOut} timed out`);
|
|
114
|
+
if (ac.analyzers) L.push(` analyzers: ${ac.analyzers.numerator} of ${ac.analyzers.denominator} completed (${pc(ac.analyzers)}); ${ac.analyzers.failed} failed, ${ac.analyzers.timedOut} timed out`);
|
|
115
|
+
if (ac.reason) L.push(` ${ac.reason}`);
|
|
116
|
+
L.push(`Evidence Conversion: ${ec.numerator} of ${ec.denominator} findings reached corroborated or stronger (${pc(ec)})`);
|
|
117
|
+
L.push(` ${EVIDENCE_STATES.map((s) => `${s} ${ec.byState[s]}`).join(', ')}`);
|
|
118
|
+
L.push(`Verified Remediation Rate: ${vr.numerator} of ${vr.denominator} fix attempts reached the strongest verification tier (${pc(vr)})${vr.denominator ? '; a lower bound where a class has no attack replay' : ''}`);
|
|
119
|
+
if (vr.disappearedOnly) L.push(` ${vr.disappearedOnly} satisfied only the re-scan, which is also what deleting the code produces`);
|
|
120
|
+
L.push(`Security Learning Rate: not measured. ${m.securityLearningRate.reason}`);
|
|
121
|
+
return L.join('\n');
|
|
122
|
+
}
|