@clear-capabilities/agentic-security-scanner 0.195.25 → 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 +14 -0
- package/bin/agentic-security.js +9 -0
- package/dist/5752.index.js +15 -2
- package/dist/{809c900a90284017aaff.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 +9 -9
- 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/engine.js +10 -0
- package/src/lineage/CLAUDE.md +4 -0
- package/src/lineage/graph-builder.js +8 -0
- package/src/lineage/index.js +2 -0
- 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/boundary-join.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/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
|
|
@@ -9777,6 +9784,9 @@ function _deterministicFileTimings(timings) {
|
|
|
9777
9784
|
// GA-03.3 (0.195.25): cross-service handoff joins in the lineage
|
|
9778
9785
|
// graph, opt-in. Off, the graph is the one 0.195.24 built.
|
|
9779
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 } : {}),
|
|
9780
9790
|
};
|
|
9781
9791
|
// DAG-07 R07.7: with AGENTIC_SECURITY_LINEAGE_DEADLINE_MS set, the build runs in an
|
|
9782
9792
|
// owned worker with a watchdog. Unset: the in-process call, exactly as before.
|
package/src/lineage/CLAUDE.md
CHANGED
|
@@ -1353,3 +1353,7 @@ A value leaving one function through a queue publish or a store write and re-ent
|
|
|
1353
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
1354
|
|
|
1355
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`.
|
|
@@ -602,6 +602,14 @@ export function buildDataFlowGraph(callGraph, opts = {}) {
|
|
|
602
602
|
// module ever passed `undefined`/omitted the key entirely).
|
|
603
603
|
generatedAt: opts.generatedAt ?? '1970-01-01T00:00:00.000Z',
|
|
604
604
|
});
|
|
605
|
+
// GA-03.7 (0.195.26): the revision the graph was built from, when the
|
|
606
|
+
// caller knows it (a scan passes the repository head). A consumer can then
|
|
607
|
+
// refuse a graph built from another revision (`loadSignedGraph`'s
|
|
608
|
+
// `expectRevision`). Absent when no commit was given, so a build without
|
|
609
|
+
// one is byte-identical to before.
|
|
610
|
+
if (typeof opts.commit === 'string' && opts.commit) {
|
|
611
|
+
graph.scope.revision = { commit: opts.commit, worktreeDirty: typeof opts.worktreeDirty === 'boolean' ? opts.worktreeDirty : null };
|
|
612
|
+
}
|
|
605
613
|
|
|
606
614
|
const nodesById = new Map();
|
|
607
615
|
const edgesById = new Map();
|
package/src/lineage/index.js
CHANGED
|
@@ -301,6 +301,8 @@ export function buildLineageGraph(callGraph, opts = {}) {
|
|
|
301
301
|
const built = buildGraphWithCoverage(lineageCallGraph, {
|
|
302
302
|
repository: opts.repository,
|
|
303
303
|
...(boundaries ? { boundaries } : {}),
|
|
304
|
+
// GA-03.7: the scanned revision, recorded in `graph.scope.revision` and in the graph id.
|
|
305
|
+
...(typeof opts.commit === 'string' && opts.commit ? { commit: opts.commit, worktreeDirty: opts.worktreeDirty } : {}),
|
|
304
306
|
generatedAt: opts.deterministic ? undefined : new Date().toISOString(),
|
|
305
307
|
perFile: opts.perFile,
|
|
306
308
|
parseFailures: opts.parseFailures,
|
|
@@ -79,7 +79,7 @@ async function loadProgram(root) {
|
|
|
79
79
|
// ---------------------------------------------------------------------------------------------------------------------
|
|
80
80
|
|
|
81
81
|
/**
|
|
82
|
-
* agentic-security dataflow view [path] [--node <id> [--direction in|out|both] [--relations a,b] [--origins a,b]] [--output <file>]
|
|
82
|
+
* agentic-security dataflow view [path] [--node <id> [--direction in|out|both] [--relations a,b] [--origins a,b]] [--expect-revision <sha|HEAD>] [--output <file>]
|
|
83
83
|
* Read-only over the signed lineage graph. Without --node it reports the view identity, counts, completion and the SCC
|
|
84
84
|
* condensation of the data-flow relation. With --node it lists the neighbours the filter allows.
|
|
85
85
|
*/
|
|
@@ -95,7 +95,18 @@ export async function cmdDataflowView(args) {
|
|
|
95
95
|
if (f.node === undefined && (f.direction || f.relations || f.origins)) { err(`${who}: --direction, --relations and --origins apply to --node.`); return 2; }
|
|
96
96
|
|
|
97
97
|
const { loadSignedGraph } = await import('../server/graph-loader.js');
|
|
98
|
-
|
|
98
|
+
// GA-03.7: `--expect-revision <sha|HEAD>` refuses a graph built from another revision (or from none).
|
|
99
|
+
let expectRevision = null;
|
|
100
|
+
if (f['expect-revision'] !== undefined) {
|
|
101
|
+
if (typeof f['expect-revision'] !== 'string' || !f['expect-revision']) { err(`${who}: --expect-revision needs a commit or HEAD.`); return 2; }
|
|
102
|
+
if (f['expect-revision'] === 'HEAD') {
|
|
103
|
+
const { getRepoState } = await import('./provenance/git-evidence.js');
|
|
104
|
+
const st = getRepoState(root);
|
|
105
|
+
if (!st || !st.head) { err(`${who}: --expect-revision HEAD needs a git repository at ${root}.`); return 2; }
|
|
106
|
+
expectRevision = st.head;
|
|
107
|
+
} else expectRevision = f['expect-revision'];
|
|
108
|
+
}
|
|
109
|
+
const loaded = loadSignedGraph(root, { expectRevision });
|
|
99
110
|
if (!loaded.ok) { err(`${who}: ${loaded.message}`); return 1; }
|
|
100
111
|
const { dataflowView, GraphViewError } = await import('../graph/index.js');
|
|
101
112
|
const { computeScc, verifyScc } = await import('../graph/scc.js');
|
|
@@ -107,6 +118,8 @@ export async function cmdDataflowView(args) {
|
|
|
107
118
|
schema: 'agentic-security/graph-view-report@1',
|
|
108
119
|
maturity: 'preview',
|
|
109
120
|
identity: view.identity,
|
|
121
|
+
revision: loaded.graph.scope && loaded.graph.scope.revision ? loaded.graph.scope.revision : null,
|
|
122
|
+
expectedRevision: expectRevision,
|
|
110
123
|
metadata: view.metadata(),
|
|
111
124
|
viewCompletion: view.completion(),
|
|
112
125
|
note: 'A view is a read-only lens over the signed graph. An absent edge means the model has no such edge, not that the software has none.',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as cp from 'node:child_process';
|
|
2
|
+
import { STATE_DIR_NAME } from '../state-dir.js';
|
|
2
3
|
import * as fs from 'node:fs';
|
|
3
4
|
import * as path from 'node:path';
|
|
4
5
|
import { hardenGitArgs, hardenGitEnv } from '../../util/git-hardening.js';
|
|
@@ -95,6 +96,26 @@ export function getRemoteUrl(scanRoot) {
|
|
|
95
96
|
return r.ok ? r.stdout.trim() : null;
|
|
96
97
|
}
|
|
97
98
|
|
|
99
|
+
/**
|
|
100
|
+
* Whether the working tree has changes OUTSIDE the given path prefixes (default: the scanner's own state
|
|
101
|
+
* directory). `getRepoState().dirty` counts the scanner's own untracked state directory as dirty, which would make the
|
|
102
|
+
* lineage graph's recorded revision flip between two scans of an unchanged tree merely because the first
|
|
103
|
+
* scan created that directory. Null when not a git repository or when git fails.
|
|
104
|
+
*/
|
|
105
|
+
export function worktreeDirtyExcluding(scanRoot, ignorePrefixes = [`${STATE_DIR_NAME}/`]) {
|
|
106
|
+
if (!isGitRepo(scanRoot)) return null;
|
|
107
|
+
const r = _run(scanRoot, ['status', '--porcelain', '--untracked-files=all']);
|
|
108
|
+
if (!r.ok) return null;
|
|
109
|
+
for (const line of r.stdout.split('\n')) {
|
|
110
|
+
if (!line.trim()) continue;
|
|
111
|
+
const p = line.slice(3).replace(/^"|"$/g, '');
|
|
112
|
+
const renamedTo = p.includes(' -> ') ? p.split(' -> ')[1] : p;
|
|
113
|
+
if (ignorePrefixes.some((pre) => p.startsWith(pre) || renamedTo.startsWith(pre))) continue;
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
return false;
|
|
117
|
+
}
|
|
118
|
+
|
|
98
119
|
export function getRepoState(scanRoot) {
|
|
99
120
|
if (!isGitRepo(scanRoot)) return null;
|
|
100
121
|
const head = _run(scanRoot, ['rev-parse', 'HEAD']);
|
|
@@ -19,7 +19,7 @@ import { verifyLastScan } from '../posture/integrity.js';
|
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* @param {string} scanRoot
|
|
22
|
-
* @returns {{ok:true, graph:object} | {ok:false, reason:'missing'|'unsigned'|'tampered'|'malformed', message:string}}
|
|
22
|
+
* @returns {{ok:true, graph:object} | {ok:false, reason:'missing'|'unsigned'|'tampered'|'malformed'|'wrong-revision', message:string}}
|
|
23
23
|
*
|
|
24
24
|
* Four, and only four, distinct failure reasons — each with its own clear
|
|
25
25
|
* message so an operator knows exactly what to do next:
|
|
@@ -36,7 +36,7 @@ import { verifyLastScan } from '../posture/integrity.js';
|
|
|
36
36
|
* valid JSON. Should not happen from a normal scan; the
|
|
37
37
|
* file may be corrupted on disk after signing.
|
|
38
38
|
*/
|
|
39
|
-
export function loadSignedGraph(scanRoot) {
|
|
39
|
+
export function loadSignedGraph(scanRoot, { expectRevision = null } = {}) {
|
|
40
40
|
const graphPath = statePath(scanRoot, 'lineage-graph.json');
|
|
41
41
|
const sigPath = graphPath + '.sig';
|
|
42
42
|
|
|
@@ -86,6 +86,23 @@ export function loadSignedGraph(scanRoot) {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
// GA-03.7 (0.195.26): wrong-revision refusal. A caller that knows which
|
|
90
|
+
// revision it expects (`dataflow view --expect-revision`) gets a graph
|
|
91
|
+
// only when the graph records that revision; a graph that records none
|
|
92
|
+
// cannot prove it and is refused for the same reason, never served on
|
|
93
|
+
// the hope that it matches.
|
|
94
|
+
if (expectRevision) {
|
|
95
|
+
const have = graph && graph.scope && graph.scope.revision ? graph.scope.revision.commit : null;
|
|
96
|
+
if (have !== expectRevision) {
|
|
97
|
+
return {
|
|
98
|
+
ok: false,
|
|
99
|
+
reason: 'wrong-revision',
|
|
100
|
+
message: have
|
|
101
|
+
? `Lineage graph at ${graphPath} was built from revision ${have}, not the expected ${expectRevision}. Re-run the scan on the expected revision.`
|
|
102
|
+
: `Lineage graph at ${graphPath} records no revision, so it cannot be shown to match the expected ${expectRevision}. Re-run the scan inside the repository (AGENTIC_SECURITY_LINEAGE_DEEP=1) so the graph records its revision.`,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
89
106
|
return { ok: true, graph };
|
|
90
107
|
}
|
|
91
108
|
|