@clear-capabilities/agentic-security-scanner 0.172.0 → 0.173.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/bin/agentic-security.js +37 -0
- package/dist/119.index.js +197 -0
- package/dist/128.index.js +129 -0
- package/dist/1293.index.js +5689 -0
- package/dist/1336.index.js +101 -101
- package/dist/1351.index.js +133 -0
- package/dist/1387.index.js +271 -0
- package/dist/1437.index.js +23 -0
- package/dist/1445.index.js +110 -0
- package/dist/1505.index.js +151 -0
- package/dist/1557.index.js +293 -0
- package/dist/1621.index.js +311 -0
- package/dist/1751.index.js +160 -0
- package/dist/1765.index.js +727 -0
- package/dist/1861.index.js +690 -0
- package/dist/1898.index.js +116 -0
- package/dist/1906.index.js +433 -0
- package/dist/1920.index.js +1 -291
- package/dist/208.index.js +1631 -0
- package/dist/2199.index.js +0 -239
- package/dist/2223.index.js +159 -0
- package/dist/2530.index.js +152 -0
- package/dist/2565.index.js +116 -0
- package/dist/2574.index.js +127 -0
- package/dist/2620.index.js +345 -0
- package/dist/2773.index.js +98 -0
- package/dist/2775.index.js +32 -0
- package/dist/2966.index.js +171 -0
- package/dist/2972.index.js +87 -0
- package/dist/2991.index.js +131 -0
- package/dist/3063.index.js +833 -0
- package/dist/3075.index.js +227 -0
- package/dist/3121.index.js +297 -0
- package/dist/3314.index.js +263 -0
- package/dist/3480.index.js +1822 -0
- package/dist/3481.index.js +24 -0
- package/dist/3506.index.js +268 -0
- package/dist/3533.index.js +591 -0
- package/dist/3653.index.js +450 -0
- package/dist/3822.index.js +131 -0
- package/dist/3868.index.js +292 -0
- package/dist/3917.index.js +152 -0
- package/dist/3918.index.js +192 -0
- package/dist/4090.index.js +288 -0
- package/dist/4121.index.js +1190 -0
- package/dist/4142.index.js +0 -239
- package/dist/4336.index.js +436 -0
- package/dist/4387.index.js +532 -0
- package/dist/4413.index.js +302 -0
- package/dist/4476.index.js +4510 -0
- package/dist/4491.index.js +133 -0
- package/dist/4538.index.js +185 -0
- package/dist/4684.index.js +319 -0
- package/dist/4740.index.js +165 -0
- package/dist/4780.index.js +209 -0
- package/dist/4802.index.js +225 -0
- package/dist/4812.index.js +379 -0
- package/dist/4835.index.js +126 -0
- package/dist/494.index.js +226 -0
- package/dist/4991.index.js +549 -0
- package/dist/5199.index.js +305 -0
- package/dist/5216.index.js +109 -0
- package/dist/5439.index.js +301 -0
- package/dist/5443.index.js +97 -0
- package/dist/5496.index.js +377 -0
- package/dist/550.index.js +2 -1
- package/dist/5801.index.js +588 -0
- package/dist/5858.index.js +223 -0
- package/dist/5933.index.js +143 -0
- package/dist/601.index.js +23 -0
- package/dist/6173.index.js +273 -0
- package/dist/6291.index.js +1871 -0
- package/dist/6312.index.js +666 -0
- package/dist/634.index.js +16182 -0
- package/dist/644.index.js +87 -0
- package/dist/6508.index.js +283 -0
- package/dist/6560.index.js +645 -0
- package/dist/6576.index.js +422 -0
- package/dist/6579.index.js +271 -0
- package/dist/6745.index.js +305 -0
- package/dist/6818.index.js +21642 -0
- package/dist/6972.index.js +167 -0
- package/dist/6995.index.js +163 -0
- package/dist/7249.index.js +191 -0
- package/dist/7372.index.js +1197 -0
- package/dist/7405.index.js +23 -6
- package/dist/7546.index.js +541 -0
- package/dist/76.index.js +448 -0
- package/dist/7879.index.js +163 -0
- package/dist/8025.index.js +130 -0
- package/dist/8054.index.js +248 -0
- package/dist/8097.index.js +201 -0
- package/dist/8126.index.js +191 -0
- package/dist/8178.index.js +78 -0
- package/dist/818.index.js +212 -0
- package/dist/8382.index.js +93 -0
- package/dist/8408.index.js +261 -0
- package/dist/8540.index.js +174 -0
- package/dist/8635.index.js +802 -0
- package/dist/8688.index.js +410 -0
- package/dist/8699.index.js +779 -0
- package/dist/8891.index.js +457 -0
- package/dist/8916.index.js +885 -0
- package/dist/9146.index.js +170 -0
- package/dist/9168.index.js +841 -0
- package/dist/9292.index.js +369 -0
- package/dist/9382.index.js +338 -48
- package/dist/9422.index.js +866 -0
- package/dist/9440.index.js +453 -0
- package/dist/9479.index.js +152 -0
- package/dist/9481.index.js +220 -0
- package/dist/9539.index.js +100 -0
- package/dist/9547.index.js +103 -0
- package/dist/9560.index.js +41 -61
- package/dist/9692.index.js +90 -0
- package/dist/9800.index.js +213 -1
- package/dist/9899.index.js +138 -0
- package/dist/9981.index.js +206 -0
- package/dist/999.index.js +117 -0
- package/dist/agentic-security.mjs +28 -28
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +31 -6
- package/src/context-intelligence/CLAUDE.md +4 -0
- package/src/context-intelligence/adapters/haskell-hie.js +28 -1
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/canonical.js +25 -0
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +16 -10
- package/src/context-intelligence/config.js +4 -1
- package/src/context-intelligence/controls.js +108 -10
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- package/src/context-intelligence/entrypoints.js +2 -2
- package/src/context-intelligence/evaluation/index.js +11 -4
- package/src/context-intelligence/evaluation/scripted-reader.js +12 -2
- package/src/context-intelligence/findings.js +166 -0
- package/src/context-intelligence/invariants.js +197 -0
- package/src/context-intelligence/queue.js +6 -1
- package/src/context-intelligence/reachability.js +10 -4
- package/src/context-intelligence/review.js +129 -16
- package/src/dataflow/access-paths.js +11 -0
- package/src/dataflow/analysis-limits.js +66 -0
- package/src/dataflow/engine.js +272 -20
- package/src/dataflow/service-edges.js +174 -0
- package/src/dataflow/summaries.js +7 -1
- package/src/discovery/judge.js +2 -1
- package/src/engine.js +82 -14
- package/src/fix/apply-fix-service.js +55 -8
- package/src/fix/batch-journal.js +84 -0
- package/src/fix/exploit-replay.js +51 -0
- package/src/fix/footprint.js +65 -0
- package/src/fix/patch-policy.js +49 -0
- package/src/fix/secret-persistence.js +39 -0
- package/src/fix/test-commands.js +84 -0
- package/src/fix/verification-adapters.js +97 -0
- package/src/fix/verification-record.js +243 -0
- package/src/fix/verify-patch.js +89 -0
- package/src/ir/CLAUDE.md +5 -0
- package/src/ir/callgraph.js +23 -1
- package/src/ir/module-resolver.js +315 -0
- package/src/ir/parser-js.js +21 -2
- package/src/ir/semantic-graph.js +245 -0
- package/src/lineage/build-attested-correlation.js +63 -0
- package/src/lineage/deployment-identity.js +76 -0
- package/src/mcp/tools.js +13 -34
- package/src/pipeline/canonical-output.js +64 -0
- package/src/pipeline/detector-runner.js +7 -0
- package/src/pipeline/scan-health.js +14 -0
- package/src/pipeline/supervised-analyzers.js +125 -0
- package/src/pipeline/supervised-child.js +17 -0
- package/src/pipeline/telemetry.js +79 -0
- package/src/posture/accuracy-scorecard.js +98 -9
- package/src/posture/artifact-registry.js +5 -0
- package/src/posture/assurance/evidence-registry.js +215 -0
- package/src/posture/assurance/release-package-verify.js +135 -0
- package/src/posture/assurance/release-package.js +118 -0
- package/src/posture/assurance/release-verdict.js +94 -0
- package/src/posture/assurance/waiver.js +200 -0
- package/src/posture/compliance-truth.js +100 -0
- package/src/posture/evaluation/scan-child.js +3 -1
- package/src/posture/evaluation/why-missed.js +3 -1
- package/src/posture/exploitability-evidence.js +141 -0
- package/src/posture/fix-verify.js +4 -2
- package/src/posture/fleet/exposure-ranking.js +115 -0
- package/src/posture/fleet/service-chain-graph.js +150 -0
- package/src/posture/graph-reconciliation.js +154 -0
- package/src/posture/provenance/coverage.js +103 -0
- package/src/posture/provenance/git-evidence.js +84 -0
- package/src/posture/provenance/origin-resolver.js +7 -1
- package/src/posture/provenance/predicate-replay.js +14 -4
- package/src/posture/runtime-correlation.js +61 -15
- package/src/posture/runtime-explanation.js +116 -0
- package/src/posture/runtime-trace-ingest.js +194 -0
- package/src/posture/scan-checkpoint.js +38 -10
- package/src/posture/waf-ingest.js +7 -2
- package/src/report/index.js +4 -0
- package/src/report/oscal.js +38 -0
- package/src/report/perspectives.js +150 -0
- package/src/sast/_comment-strip.js +8 -1
- package/src/sast/ruby.js +26 -0
- package/src/util/string-memo.js +38 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
eab94f59f539296c01e17e2d914f419e131e2b4d1234865c011f755f11a64441 agentic-security.mjs
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clear-capabilities/agentic-security-scanner",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.173.0",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin: SAST, SCA with reachability and CISA KEV, secrets, IaC, Haskell and Nix/NixOS, prompt-injection, MCP/agent-tool audit, authZ analysis, SBOM, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -43,13 +43,13 @@
|
|
|
43
43
|
"@babel/core": "^8.0.5",
|
|
44
44
|
"@babel/preset-react": "^8.0.1",
|
|
45
45
|
"@babel/preset-typescript": "^8.0.1",
|
|
46
|
-
"js-yaml": "^5.
|
|
46
|
+
"js-yaml": "^5.5.0",
|
|
47
47
|
"safe-regex": "^2.1.1"
|
|
48
48
|
},
|
|
49
49
|
"optionalDependencies": {
|
|
50
50
|
"tree-sitter-wasms": "0.1.13",
|
|
51
51
|
"web-tree-sitter": "0.20.8",
|
|
52
|
-
"z3-solver": "^5.3.
|
|
52
|
+
"z3-solver": "^5.3.1"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"@types/node": "^26.6.5",
|
|
@@ -67,6 +67,17 @@
|
|
|
67
67
|
"test": "node ../scripts/run-unit-tests.mjs",
|
|
68
68
|
"test:extras": "AGENTIC_SECURITY_CPP_DATAFLOW=1 node --test test/cpp-dataflow.test.js && node --experimental-test-module-mocks --test test/fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection.test.js && node --experimental-test-module-mocks --test test/detector-fault-injection-k8s-admission.test.js && node --experimental-test-module-mocks --test test/lineage-fault-injection.test.js && npm run test:python",
|
|
69
69
|
"loop": "node ../scripts/loop-engineering/run.mjs",
|
|
70
|
+
"test:engineering-excellence": "node --test --test-concurrency=1 test/engineering-excellence/e0-01-inventory.test.js test/engineering-excellence/e0-02-baseline.test.js test/engineering-excellence/e0-03-contract.test.js test/engineering-excellence/e0-04-task-manifest.test.js test/engineering-excellence/e0-05-status-runner.test.js test/engineering-excellence/e0-06-backlog.test.js test/engineering-excellence/e1-01-dataset-provenance.test.js test/engineering-excellence/e1-02-splits.test.js test/engineering-excellence/e1-03-scoring.test.js test/engineering-excellence/e1-04-baseline-run.test.js test/engineering-excellence/e1-05-forensics.test.js test/engineering-excellence/e1-05-baseline-forensics.test.js test/engineering-excellence/e1-06-adversarial-controls.test.js test/engineering-excellence/e1-07-realcode-policy.test.js test/engineering-excellence/e1-07-baseline-gate.test.js test/engineering-excellence/e2-06-cross-language.test.js test/engineering-excellence/e2-08-suppression-attribution.test.js test/engineering-excellence/e2-09-semantic-gates.test.js test/engineering-excellence/e4-06-dci-real-app.test.js test/engineering-excellence/e6-08-perf-budget.test.js test/engineering-excellence/e8-02-e2e-demo.test.js test/engineering-excellence/e8-03-regression-report.test.js test/engineering-excellence/e8-04-docs.test.js test/engineering-excellence/e8-05-handoff.test.js test/engineering-excellence/e8-06-release-decision.test.js test/engineering-excellence/u01-ruby-path-resolver.test.js test/engineering-excellence/e1-08-scorecard.test.js test/engineering-excellence/e3-01-patch-routes.test.js test/engineering-excellence/e3-02-fix-verification-record.test.js test/engineering-excellence/e3-03-central-verifier.test.js test/engineering-excellence/e3-04-exploit-replay.test.js test/engineering-excellence/e3-05-functional-preservation.test.js test/engineering-excellence/e3-06-patch-policy.test.js test/engineering-excellence/e3-07-rollback-crash-safety.test.js test/engineering-excellence/e3-08-fix-benchmark.test.js test/engineering-excellence/e2-01-semantic-graph.test.js test/engineering-excellence/e2-02-cross-file-resolution.test.js test/engineering-excellence/e2-03-heap-fields-aliases.test.js test/engineering-excellence/e2-04-async-callbacks-frameworks.test.js test/engineering-excellence/e2-05-interprocedural-contexts.test.js test/engineering-excellence/e2-07-service-edges.test.js test/engineering-excellence/e4-01-dci-audit.test.js test/engineering-excellence/e4-02-invariants.test.js test/engineering-excellence/e4-03-cross-route.test.js test/engineering-excellence/e4-04-counterexample.test.js test/engineering-excellence/e4-05-llm-safeguards.test.js test/engineering-excellence/e4-07-dci-findings.test.js test/engineering-excellence/e6-01-telemetry.test.js test/engineering-excellence/e5-01-runtime-audit.test.js test/engineering-excellence/e5-02-build-identity.test.js test/engineering-excellence/e5-03-privacy-ingest.test.js test/engineering-excellence/e5-04-graph-reconciliation.test.js test/engineering-excellence/e5-05-exploitability-evidence.test.js test/engineering-excellence/e5-06-service-chain-graph.test.js test/engineering-excellence/e5-07-runtime-explanation.test.js test/engineering-excellence/e6-05-supervised-analyzers.test.js test/engineering-excellence/e6-06-determinism.test.js test/engineering-excellence/e6-07-adversarial-boundaries.test.js test/engineering-excellence/e6-03-shared-preprocessing.test.js test/engineering-excellence/e6-04-analysis-cache.test.js test/engineering-excellence/e7-01-evidence-contracts.test.js test/engineering-excellence/e7-03-release-package.test.js test/engineering-excellence/e7-04-compliance-truth.test.js test/engineering-excellence/e7-05-waivers-tenancy.test.js test/engineering-excellence/e7-02-provenance-coverage.test.js test/engineering-excellence/e7-06-fleet-exposure.test.js test/engineering-excellence/e7-07-perspectives.test.js test/engineering-excellence/e7-08-assurance-e2e.test.js test/engineering-excellence/e6-02-completion.test.js",
|
|
71
|
+
"ee:status": "node ../scripts/engineering-excellence/status.mjs --once",
|
|
72
|
+
"ee:init": "node ../scripts/engineering-excellence/init.mjs",
|
|
73
|
+
"ee:inventory": "node ../scripts/engineering-excellence/inventory.mjs",
|
|
74
|
+
"ee:contract": "node ../scripts/engineering-excellence/contract.mjs",
|
|
75
|
+
"ee:backlog": "node ../scripts/engineering-excellence/backlog.mjs",
|
|
76
|
+
"ee:dataset-audit": "node ../scripts/engineering-excellence/dataset-audit.mjs",
|
|
77
|
+
"ee:independent-baseline": "node ../scripts/engineering-excellence/independent-baseline.mjs",
|
|
78
|
+
"ee:dci-audit": "node ../scripts/engineering-excellence/dci-audit.mjs",
|
|
79
|
+
"ee:runtime-audit": "node ../scripts/engineering-excellence/runtime-audit.mjs",
|
|
80
|
+
"ee:ir-matrix": "node ../scripts/engineering-excellence/ir-matrix.mjs",
|
|
70
81
|
"test:loop-real-prd": "node --test ../scripts/loop-engineering/test/loop-real-prd.test.js",
|
|
71
82
|
"test:loop": "node --test --test-concurrency=1 ../scripts/loop-engineering/test/loop-manifest.test.js ../scripts/loop-engineering/test/loop-watchdog.test.js ../scripts/loop-engineering/test/loop-background.test.js ../scripts/loop-engineering/test/loop-dashboard.test.js ../scripts/loop-engineering/test/loop-evidence.test.js ../scripts/loop-engineering/test/loop-recovery.test.js ../scripts/loop-engineering/test/loop-remote.test.js ../scripts/loop-engineering/test/loop-runbook.test.js ../scripts/loop-engineering/test/loop-assurance-profile.test.js ../scripts/loop-engineering/test/loop-assurance-runtime.test.js ../scripts/loop-engineering/test/loop-bounds.test.js ../scripts/loop-engineering/test/loop-spend.test.js ../scripts/loop-engineering/test/loop-ci-attest.test.js",
|
|
72
83
|
"test:python": "node ../scripts/run-python-tests.mjs",
|
|
@@ -121,7 +132,7 @@
|
|
|
121
132
|
"verification:replay-example": "node ../scripts/verification-replay-example.mjs",
|
|
122
133
|
"test:lifecycle": "node --test test/ci-hang-protection.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/dist-chunks-tracked.test.js && node ../scripts/lint-command-descriptions.mjs test/workflow-confinement.test.js test/check-bundle-source.test.js test/ship.test.js",
|
|
123
134
|
"test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
|
|
124
|
-
"test:discovery": "node --test 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/git-diff-candidates.test.js",
|
|
135
|
+
"test:discovery": "node --test 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",
|
|
125
136
|
"eval:independent": "node ../bench/independent-eval/runner.mjs",
|
|
126
137
|
"eval:independent:gate": "node ../bench/independent-eval/runner.mjs --gate default",
|
|
127
138
|
"smoke": "node bin/agentic-security.js scan test/fixtures/vulnerable-js",
|
|
@@ -137,6 +148,9 @@
|
|
|
137
148
|
"bench:cve-replay:ci": "node ../bench/cve-replay/runner.mjs --ci",
|
|
138
149
|
"bench:cve-replay:check": "node ../bench/cve-replay/runner.mjs --check-baseline",
|
|
139
150
|
"bench:cve-replay:update-baseline": "node ../bench/cve-replay/runner.mjs --update-baseline",
|
|
151
|
+
"bench:realcode:report": "node ../bench/independent/realcode.mjs report",
|
|
152
|
+
"bench:realcode:check": "node ../bench/independent/realcode.mjs check --tier release",
|
|
153
|
+
"bench:realcode:check:quick": "node ../bench/independent/realcode.mjs check --tier quick",
|
|
140
154
|
"bench:independent:gate:check": "node ../scripts/independent-population-gate.mjs",
|
|
141
155
|
"bench:independent:update-gate-baseline": "node ../scripts/independent-population-gate.mjs --update-baseline",
|
|
142
156
|
"bench:router-replay": "node ../bench/router-replay/runner.mjs",
|
|
@@ -163,9 +177,12 @@
|
|
|
163
177
|
"bench:self-scan": "node ../bench/self-scan/measure.mjs",
|
|
164
178
|
"bench:self-scan:check": "node ../bench/self-scan/check.mjs",
|
|
165
179
|
"bench:nist-800-171-labels:check": "node ../bench/nist-800-171-labels/check.mjs",
|
|
166
|
-
"bench:mutation:check": "node ../bench/mutation/runner.mjs",
|
|
180
|
+
"bench:dci-real-app": "node ../bench/dci-real-app/runner.mjs", "bench:dci-real-app:check": "node ../bench/dci-real-app/runner.mjs --check", "ee:perf-budget": "node ../scripts/engineering-excellence/perf-budget.mjs", "ee:perf-budget:check": "node ../scripts/engineering-excellence/perf-budget.mjs --check", "ee:e2e-demo": "node ../scripts/engineering-excellence/e2e-demo.mjs", "ee:e2e-demo:check": "node ../scripts/engineering-excellence/e2e-demo.mjs --check", "ee:doc-captures": "node ../scripts/engineering-excellence/doc-captures.mjs", "ee:doc-captures:check": "node ../scripts/engineering-excellence/doc-captures.mjs --check", "ee:regression-report": "node ../scripts/engineering-excellence/regression-report.mjs", "ee:regression-report:check": "node ../scripts/engineering-excellence/regression-report.mjs --check", "ee:final-report": "node ../scripts/engineering-excellence/final-report.mjs", "ee:final-report:check": "node ../scripts/engineering-excellence/final-report.mjs --check", "bench:mutation:check": "node ../bench/mutation/runner.mjs",
|
|
167
181
|
"baseline:capture": "node ../scripts/baseline-capture.mjs",
|
|
168
182
|
"bench:protection-verdict:check": "node ../bench/protection-verdict/runner.mjs",
|
|
183
|
+
"bench:independent:forensics": "node ../bench/independent/forensics.mjs",
|
|
184
|
+
"bench:fix-verify": "node ../bench/fix-verification/runner.mjs",
|
|
185
|
+
"bench:fix-verify:check": "node ../bench/fix-verification/runner.mjs --check",
|
|
169
186
|
"bench:fix-correctness": "node ../bench/fix-correctness/runner.mjs",
|
|
170
187
|
"bench:iac-coverage": "node ../bench/iac-coverage/runner.mjs",
|
|
171
188
|
"bench:prompt-injection": "node ../bench/prompt-injection/runner.mjs",
|
|
@@ -241,7 +258,15 @@
|
|
|
241
258
|
"test:nixos-host": "node --test test/nix/nixos-host-runtime.test.js",
|
|
242
259
|
"test:language-gates": "node --test --test-concurrency=1 test/language/language-benchmark-gates.test.js",
|
|
243
260
|
"test:language-slow": "node --test --test-concurrency=1 test/language/language-doc-examples-metrics.test.js test/language/language-end-to-end.test.js test/language/language-package-ci.test.js test/language/language-scan-modes.test.js",
|
|
244
|
-
"test:chrome": "AGENTIC_SECURITY_CHROME_TESTS=1 node --test test/export-image.test.js test/server/cmd-dataflow-export.test.js"
|
|
261
|
+
"test:chrome": "AGENTIC_SECURITY_CHROME_TESTS=1 node --test test/export-image.test.js test/server/cmd-dataflow-export.test.js",
|
|
262
|
+
"ee:parse-reuse": "node ../scripts/engineering-excellence/parse-reuse-measure.mjs",
|
|
263
|
+
"ee:analysis-cache": "node ../scripts/engineering-excellence/analysis-cache-measure.mjs",
|
|
264
|
+
"ee:evidence-contracts": "node ../scripts/engineering-excellence/evidence-inventory.mjs",
|
|
265
|
+
"release:verify-package": "node ../scripts/verify-release-package.mjs",
|
|
266
|
+
"ee:completion": "node ../scripts/engineering-excellence/completion-analysis.mjs",
|
|
267
|
+
"ee:reproduce-timeouts": "node ../scripts/engineering-excellence/reproduce-timeouts.mjs",
|
|
268
|
+
"ee:provenance-measure": "node ../scripts/engineering-excellence/provenance-measure.mjs",
|
|
269
|
+
"ee:provenance-baseline": "node ../scripts/engineering-excellence/provenance-baseline.mjs"
|
|
245
270
|
},
|
|
246
271
|
"author": "Ross Young <ross@clearcapabilities.com>",
|
|
247
272
|
"license": "PolyForm-Internal-Use-1.0.0",
|
|
@@ -10,6 +10,10 @@ Deterministic Context Intelligence (the `context` command). Opt-in and additive:
|
|
|
10
10
|
- Model output cannot set severity. A citation outside the bundle invalidates the review. Severity comes from the existing confirmation tier.
|
|
11
11
|
- Store writes are atomic and refuse links and unsafe paths. Stored packages are re-checked against their hashes.
|
|
12
12
|
- A job with no verified result is missing coverage, never a clean result.
|
|
13
|
+
- Names from the repository (route strings, file names) are data: sanitised before they are printed, never placed in prompt text outside the fence.
|
|
14
|
+
- Source reaches a non-loopback endpoint only with an explicit approval, and the egress policy still applies after it.
|
|
15
|
+
- A citation is checked for range AND for being about the cited lines. `no_issue_observed` needs a control the deterministic pass also found.
|
|
16
|
+
- Orderings that reach a hashed value use `cmpCodeUnit`, never the locale.
|
|
13
17
|
- Evaluation is synthetic. Scorecards must say so and must not state an accuracy claim. Holdout use is ledgered.
|
|
14
18
|
|
|
15
19
|
## Where things are
|
|
@@ -85,15 +85,42 @@ export function loadHieMetadata(rootReal, hieDir, { fileContents }) {
|
|
|
85
85
|
return { status: 'ok', tool: doc.tool, compiler: doc.compiler, declarations, references, exportHash: hashOf({ declarations, references }) };
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
+
function uncorroboratedReferences(graph, references, fileContents) {
|
|
89
|
+
const byName = new Map();
|
|
90
|
+
for (const sym of graph.symbols) {
|
|
91
|
+
if (sym.kind !== 'function' || sym.id.includes('::<anon>@')) continue;
|
|
92
|
+
if (!byName.has(sym.name)) byName.set(sym.name, []);
|
|
93
|
+
byName.get(sym.name).push(sym);
|
|
94
|
+
}
|
|
95
|
+
const bad = [];
|
|
96
|
+
for (const r of references) {
|
|
97
|
+
const owners = byName.get(r.from) || [];
|
|
98
|
+
if (owners.length !== 1) continue; // an ambiguous or unknown name is already counted as unmatched and adds nothing
|
|
99
|
+
const sym = owners[0];
|
|
100
|
+
const text = fileContents[sym.file];
|
|
101
|
+
const callee = String(r.to).split(/[.:]/).pop();
|
|
102
|
+
const lineText = typeof text === 'string' ? text.split('\n')[r.line - 1] : undefined;
|
|
103
|
+
const inside = r.line >= sym.startLine && r.line <= sym.endLine;
|
|
104
|
+
if (!inside || lineText === undefined || !callee || !lineText.includes(callee)) bad.push(`${r.from}->${r.to}@${r.line}`);
|
|
105
|
+
}
|
|
106
|
+
return bad;
|
|
107
|
+
}
|
|
108
|
+
|
|
88
109
|
/**
|
|
89
110
|
* Merge an imported export into the static graph. Returns { graph, semantic } where `semantic` carries the provenance
|
|
90
111
|
* and the static-versus-compiler difference report.
|
|
91
112
|
*/
|
|
92
113
|
export function mergeHieIntoGraph(graph, meta, fileContents) {
|
|
93
|
-
void fileContents;
|
|
94
114
|
if (meta.status !== 'ok') {
|
|
95
115
|
return { graph, semantic: { source: ['static-ir'], status: meta.status, reason: meta.reason || null, detail: meta.detail || null, diff: null } };
|
|
96
116
|
}
|
|
117
|
+
// The export is untrusted even when its file hashes match: it is checked into the repository it describes. A reference
|
|
118
|
+
// is accepted only if the line it names, inside the declaration it names, actually mentions the callee. One that does
|
|
119
|
+
// not degrades the whole import, so a crafted export cannot add edges that the source does not contain.
|
|
120
|
+
const uncorroborated = uncorroboratedReferences(graph, meta.references, fileContents || {});
|
|
121
|
+
if (uncorroborated.length) {
|
|
122
|
+
return { graph, semantic: { source: ['static-ir'], status: 'degraded', reason: 'reference-not-in-source', detail: uncorroborated.slice(0, 5).join(', '), diff: null } };
|
|
123
|
+
}
|
|
97
124
|
const idsByName = new Map();
|
|
98
125
|
for (const s of graph.symbols) {
|
|
99
126
|
if (s.kind !== 'function' || s.id.includes('::<anon>@')) continue;
|
|
@@ -101,7 +101,8 @@ export async function adjudicate(candidates, opts = {}) {
|
|
|
101
101
|
origin: 'context-intelligence', questionPack: c.questionPack, witnesses: c.witnesses, witnessCount: c.witnesses.length,
|
|
102
102
|
sourceSpanEvidence: c.sourceSpanEvidence, scopeIntegrity: c.scopeIntegrity, reviewStatus: c.reviewStatus,
|
|
103
103
|
uncertainties: c.uncertainties, recommendedValidation: c.recommendedValidation,
|
|
104
|
-
|
|
104
|
+
...(c.deterministic ? { deterministic: c.deterministic } : {}),
|
|
105
|
+
note: c.deterministic ? 'Derived from the code analysis and a deterministic counterexample attempt; no model produced this. Severity comes from the confirmation tier only.' : 'A model hypothesis checked for citations, not a proof. Severity comes from the confirmation tier only.',
|
|
105
106
|
},
|
|
106
107
|
};
|
|
107
108
|
};
|
|
@@ -14,6 +14,31 @@ export function sha256(text) {
|
|
|
14
14
|
return crypto.createHash('sha256').update(text).digest('hex');
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Code-unit order, independent of the host locale. `localeCompare` orders by the process locale, so the same checkout
|
|
19
|
+
* could be sorted differently on two machines; every ordering that reaches a hashed value goes through this instead.
|
|
20
|
+
*/
|
|
21
|
+
export function cmpCodeUnit(a, b) {
|
|
22
|
+
const x = String(a); const y = String(b);
|
|
23
|
+
return x < y ? -1 : x > y ? 1 : 0;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Control characters (C0, DEL, C1), the bidirectional override/isolate marks and the line/paragraph separators. A
|
|
27
|
+
// repository controls the text of its own route strings and file names, so anything printed to a terminal or placed
|
|
28
|
+
// in a prompt outside the untrusted fence must not be able to carry an escape sequence or reorder what a reader sees.
|
|
29
|
+
const UNSAFE_DISPLAY = /[\u0000-\u0008\u000b-\u001f\u007f-\u009f\u061c\u200e\u200f\u2028\u2029\u202a-\u202e\u2066-\u2069]/g;
|
|
30
|
+
|
|
31
|
+
/** Make text safe to print: unsafe characters become '?'. Newline and tab are kept; use `safeLabel` for one-line values. */
|
|
32
|
+
export function cleanForTerminal(text) {
|
|
33
|
+
return String(text).replace(UNSAFE_DISPLAY, '?');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** A one-line, bounded, printable form of a name taken from source (a route path, a file name). */
|
|
37
|
+
export function safeLabel(text, max = 120) {
|
|
38
|
+
const t = String(text).replace(/[\n\r\t]+/g, ' ').replace(UNSAFE_DISPLAY, '?').replace(/ {2,}/g, ' ').trim();
|
|
39
|
+
return t.length > max ? `${t.slice(0, max - 3)}...` : t;
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
/** CRLF/CR -> LF so a Windows checkout and a Unix one produce the same spans and hashes. */
|
|
18
43
|
export function normalizeText(text) {
|
|
19
44
|
return String(text).replace(/\r\n?/g, '\n');
|
|
@@ -16,7 +16,8 @@ const BUILD = [
|
|
|
16
16
|
{ language: 'javascript/typescript', kind: 'http', frameworks: 'Express-style call registration (get/post/put/patch/delete/all, router.use, app.use)', status: 'verified', evidence: [T('build-js.test.js')], limits: 'Router mounts in another file and middleware factories are not followed (reported as router-mount-not-resolved); destructured and named imports are followed, re-exports and tsconfig path aliases are not.' },
|
|
17
17
|
{ language: 'python', kind: 'http', frameworks: 'decorator registration (Flask/FastAPI-style @app.route / @router.get), FastAPI Depends()/Security() guards', status: 'experimental', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Decorator guards are read from the decorator lines and Depends() guards from the signature (only dependencies named like an auth or user provider bind the principal); framework-level before_request hooks and Django URL conf are not modelled.' },
|
|
18
18
|
{ language: 'haskell', kind: 'http', frameworks: 'Scotty, WAI, Servant, Yesod (through the existing web model)', status: 'verified', evidence: [T('build-python-haskell.test.js'), T('frameworks.test.js')], limits: 'Static source analysis; the existing web model decides what a route is. Dynamic routes are reported unresolved.' },
|
|
19
|
-
{ language: '
|
|
19
|
+
{ language: 'php', kind: 'http', frameworks: 'Laravel Route:: registration (the entry point only)', status: 'experimental', evidence: ['test/engineering-excellence/e4-01-dci-audit.test.js'], limits: 'Measured by the E4-01 audit on one fixture: the routes resolve, but no control or sensitive operation was extracted from the handlers.' },
|
|
20
|
+
{ language: 'java/kotlin/c#/go/ruby/rust', kind: 'http', frameworks: 'whatever the engine route scan recognises', status: 'unsupported', evidence: [], limits: 'Measured by the E4-01 audit (docs/engineering-excellence/DCI_MATURITY.md) on one fixture per language (Spring, net/http, ASP.NET, Rails): no entry point was found, so no bundle is built.' },
|
|
20
21
|
{ language: 'any with a recognised surface', kind: 'webhook/queue/cron/upload/cli', frameworks: 'existing entry-point inventory', status: 'experimental', evidence: [T('build-js.test.js')], limits: 'Handler is the enclosing declaration of the surface; no framework wiring beyond that.' },
|
|
21
22
|
{ language: 'haskell', kind: 'compiler metadata import', frameworks: 'hie-export/1 JSON export of GHC declarations and references', status: 'experimental', evidence: [T('hie-adapter.test.js')], limits: 'Reads an export; never runs GHC or hiedb. The export is validated against the source snapshot. No export produced by a real GHC run is checked in.' },
|
|
22
23
|
{ language: 'any', kind: 'compiler metadata build', frameworks: 'automatic metadata generation', status: 'blocked', evidence: [], limits: 'Running a compiler on untrusted source needs a sandbox proof this build does not have (Template Haskell, custom Setup and plugins run code).' },
|
|
@@ -16,6 +16,7 @@ import { readStoreJson, writeStoreJson, readManifest, readPackageText, verifySto
|
|
|
16
16
|
import { diffManifestSets, cacheReport } from './diff.js';
|
|
17
17
|
import { readGitSnapshot, readSnapshot } from './files.js';
|
|
18
18
|
import { QUESTION_PACKS, packById } from './prompts/packs.js';
|
|
19
|
+
import { cleanForTerminal } from './canonical.js';
|
|
19
20
|
import { describeModel, resolveInvoker, buildReviewPrompt, redactPackageText, checkEgressForFiles } from './review.js';
|
|
20
21
|
import { planJobs, runQueue, exportBatch, importBatch, HARD_LIMITS } from './queue.js';
|
|
21
22
|
import { reviewsToCandidates, adjudicate } from './candidates.js';
|
|
@@ -26,21 +27,24 @@ const flag = (a, k) => (a.flags[k] === undefined ? undefined : a.flags[k]);
|
|
|
26
27
|
const truthy = (v) => v === true || (typeof v === 'string' && v !== 'false' && v !== '0');
|
|
27
28
|
const num = (v) => (v === undefined || v === true ? undefined : Number(v));
|
|
28
29
|
|
|
30
|
+
/** Everything this command prints goes through here: repository-supplied names must not carry escape sequences to a terminal. */
|
|
31
|
+
function say(text) { console.log(cleanForTerminal(text)); }
|
|
32
|
+
|
|
29
33
|
function out(format, text, obj) {
|
|
30
|
-
if (format === 'json')
|
|
31
|
-
else if (format === 'jsonl')
|
|
32
|
-
else
|
|
34
|
+
if (format === 'json') say(JSON.stringify(obj, null, 2));
|
|
35
|
+
else if (format === 'jsonl') say(JSON.stringify(obj));
|
|
36
|
+
else say(text);
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
function usage() {
|
|
36
|
-
|
|
40
|
+
say([
|
|
37
41
|
'Usage: agentic-security context <subcommand> [--root <dir>] [--format text|json|jsonl]',
|
|
38
42
|
'',
|
|
39
43
|
' build Resolve entry points and write one inspectable context bundle each (no model, no network)',
|
|
40
44
|
' [--entrypoints http,webhook,queue,cron,upload,cli] [--profile compact|standard|extended] [--max-tokens N] [--route <label|id>] [--no-write]',
|
|
41
45
|
' inspect Show one bundle: --route <label|id> | --hash <contextHash> [--show-context] [--package N]',
|
|
42
46
|
' diff Routes added/removed/changed between two refs: --from <ref> [--to <ref>] (omit --to for the working tree)',
|
|
43
|
-
' review Focused one-call reviews over frozen bundles: --question <pack[,pack]> [--route x] [--local-only] [--concurrency N]',
|
|
47
|
+
' review Focused one-call reviews over frozen bundles: --question <pack[,pack]> [--route x] [--local-only] [--allow-remote] [--concurrency N]',
|
|
44
48
|
' [--max-calls N] [--max-wall-seconds N] [--max-cost-usd N --cost-per-call-usd N] [--retry-failed] [--refute] [--strict]',
|
|
45
49
|
' [--dry-run] [--export-batch] [--import-batch <results.jsonl> --batch-id <id>]',
|
|
46
50
|
' status Coverage so far: entry points, bundles, review jobs',
|
|
@@ -62,7 +66,7 @@ async function doBuild(root, args, { write = true } = {}) {
|
|
|
62
66
|
if (flag(args, 'max-tokens')) config.context.maxTokens = num(flag(args, 'max-tokens'));
|
|
63
67
|
const events = [];
|
|
64
68
|
const jsonl = flag(args, 'format') === 'jsonl';
|
|
65
|
-
const built = await buildContext(root, { config, route: entrypointFilter(flag(args, 'route')), onEvent: (e) => { events.push(e); if (jsonl)
|
|
69
|
+
const built = await buildContext(root, { config, route: entrypointFilter(flag(args, 'route')), onEvent: (e) => { events.push(e); if (jsonl) say(JSON.stringify(e)); } });
|
|
66
70
|
if (!built.ok) return { ok: false, built, config, problems };
|
|
67
71
|
let persisted = null;
|
|
68
72
|
if (write) persisted = persistBuild(root, built, config);
|
|
@@ -155,7 +159,7 @@ async function cmdInspect(root, args, format) {
|
|
|
155
159
|
` boundaries (calls into libraries, not first-party code): ${m.boundaries.length}`,
|
|
156
160
|
];
|
|
157
161
|
if (text) lines.push('', text.ok ? text.text : `(context text unavailable: ${text.code})`);
|
|
158
|
-
|
|
162
|
+
say(lines.join('\n'));
|
|
159
163
|
return 0;
|
|
160
164
|
}
|
|
161
165
|
|
|
@@ -255,7 +259,7 @@ async function cmdExport(root, args, format) {
|
|
|
255
259
|
if (!h || h === true) { out(format, 'context export: --hash <contextHash> is required', { ok: false }); return 1; }
|
|
256
260
|
const r = exportBundle(root, String(h));
|
|
257
261
|
if (!r.ok) { out(format, `context export: refused (${r.problems.join(', ')})`, { ok: false, problems: r.problems }); return 1; }
|
|
258
|
-
|
|
262
|
+
say(JSON.stringify(r.export, null, 2));
|
|
259
263
|
return 0;
|
|
260
264
|
}
|
|
261
265
|
|
|
@@ -303,7 +307,7 @@ async function cmdReview(root, args, format) {
|
|
|
303
307
|
const r = await doBuild(root, args, { write: true });
|
|
304
308
|
if (!r.ok) { out(format, `context review: build failed (${r.built.reason})`, { ok: false, reason: r.built.reason }); return r.built.reason === 'too-many-files' ? 2 : 1; }
|
|
305
309
|
const { built } = r;
|
|
306
|
-
const model = describeModel();
|
|
310
|
+
const model = describeModel({ allowRemote: truthy(flag(args, 'allow-remote')) || r.config.review.allowRemote === true });
|
|
307
311
|
const { jobs, droppedByJobLimit } = planJobs({ bundles: built.bundles, packs, model, maxJobs: Math.min(HARD_LIMITS.maxJobs, num(flag(args, 'max-jobs')) || HARD_LIMITS.maxJobs) });
|
|
308
312
|
const loadJob = loadJobFrom(built, packs);
|
|
309
313
|
const ctxBase = { root: built.root, fileContents: built.fileContents, model };
|
|
@@ -341,12 +345,14 @@ async function cmdReview(root, args, format) {
|
|
|
341
345
|
try {
|
|
342
346
|
result = await runQueue(jobs, {
|
|
343
347
|
...ctxBase, invoker, loadJob, signal: ac.signal, retryFailed: truthy(flag(args, 'retry-failed')),
|
|
344
|
-
onEvent: jsonl ? (e) =>
|
|
348
|
+
onEvent: jsonl ? (e) => say(JSON.stringify(e)) : undefined,
|
|
345
349
|
limits: {
|
|
346
350
|
concurrency: num(flag(args, 'concurrency')) || config.review.concurrency,
|
|
347
351
|
maxCalls: num(flag(args, 'max-calls')) || config.review.maxCalls,
|
|
348
352
|
maxWallSeconds: num(flag(args, 'max-wall-seconds')) || config.review.maxWallSeconds,
|
|
349
353
|
maxTokensPerJob: config.review.maxTokensPerJob,
|
|
354
|
+
maxTokensPerRepo: config.review.maxTokensPerRepo,
|
|
355
|
+
anonymizePrompts: config.review.anonymizePrompts,
|
|
350
356
|
maxRetries: config.review.maxRetries,
|
|
351
357
|
jobTimeoutMs: (num(flag(args, 'job-timeout-seconds')) || 120) * 1000,
|
|
352
358
|
maxCostUsd: num(flag(args, 'max-cost-usd')) ?? config.review.maxCostUsd,
|
|
@@ -22,7 +22,7 @@ export const DEFAULT_CONFIG = Object.freeze({
|
|
|
22
22
|
maxFiles: 3000,
|
|
23
23
|
semanticMetadata: { mode: 'off', haskellHieDir: null },
|
|
24
24
|
context: { profile: 'standard', maxTokens: null, pruneLowValue: true, maxSymbols: 60, maxDepth: 12 },
|
|
25
|
-
review: { enabled: false, questionPacks: ['authz-idor'], concurrency: 2, maxCalls: 100, maxWallSeconds: 900, maxCostUsd: null, maxTokensPerJob: 40000, maxRetries: 3 },
|
|
25
|
+
review: { enabled: false, allowRemote: false, questionPacks: ['authz-idor'], concurrency: 2, maxCalls: 100, maxWallSeconds: 900, maxCostUsd: null, maxTokensPerJob: 40000, maxRetries: 3, maxTokensPerRepo: null, anonymizePrompts: false },
|
|
26
26
|
replay: { contentAddressed: true, retention: 'inherit' },
|
|
27
27
|
});
|
|
28
28
|
|
|
@@ -63,6 +63,7 @@ export function normalizeConfig(raw) {
|
|
|
63
63
|
},
|
|
64
64
|
review: {
|
|
65
65
|
enabled: rv.enabled === true,
|
|
66
|
+
allowRemote: rv.allowRemote === true,
|
|
66
67
|
questionPacks: Array.isArray(rv.questionPacks) && rv.questionPacks.length ? rv.questionPacks.filter(x => typeof x === 'string') : d.review.questionPacks,
|
|
67
68
|
concurrency: clamp(rv.concurrency, 1, 16, d.review.concurrency),
|
|
68
69
|
maxCalls: clamp(rv.maxCalls, 1, 5000, d.review.maxCalls),
|
|
@@ -70,6 +71,8 @@ export function normalizeConfig(raw) {
|
|
|
70
71
|
maxCostUsd: Number.isFinite(rv.maxCostUsd) && rv.maxCostUsd >= 0 ? rv.maxCostUsd : null,
|
|
71
72
|
maxTokensPerJob: clamp(rv.maxTokensPerJob, 1000, 400000, d.review.maxTokensPerJob),
|
|
72
73
|
maxRetries: clamp(rv.maxRetries, 0, 3, d.review.maxRetries),
|
|
74
|
+
maxTokensPerRepo: Number.isFinite(rv.maxTokensPerRepo) && rv.maxTokensPerRepo >= 1000 ? Math.trunc(rv.maxTokensPerRepo) : null,
|
|
75
|
+
anonymizePrompts: rv.anonymizePrompts === true,
|
|
73
76
|
},
|
|
74
77
|
replay: { contentAddressed: rp.contentAddressed !== false, retention: typeof rp.retention === 'string' ? rp.retention : 'inherit' },
|
|
75
78
|
};
|
|
@@ -47,7 +47,46 @@ export function signatureText(lines, declLine, maxLines = 12) {
|
|
|
47
47
|
for (let i = declLine - 1; i < Math.min(lines.length, declLine - 1 + maxLines); i++) { t += ' ' + lines[i]; if (/\)\s*(?:->[^:]*)?:\s*(?:#.*)?$/.test(lines[i])) break; }
|
|
48
48
|
return t;
|
|
49
49
|
}
|
|
50
|
-
|
|
50
|
+
// An owner/tenant FIELD NAME, not any word that merely contains one: `ownerId`, `owner_id`, `tenant`, `getOwnerId`
|
|
51
|
+
// qualify; `owner_name` and `cols` do not (the trailing boundary rejects them).
|
|
52
|
+
const OWNER_KEY = '\\w*?(?:[Oo]wner|[Tt]enant|[Oo]rg(?:ani[sz]ation)?|[Aa]ccount|[Cc]reated_?[Bb]y|[Aa]uthor|[Tt]eam|[Ww]orkspace)(?:_?(?:[Ii]d|ID))?\\b|[Uu]ser_?(?:[Ii]d|ID)\\b';
|
|
53
|
+
const OWNER_KEY_RE = new RegExp(`(?<![\\w])(?:${OWNER_KEY})`);
|
|
54
|
+
const TENANT_WORD = /tenant|org|workspace|team|account/i;
|
|
55
|
+
// a SQL string comparing an owner column with a bound parameter: `owner_id = ?`, `owner_id = $1`, `tenant_id = :t`
|
|
56
|
+
const SQL_OWNER_PLACEHOLDER = new RegExp(`(?<![\\w])(?:${OWNER_KEY})\\s*(?:=|\\bIN\\b|\\bin\\b)\\s*\\(?\\s*(?:\\?|\\$\\d+|:\\w+|%s|@\\w+)`);
|
|
57
|
+
// an owner column compared with the principal written straight into the string: `owner_id = ${req.user.id}`
|
|
58
|
+
const SQL_OWNER_INTERPOLATED = new RegExp(`(?<![\\w])(?:${OWNER_KEY})\\s*=\\s*(?:'?\\s*\\+\\s*|\\$\\{\\s*)(?:${QUALIFIED_PRINCIPAL.source}|${BARE_PRINCIPAL.source})`);
|
|
59
|
+
const PRINCIPAL_GLOBAL = new RegExp(QUALIFIED_PRINCIPAL.source, 'g');
|
|
60
|
+
const STRING_LITERAL = /(["'`])(?:\\.|(?!\1).)*\1/g;
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Is an owner/tenant FIELD actually tied to the principal in this text? The field and the principal must sit in the
|
|
64
|
+
* same clause (split on , ; && || braces), so `{ audit: req.user.id, cols: 'owner_name' }` is a decoy and
|
|
65
|
+
* `{ ownerId: req.user.id }` is a filter. A SQL string with an owner column against a bound placeholder counts when the
|
|
66
|
+
* principal is passed in the same call. Returns null or { level: 'qualified'|'bare', tenant }.
|
|
67
|
+
*/
|
|
68
|
+
export function ownerBinding(text, extra = null) {
|
|
69
|
+
let best = null;
|
|
70
|
+
const take = (level, tenant) => { if (!best || (level === 'qualified' && best.level !== 'qualified')) best = { level, tenant }; };
|
|
71
|
+
const code = text.replace(STRING_LITERAL, (m) => (SQL_OWNER_PLACEHOLDER.test(m) || SQL_OWNER_INTERPOLATED.test(m) ? m : '""'));
|
|
72
|
+
for (const clause of code.split(/[,;{}]|&&|\|\|/)) {
|
|
73
|
+
// the principal's own name (`req.user`, `current_user`) is not an owner field, so it is removed before looking for one
|
|
74
|
+
const forKey = clause.replace(STRING_LITERAL, ' ').replace(PRINCIPAL_GLOBAL, ' ');
|
|
75
|
+
if (!OWNER_KEY_RE.test(forKey)) continue;
|
|
76
|
+
const pr = principalIn(clause, extra);
|
|
77
|
+
if (pr) take(pr, TENANT_WORD.test(clause));
|
|
78
|
+
}
|
|
79
|
+
for (const m of text.match(STRING_LITERAL) || []) {
|
|
80
|
+
const placeholder = SQL_OWNER_PLACEHOLDER.test(m);
|
|
81
|
+
if (placeholder) {
|
|
82
|
+
const pr = principalIn(text, extra);
|
|
83
|
+
if (pr) take(pr, TENANT_WORD.test(m));
|
|
84
|
+
}
|
|
85
|
+
if (SQL_OWNER_INTERPOLATED.test(m)) take('qualified', TENANT_WORD.test(m));
|
|
86
|
+
}
|
|
87
|
+
return best;
|
|
88
|
+
}
|
|
89
|
+
|
|
51
90
|
const COMPARE = /(?:===|!==|==|!=|\.equals\(|\bis not\b|\bis\b|\bnot in\b|\bin\b|\.includes\(|\.has\()/;
|
|
52
91
|
const REJECT = /(?:\breturn\b|\bthrow\b|\braise\b|\babort\b|\bexit\b|\b40[0-9]\b|\bforbidden\b|\bunauthori[sz]ed\b|\bHttpError\b|\breject\b|\bnot_?allowed\b|\bdeny\b|\bdenied\b|throwError|\bfinish\b|\bhalt\b|\bPermissionDenied\b|\bAccessDenied\b|status\()/i;
|
|
53
92
|
|
|
@@ -58,6 +97,13 @@ const DB_RECEIVER = /(?:^|\b)(?:db|database|conn|connection|pool|client|cursor|s
|
|
|
58
97
|
const SQL_TEXT = /\b(?:select\s[\s\S]{1,120}\sfrom|insert\s+into|update\s+\w+\s+set|delete\s+from)\b/i;
|
|
59
98
|
const HS_SENSITIVE = /(?:^|\.)(?:runDB|selectList|selectFirst|getBy|getJust|insert_?|insertEntity|update|updateWhere|delete|deleteWhere|replace|upsert|query|query_|execute|execute_|readFile|writeFile|callProcess|readProcess)$|^(?:Database\.|System\.Process\.|System\.Directory\.)/;
|
|
60
99
|
|
|
100
|
+
// Names that answer a question rather than enforce a rule.
|
|
101
|
+
const PREDICATE_NAME = /^(?:is|has|can|cannot|allow|owned|belongs)/i;
|
|
102
|
+
function bareCallStatement(trimmedLine, name) {
|
|
103
|
+
const esc = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
104
|
+
return new RegExp(`^(?:await\\s+)?${esc}\\s*\\(`).test(trimmedLine) && /\)\s*;?\s*(?:\/\/.*|\/\*.*)?$/.test(trimmedLine);
|
|
105
|
+
}
|
|
106
|
+
|
|
61
107
|
const WRITE_VERB = /(?:insert|update|upsert|delete|remove|destroy|save|create|write|append|unlink|rm|exec|spawn|system|popen|post|put|patch|replace)/i;
|
|
62
108
|
|
|
63
109
|
function kindOfAuthName(name) {
|
|
@@ -93,6 +139,51 @@ function clip(s) { return s.trim().slice(0, 160); }
|
|
|
93
139
|
* lines the FILE's lines (0-based array)
|
|
94
140
|
* Returns [{ symbolId, line, category:'control'|'sensitive', kind, subkind, evidence, basis, rejectionSeen, text }]
|
|
95
141
|
*/
|
|
142
|
+
/**
|
|
143
|
+
* Blank the comments (and Python docstrings) of a source file, keeping every character position and newline, so line numbers do not move.
|
|
144
|
+
* The analysis looks for calls and comparisons in code; a comment that SAYS `ownerId: req.user.id` or `requireUser(req)` is prose, and prose is the
|
|
145
|
+
* easiest thing for a hostile or merely careless author to write. Strings are left alone (the principal and SQL rules already read around them).
|
|
146
|
+
*/
|
|
147
|
+
export function blankComments(text, file = '') {
|
|
148
|
+
const src = String(text || '');
|
|
149
|
+
const hash = /\.(?:py|rb|sh|php|pl|r)$/i.test(file);
|
|
150
|
+
const dashes = /\.l?hs$/i.test(file);
|
|
151
|
+
const cLike = !dashes && !/\.(?:py|rb|sh|pl|r)$/i.test(file);
|
|
152
|
+
const out = src.split('');
|
|
153
|
+
const n = src.length;
|
|
154
|
+
let i = 0;
|
|
155
|
+
const blank = (a, b) => { for (let k = a; k < b && k < n; k++) if (out[k] !== '\n') out[k] = ' '; };
|
|
156
|
+
while (i < n) {
|
|
157
|
+
const c = src[i]; const d = src[i + 1];
|
|
158
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
159
|
+
if (/\.py$/i.test(file) && src.startsWith(c.repeat(3), i)) {
|
|
160
|
+
const end = src.indexOf(c.repeat(3), i + 3); const stop = end < 0 ? n : end + 3;
|
|
161
|
+
blank(i, stop); i = stop; continue; // a docstring or triple-quoted block is prose
|
|
162
|
+
}
|
|
163
|
+
let j = i + 1;
|
|
164
|
+
while (j < n && src[j] !== c && !(src[j] === '\n' && c !== '`')) { if (src[j] === '\\') j++; j++; }
|
|
165
|
+
i = j + 1; continue;
|
|
166
|
+
}
|
|
167
|
+
if (cLike && c === '/' && d === '*') { const end = src.indexOf('*/', i + 2); const stop = end < 0 ? n : end + 2; blank(i, stop); i = stop; continue; }
|
|
168
|
+
if (cLike && c === '/' && d === '/' && (i === 0 || /[\s;,)}{\]]/.test(src[i - 1]))) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
|
|
169
|
+
if (hash && c === '#' && !(file.endsWith('.php') && d === '[')) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
|
|
170
|
+
if (dashes && c === '-' && d === '-' && !/[!#$%&*+./<=>?@\\^|~:]/.test(src[i + 2] || ' ')) { let j = src.indexOf('\n', i); if (j < 0) j = n; blank(i, j); i = j; continue; }
|
|
171
|
+
if (dashes && c === '{' && d === '-') { const end = src.indexOf('-}', i + 2); const stop = end < 0 ? n : end + 2; blank(i, stop); i = stop; continue; }
|
|
172
|
+
i++;
|
|
173
|
+
}
|
|
174
|
+
return out.join('');
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function bodyAfterSignature(raw) {
|
|
178
|
+
const arrow = raw.indexOf('=>');
|
|
179
|
+
let rest = null;
|
|
180
|
+
if (arrow >= 0) rest = raw.slice(arrow + 2);
|
|
181
|
+
else { const m = /\)\s*\{(.*)$/.exec(raw); if (m) rest = m[1]; }
|
|
182
|
+
if (rest === null) return '';
|
|
183
|
+
const brace = /^\s*\{(.*)$/.exec(rest);
|
|
184
|
+
return brace ? brace[1] : rest;
|
|
185
|
+
}
|
|
186
|
+
|
|
96
187
|
export function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
|
|
97
188
|
const out = [];
|
|
98
189
|
const seen = new Set();
|
|
@@ -125,15 +216,22 @@ export function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
|
|
|
125
216
|
const calls = [];
|
|
126
217
|
const re = /([A-Za-z_$][\w$]*(?:(?:\.|::)[A-Za-z_$][\w$]*)*)\s*\(/g;
|
|
127
218
|
let m;
|
|
128
|
-
|
|
219
|
+
// A one-line handler (`app.get('/x', guard, async (req, res) => { res.json(await db.docs.findOne({ id })); })`) is its own declaration
|
|
220
|
+
// line, so skipping the whole line also skipped its body. Only the SIGNATURE is skipped: the calls after the arrow or the opening brace count.
|
|
221
|
+
const scanText = isDeclLine ? bodyAfterSignature(raw) : raw;
|
|
222
|
+
while ((m = re.exec(scanText))) calls.push(m[1]);
|
|
129
223
|
if (haskell && !isDeclLine) {
|
|
130
224
|
const hre = /\b([A-Z][\w.]*\.[a-z][\w']*|[a-z][\w']*)\b/g;
|
|
131
225
|
while ((m = hre.exec(raw))) if (AUTH_NAME.test(m[1]) || HS_SENSITIVE.test(m[1])) calls.push(m[1]);
|
|
132
226
|
}
|
|
133
227
|
for (const name of calls) {
|
|
134
228
|
if (AUTH_NAME.test(name) && !/^(?:if|for|while|switch|catch|function)$/.test(name)) {
|
|
135
|
-
const
|
|
136
|
-
|
|
229
|
+
const lastSeg = name.split(/[.:]/).pop();
|
|
230
|
+
// A predicate (isOwner, hasRole, can) only returns an answer. Called as a bare statement its answer is thrown away,
|
|
231
|
+
// so it guards nothing, whatever the lines around it say.
|
|
232
|
+
const discarded = !haskell && PREDICATE_NAME.test(lastSeg) && bareCallStatement(t, name);
|
|
233
|
+
const rej = !discarded && (REJECT.test(lines.slice(ln - 1, ln + 3).join(' ')) || /^(?:require|ensure|enforce|assert|authori[sz]e|authenticate|protect)/i.test(lastSeg));
|
|
234
|
+
push({ line: ln, category: 'control', kind: kindOfAuthName(name), subkind: null, evidence: 'inferred', basis: discarded ? 'call-name-result-discarded' : 'call-name', rejectionSeen: rej, text: clip(raw) });
|
|
137
235
|
}
|
|
138
236
|
const last = name.split(/[.:]/).pop();
|
|
139
237
|
const recv = name.includes('.') ? name.slice(0, name.lastIndexOf('.')) : '';
|
|
@@ -149,9 +247,9 @@ export function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
|
|
|
149
247
|
push({ line: ln, category: 'sensitive', kind: sub, subkind: verb, evidence: 'observed', basis: 'call-name', rejectionSeen: null, text: clip(raw) });
|
|
150
248
|
// a data operation whose own statement mentions the principal and an owner/tenant field is scoped by it
|
|
151
249
|
const stmt = statementAt(lines, ln);
|
|
152
|
-
const pr = sub.startsWith('data-')
|
|
250
|
+
const pr = sub.startsWith('data-') ? ownerBinding(stmt, extraPrincipal) : null;
|
|
153
251
|
if (pr) {
|
|
154
|
-
push({ line: ln, category: 'control', kind:
|
|
252
|
+
push({ line: ln, category: 'control', kind: pr.tenant ? 'tenant-scope' : 'ownership', subkind: null, evidence: pr.level === 'qualified' ? 'observed' : 'inferred', basis: pr.level === 'qualified' ? 'query-filter' : 'query-filter-bare-identifier', rejectionSeen: null, text: clip(raw) });
|
|
155
253
|
}
|
|
156
254
|
}
|
|
157
255
|
}
|
|
@@ -159,17 +257,17 @@ export function analyzeSymbolText(symbol, lines, { haskell = false } = {}) {
|
|
|
159
257
|
const verb = /\b(?:insert|update|delete)\b/i.test(raw) ? 'write' : 'read';
|
|
160
258
|
push({ line: ln, category: 'sensitive', kind: `data-${verb}`, subkind: verb, evidence: 'observed', basis: 'sql-text', rejectionSeen: null, text: clip(raw) });
|
|
161
259
|
const stmt = statementAt(lines, ln);
|
|
162
|
-
const pr =
|
|
260
|
+
const pr = ownerBinding(stmt, extraPrincipal);
|
|
163
261
|
if (pr) {
|
|
164
|
-
push({ line: ln, category: 'control', kind:
|
|
262
|
+
push({ line: ln, category: 'control', kind: pr.tenant ? 'tenant-scope' : 'ownership', subkind: null, evidence: pr.level === 'qualified' ? 'observed' : 'inferred', basis: pr.level === 'qualified' ? 'query-filter' : 'query-filter-bare-identifier', rejectionSeen: null, text: clip(raw) });
|
|
165
263
|
}
|
|
166
264
|
}
|
|
167
265
|
|
|
168
266
|
// a resource field compared with the principal: the ownership check itself
|
|
169
|
-
const cmpPr = COMPARE.test(raw) &&
|
|
267
|
+
const cmpPr = COMPARE.test(raw) && /\b(?:if|unless|assert|when|guard|require|expect|where|elif|&&|\|\|)\b|\?/.test(raw) ? ownerBinding(raw, extraPrincipal) : null;
|
|
170
268
|
if (cmpPr) {
|
|
171
269
|
const rej = REJECT.test(lines.slice(ln - 1, ln + 3).join(' '));
|
|
172
|
-
push({ line: ln, category: 'control', kind:
|
|
270
|
+
push({ line: ln, category: 'control', kind: cmpPr.tenant ? 'tenant-scope' : 'ownership', subkind: null, evidence: cmpPr.level === 'qualified' ? 'observed' : 'inferred', basis: cmpPr.level === 'qualified' ? 'comparison' : 'comparison-bare-identifier', rejectionSeen: rej, text: clip(raw) });
|
|
173
271
|
}
|
|
174
272
|
}
|
|
175
273
|
// A function whose NAME says it is a guard and whose BODY shows a rejection path is a guard function. This is
|