@clear-capabilities/agentic-security-scanner 0.195.24 → 0.195.26
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 +32 -0
- package/bin/agentic-security.js +9 -0
- package/dist/5752.index.js +16 -3
- package/dist/{daefda0819bb18309b75.json → 60f58f89664e53fc2f96.json} +3 -3
- package/dist/8218.index.js +19 -2
- package/dist/9382.index.js +315 -174
- package/dist/9560.index.js +19 -2
- package/dist/agentic-security.mjs +2 -2
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +3 -3
- package/src/context-intelligence/CLAUDE.md +4 -0
- package/src/context-intelligence/entrypoints.js +141 -9
- package/src/context-intelligence/imports.js +1 -1
- package/src/context-intelligence/review.js +8 -3
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/engine.js +13 -0
- package/src/lineage/CLAUDE.md +17 -0
- package/src/lineage/boundary-join.js +384 -0
- package/src/lineage/coverage.js +4 -1
- package/src/lineage/engine.js +20 -0
- package/src/lineage/graph-builder.js +172 -34
- package/src/lineage/index.js +39 -0
- package/src/lineage/source-seeding.js +11 -1
- package/src/posture/dag-commands.js +15 -2
- package/src/posture/provenance/git-evidence.js +21 -0
- package/src/server/graph-loader.js +19 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
ee0c77de1bba97155580cc7647e6723e10bde63e9944dbc5050f1db7b54d46a2 agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.195.
|
|
3
|
+
"version": "0.195.26",
|
|
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",
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
"test:dataflow": "node --test test/privacy-catalog.test.js test/privacy-deep-walker.test.js test/privacy-deep-e2e.test.js test/container-taint.test.js test/stored-taint-families.test.js test/py-annotation-sources.test.js test/fn-reach.test.js test/deep-taint.test.js test/calibration.test.js test/holdout-eval.test.js test/cross-lang-meta.test.js test/cross-lang-queues.test.js test/phase5-xlang.test.js test/phase5-coverage.test.js test/phase6-taint.test.js test/llm-validator-consistency.test.js test/llm-validator-default-on.test.js test/llm-validator-preset.test.js test/parser-py-cst.test.js test/chrome-probe.test.js test/parser-cs-kt.test.js test/parser-cs-annotations.test.js test/parser-cs-control-flow.test.js test/parser-kt-control-flow.test.js test/parser-js-annotations.test.js test/parser-java-annotations.test.js test/parser-java-control-flow.test.js test/parser-java-calls.test.js test/parser-go.test.js test/parser-rust.test.js test/rust-taint-flow.test.js test/parser-php-rb.test.js test/parser-php-control-flow.test.js test/interproc-k2.test.js test/proven-clean.test.js test/backward-default.test.js test/incremental-cache.test.js test/string-regex-lattice.test.js test/closure-capture.test.js test/points-to.test.js test/type-stubs.test.js test/soft-taint.test.js test/ifds.test.js test/symbolic-exec-proof.test.js test/ifds-summary-edges.test.js test/stub-aware-filter.test.js test/cross-repo.test.js test/proof-gate.test.js test/layer-recall.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/sanitizer-typed-flow.test.js test/sanitizer-gate-unit.test.js test/proof-safe.test.js test/collection-taint.test.js test/kcfa-context.test.js test/kcfa-callstring.test.js test/flow-parity.test.js test/callgraph-resolve.test.js test/import-reachability.test.js test/ir-stats.test.js test/parser-cpp.test.js test/parser-js-decorators.test.js test/parser-js-if-else-cfg.test.js test/cpp-integration.test.js test/engine-reconnect.test.js test/phase2-scoping.test.js test/engine-recall.test.js test/catalog-annotation-source.test.js test/catalog-cs-p1.test.js test/catalog-expanded.test.js test/builtin-summaries.test.js test/interproc-mutation-crash.test.js test/mutated-param-taint.test.js test/return-sink-check.test.js test/higher-order-inline-callback.test.js test/interproc-findings-discard.test.js test/runscan-deep-option.test.js test/summary-cache-convergence.test.js test/parser-rb-calls.test.js test/parser-rb-module-level.test.js test/balanced-call.test.js test/destructuring-taint.test.js test/computed-member-taint.test.js test/string-interpolation-taint.test.js test/dataflow-deadcode-severity.test.js test/catalog-dotted-callee-lookup.test.js test/builtin-summary-family-blind-kill.test.js test/deep-mode-annotator-pipeline.test.js test/receiver-type-and-nested-calls.test.js test/member-write-and-loop-taint.test.js test/parser-py-module-level.test.js test/parser-php-module-level.test.js test/r14b-module-level-e2e.test.js test/annotation-taint-engine.test.js test/interproc-nested-call-taint.test.js test/catalog-xpath-injection.test.js test/catalog-ldap-injection.test.js test/catalog-xxe.test.js test/catalog-response-splitting.test.js test/catalog-code-injection.test.js test/parser-rb-control-flow.test.js test/catalog-ruby-p1.test.js test/callee-receiver-taint.test.js test/catalog-command-injection-p4.test.js test/catalog-xss-p4.test.js test/catalog-path-ssrf-p3.test.js test/catalog-ai-model-provider-precision.test.js test/catalog-orm-write.test.js test/dataflow/provenance-pipeline-integration.test.js test/dataflow-progress.test.js test/scan-progress.test.js test/sard-leakage-pipeline.test.js test/sard-split-scoring.test.js test/sard-llm-isolation.test.js test/sard-mutation-adversarial.test.js test/sard-baseline-adaptive-tolerance.test.js test/sard-independent-holdout.test.js test/sard-flow-aware-scoring.test.js test/sard-macro-score-support-floor.test.js test/sard-php-split.test.js test/parser-java-nary-concat.test.js test/sard-naive-baseline.test.js test/field-taint-cross-method.test.js test/field-taint-engine-unit.test.js test/coercion-sanitizer-kill.test.js test/constant-propagation-path-sensitivity.test.js test/parser-java-assignments.test.js test/catalog-java-sard.test.js test/parser-cs-juliet-shapes.test.js test/catalog-cs-sard-p2.test.js test/sard-cwe-key-merge.test.js test/parser-php-backtick.test.js test/parser-php-include.test.js test/php-include-merge.test.js test/parser-php-array-field-taint.test.js test/parser-php-cond-assign.test.js test/parser-rust-redos.test.js test/multi-sink-taint-chain.test.js test/static-field-cross-class-call-string.test.js test/regex-guard-narrowing.test.js test/object-instance-field-taint.test.js test/param-type-inference.test.js test/smt-path-feasibility.test.js test/cross-service-taint.test.js test/dataflow-expr-taint-scaling.test.js test/evaluation/guard-dominance.test.js test/evaluation/engine-mechanisms.test.js test/rx-exec-receiver.test.js test/incremental-dependency.test.js test/incremental-integrity.test.js test/demand-query.test.js test/ir-cache.test.js test/canonicalize-parity.test.js test/dataflow-source-attribution.test.js",
|
|
91
91
|
"test:mcp": "node --test test/mcp.test.js test/mcp-apply-fix-multifile.test.js test/mcp-protocol-smoke.test.js test/mcp-audit.test.js test/audit-cli.test.js test/mcp-scratchpad.test.js test/mcp-offload.test.js test/sca-upgrade.test.js test/lsp-server.test.js test/mcp-dataflow-tools.test.js test/cli/governance-propose-edit.test.js test/cli/remediation-open-update.test.js test/cli/remediation-verify-reopen.test.js test/cli/dataflow-observations.test.js test/cli/federate-declare-list.test.js test/capabilities/tools.test.js",
|
|
92
92
|
"test:report": "node --test test/report-scan-invariants.test.js test/junit.test.js test/ci.test.js test/poc-generator.test.js test/verifier.test.js test/verifier-target.test.js test/annotator-errors.test.js test/git-history-batch.test.js test/default-scan-drift.test.js test/bundle-optional-deps.test.js test/scan-health.test.js test/finding-schema.test.js test/pipeline/finding-schema-provenance.test.js test/evidence-provenance.test.js test/report/provenance-output.test.js test/report/provenance-format-parity.test.js test/legacy-compat.test.js test/annotator-runner.test.js test/detector-runner.test.js test/detector-errors.test.js test/analyzer-supervisor.test.js test/cascade-worker-pool.test.js test/cascade-pool-wiring.test.js test/coverage-ledger.test.js test/coverage-ledger-completeness.test.js test/assurance-mode.test.js test/artifact-registry.test.js test/artifact-registry-completeness.test.js test/retention-policy.test.js test/state-lifecycle-report.test.js test/legal-hold.test.js test/production-feedback.test.js test/calibration-feedback.test.js test/encryption-provider.test.js test/grader-calibration.test.js test/pr-delta-gate.test.js test/vex.test.js test/report-render.test.js test/oscal-conformance.test.js test/cli-fix-apply.test.js test/apply-fix-service.test.js test/high-impact-approval-gate.test.js test/approver-registry.test.js test/suppression-exceptions.test.js test/fix-history.test.js test/producer-registry.test.js test/producer-collector.test.js test/enrichment-completion.test.js",
|
|
93
|
-
"test:lineage": "node --test test/lineage/analysis-completion.test.js 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/seeding-across-calls.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/lineage/runtime-edge-states.test.js test/lineage/runtime-explorer-view.test.js test/cli/runtime-explorer-cli.test.js test/cli/runtime-identity-cli.test.js test/cli/runtime-twin-cli.test.js test/lineage/cut-planner.test.js test/lineage/scc.test.js test/lineage/scc-solver.test.js test/graph/graph-views.test.js test/lineage/dag07-completion.test.js test/lineage/owned-build.test.js test/dataflow/boundary-propagation.test.js test/lineage/boundary-contract.test.js test/lineage/correlation-v2.test.js test/lineage/path-control-evidence.test.js test/lineage/smt-z3-backend.test.js test/cli/dag-commands.test.js",
|
|
93
|
+
"test:lineage": "node --test test/lineage/analysis-completion.test.js 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/seeding-across-calls.test.js test/lineage/boundary-join.test.js test/contracts/code-boundaries.contract.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/lineage/runtime-edge-states.test.js test/lineage/runtime-explorer-view.test.js test/cli/runtime-explorer-cli.test.js test/cli/runtime-identity-cli.test.js test/cli/runtime-twin-cli.test.js test/lineage/cut-planner.test.js test/lineage/scc.test.js test/lineage/scc-solver.test.js test/graph/graph-views.test.js test/lineage/dag07-completion.test.js test/lineage/owned-build.test.js test/dataflow/boundary-propagation.test.js test/lineage/boundary-contract.test.js test/lineage/correlation-v2.test.js test/lineage/path-control-evidence.test.js test/lineage/smt-z3-backend.test.js test/cli/dag-commands.test.js",
|
|
94
94
|
"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",
|
|
95
95
|
"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",
|
|
96
96
|
"test:haskell": "node --test test/haskell/haskell-reachability.test.js test/haskell/haskell-real-code-shapes.test.js test/haskell/haskell-unseen-v5-shapes.test.js test/haskell/haskell-unseen-v4-shapes.test.js test/haskell/haskell-alias-taint.test.js test/haskell/haskell-weak-hash-names.test.js test/haskell/haskell-models-extended.test.js test/haskell/haskell-rules-extended.test.js 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-remediation-0190.test.js test/haskell/haskell-remediation-kdf.test.js test/haskell/haskell-remediation-html-text.test.js test/haskell/haskell-remediation-scotty-body.test.js test/haskell/haskell-remediation-entropy.test.js test/haskell/haskell-remediation-text-command.test.js test/haskell/haskell-remediation-path-contain.test.js test/haskell/haskell-remediation-0195-16.test.js test/haskell/haskell-taint-shapes.test.js test/haskell/haskell-real-advisory-shapes.test.js test/haskell/haskell-discovery-shapes.test.js test/haskell/haskell-quasiquote-interp.test.js test/haskell/haskell-record-dot.test.js test/haskell/haskell-global-cell.test.js test/haskell/haskell-syntax-extensions.test.js test/haskell/haskell-rule-precision.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 test/haskell/haskell-boundary-propagation.test.js test/haskell/haskell-generalization.test.js test/haskell/haskell-trust-boundary.test.js test/haskell/haskell-trust-fixed-file.test.js test/haskell/haskell-predictable-random-flow.test.js test/haskell/haskell-comment-invariance.test.js test/haskell/haskell-remediation-classes.test.js test/haskell/haskell-trust-boundary-frameworks.test.js test/haskell/haskell-generic-detectors.test.js",
|
|
@@ -133,7 +133,7 @@
|
|
|
133
133
|
"verification:replay-example": "node ../scripts/verification-replay-example.mjs",
|
|
134
134
|
"test:lifecycle": "node --test test/ci-hang-protection.test.js test/gate-select.test.js test/ci-test-shards.test.js test/pre-bash-guard.test.js test/lsp-protocol-smoke.test.js test/ide-surfaces.test.js test/detector-liveness.test.js test/bench-watchdog.test.js test/ci-parity.test.js test/dead-code.test.js test/no-dead-modules.test.js test/no-orphan-scripts.test.js test/no-stray-state.test.js test/tree-integrity.test.js test/stop-hook.test.js test/plugin-self-check.test.js test/skills-registry.test.js test/bodyguard.test.js test/cache-invalidator-guard.test.js test/dispatch-pre-tool.test.js test/dispatch-user-prompt.test.js test/session-start-model-capture.test.js test/sandbox.test.js test/sandbox-escape.test.js test/trust-boundary.test.js test/ci-templates.test.js test/agent-mcp-tools.test.js test/triage-command.test.js test/no-orphan-tests.test.js test/supply-command.test.js test/posture-command.test.js test/labs-command.test.js test/compliance-command.test.js test/check-doc-drift.test.js test/gate-warm.test.js test/docs-positioning.test.js test/dist-chunks-tracked.test.js test/ga-program.test.js test/workflow-confinement.test.js test/check-bundle-source.test.js test/ship.test.js test/release-prepare.test.js && node ../scripts/lint-command-descriptions.mjs",
|
|
135
135
|
"test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
|
|
136
|
-
"test:discovery": "node --test test/discovery-spec-synthesis.test.js test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js test/context-intelligence/build-js.test.js test/context-intelligence/build-python-haskell.test.js test/context-intelligence/hie-adapter.test.js test/context-intelligence/store.test.js test/context-intelligence/review-queue.test.js test/context-intelligence/packer-egress.test.js test/context-intelligence/evaluation.test.js test/context-intelligence/queue-diff.test.js test/context-intelligence/cli.test.js test/context-intelligence/resilience.test.js test/context-intelligence/frameworks.test.js test/context-intelligence/skew-git-rls.test.js test/context-intelligence/premortem.test.js test/context-intelligence/git-diff-candidates.test.js",
|
|
136
|
+
"test:discovery": "node --test test/discovery-spec-synthesis.test.js test/discovery-partition.test.js test/discovery-lenses.test.js test/discovery-hunter.test.js test/discovery-confirm.test.js test/discovery-disprove.test.js test/discovery-judge.test.js test/discovery-run.test.js test/discovery-wiring.test.js test/discovery-llm-invoke.test.js test/local-only-mode.test.js test/context-intelligence/build-js.test.js test/context-intelligence/build-python-haskell.test.js test/context-intelligence/hie-adapter.test.js test/context-intelligence/store.test.js test/context-intelligence/review-queue.test.js test/context-intelligence/route-mounts.test.js test/context-intelligence/packer-egress.test.js test/context-intelligence/evaluation.test.js test/context-intelligence/queue-diff.test.js test/context-intelligence/cli.test.js test/context-intelligence/resilience.test.js test/context-intelligence/frameworks.test.js test/context-intelligence/skew-git-rls.test.js test/context-intelligence/premortem.test.js test/context-intelligence/git-diff-candidates.test.js",
|
|
137
137
|
"eval:independent": "node ../bench/independent-eval/runner.mjs",
|
|
138
138
|
"eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
|
|
139
139
|
"smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
|
|
@@ -23,3 +23,7 @@ See the module table in the architecture doc. Tests: `test/context-intelligence/
|
|
|
23
23
|
## Reuse, do not fork
|
|
24
24
|
|
|
25
25
|
Model calls go through `discovery/llm-invoke.js` and the egress policy. Confirmation, refutation and judging are `discovery/{confirm,disprove,judge}.js`. Grouped intervals come from `posture/evaluation/interval.js`. State paths come from `posture/state-dir.js`.
|
|
26
|
+
|
|
27
|
+
## Router mounts and stale citations (GA-03.2, 0.195.26)
|
|
28
|
+
|
|
29
|
+
`entrypoints.js` composes router mounts: `router.get('/:id')` with `app.use('/api', auth, router)` is the entry point `/api/:id` (`pathTemplate`), with `registeredPath` keeping `/:id` and `mount` recording the chain (root first) and the number of alternatives. A mount is found in the same file, through the import map (`import users from './users.js'`, `const users = require('./users')`) or an inline `require('./users')`, when the target module's `module.exports` / `export default` receiver registers routes or mounts; mounts compose through intermediate routers (depth 4, at most 8 paths); a router mounted at several prefixes is one entry point per prefix; the mount statement's other identifier arguments are inherited middleware scoped to that mount (`viaMount: true`), per chain. A dynamic prefix (`app.use(prefixFor(), router)`) mounts nothing and the route keeps `router-mount-not-resolved`. `review.js`'s `resolveCitation` refuses `stale-context` when the cited lines sit in a segment whose current text no longer hashes to the segment's `spanHash`. Tests: `test/context-intelligence/route-mounts.test.js`.
|
|
@@ -16,6 +16,7 @@ import { hashOf, safeLabel } from './canonical.js';
|
|
|
16
16
|
import { callStatement, inlineBlockSpan, splitArgs } from './spans.js';
|
|
17
17
|
import { dependencyParams, signatureText } from './controls.js';
|
|
18
18
|
import { buildEntrypointInventory } from '../posture/entrypoint-inventory.js';
|
|
19
|
+
import { buildImportMap, resolveViaImports, resolveJsModule } from './imports.js';
|
|
19
20
|
|
|
20
21
|
export const DISPOSITIONS = Object.freeze(['resolved', 'partially_resolved', 'unresolved', 'excluded_by_policy']);
|
|
21
22
|
export const ENTRYPOINT_KINDS = Object.freeze(['http', 'webhook', 'queue', 'cron', 'upload', 'cli']);
|
|
@@ -88,8 +89,11 @@ function collectUses(fileContents) {
|
|
|
88
89
|
let rest = args;
|
|
89
90
|
const q = args[0] && /^(['"`])(.*)\1$/.exec(args[0]);
|
|
90
91
|
if (q) { prefix = q[2]; rest = args.slice(1); }
|
|
92
|
+
// GA-03.2: a first argument that is a call, a template with interpolation or a concatenation is a prefix
|
|
93
|
+
// the scan cannot read; the use still records its middleware, but nothing is mounted under a guessed '/'.
|
|
94
|
+
const prefixDynamic = !q && !!args[0] && (/^[A-Za-z_$][\w$.]*\s*\(/.test(args[0]) && !/^require\s*\(/.test(args[0]) || /\$\{|\+/.test(args[0]));
|
|
91
95
|
const names = rest.map(a => { const id = IDENT_CHAIN.exec(a.replace(/^\[|\]$/g, '')); return id ? { name: id[0], text: a } : { name: null, text: a }; });
|
|
92
|
-
uses.push({ file, line: i + 1, receiver: m[1], prefix, args: names });
|
|
96
|
+
uses.push({ file, line: i + 1, receiver: m[1], prefix, prefixDynamic, args: names });
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
return uses;
|
|
@@ -212,6 +216,107 @@ function resolveHandler(route, ctx) {
|
|
|
212
216
|
return out;
|
|
213
217
|
}
|
|
214
218
|
|
|
219
|
+
// ── Router mounts (GA-03.2, 0.195.26) ──────────────────────────────────────
|
|
220
|
+
//
|
|
221
|
+
// `router.get('/:id', h)` is reachable at `/users/:id` only through `app.use('/users', router)`, and that mount
|
|
222
|
+
// can sit in another file (`app.use('/users', require('./users'))`, `import users from './users.js'`). Before
|
|
223
|
+
// this, every route on a router-looking receiver was `partially_resolved` with `router-mount-not-resolved` and
|
|
224
|
+
// its path was the local one. Now a mount is found when the mounted argument is (a) a receiver with routes in
|
|
225
|
+
// the same file, (b) a name imported from a file whose `module.exports` / `export default` is that file's
|
|
226
|
+
// router receiver, or (c) an inline `require('./x')` of such a file. Mounts compose: `app.use('/api', v1)` and
|
|
227
|
+
// `v1.use('/users', users)` make `/api/users/:id`. A router mounted at several prefixes is reachable at each,
|
|
228
|
+
// so it yields one entry point per prefix (sorted; at most MAX_MOUNT_PATHS). Depth is bounded; a cycle stops.
|
|
229
|
+
// A mount's other identifier arguments (`app.use('/api', auth, router)`) are middleware every mounted route
|
|
230
|
+
// inherits, recorded with the mount's prefix as their scope. What stays unresolved: a router passed through a
|
|
231
|
+
// variable the import map cannot follow, a mount whose prefix is not a string literal, a dynamic
|
|
232
|
+
// `app.use(prefixVar, router)`, and anything registered outside the scanned files.
|
|
233
|
+
const MAX_MOUNT_DEPTH = 4;
|
|
234
|
+
const MAX_MOUNT_PATHS = 8;
|
|
235
|
+
const ROUTE_RECEIVER_RE = /([A-Za-z_$][\w$]*)\.(?:get|post|put|patch|delete|all|options|head|route)\s*\(/;
|
|
236
|
+
const ROUTER_NAME_RE = /^(?:router|routes?|r|api|\w*Router)$/i;
|
|
237
|
+
|
|
238
|
+
function routeReceiver(fileContents, route) {
|
|
239
|
+
if (!JS_FILE.test(route.file)) return null;
|
|
240
|
+
const line = (fileContents[route.file] || '').split('\n')[route.line - 1] || '';
|
|
241
|
+
const m = ROUTE_RECEIVER_RE.exec(line);
|
|
242
|
+
return m ? m[1] : null;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/** The receiver a file exports as its module (`module.exports = router`, `export default router`), or null. */
|
|
246
|
+
function exportedReceiver(text) {
|
|
247
|
+
const t = String(text || '');
|
|
248
|
+
const m = /\bmodule\.exports\s*=\s*([A-Za-z_$][\w$]*)\s*;?/.exec(t) || /\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*;?/.exec(t);
|
|
249
|
+
return m ? m[1] : null;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function joinMountPath(prefix, routePath) {
|
|
253
|
+
const p = String(prefix || '/').replace(/\/+$/, '');
|
|
254
|
+
const r = String(routePath || '');
|
|
255
|
+
if (!p) return r.startsWith('/') ? r : '/' + r;
|
|
256
|
+
if (!r || r === '/') return p;
|
|
257
|
+
return p + (r.startsWith('/') ? r : '/' + r);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Every mount of a router: key `${file}|${receiver}` -> [{ prefix, file, line, receiver, middleware }], where
|
|
262
|
+
* `receiver` is the receiver the router was mounted ON (so a chain can be followed upward).
|
|
263
|
+
*/
|
|
264
|
+
function buildMountIndex(fileContents, uses, routes) {
|
|
265
|
+
const files = new Set(Object.keys(fileContents));
|
|
266
|
+
const imports = buildImportMap(fileContents);
|
|
267
|
+
const receiversByFile = new Map();
|
|
268
|
+
for (const r of routes) {
|
|
269
|
+
const recv = routeReceiver(fileContents, r);
|
|
270
|
+
if (!recv) continue;
|
|
271
|
+
if (!receiversByFile.has(r.file)) receiversByFile.set(r.file, new Set());
|
|
272
|
+
receiversByFile.get(r.file).add(recv);
|
|
273
|
+
}
|
|
274
|
+
// a receiver that mounts something is a router too (an intermediate `v1.use('/users', users)`)
|
|
275
|
+
for (const u of uses) { if (!receiversByFile.has(u.file)) receiversByFile.set(u.file, new Set()); receiversByFile.get(u.file).add(u.receiver); }
|
|
276
|
+
const index = new Map();
|
|
277
|
+
const add = (file, receiver, mount) => { const k = `${file}|${receiver}`; if (!index.has(k)) index.set(k, []); index.get(k).push(mount); };
|
|
278
|
+
// a module is a router module only when its exported receiver registers routes or mounts in that file
|
|
279
|
+
const isRouter = (file, receiver) => !!receiver && (receiversByFile.get(file) || new Set()).has(receiver);
|
|
280
|
+
for (const u of uses) {
|
|
281
|
+
if (u.prefixDynamic) continue;
|
|
282
|
+
const middleware = [];
|
|
283
|
+
for (const a of u.args) {
|
|
284
|
+
let target = null;
|
|
285
|
+
const req = /^require\(\s*(['"])([^'"]+)\1\s*\)$/.exec(String(a.text || '').trim());
|
|
286
|
+
if (req) {
|
|
287
|
+
const res = resolveJsModule(u.file, req[2], files);
|
|
288
|
+
const recv = res && res.target && files.has(res.target) ? exportedReceiver(fileContents[res.target]) : null;
|
|
289
|
+
if (recv && isRouter(res.target, recv)) target = { file: res.target, receiver: recv };
|
|
290
|
+
} else if (a.name && a.name !== u.receiver && isRouter(u.file, a.name)) {
|
|
291
|
+
target = { file: u.file, receiver: a.name };
|
|
292
|
+
} else if (a.name) {
|
|
293
|
+
const via = resolveViaImports(imports, u.file, a.name);
|
|
294
|
+
const recv = via && via.kind === 'exact-import' && via.targetFile && files.has(via.targetFile) ? exportedReceiver(fileContents[via.targetFile]) : null;
|
|
295
|
+
if (recv && isRouter(via.targetFile, recv)) target = { file: via.targetFile, receiver: recv };
|
|
296
|
+
}
|
|
297
|
+
if (target) add(target.file, target.receiver, { prefix: u.prefix, file: u.file, line: u.line, receiver: u.receiver, middleware: [...middleware] });
|
|
298
|
+
else if (a.name && !req) middleware.push(a.name);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return index;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** The mount chains (root first) through which (file, receiver) is reachable; [] when it is mounted nowhere. */
|
|
305
|
+
function mountChains(index, file, receiver, depth = 0, seen = new Set()) {
|
|
306
|
+
const key = `${file}|${receiver}`;
|
|
307
|
+
const mounts = index.get(key) || [];
|
|
308
|
+
if (!mounts.length || depth >= MAX_MOUNT_DEPTH || seen.has(key)) return [];
|
|
309
|
+
const out = [];
|
|
310
|
+
const next = new Set(seen); next.add(key);
|
|
311
|
+
for (const m of mounts) {
|
|
312
|
+
const parents = mountChains(index, m.file, m.receiver, depth + 1, next);
|
|
313
|
+
if (!parents.length) out.push([m]);
|
|
314
|
+
else for (const pc of parents) out.push([...pc, m]);
|
|
315
|
+
if (out.length >= MAX_MOUNT_PATHS) break;
|
|
316
|
+
}
|
|
317
|
+
return out.slice(0, MAX_MOUNT_PATHS);
|
|
318
|
+
}
|
|
319
|
+
|
|
215
320
|
function pathMatchesPrefix(routePath, prefix) {
|
|
216
321
|
if (prefix === '/' || prefix === '') return true;
|
|
217
322
|
const p = prefix.replace(/\/+$/, '');
|
|
@@ -226,23 +331,24 @@ export async function collectEntrypoints({ fileContents, graph, config = {} }) {
|
|
|
226
331
|
const kinds = new Set(config.entrypointKinds && config.entrypointKinds.length ? config.entrypointKinds : ['http']);
|
|
227
332
|
const idx = symbolIndexes(graph);
|
|
228
333
|
const uses = collectUses(fileContents);
|
|
334
|
+
const routes = (kinds.has('http') || kinds.has('webhook')) ? await collectRoutes(fileContents) : [];
|
|
335
|
+
const mountIndex = buildMountIndex(fileContents, uses, routes);
|
|
229
336
|
const exclude = (config.exclude || []).map(globToRe);
|
|
230
337
|
const cap = Number.isInteger(config.maxEntrypoints) && config.maxEntrypoints > 0 ? config.maxEntrypoints : 5000;
|
|
231
338
|
const ctx = { fileContents, idx, graph };
|
|
232
339
|
const records = [];
|
|
233
340
|
|
|
234
341
|
if (kinds.has('http') || kinds.has('webhook')) {
|
|
235
|
-
for (const route of
|
|
342
|
+
for (const route of routes) {
|
|
236
343
|
const isWebhook = /webhook/i.test(route.path || '');
|
|
237
344
|
if (isWebhook && !kinds.has('webhook') && !kinds.has('http')) continue;
|
|
238
345
|
const h = resolveHandler(route, ctx);
|
|
239
346
|
const reasons = [...h.reasons];
|
|
240
347
|
const middleware = [...h.middleware];
|
|
348
|
+
const receiver = routeReceiver(fileContents, route);
|
|
349
|
+
const chains = receiver ? mountChains(mountIndex, route.file, receiver) : [];
|
|
241
350
|
// inherited middleware registered with `X.use(...)` on the same receiver, earlier in the same file
|
|
242
351
|
if (JS_FILE.test(route.file)) {
|
|
243
|
-
const lines = fileContents[route.file].split('\n');
|
|
244
|
-
const recvM = /([A-Za-z_$][\w$]*)\.(?:get|post|put|patch|delete|all|options|head|route)\s*\(/.exec(lines[route.line - 1] || '');
|
|
245
|
-
const receiver = recvM ? recvM[1] : null;
|
|
246
352
|
for (const u of uses) {
|
|
247
353
|
if (u.file !== route.file || u.line >= route.line || u.receiver !== receiver) continue;
|
|
248
354
|
if (!pathMatchesPrefix(String(route.path || ''), u.prefix)) continue;
|
|
@@ -253,17 +359,35 @@ export async function collectEntrypoints({ fileContents, graph, config = {} }) {
|
|
|
253
359
|
if (!r.symbol) reasons.push(`inherited-middleware-${r.resolution}:${a.name}`);
|
|
254
360
|
}
|
|
255
361
|
}
|
|
256
|
-
if (receiver &&
|
|
257
|
-
reasons.push('router-mount-not-resolved');
|
|
258
|
-
}
|
|
362
|
+
if (receiver && !chains.length && ROUTER_NAME_RE.test(receiver)) reasons.push('router-mount-not-resolved');
|
|
259
363
|
}
|
|
364
|
+
// middleware the mount statements apply to everything mounted under them (`app.use('/api', auth, router)`):
|
|
365
|
+
// per chain, so a router mounted twice inherits each mount's own middleware and not the other's
|
|
366
|
+
const chainMiddleware = (chain) => {
|
|
367
|
+
const mw = []; const why = [];
|
|
368
|
+
for (const hop of chain) {
|
|
369
|
+
for (const name of hop.middleware) {
|
|
370
|
+
const r = resolveName(name, hop.file, idx);
|
|
371
|
+
mw.push({ name, symbolId: r.symbol ? r.symbol.id : null, resolution: r.resolution, line: hop.line, scope: hop.prefix, inherited: true, viaMount: true });
|
|
372
|
+
if (!r.symbol) why.push(`inherited-middleware-${r.resolution}:${name}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
return { mw, why };
|
|
376
|
+
};
|
|
260
377
|
const authScheme = route.webModel
|
|
261
378
|
? { status: route.webModel.auth.status, kind: route.webModel.auth.kind || null, basis: 'haskell-web-model', role: route.webModel.role.status, ownership: route.webModel.ownership || null, idLookups: route.webModel.idLookups || [], csrf: !!route.webModel.csrf }
|
|
262
379
|
: { status: route.hasAuth ? 'authenticated' : 'none', basis: 'route-scan-heuristic', note: 'authentication seen near the registration; says nothing about ownership' };
|
|
380
|
+
const mountsOf = chains.map(chain => ({
|
|
381
|
+
prefix: chain.reduce((acc, hop) => joinMountPath(acc, hop.prefix), '/'),
|
|
382
|
+
chain: chain.map(hop => ({ file: hop.file, line: hop.line, receiver: hop.receiver, prefix: hop.prefix })),
|
|
383
|
+
...chainMiddleware(chain),
|
|
384
|
+
})).sort((a, b) => (a.prefix < b.prefix ? -1 : a.prefix > b.prefix ? 1 : 0));
|
|
263
385
|
const base = {
|
|
264
386
|
kind: isWebhook ? 'webhook' : 'http',
|
|
265
387
|
method: String(route.method || 'GET'),
|
|
266
388
|
pathTemplate: String(route.path || ''),
|
|
389
|
+
registeredPath: String(route.path || ''),
|
|
390
|
+
mount: null,
|
|
267
391
|
framework: route.framework || null,
|
|
268
392
|
language: route.language === 'haskell' ? 'haskell' : (JS_FILE.test(route.file) ? 'javascript' : (route.file.split('.').pop())),
|
|
269
393
|
registrationSite: { file: route.file, line: route.line },
|
|
@@ -276,7 +400,15 @@ export async function collectEntrypoints({ fileContents, graph, config = {} }) {
|
|
|
276
400
|
externalDependencies: [],
|
|
277
401
|
resolutionReasons: [...new Set(reasons)].sort(),
|
|
278
402
|
};
|
|
279
|
-
records.push(base);
|
|
403
|
+
if (!mountsOf.length) { records.push(base); continue; }
|
|
404
|
+
for (const m of mountsOf) {
|
|
405
|
+
const { mw, why, ...mount } = m;
|
|
406
|
+
records.push({
|
|
407
|
+
...base, pathTemplate: joinMountPath(mount.prefix, base.registeredPath), mount: { ...mount, alternatives: mountsOf.length },
|
|
408
|
+
middleware: [...base.middleware.map(x => ({ ...x })), ...mw],
|
|
409
|
+
resolutionReasons: [...new Set([...base.resolutionReasons, ...why])].sort(),
|
|
410
|
+
});
|
|
411
|
+
}
|
|
280
412
|
}
|
|
281
413
|
}
|
|
282
414
|
|
|
@@ -17,7 +17,7 @@ import * as path from 'node:path';
|
|
|
17
17
|
|
|
18
18
|
const JS_EXTS = ['', '.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '/index.js', '/index.mjs', '/index.ts', '/index.tsx'];
|
|
19
19
|
|
|
20
|
-
function resolveJsModule(fromFile, source, files) {
|
|
20
|
+
export function resolveJsModule(fromFile, source, files) {
|
|
21
21
|
if (!source.startsWith('.')) return { kind: 'third-party', target: null };
|
|
22
22
|
const base = path.posix.normalize(path.posix.join(path.posix.dirname(fromFile), source));
|
|
23
23
|
for (const ext of JS_EXTS) if (files.has(base + ext)) return { kind: 'repo', target: base + ext };
|
|
@@ -20,7 +20,7 @@ import { redactPayload } from '../egress/redact.js';
|
|
|
20
20
|
import { evaluateEgress } from '../egress/policy.js';
|
|
21
21
|
import { resolveProvider } from '../llm-validator/providers.js';
|
|
22
22
|
import { resolveLlmInvokeWithDecision } from '../discovery/llm-invoke.js';
|
|
23
|
-
import { hashOf, safeLabel, safeRelPath, sha256 } from './canonical.js';
|
|
23
|
+
import { hashOf, safeLabel, safeRelPath, sha256, sliceLines } from './canonical.js';
|
|
24
24
|
import { estimateTokens } from './packer.js';
|
|
25
25
|
import { REVIEW_SCHEMA_VERSION, packRef } from './prompts/packs.js';
|
|
26
26
|
|
|
@@ -249,8 +249,13 @@ export function resolveCitation(c, segments, fileContents) {
|
|
|
249
249
|
if (typeof text !== 'string') return 'file-not-in-snapshot';
|
|
250
250
|
const fileLines = text.split('\n');
|
|
251
251
|
if (c.endLine > fileLines.length) return 'beyond-end-of-file';
|
|
252
|
-
const inside = segments.
|
|
253
|
-
if (!inside) return 'outside-provided-context';
|
|
252
|
+
const inside = segments.filter(s => s.file === c.file && c.startLine >= s.startLine && c.endLine <= s.endLine);
|
|
253
|
+
if (!inside.length) return 'outside-provided-context';
|
|
254
|
+
// GA-03.2 (0.195.26): the citation is bound to the exact lines the bundle was built from. A segment carries the
|
|
255
|
+
// hash of its span at build time; if the snapshot's lines no longer hash to it, the model reviewed text that is
|
|
256
|
+
// not what is on disk now, and the citation is stale rather than evidence. A segment with no recorded hash
|
|
257
|
+
// (a hand-built one) cannot be judged and is not refused for that reason.
|
|
258
|
+
if (inside.every(s => typeof s.spanHash === 'string' && sha256(sliceLines(text, s.startLine, s.endLine)) !== s.spanHash)) return 'stale-context';
|
|
254
259
|
return claimGrounded(c.claim, fileLines.slice(c.startLine - 1, c.endLine).join('\n')) ? null : 'claim-not-grounded-in-cited-lines';
|
|
255
260
|
}
|
|
256
261
|
|
package/src/dataflow/CLAUDE.md
CHANGED
|
@@ -118,7 +118,7 @@ Tests: `test/evaluation/engine-mechanisms.test.js`.
|
|
|
118
118
|
|
|
119
119
|
## Boundary-contract consumer seeding (DAG-08)
|
|
120
120
|
|
|
121
|
-
`cross-service-taint.js` still only ANNOTATES findings for `consumes` declarations. The separate `runBoundaryTaint(perFileIR, callGraph, contracts)` seeds bound boundary contracts (see `../lineage/boundary-contract.js`): it sets `fn.boundarySeeds` (access paths such as `evt.customerId`, read by `_unionAnnotationTaint`) for one seed per engine run and always removes it afterwards. Findings new relative to an unseeded baseline carry `boundary` evidence (`modeledInput: true`, `observed: false`) and the consumer field that reached them. A contract that does not bind yields a coverage gap, no finding. Cost is one `runTaintEngine` pass per seed, capped by `maxRuns` (default 64, reported as a `budget` gap when hit).
|
|
121
|
+
`cross-service-taint.js` still only ANNOTATES findings for `consumes` declarations. The separate `runBoundaryTaint(perFileIR, callGraph, contracts)` seeds bound boundary contracts (see `../lineage/boundary-contract.js`): it sets `fn.boundarySeeds` (access paths such as `evt.customerId`, read by `_unionAnnotationTaint`) for one seed per engine run and always removes it afterwards. Findings new relative to an unseeded baseline carry `boundary` evidence (`modeledInput: true`, `observed: false`) and the consumer field that reached them. A contract that does not bind yields a coverage gap, no finding. Cost is one `runTaintEngine` pass per seed, capped by `maxRuns` (default 64, reported as a `budget` gap when hit). The LINEAGE graph has its own, separate consumer of the same contracts since 0.195.25 (`../lineage/boundary-join.js`, opt-in with `AGENTIC_SECURITY_LINEAGE_BOUNDARIES=1`): it seeds field identity, not taint, so nothing here, and no SAST finding, changes when it runs.
|
|
122
122
|
## Demand-driven sink queries (DAG-02, `demand-query.js`)
|
|
123
123
|
|
|
124
124
|
Opt-in (`AGENTIC_SECURITY_DEMAND=1` plus an explicit query list or the `runDemandSinkQuery` API; the default scan never calls it). A query is keyed by exact sink site (file, line, optional node id, callee, sink id), argument index, field/access path inside that argument, context (only `whole-program` is supported; anything else is `unavailable`), rule family (CWE or family name) and the graph and configuration digests (`graphDigest`, `configDigest`, `sinkQueryKey`).
|
package/src/engine.js
CHANGED
|
@@ -222,6 +222,7 @@ import { createStringMemo } from './util/string-memo.js';
|
|
|
222
222
|
import { collectIrStats, irStatsTarget, writeIrStats } from './ir/ir-stats.js';
|
|
223
223
|
import { runDeepAnalysis } from './dataflow/index.js';
|
|
224
224
|
import { buildLineageGraph } from './lineage/index.js';
|
|
225
|
+
import { getRepoState, worktreeDirtyExcluding } from './posture/provenance/git-evidence.js';
|
|
225
226
|
import { buildLineageGraphOwned, resolveLineageDeadlineMs } from './lineage/owned-build.js';
|
|
226
227
|
import { completionOfGraph, summarizeForHealth as summarizeCompletionForHealth } from './lineage/completion.js';
|
|
227
228
|
// v3 next-gen — Pillars 1, 4, 5, 6, 8, 9.
|
|
@@ -9747,6 +9748,12 @@ function _deterministicFileTimings(timings) {
|
|
|
9747
9748
|
const _lineageDeadlineMs = resolveLineageDeadlineMs();
|
|
9748
9749
|
const { perFile, callGraph } = _lineageDeadlineMs ? {} : (_sharedIR || (_sharedIR = await _buildIR()));
|
|
9749
9750
|
const _lineageT0 = Date.now();
|
|
9751
|
+
let _lineageRepo = null;
|
|
9752
|
+
try {
|
|
9753
|
+
_lineageRepo = scanRoot ? getRepoState(scanRoot) : null;
|
|
9754
|
+
// The scanner's own state directory must not make an unchanged tree read as dirty between two scans.
|
|
9755
|
+
if (_lineageRepo) _lineageRepo = { ..._lineageRepo, dirty: worktreeDirtyExcluding(scanRoot) };
|
|
9756
|
+
} catch { _lineageRepo = null; }
|
|
9750
9757
|
setProgress({ phase: 'Lineage graph', current: 0, total: 0, file: `starting (budget ${Math.round(_lineageBudgetMs / 1000)}s)` });
|
|
9751
9758
|
const _lineageOpts = {
|
|
9752
9759
|
// Live per-function progress from the field-identity driver's own
|
|
@@ -9774,6 +9781,12 @@ function _deterministicFileTimings(timings) {
|
|
|
9774
9781
|
deterministic: isDeterministic(),
|
|
9775
9782
|
perFile,
|
|
9776
9783
|
fileContents,
|
|
9784
|
+
// GA-03.3 (0.195.25): cross-service handoff joins in the lineage
|
|
9785
|
+
// graph, opt-in. Off, the graph is the one 0.195.24 built.
|
|
9786
|
+
...(process.env.AGENTIC_SECURITY_LINEAGE_BOUNDARIES === '1' ? { boundaries: { enabled: true } } : {}),
|
|
9787
|
+
// GA-03.7 (0.195.26): the revision the graph is built from, so a
|
|
9788
|
+
// consumer can refuse a graph from another one. Null outside git.
|
|
9789
|
+
...(_lineageRepo && _lineageRepo.head ? { commit: _lineageRepo.head, worktreeDirty: _lineageRepo.dirty } : {}),
|
|
9777
9790
|
};
|
|
9778
9791
|
// DAG-07 R07.7: with AGENTIC_SECURITY_LINEAGE_DEADLINE_MS set, the build runs in an
|
|
9779
9792
|
// owned worker with a watchdog. Unset: the in-process call, exactly as before.
|
package/src/lineage/CLAUDE.md
CHANGED
|
@@ -1340,3 +1340,20 @@ The data-lineage corpus (`bench/data-lineage/`) went from 17 of 24 to 24 of 24,
|
|
|
1340
1340
|
| `summaries.js` (extended) | `createCallSummaryResolver(cache, lookupCallee, { seedEntryState })` joins the callee's OWN seeds onto the entry state derived from the call arguments: `req.body.card_number` read inside `extractCard` is a source wherever it is called from, so the caller's resolution of `extractCard(req)` returns the PCI element and the `call-resolved` hop joins the caller's path to the callee's return node. The cache key is the joined state, so the driver's analysis of the callee and a clean call site share one key; §3.6's rule (no seeded summary under the empty-entry key) stands, see `DESIGN_GRAPH_BUILDER.md` §3.8. Bind hops and the cap-degradation loss hops enumerate only the call-derived half. `ctx.seedAfterNode` is forwarded to the callee's ctx. `driver.js` memoises the hook per function and passes it to the resolver. |
|
|
1341
1341
|
| `engine.js` (extended) | After `step()`, the worklist adds that node's `after-node` seeds to the outgoing state (idempotent on revisit, no hop: the first read creates the path node with no in-edges, an origin, as for an entry seed). **Disclosed gap:** a bare call statement is still never resolved through the summary, so a sink inside the callee never sees an argument's identity; resolving it was tried and reverted (it consumes a summary context per call site and under a tight cap degrades a later resolution, moving seven pinned provenance measurements). Pinned by `seeding-across-calls.test.js` IP/2. |
|
|
1342
1342
|
| `bench/data-lineage/runner.mjs` (extended) | `expectUnsupportedSink: true` asserts a `kind: 'process'`, `subtype: null`, `coverageStatus: 'unsupported'` node with a reason (the `exec` fixture). An EMPTY `dataClass` label matches the flow on the two categories alone (a file's contents, a positional argument); a non-empty label is unchanged. |
|
|
1343
|
+
|
|
1344
|
+
## Cross-service handoffs (GA-03.3, 0.195.25)
|
|
1345
|
+
|
|
1346
|
+
A value leaving one function through a queue publish or a store write and re-entering another through a queue receive or a store read is a HANDOFF. The engine sees an escape and an unrelated call result; nothing in the hop stream connects them, so the multi-service fixture of `bench/dag` reported 3 of its 8 labelled positives (the two site labels it still misses are for the taint engine, not for a flow). `boundary-join.js` joins the two halves, opt-in: `buildDataFlowGraph(callGraph, { boundaries: { enabled: true, contracts } })`, `AGENTIC_SECURITY_LINEAGE_BOUNDARIES=1` on a scan. Off, a build is byte-identical to 0.195.24 (`test/lineage/boundary-join.test.js` BJ/9).
|
|
1347
|
+
|
|
1348
|
+
| Module | Responsibility |
|
|
1349
|
+
|---|---|
|
|
1350
|
+
| `boundary-join.js` | Pure (imports `node:crypto`, `dataflow/access-paths.js`, `source-seeding.js`'s walkers, `boundary-contract.js`, `classification.js`). `collectBoundaryEndpoints(callGraph, sites)`: producers are the sink sites of category `queue` (including the AWS SDK v3 shape `client.send(new SendMessageCommand(input))`) and the store writes `collection('<literal>').insertOne|insertMany|replaceOne(record)`; consumers are assignments whose source is `client.send(new ReceiveMessageCommand(input))` (record path `Messages.Body`, JSON) or `collection('<literal>').find(q).toArray()` / `.findOne(q)`. An identity is a string or number literal, or `process.env.NAME`, read through at most three module-scope constants (`resolveIdentityExpr`); a store identity is the collection literal, and two sides with different literal `db('<name>')` names never join. A computed identity is `unjoined` with `dynamic-identity`; an identity with no partner is `no-producer-in-scope`. Two sites that share only a field name are never joined. `recordShape(recordExpr, siteHops)`: the ids that reached the producer site (`write-out/call-arg` hops) keyed by the record's object-literal keys, each key taking the ids whose contributing state key (`production/ident` and `selection/member` hops) its value expression references through its OUTERMOST access paths; a record that is not an object literal is `whole-record`; nothing reached is `none`. `planBoundaryJoins`: a discovered join seeds the consumer's assignment target under `target.recordPath.key` as an `after-node` seed carrying the PRODUCER's data-element id; a declared DAG-08 contract (bound by `bindBoundaryContracts`) seeds the handler parameter in the entry state under the contract's `fieldMappings` (renames applied), from the producer site the contract names, and a contract without a producer site is a gap (`producer-site-required`). Every seed carries `boundary: { joinId, kind, identity, provenance: 'code' \| 'manual', contractId, basis, producer, modeledInput: true, observed: false, exploited: false }`. |
|
|
1351
|
+
| `graph-builder.js` (extended) | Sites are enumerated before the analysis so the producers exist for the rounds. With boundaries on, a round plans the joins the current hop stream supports and, when it minted seeds, re-runs the field analysis (`MAX_BOUNDARY_ROUNDS` = 4, each round one full `runFieldIdentityAnalysis`); a still-productive last round is `truncated` and the graph `partial` with `analysisCompletion.boundaries` (BJ/10), never read as convergence. The source node of a flow from a boundary seed is the PRODUCER's sink node, so the graph reads as one chain (HTTP to queue to store to HTTP response); the edge carries `provenance: 'code'` or `'manual'` and one `boundaryCrossings` entry; the flow's limitations say the handoff is modeled, not observed. A data element seeded in several functions keeps the name and classes of its collection point (`{contact: email}` is still `email`) and gains one `sourceLocations` entry per consumer; `seedForPath` picks the seed in the function the path's terminal node lives in. `enumerateSinkSites` resolves the v3 send command through the privacy catalog's queue entry (the general catalog's bare `send` entry had claimed it as a koa-send path sink) and skips the receive command (a read, not a sink). `coverage.boundaries` (also in `coverage.js`'s ledger, only when enabled): rounds, joins, joined fields, declared vs discovered, every unjoined endpoint with its reason, contract gaps. |
|
|
1352
|
+
| `engine.js` (extended) | `JSON.parse(x)` and `JSON.stringify(x)` are structure-preserving: the call forwards its first argument's `{flat, byPath, widened}` the way `assign-expr` forwards its source, and records no hop of its own. Without this the consumer's `body = JSON.parse(m.Body)` collapsed to the whole record's flat union and every key crossed under every field. `source-seeding.js`'s alias extension unwraps the same pair (`const back = JSON.parse(s)` is an alias of the container `s` was serialized from). |
|
|
1353
|
+
| `index.js` / `engine.js` (scan) | `AGENTIC_SECURITY_LINEAGE_BOUNDARIES=1` sets `boundaries.enabled`; `index.js` reads `.agentic-security/boundary-contracts.json` (the file `dataflow contracts` reads) once, existence-gated, a malformed file printed and treated as no contracts. `bench/dag/lib/scan-unit.mjs` forces the variable on, as it does deep mode. |
|
|
1354
|
+
|
|
1355
|
+
Measured on the multi-service fixture, scan path (`node bench/dag/run.mjs --only=fixture`): flow labels 6 of 6 positive and 3 of 3 negative, where 0.195.24 found 3 of 6; the two site labels (stored value into HTML) stay FN, they need a storage-read source in the taint engine; one reported flow matches no label (`qty` into `orders.total`, a derived numeric write the label set does not list; left unlabelled under the no-relabelling rule). Precision of the join rests on three things stated above: a literal identity on both sides, the ids that really reached the producer site, and outermost-path field mapping. What it does not do: a queue consumed through a callback (`consumer.on('message', cb)`), a store read through an ORM model, a batch send's per-entry bodies, an identity that is a template string or a concatenation, and a producer in another repository (that is `cross-repo-link.js`'s declared edge, unchanged).
|
|
1356
|
+
|
|
1357
|
+
## Frozen artifact contracts and the scanned revision (GA-03.7, 0.195.26)
|
|
1358
|
+
|
|
1359
|
+
`docs/reference/code-boundaries-contract.md` freezes the shape of the signed lineage graph, witness bundles, boundary contracts, context bundles, sink query keys and cut plans, and `test/contracts/code-boundaries.contract.test.js` holds every table in both directions and exercises each rule with a real tamper, a real wrong revision and a real partial analysis (it is the `contractTest` of the `code-boundaries`, `graph-precision` and `context-intelligence` registry rows, so `check-ga-contracts` runs it). Two things changed in code for it: `graph-builder.js` records `graph.scope.revision = { commit, worktreeDirty }` when the caller passes `commit` (the scan passes the repository head from `getRepoState`; a build without one is byte-identical to before), and `server/graph-loader.js`'s `loadSignedGraph(root, { expectRevision })` refuses with `wrong-revision` a graph that records another commit, or none (`dataflow view --expect-revision <sha|HEAD>`). The incremental 3x end-to-end target (GA-03.6) was measured, not met, and the measured reason is under "Open items" in `docs/implementation-status/dag-improvements.md`.
|