@clear-capabilities/agentic-security-scanner 0.168.0 → 0.170.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 +74 -0
- package/bin/agentic-security.js +567 -0
- package/dist/2199.index.js +1 -1
- package/dist/2675.index.js +174 -0
- package/dist/2923.index.js +369 -1
- package/dist/4142.index.js +6 -6
- package/dist/4700.index.js +151 -0
- package/dist/5014.index.js +2 -2
- package/dist/5086.index.js +645 -0
- package/dist/5144.index.js +2 -2
- package/dist/5206.index.js +32 -0
- package/dist/642.index.js +197 -0
- package/dist/6447.index.js +131 -0
- package/dist/6645.index.js +87 -0
- package/dist/6730.index.js +2 -2
- package/dist/6932.index.js +2 -2
- package/dist/7055.index.js +369 -0
- package/dist/7232.index.js +24 -0
- package/dist/7552.index.js +2 -2
- package/dist/7952.index.js +301 -0
- package/dist/8714.index.js +2 -2
- package/dist/9102.index.js +110 -0
- package/dist/9800.index.js +1 -1
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/artifact/CLAUDE.md +100 -0
- package/src/artifact/filesystem.js +100 -0
- package/src/artifact/image-archive.js +159 -0
- package/src/artifact/inventory.js +69 -0
- package/src/artifact/layer-secrets.js +92 -0
- package/src/artifact/mismatch.js +72 -0
- package/src/artifact/provenance-graph.js +115 -0
- package/src/artifact/safe-tar.js +175 -0
- package/src/artifact/signature.js +156 -0
- package/src/artifact/vex.js +94 -0
- package/src/artifact/vuln-correlate.js +17 -0
- package/src/lineage/CLAUDE.md +19 -0
- package/src/lineage/observation-adapters.js +368 -0
- package/src/lineage/runtime-assurance.js +135 -0
- package/src/lineage/runtime-observation.js +37 -4
- package/src/posture/artifact-registry.js +8 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
40d5efd2eaa9410528e168bd5055148ac0de203d57c44b68abc12c25618db762 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.170.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",
|
|
@@ -74,11 +74,11 @@
|
|
|
74
74
|
"test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
|
|
75
75
|
"test:glob": "node --test test/glob-compat.test.js",
|
|
76
76
|
"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/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",
|
|
77
|
-
"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/proof-fabric/core.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/cli/rule-compiler-cli.test.js test/fleet/fleet-graph.test.js test/fleet/fleet-manifest.test.js test/fleet/fleet-cli.test.js",
|
|
77
|
+
"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/proof-fabric/core.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/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/cli/artifact-provenance-cli.test.js",
|
|
78
78
|
"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",
|
|
79
79
|
"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",
|
|
80
80
|
"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",
|
|
81
|
-
"test:lineage": "node --test test/lineage/schema.test.js test/lineage/ids.test.js test/lineage/protection.test.js test/lineage/classification.test.js test/lineage/validate.test.js test/lineage/json-schema-parity.test.js test/lineage/flagship-fixture.test.js test/lineage/flagship-fixture-semantics.test.js test/lineage/field-identity.test.js test/lineage/engine-expr-resolver.test.js test/lineage/engine-walker.test.js test/lineage/engine-integration.test.js test/lineage/engine-receiver-identity.test.js test/lineage/summaries.test.js test/lineage/driver.test.js test/lineage/engine-provenance.test.js test/lineage/engine-provenance-interprocedural.test.js test/lineage/path-store.test.js test/lineage/path-query.test.js test/lineage/flow-grade.test.js test/lineage/source-registry.test.js test/lineage/sink-registry.test.js test/lineage/transform-catalog.test.js test/lineage/handling-analyzer.test.js test/lineage/registry-real-code.test.js test/lineage/source-seeding.test.js test/lineage/graph-builder.test.js test/lineage/coverage.test.js test/lineage/resolve-destination.test.js test/lineage/queue-detail.test.js test/lineage/index.test.js test/lineage/obligation-mapping.test.js test/lineage/obligation-predicates.test.js test/lineage/obligation-predicates-walkthrough.test.js test/lineage/transit-protection.test.js test/lineage/at-rest-protection.test.js test/lineage/protection-summary.test.js test/lineage/ac01-multi-sink.test.js test/lineage/policy-verdict.test.js test/lineage/governance-refs.test.js test/lineage/export-privacy.test.js test/lineage/export-json.test.js test/lineage/export-csv.test.js test/lineage/decision-story.test.js test/lineage/export-briefing.test.js test/lineage/graph-snapshot.test.js test/lineage/graph-diff.test.js test/lineage/drift-policy.test.js test/lineage/recipient-profile.test.js test/lineage/recipient-registry.test.js test/lineage/recipient-wiring.test.js test/lineage/redact-graph.test.js test/lineage-scan-wiring.test.js test/lineage-artifact-write.test.js test/bench-data-lineage-runner.test.js test/bundle-frontend.test.js test/bundle-frontend-golden.test.js test/generate-html-report.test.js test/export-image.test.js test/lineage/scenario.test.js test/lineage/scenario-engine.test.js test/lineage/scenario-diff.test.js test/lineage/scenario-no-obligation-wiring.test.js test/lineage/impact-assessment.test.js test/lineage/impact-engine.test.js test/lineage/impact-no-obligation-wiring.test.js test/lineage/governance-edit.test.js test/lineage/remediation.test.js test/lineage/runtime-observation.test.js test/lineage/observation-correlation.test.js test/lineage/observation-adapters.test.js test/lineage/observation-store.test.js test/lineage/runtime-corroboration-wiring.test.js test/lineage/cross-repo-link.test.js test/lineage/federation-loader.test.js test/lineage/cross-repo-link-wiring.test.js",
|
|
81
|
+
"test:lineage": "node --test test/lineage/schema.test.js test/lineage/ids.test.js test/lineage/protection.test.js test/lineage/classification.test.js test/lineage/validate.test.js test/lineage/json-schema-parity.test.js test/lineage/flagship-fixture.test.js test/lineage/flagship-fixture-semantics.test.js test/lineage/field-identity.test.js test/lineage/engine-expr-resolver.test.js test/lineage/engine-walker.test.js test/lineage/engine-integration.test.js test/lineage/engine-receiver-identity.test.js test/lineage/summaries.test.js test/lineage/driver.test.js test/lineage/engine-provenance.test.js test/lineage/engine-provenance-interprocedural.test.js test/lineage/path-store.test.js test/lineage/path-query.test.js test/lineage/flow-grade.test.js test/lineage/source-registry.test.js test/lineage/sink-registry.test.js test/lineage/transform-catalog.test.js test/lineage/handling-analyzer.test.js test/lineage/registry-real-code.test.js test/lineage/source-seeding.test.js test/lineage/graph-builder.test.js test/lineage/coverage.test.js test/lineage/resolve-destination.test.js test/lineage/queue-detail.test.js test/lineage/index.test.js test/lineage/obligation-mapping.test.js test/lineage/obligation-predicates.test.js test/lineage/obligation-predicates-walkthrough.test.js test/lineage/transit-protection.test.js test/lineage/at-rest-protection.test.js test/lineage/protection-summary.test.js test/lineage/ac01-multi-sink.test.js test/lineage/policy-verdict.test.js test/lineage/governance-refs.test.js test/lineage/export-privacy.test.js test/lineage/export-json.test.js test/lineage/export-csv.test.js test/lineage/decision-story.test.js test/lineage/export-briefing.test.js test/lineage/graph-snapshot.test.js test/lineage/graph-diff.test.js test/lineage/drift-policy.test.js test/lineage/recipient-profile.test.js test/lineage/recipient-registry.test.js test/lineage/recipient-wiring.test.js test/lineage/redact-graph.test.js test/lineage-scan-wiring.test.js test/lineage-artifact-write.test.js test/bench-data-lineage-runner.test.js test/bundle-frontend.test.js test/bundle-frontend-golden.test.js test/generate-html-report.test.js test/export-image.test.js test/lineage/scenario.test.js test/lineage/scenario-engine.test.js test/lineage/scenario-diff.test.js test/lineage/scenario-no-obligation-wiring.test.js test/lineage/impact-assessment.test.js test/lineage/impact-engine.test.js test/lineage/impact-no-obligation-wiring.test.js test/lineage/governance-edit.test.js test/lineage/remediation.test.js test/lineage/runtime-observation.test.js test/lineage/observation-correlation.test.js test/lineage/observation-adapters.test.js test/lineage/observation-store.test.js test/lineage/runtime-corroboration-wiring.test.js test/lineage/cross-repo-link.test.js test/lineage/federation-loader.test.js test/lineage/cross-repo-link-wiring.test.js test/lineage/runtime-assurance.test.js test/cli/runtime-twin-cli.test.js",
|
|
82
82
|
"test:bench-modules": "node --test test/phase4-harness.test.js test/pipeline.test.js test/proof-corpus-lib.test.js test/proof-corpus-runner.test.js",
|
|
83
83
|
"test:server": "node --test test/server/graph-loader.test.js test/server/security.test.js test/server/routes.test.js test/server/http-server.test.js test/server/cmd-explore.test.js test/server/cmd-dataflow-export.test.js test/server/static-assets.test.js test/server/dataflow-scenario-cli.test.js test/server/dataflow-impact-cli.test.js",
|
|
84
84
|
"test:haskell": "node --test test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-sca-severity.test.js test/haskell/haskell-web-guards.test.js test/haskell/haskell-advisory-feed.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js test/haskell/haskell-boundaries.test.js test/haskell/haskell-live-feed-scan.test.js test/haskell/haskell-supply-bounds.test.js test/haskell/haskell-ir-scaling.test.js",
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# src/artifact/
|
|
2
|
+
|
|
3
|
+
PRD 5 — Source-to-Artifact Security & Provenance. Binds a finding to the actual **built artifact** (a
|
|
4
|
+
container image, today) that ships it, closing the gap between "this source file has a vulnerability" and
|
|
5
|
+
"this specific image digest contains it." Isolated from every other subsystem this package already uses the
|
|
6
|
+
word "provenance" for — see the naming section below before adding a new field or function name here.
|
|
7
|
+
|
|
8
|
+
## Naming discipline (read this before adding a field)
|
|
9
|
+
|
|
10
|
+
This repo already has 5 unrelated uses of "provenance"/"artifact": `posture/provenance/`'s `findingProvenance`
|
|
11
|
+
(git-commit origin), `posture/provenance.js`'s `annotateFindingProvenance` (parser corroboration),
|
|
12
|
+
`sca/sigstore-verify.js`'s `annotateProvenance` (package-build Sigstore verification), a pre-existing bare
|
|
13
|
+
`finding.provenance` field, and `posture/artifact-registry.js` (internal `.agentic-security/*` state
|
|
14
|
+
classification — nothing to do with container images). **Never** introduce a bare `.provenance`/`*Provenance`
|
|
15
|
+
field or a module/field literally named `artifact-registry` here. This package's own graph field is
|
|
16
|
+
`artifactProvenanceGraph`, built by `buildArtifactProvenanceGraph` — the compound name is deliberate. The
|
|
17
|
+
PRD's own literal CLI surface (`agentic-security provenance trace`) is honored as user-facing text regardless;
|
|
18
|
+
the discipline is about internal names, not the CLI vocabulary the PRD mandates.
|
|
19
|
+
|
|
20
|
+
## Module map (read in this order — each builds on the last)
|
|
21
|
+
|
|
22
|
+
- `safe-tar.js` — hardened POSIX/GNU tar reader. Every byte any other module in this package reads passes
|
|
23
|
+
through here first. Hard caps on entry count/size/archive size (`DEFAULT_LIMITS`), gzip decompression with
|
|
24
|
+
Node's native `maxOutputLength` guard (no custom streaming counter needed — see the file header), and an
|
|
25
|
+
entry-name safety check (`isSafeEntryName`) that flags `..`/absolute/NUL paths rather than normalizing
|
|
26
|
+
them. No caller ever writes extracted content to a real filesystem path — every result is an in-memory
|
|
27
|
+
`Map`/object keyed by the (safety-checked) tar entry name, which is what makes zip-slip structurally
|
|
28
|
+
impossible here rather than merely guarded against.
|
|
29
|
+
- `image-archive.js` — parses a `docker save`-shaped archive (`manifest.json` + a config blob + per-layer
|
|
30
|
+
tars) or an OCI-layout archive (`oci-layout` + `index.json` + `blobs/sha256/*`) into one normalized shape.
|
|
31
|
+
Rejects (never guesses) a multi-image archive, a missing referenced blob, or malformed JSON.
|
|
32
|
+
- `filesystem.js` — merges an image's layers in order, applying AUFS/overlay whiteout semantics
|
|
33
|
+
(`.wh.<name>` deletes, `.wh..wh..opq` makes a directory opaque to earlier layers). `readLayers` keeps the
|
|
34
|
+
RAW per-layer, pre-merge view (what `layer-secrets.js` needs to see "this existed in layer 3" even after a
|
|
35
|
+
later layer's whiteout hides it); `mergeLayers` produces the final filesystem view, bounded by
|
|
36
|
+
`DEFAULT_FS_LIMITS` (per-file and total materialization caps — large/binary files are tracked in `files`
|
|
37
|
+
metadata but never fully read into memory).
|
|
38
|
+
- `inventory.js` — PRD P0 §1-2 (OCI image scanning + inventory). Ties the above together and feeds the merged
|
|
39
|
+
filesystem's text content through the EXISTING SCA pipeline: `sca/image-packages.js#extractImagePackages`
|
|
40
|
+
for OS packages, and an injected `manifestParser` (the engine's own `parseManifests`, from `sca/index.js`)
|
|
41
|
+
for application packages — no parallel manifest-parsing logic. `baseImage` is read ONLY from the
|
|
42
|
+
`org.opencontainers.image.base.name`/`.digest` labels when present; absent that, it is `null`, never a
|
|
43
|
+
guess from layer-content heuristics (no base-image fingerprint table exists in this codebase, and inventing
|
|
44
|
+
one would produce a confident-looking wrong answer).
|
|
45
|
+
- `vuln-correlate.js` — PRD P0 §4. A thin wrapper handing `inventory`'s components to an injected
|
|
46
|
+
`queryOSVFn` (the engine's own `queryOSV`) — "reuse current SCA semantics" literally, not a reimplementation.
|
|
47
|
+
- `layer-secrets.js` — PRD P0 §3 (layer secret analysis). `detectFn` is injected, mirroring
|
|
48
|
+
`posture/secret-history.js`'s own `(filePath, rawText) -> Finding[]` detector-injection shape for the exact
|
|
49
|
+
same reason: this package never imports `engine.js` directly, and the real `scanCredentials` detector
|
|
50
|
+
already returns redacted/masked values — this module never re-touches raw content once a finding exists,
|
|
51
|
+
so there's no second place a secret value could leak back in. `deletedButRecoverable` is the subset of
|
|
52
|
+
findings whose path (or an ancestor opaque-marked directory) was removed by a LATER layer's whiteout — the
|
|
53
|
+
PRD's named scenario ("added in layer 3, deleted in layer 5, but remains recoverable").
|
|
54
|
+
- `signature.js` — PRD P0 §5. States are exactly the PRD's own four (`verified`/`invalid`/`missing`/
|
|
55
|
+
`unavailable`) — deliberately different words from `sigstore-verify.js`'s own vocabulary so the two are
|
|
56
|
+
never confused. `verifyImageSignature` is this tool's own local detached-signature convention (NOT cosign's
|
|
57
|
+
"simple signing" format). `verifyProvenanceAttestation` implements the REAL DSSE PAE (Pre-Authentication
|
|
58
|
+
Encoding) cosign/Sigstore attestations actually use — a real cosign-keyed attestation bundle verifies here
|
|
59
|
+
given the matching public key. Keyless/Rekor-transparency-log verification is NOT implemented (disclosed
|
|
60
|
+
gap; `sigstore-verify.js`'s Rekor-query pattern is the template a future increment could extend this with).
|
|
61
|
+
- `provenance-graph.js` — PRD P0 §6. Assembles Git Commit → Build → OCI Digest → SBOM Component → Finding →
|
|
62
|
+
Deployment from evidence the modules above already computed — no new detection. The Git-Commit edge's only
|
|
63
|
+
evidence source is the `org.opencontainers.image.revision` label; absent that, the commit node is
|
|
64
|
+
`known: false`, never inferred from local git history by heuristic. `deployment` is always
|
|
65
|
+
OPERATOR-DECLARED (`{id, environment, digest}`) — nothing in this codebase discovers "which digest is
|
|
66
|
+
running where" on its own (`lineage/deployment/` models topology, not running digests), so a missing
|
|
67
|
+
`deployment` argument means the graph simply stops short, never a fabricated edge.
|
|
68
|
+
- `mismatch.js` — PRD P0 §7, the PRD's own "Killer Demonstration". Compares a source-tree SCA scan against
|
|
69
|
+
one artifact's correlation by **OSV advisory id alone** — never by purl (which bakes in the version). Keying
|
|
70
|
+
by purl was tried first and is wrong: source and artifact are usually running different versions of the
|
|
71
|
+
same package by the entire premise of this comparison, so a purl-keyed match would report every version
|
|
72
|
+
difference as a double mismatch instead of asking "does this specific advisory affect each side's own
|
|
73
|
+
installed version." Scenario A ("Git: fixed / Production: still vulnerable") = an advisory present against
|
|
74
|
+
the artifact's version, absent against source's. Scenario B ("Git: vulnerable / Built artifact: absent") =
|
|
75
|
+
an advisory source flags whose PACKAGE (ecosystem+name, not version) never appears in the artifact's
|
|
76
|
+
inventory at all.
|
|
77
|
+
- `vex.js` — PRD P0 §8. `not_affected` is reachable from exactly two evidence paths (component absent from
|
|
78
|
+
the artifact; confirmed-unreachable per existing dataflow/reachability evidence) — never from the absence
|
|
79
|
+
of information, per the PRD's own "`not affected` requires positive evidence" rule. Symmetrically,
|
|
80
|
+
`affected` requires CONFIRMED reachability, not merely "OSV lists this version as vulnerable"; absent that
|
|
81
|
+
confirmation the honest status is `under_investigation`, never a confident `affected` the evidence doesn't
|
|
82
|
+
support. `fixed` requires a clean dotted-numeric version comparison against a real `fixedVersions` entry —
|
|
83
|
+
an unparseable version pair skips that check rather than guessing.
|
|
84
|
+
|
|
85
|
+
## CLI (`bin/agentic-security.js`)
|
|
86
|
+
|
|
87
|
+
`image scan`/`image verify`/`provenance trace`/`supply vex` — the PRD's own literal CLI shape (NOT a mode of
|
|
88
|
+
the `/supply` SLASH COMMAND, which is a different layer: Claude Code UI sugar that shells out to `scan --only
|
|
89
|
+
sca`/`cve-watch`). A scanned artifact persists to `.agentic-security/artifact-scans/<digest-hex>.json`
|
|
90
|
+
(registered in `posture/artifact-registry.js`) so `provenance trace <digest>`/`supply vex <digest>` can
|
|
91
|
+
re-reference it without re-parsing the archive. Local-daemon image input (`docker save`) and a remote OCI
|
|
92
|
+
registry reference are **NOT implemented** — only `docker save`/OCI-layout TAR FILE input — disclosed in
|
|
93
|
+
`docs/implementation-status/source-to-artifact-provenance.md`, not silently dropped.
|
|
94
|
+
|
|
95
|
+
## Test scope
|
|
96
|
+
|
|
97
|
+
`test/artifact/*.test.js` + `test/cli/artifact-provenance-cli.test.js`, wired into `npm run test:posture`
|
|
98
|
+
(same precedent as `fleet/`, `rule-compiler/`, `proof-fabric/` — not large enough for its own scoped script).
|
|
99
|
+
`test/helpers/build-tar-fixture.js` is a test-only tar/OCI-archive WRITER (production code only ever reads
|
|
100
|
+
tar, via `safe-tar.js`) — mirrors `build-git-fixture.js`'s role for the lineage/deployment suites.
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Merges an image's layers (in order) into one virtual filesystem view,
|
|
2
|
+
// applying AUFS/overlay whiteout semantics (`.wh.<name>` deletes a file,
|
|
3
|
+
// `.wh..wh..opq` makes a directory opaque to everything below it in earlier
|
|
4
|
+
// layers). Nothing here ever writes to a real filesystem path — the merged
|
|
5
|
+
// view is an in-memory Map keyed by the tar entry's (already path-safety-
|
|
6
|
+
// checked) name, which is what makes this immune to zip-slip by construction
|
|
7
|
+
// rather than by convention.
|
|
8
|
+
import { readTarEntries, isWhiteoutName, whiteoutTarget, DEFAULT_LIMITS } from './safe-tar.js';
|
|
9
|
+
|
|
10
|
+
export const DEFAULT_FS_LIMITS = Object.freeze({
|
|
11
|
+
maxFileBytes: 1 * 1024 * 1024, // per-file materialization cap
|
|
12
|
+
maxMaterializedTotalBytes: 256 * 1024 * 1024, // total content kept in memory
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Reads every layer's tar into per-layer entry lists (bounded by the same
|
|
17
|
+
* safe-tar limits as the outer archive) without merging — the raw, ordered
|
|
18
|
+
* per-layer view that layer-secrets.js needs to see what a LATER layer
|
|
19
|
+
* deleted while it was still present in an EARLIER one.
|
|
20
|
+
*/
|
|
21
|
+
export function readLayers(layers, tarLimits = DEFAULT_LIMITS) {
|
|
22
|
+
const result = [];
|
|
23
|
+
const unsafeEntries = [];
|
|
24
|
+
for (const layer of layers) {
|
|
25
|
+
const entries = [];
|
|
26
|
+
for (const e of readTarEntries(layer.tar, tarLimits)) {
|
|
27
|
+
if (e.unsafe) {
|
|
28
|
+
unsafeEntries.push({ layerIndex: layer.index, name: e.name });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
entries.push(e);
|
|
32
|
+
}
|
|
33
|
+
result.push({ layerIndex: layer.index, entries });
|
|
34
|
+
}
|
|
35
|
+
return { layers: result, unsafeEntries };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Merges the per-layer entry lists (from readLayers) into one final
|
|
40
|
+
* filesystem view, applying whiteouts in layer order. Returns:
|
|
41
|
+
* - `files`: Map<path, { sizeBytes, sourceLayerIndex, typeflag }> — every
|
|
42
|
+
* surviving path, metadata only.
|
|
43
|
+
* - `fileContents`: { path: string } — decoded text content, capped per
|
|
44
|
+
* file and in total (see DEFAULT_FS_LIMITS); large/binary files are
|
|
45
|
+
* present in `files` but absent here.
|
|
46
|
+
* - `deletedPaths`: [{ path, deletedAtLayer, opaque }] — every whiteout
|
|
47
|
+
* seen, in order, regardless of whether the path ever existed.
|
|
48
|
+
* - `materializationTruncated`: true once the total content cap was hit —
|
|
49
|
+
* disclosed, not silent.
|
|
50
|
+
*/
|
|
51
|
+
export function mergeLayers(readLayersResult, fsLimits = DEFAULT_FS_LIMITS) {
|
|
52
|
+
const files = new Map(); // path -> { sizeBytes, sourceLayerIndex, typeflag, data }
|
|
53
|
+
const deletedPaths = [];
|
|
54
|
+
let materializedTotal = 0;
|
|
55
|
+
let materializationTruncated = false;
|
|
56
|
+
|
|
57
|
+
for (const { layerIndex, entries } of readLayersResult.layers) {
|
|
58
|
+
const opaqueDirs = [];
|
|
59
|
+
for (const e of entries) {
|
|
60
|
+
if (isWhiteoutName(e.name)) {
|
|
61
|
+
const wh = whiteoutTarget(e.name);
|
|
62
|
+
if (wh.opaque) {
|
|
63
|
+
opaqueDirs.push(wh.dir);
|
|
64
|
+
deletedPaths.push({ path: wh.dir, deletedAtLayer: layerIndex, opaque: true });
|
|
65
|
+
for (const existing of [...files.keys()]) {
|
|
66
|
+
if (existing === wh.dir || existing.startsWith(`${wh.dir}/`)) files.delete(existing);
|
|
67
|
+
}
|
|
68
|
+
} else {
|
|
69
|
+
deletedPaths.push({ path: wh.path, deletedAtLayer: layerIndex, opaque: false });
|
|
70
|
+
files.delete(wh.path);
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
// An opaque marker earlier in THIS layer hides any further entry in
|
|
75
|
+
// this same layer that would resurrect something under that dir —
|
|
76
|
+
// real overlay semantics scope opaque to "everything from layers
|
|
77
|
+
// BELOW this one", so same-layer adds after the marker still apply.
|
|
78
|
+
files.set(e.name, { sizeBytes: e.size, sourceLayerIndex: layerIndex, typeflag: e.typeflag, data: e.data });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const fileContents = {};
|
|
83
|
+
for (const [path, meta] of files) {
|
|
84
|
+
if (meta.typeflag !== '0' && meta.typeflag !== '') continue; // only regular files get content
|
|
85
|
+
if (meta.sizeBytes > fsLimits.maxFileBytes) continue;
|
|
86
|
+
if (materializedTotal + meta.sizeBytes > fsLimits.maxMaterializedTotalBytes) {
|
|
87
|
+
materializationTruncated = true;
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
fileContents[path] = meta.data.toString('utf8');
|
|
91
|
+
materializedTotal += meta.sizeBytes;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const fileMeta = new Map();
|
|
95
|
+
for (const [path, meta] of files) {
|
|
96
|
+
fileMeta.set(path, { sizeBytes: meta.sizeBytes, sourceLayerIndex: meta.sourceLayerIndex, typeflag: meta.typeflag });
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { files: fileMeta, fileContents, deletedPaths, materializationTruncated };
|
|
100
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
// Parses a Docker-save ("docker-archive") or OCI-layout ("oci-archive") tar
|
|
2
|
+
// into one normalized shape, without executing or daemon-mounting the image
|
|
3
|
+
// (PRD 5 Safety Requirements: "OCI inspection should not require executing
|
|
4
|
+
// the image"). Built entirely on `safe-tar.js` — every byte read here passes
|
|
5
|
+
// through the same entry-count/size/path/decompression guards.
|
|
6
|
+
import crypto from 'node:crypto';
|
|
7
|
+
import { readTarEntries, decompressIfGzip, DEFAULT_LIMITS } from './safe-tar.js';
|
|
8
|
+
|
|
9
|
+
export class ImageArchiveError extends Error {
|
|
10
|
+
constructor(message, code) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = 'ImageArchiveError';
|
|
13
|
+
this.code = code;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function safeJsonParse(buf, what) {
|
|
18
|
+
try {
|
|
19
|
+
return JSON.parse(buf.toString('utf8'));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
throw new ImageArchiveError(`${what} is not valid JSON: ${err.message}`, 'malformed_manifest');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function readOuterEntries(buf, limits) {
|
|
26
|
+
const outer = decompressIfGzip(buf, limits);
|
|
27
|
+
const byName = new Map();
|
|
28
|
+
const unsafeEntries = [];
|
|
29
|
+
for (const e of readTarEntries(outer, limits)) {
|
|
30
|
+
if (e.unsafe) {
|
|
31
|
+
unsafeEntries.push({ name: e.name });
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
byName.set(e.name, e);
|
|
35
|
+
}
|
|
36
|
+
return { byName, unsafeEntries };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function extractConfigShape(configObj) {
|
|
40
|
+
const cfg = (configObj && configObj.config) || {};
|
|
41
|
+
return {
|
|
42
|
+
architecture: configObj.architecture || null,
|
|
43
|
+
os: configObj.os || null,
|
|
44
|
+
labels: (cfg.Labels && typeof cfg.Labels === 'object') ? { ...cfg.Labels } : {},
|
|
45
|
+
entrypoint: Array.isArray(cfg.Entrypoint) ? [...cfg.Entrypoint] : null,
|
|
46
|
+
cmd: Array.isArray(cfg.Cmd) ? [...cfg.Cmd] : null,
|
|
47
|
+
history: Array.isArray(configObj.history)
|
|
48
|
+
? configObj.history.map((h) => ({ createdBy: h.created_by || null, created: h.created || null, emptyLayer: !!h.empty_layer }))
|
|
49
|
+
: [],
|
|
50
|
+
rootfsDiffIds: (configObj.rootfs && Array.isArray(configObj.rootfs.diff_ids)) ? [...configObj.rootfs.diff_ids] : [],
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseDockerArchive(byName, outerUnsafe, limits) {
|
|
55
|
+
const manifestEntry = byName.get('manifest.json');
|
|
56
|
+
const manifestArr = safeJsonParse(manifestEntry.data, 'manifest.json');
|
|
57
|
+
if (!Array.isArray(manifestArr) || manifestArr.length === 0) {
|
|
58
|
+
throw new ImageArchiveError('manifest.json contains no images', 'empty_manifest');
|
|
59
|
+
}
|
|
60
|
+
if (manifestArr.length > 1) {
|
|
61
|
+
throw new ImageArchiveError(`manifest.json describes ${manifestArr.length} images; image scan expects exactly one`, 'multi_image_archive');
|
|
62
|
+
}
|
|
63
|
+
const m = manifestArr[0];
|
|
64
|
+
if (typeof m.Config !== 'string' || !Array.isArray(m.Layers)) {
|
|
65
|
+
throw new ImageArchiveError('manifest.json entry is missing Config or Layers', 'malformed_manifest');
|
|
66
|
+
}
|
|
67
|
+
const configEntry = byName.get(m.Config);
|
|
68
|
+
if (!configEntry) throw new ImageArchiveError(`manifest.json references missing config "${m.Config}"`, 'missing_blob');
|
|
69
|
+
const configObj = safeJsonParse(configEntry.data, m.Config);
|
|
70
|
+
const configDigest = `sha256:${crypto.createHash('sha256').update(configEntry.data).digest('hex')}`;
|
|
71
|
+
|
|
72
|
+
const layers = [];
|
|
73
|
+
for (let i = 0; i < m.Layers.length; i++) {
|
|
74
|
+
const layerPath = m.Layers[i];
|
|
75
|
+
const entry = byName.get(layerPath);
|
|
76
|
+
if (!entry) throw new ImageArchiveError(`manifest.json references missing layer "${layerPath}"`, 'missing_blob');
|
|
77
|
+
const tar = decompressIfGzip(entry.data, limits);
|
|
78
|
+
layers.push({ index: i, path: layerPath, diffId: configObj?.rootfs?.diff_ids?.[i] || null, tar, sizeBytes: entry.size });
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
format: 'docker-archive',
|
|
83
|
+
digest: configDigest,
|
|
84
|
+
digestKind: 'config',
|
|
85
|
+
repoTags: Array.isArray(m.RepoTags) ? m.RepoTags : [],
|
|
86
|
+
config: extractConfigShape(configObj),
|
|
87
|
+
layers,
|
|
88
|
+
unsafeEntries: outerUnsafe,
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function parseOciArchive(byName, outerUnsafe, limits) {
|
|
93
|
+
const indexObj = safeJsonParse(byName.get('index.json').data, 'index.json');
|
|
94
|
+
const manifests = Array.isArray(indexObj.manifests) ? indexObj.manifests : [];
|
|
95
|
+
if (manifests.length === 0) throw new ImageArchiveError('index.json lists no manifests', 'empty_manifest');
|
|
96
|
+
if (manifests.length > 1) {
|
|
97
|
+
throw new ImageArchiveError(`index.json describes ${manifests.length} manifests; image scan expects exactly one`, 'multi_image_archive');
|
|
98
|
+
}
|
|
99
|
+
const manifestDigest = manifests[0].digest;
|
|
100
|
+
if (typeof manifestDigest !== 'string' || !manifestDigest.startsWith('sha256:')) {
|
|
101
|
+
throw new ImageArchiveError('index.json manifest entry has no sha256 digest', 'malformed_manifest');
|
|
102
|
+
}
|
|
103
|
+
const manifestBlobPath = `blobs/sha256/${manifestDigest.slice('sha256:'.length)}`;
|
|
104
|
+
const manifestEntry = byName.get(manifestBlobPath);
|
|
105
|
+
if (!manifestEntry) throw new ImageArchiveError(`index.json references missing manifest blob "${manifestBlobPath}"`, 'missing_blob');
|
|
106
|
+
const manifestObj = safeJsonParse(manifestEntry.data, manifestBlobPath);
|
|
107
|
+
|
|
108
|
+
const configDigest = manifestObj.config && manifestObj.config.digest;
|
|
109
|
+
if (typeof configDigest !== 'string' || !configDigest.startsWith('sha256:')) {
|
|
110
|
+
throw new ImageArchiveError('image manifest has no config digest', 'malformed_manifest');
|
|
111
|
+
}
|
|
112
|
+
const configBlobPath = `blobs/sha256/${configDigest.slice('sha256:'.length)}`;
|
|
113
|
+
const configEntry = byName.get(configBlobPath);
|
|
114
|
+
if (!configEntry) throw new ImageArchiveError(`manifest references missing config blob "${configBlobPath}"`, 'missing_blob');
|
|
115
|
+
const configObj = safeJsonParse(configEntry.data, configBlobPath);
|
|
116
|
+
|
|
117
|
+
const layerDescs = Array.isArray(manifestObj.layers) ? manifestObj.layers : [];
|
|
118
|
+
const layers = [];
|
|
119
|
+
for (let i = 0; i < layerDescs.length; i++) {
|
|
120
|
+
const d = layerDescs[i];
|
|
121
|
+
if (typeof d.digest !== 'string' || !d.digest.startsWith('sha256:')) {
|
|
122
|
+
throw new ImageArchiveError(`layer ${i} has no sha256 digest`, 'malformed_manifest');
|
|
123
|
+
}
|
|
124
|
+
const blobPath = `blobs/sha256/${d.digest.slice('sha256:'.length)}`;
|
|
125
|
+
const entry = byName.get(blobPath);
|
|
126
|
+
if (!entry) throw new ImageArchiveError(`manifest references missing layer blob "${blobPath}"`, 'missing_blob');
|
|
127
|
+
const tar = decompressIfGzip(entry.data, limits);
|
|
128
|
+
layers.push({ index: i, path: blobPath, diffId: d.digest, mediaType: d.mediaType || null, tar, sizeBytes: entry.size });
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
format: 'oci-archive',
|
|
133
|
+
digest: manifestDigest,
|
|
134
|
+
digestKind: 'manifest',
|
|
135
|
+
repoTags: [],
|
|
136
|
+
config: extractConfigShape(configObj),
|
|
137
|
+
layers,
|
|
138
|
+
unsafeEntries: outerUnsafe,
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Parses an in-memory tar buffer (already read from disk/stdin by the
|
|
144
|
+
* caller — this module never touches the filesystem or network) as either a
|
|
145
|
+
* `docker save`-shaped archive or an OCI-layout archive. Throws
|
|
146
|
+
* ImageArchiveError for anything it cannot recognize or whose manifest
|
|
147
|
+
* references a blob that isn't present — never guesses or substitutes a
|
|
148
|
+
* default.
|
|
149
|
+
*/
|
|
150
|
+
export function parseImageArchive(buf, limits = DEFAULT_LIMITS) {
|
|
151
|
+
if (!Buffer.isBuffer(buf)) throw new ImageArchiveError('image archive input must be a Buffer', 'bad_input');
|
|
152
|
+
const { byName, unsafeEntries } = readOuterEntries(buf, limits);
|
|
153
|
+
if (byName.has('manifest.json')) return parseDockerArchive(byName, unsafeEntries, limits);
|
|
154
|
+
if (byName.has('index.json') && byName.has('oci-layout')) return parseOciArchive(byName, unsafeEntries, limits);
|
|
155
|
+
throw new ImageArchiveError(
|
|
156
|
+
'archive is neither a docker-archive (manifest.json) nor an oci-archive (oci-layout + index.json)',
|
|
157
|
+
'unrecognized_format',
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
// PRD 5 P0 §1-2 — OCI image scanning + inventory. Ties image-archive.js
|
|
2
|
+
// (manifest/config/layer parsing) and filesystem.js (whiteout-aware layer
|
|
3
|
+
// merge) into the single inventory shape the PRD names: digest, OS, OS
|
|
4
|
+
// packages, application packages, architecture, base image, layers,
|
|
5
|
+
// entrypoint, build labels.
|
|
6
|
+
import { parseImageArchive } from './image-archive.js';
|
|
7
|
+
import { readLayers, mergeLayers, DEFAULT_FS_LIMITS } from './filesystem.js';
|
|
8
|
+
import { extractImagePackages } from '../sca/image-packages.js';
|
|
9
|
+
import { DEFAULT_LIMITS as DEFAULT_TAR_LIMITS } from './safe-tar.js';
|
|
10
|
+
|
|
11
|
+
// Buildkit/Docker provenance convention — not an OCI spec requirement, so
|
|
12
|
+
// its absence is reported as "not declared", never guessed from layer
|
|
13
|
+
// heuristics (no base-image fingerprint table exists in this codebase, and
|
|
14
|
+
// inventing one would produce a confident-looking wrong answer, which is the
|
|
15
|
+
// one failure mode PRD 5's own Safety Requirements single out by name for
|
|
16
|
+
// signature state — the same discipline applies here).
|
|
17
|
+
const BASE_IMAGE_NAME_LABEL = 'org.opencontainers.image.base.name';
|
|
18
|
+
const BASE_IMAGE_DIGEST_LABEL = 'org.opencontainers.image.base.digest';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* `buf` is an already-read image archive buffer (tar, optionally
|
|
22
|
+
* gzip-wrapped). `manifestParser` is the engine's existing
|
|
23
|
+
* `parseManifests(allFileContents)` (injected, mirroring
|
|
24
|
+
* `src/posture/secret-history.js`'s detectFn injection, so this module never
|
|
25
|
+
* imports `engine.js` directly and `src/sca/index.js` stays the one seam).
|
|
26
|
+
* Returns `{ image, inventory }` where `image` is the raw parsed archive
|
|
27
|
+
* (image-archive.js's shape, kept for callers that need layer tars — vuln
|
|
28
|
+
* correlation, signature verification) and `inventory` is the PRD-named
|
|
29
|
+
* report.
|
|
30
|
+
*/
|
|
31
|
+
export function buildImageInventory(buf, { manifestParser, tarLimits = DEFAULT_TAR_LIMITS, fsLimits = DEFAULT_FS_LIMITS } = {}) {
|
|
32
|
+
if (typeof manifestParser !== 'function') throw new TypeError('buildImageInventory requires manifestParser(allFileContents)');
|
|
33
|
+
const image = parseImageArchive(buf, tarLimits);
|
|
34
|
+
const rl = readLayers(image.layers, tarLimits);
|
|
35
|
+
const merged = mergeLayers(rl, fsLimits);
|
|
36
|
+
|
|
37
|
+
const osPackages = extractImagePackages(merged.fileContents);
|
|
38
|
+
let appPackages = [];
|
|
39
|
+
try {
|
|
40
|
+
appPackages = manifestParser(merged.fileContents) || [];
|
|
41
|
+
} catch {
|
|
42
|
+
appPackages = []; // a parser failure on hostile layer content must never abort inventory extraction
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const labels = image.config.labels || {};
|
|
46
|
+
const baseImage = labels[BASE_IMAGE_NAME_LABEL]
|
|
47
|
+
? { name: labels[BASE_IMAGE_NAME_LABEL], digest: labels[BASE_IMAGE_DIGEST_LABEL] || null, source: 'label' }
|
|
48
|
+
: null;
|
|
49
|
+
|
|
50
|
+
const inventory = {
|
|
51
|
+
digest: image.digest,
|
|
52
|
+
digestKind: image.digestKind,
|
|
53
|
+
repoTags: image.repoTags,
|
|
54
|
+
os: image.config.os,
|
|
55
|
+
architecture: image.config.architecture,
|
|
56
|
+
baseImage,
|
|
57
|
+
osPackages,
|
|
58
|
+
appPackages,
|
|
59
|
+
layers: image.layers.map((l) => ({ index: l.index, sizeBytes: l.sizeBytes, diffId: l.diffId || null })),
|
|
60
|
+
entrypoint: image.config.entrypoint,
|
|
61
|
+
cmd: image.config.cmd,
|
|
62
|
+
buildLabels: { ...labels },
|
|
63
|
+
history: image.config.history,
|
|
64
|
+
unsafeEntries: [...image.unsafeEntries, ...rl.unsafeEntries],
|
|
65
|
+
materializationTruncated: merged.materializationTruncated,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
return { image, inventory, mergedFilesystem: merged, readLayersResult: rl };
|
|
69
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// PRD 5 P0 §3 — Layer Secret Analysis: a secret added in one layer and later
|
|
2
|
+
// deleted (via an overlay whiteout) is still sitting in that earlier layer's
|
|
3
|
+
// raw tar bytes inside the image archive — recoverable by anyone who has the
|
|
4
|
+
// archive, even though it is invisible in `docker run`'s final filesystem.
|
|
5
|
+
//
|
|
6
|
+
// The actual credential detection is INJECTED as `detectFn` (the exact
|
|
7
|
+
// `(filePath, rawText) -> Finding[]` shape `src/posture/secret-history.js`
|
|
8
|
+
// already uses for the git-history analog of this same idea), so this module
|
|
9
|
+
// never imports `engine.js` and stays a pure, independently-testable
|
|
10
|
+
// transform over the layer data `filesystem.js` already extracted. The
|
|
11
|
+
// injected detector (`scanCredentials`) already returns only redacted/masked
|
|
12
|
+
// values (`masked`, a redacted `snippet`) — this module never re-touches raw
|
|
13
|
+
// file content once a finding exists, so there is no second place a value
|
|
14
|
+
// could leak back in.
|
|
15
|
+
import { isWhiteoutName } from './safe-tar.js';
|
|
16
|
+
|
|
17
|
+
export const DEFAULT_SECRET_SCAN_LIMITS = Object.freeze({
|
|
18
|
+
maxFileBytes: 512 * 1024,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* `readLayersResult` is `filesystem.js#readLayers`'s output (ordered,
|
|
23
|
+
* per-layer, pre-merge entries — the only shape that preserves "this path
|
|
24
|
+
* existed in layer 3" even after a later layer's whiteout removes it from
|
|
25
|
+
* the merged view). `deletedPaths` is `mergeLayers`'s output.
|
|
26
|
+
*
|
|
27
|
+
* Returns `{ findings, deletedButRecoverable, materializationSkipped }`:
|
|
28
|
+
* - `findings`: every detector hit across every layer's added files, each
|
|
29
|
+
* tagged with `path`/`layerIndex` and the detector's own (already
|
|
30
|
+
* redacted) finding fields — this is the full history sweep, not just
|
|
31
|
+
* the deleted-but-recoverable subset.
|
|
32
|
+
* - `deletedButRecoverable`: the subset of `findings` whose `path` was
|
|
33
|
+
* later removed by a whiteout in a LATER layer — PRD 5's named scenario.
|
|
34
|
+
* - `materializationSkipped`: paths whose content was too large to scan
|
|
35
|
+
* (disclosed, never silently treated as "no secret found").
|
|
36
|
+
*/
|
|
37
|
+
export function analyzeLayerSecrets({ readLayersResult, deletedPaths, detectFn, limits = DEFAULT_SECRET_SCAN_LIMITS }) {
|
|
38
|
+
if (typeof detectFn !== 'function') throw new TypeError('analyzeLayerSecrets requires a detectFn(filePath, rawText) -> Finding[]');
|
|
39
|
+
if (!readLayersResult || !Array.isArray(readLayersResult.layers)) throw new TypeError('analyzeLayerSecrets requires filesystem.js#readLayers output');
|
|
40
|
+
|
|
41
|
+
const deletedAtByPath = new Map(); // path -> earliest layer index that deleted it
|
|
42
|
+
for (const d of deletedPaths || []) {
|
|
43
|
+
if (d.opaque) continue; // opaque-directory deletions are reported separately below
|
|
44
|
+
if (!deletedAtByPath.has(d.path) || d.deletedAtLayer < deletedAtByPath.get(d.path)) {
|
|
45
|
+
deletedAtByPath.set(d.path, d.deletedAtLayer);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const opaqueDirs = (deletedPaths || []).filter((d) => d.opaque);
|
|
49
|
+
|
|
50
|
+
const findings = [];
|
|
51
|
+
const materializationSkipped = [];
|
|
52
|
+
|
|
53
|
+
for (const { layerIndex, entries } of readLayersResult.layers) {
|
|
54
|
+
for (const e of entries) {
|
|
55
|
+
if (isWhiteoutName(e.name)) continue;
|
|
56
|
+
if (e.typeflag !== '0' && e.typeflag !== '') continue; // regular files only
|
|
57
|
+
if (e.size > limits.maxFileBytes) {
|
|
58
|
+
materializationSkipped.push({ path: e.name, layerIndex, sizeBytes: e.size });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
text = e.data.toString('utf8');
|
|
64
|
+
} catch {
|
|
65
|
+
continue; // not decodable as text — not a candidate for a text-pattern secret scan
|
|
66
|
+
}
|
|
67
|
+
let hits;
|
|
68
|
+
try {
|
|
69
|
+
hits = detectFn(e.name, text) || [];
|
|
70
|
+
} catch {
|
|
71
|
+
hits = []; // a detector failure on hostile/malformed content must never abort the sweep
|
|
72
|
+
}
|
|
73
|
+
for (const hit of hits) {
|
|
74
|
+
const deletedAtLayer = deletedAtByPath.has(e.name) && deletedAtByPath.get(e.name) > layerIndex
|
|
75
|
+
? deletedAtByPath.get(e.name)
|
|
76
|
+
: null;
|
|
77
|
+
const underOpaqueDir = opaqueDirs.some((d) => d.deletedAtLayer > layerIndex && (e.name === d.path || e.name.startsWith(`${d.path}/`)));
|
|
78
|
+
findings.push({
|
|
79
|
+
...hit,
|
|
80
|
+
path: e.name,
|
|
81
|
+
addedAtLayer: layerIndex,
|
|
82
|
+
deletedAtLayer: deletedAtLayer ?? (underOpaqueDir ? opaqueDirs.find((d) => d.deletedAtLayer > layerIndex && (e.name === d.path || e.name.startsWith(`${d.path}/`))).deletedAtLayer : null),
|
|
83
|
+
recoverable: deletedAtLayer != null || underOpaqueDir,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const deletedButRecoverable = findings.filter((f) => f.recoverable);
|
|
90
|
+
|
|
91
|
+
return { findings, deletedButRecoverable, materializationSkipped };
|
|
92
|
+
}
|