@clear-capabilities/agentic-security-scanner 0.153.1 → 0.154.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 +67 -0
- package/bin/agentic-security.js +123 -19
- package/dist/1444.index.js +64 -19
- package/dist/1905.index.js +46 -3
- package/dist/2862.index.js +325 -0
- package/dist/3499.index.js +5 -1
- package/dist/3531.index.js +261 -0
- package/dist/3596.index.js +103 -0
- package/dist/4113.index.js +15 -3
- package/dist/4171.index.js +319 -0
- package/dist/5637.index.js +10 -4
- package/dist/6359.index.js +433 -0
- package/dist/7838.index.js +15 -3
- package/dist/8551.index.js +1164 -0
- package/dist/8587.index.js +410 -0
- package/dist/8714.index.js +98 -0
- package/dist/8798.index.js +218 -0
- package/dist/9220.index.js +35 -3
- package/dist/9560.index.js +26 -3
- package/dist/agentic-security.mjs +51 -51
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +18 -3
- package/src/dataflow/CLAUDE.md +9 -0
- package/src/dataflow/catalog-haskell.js +37 -0
- package/src/dataflow/catalog.js +4 -0
- package/src/dataflow/engine.js +33 -3
- package/src/dataflow/privacy-catalog-haskell.js +50 -0
- package/src/dataflow/privacy-catalog.js +5 -1
- package/src/discovery/CLAUDE.md +6 -0
- package/src/discovery/index.js +12 -3
- package/src/discovery/lenses.js +4 -1
- package/src/discovery/partition.js +28 -0
- package/src/egress/redact.js +8 -0
- package/src/engine.js +140 -19
- package/src/ir/index.js +39 -2
- package/src/language/CLAUDE.md +74 -0
- package/src/language/accuracy.js +90 -0
- package/src/language/aibom.js +307 -0
- package/src/language/assurance.js +193 -0
- package/src/language/bom-validate.js +227 -0
- package/src/language/bom.js +236 -0
- package/src/language/bridges.js +392 -0
- package/src/language/compliance-map.js +61 -0
- package/src/language/context.js +199 -0
- package/src/language/contracts.js +339 -0
- package/src/language/discovery.js +359 -0
- package/src/language/engine-pass.js +120 -0
- package/src/language/fix-lifecycle.js +120 -0
- package/src/language/haskell-adapter.js +51 -0
- package/src/language/haskell-disclosure.js +170 -0
- package/src/language/haskell-findings.js +44 -0
- package/src/language/haskell-fix.js +409 -0
- package/src/language/haskell-grammar.js +78 -0
- package/src/language/haskell-guards.js +382 -0
- package/src/language/haskell-ir.js +1377 -0
- package/src/language/haskell-llm.js +128 -0
- package/src/language/haskell-manifests.js +1290 -0
- package/src/language/haskell-models.js +278 -0
- package/src/language/haskell-parser.js +1073 -0
- package/src/language/haskell-resolved-graph.js +414 -0
- package/src/language/haskell-sca.js +387 -0
- package/src/language/haskell-security-rules.js +332 -0
- package/src/language/haskell-supply.js +140 -0
- package/src/language/haskell-sweep.js +85 -0
- package/src/language/haskell-syntax.js +895 -0
- package/src/language/haskell-web.js +749 -0
- package/src/language/nix-adapter.js +42 -0
- package/src/language/nix-agents.js +155 -0
- package/src/language/nix-build-trust.js +790 -0
- package/src/language/nix-closure.js +331 -0
- package/src/language/nix-eval-isolation.js +274 -0
- package/src/language/nix-fix.js +268 -0
- package/src/language/nix-grammar.js +97 -0
- package/src/language/nix-inventory.js +379 -0
- package/src/language/nix-ir.js +642 -0
- package/src/language/nix-parser.js +908 -0
- package/src/language/nix-privacy.js +114 -0
- package/src/language/nix-sca.js +358 -0
- package/src/language/nix-script-taint.js +885 -0
- package/src/language/nix-secrets.js +396 -0
- package/src/language/nixos-hardening.js +564 -0
- package/src/language/nixos-module-resolver.js +595 -0
- package/src/language/nixos-option-catalog.js +96 -0
- package/src/language/pragma.js +150 -0
- package/src/language/resolved-pass.js +146 -0
- package/src/language/secrets.js +323 -0
- package/src/language/state-artifacts.js +78 -0
- package/src/language/support-registry.js +224 -0
- package/src/language/witness.js +185 -0
- package/src/lineage/CLAUDE.md +8 -0
- package/src/lineage/coverage.js +2 -0
- package/src/lineage/cross-repo-link.js +22 -0
- package/src/lineage/graph-builder.js +5 -1
- package/src/lineage/haskell-view.js +319 -0
- package/src/lineage/index.js +8 -1
- package/src/lineage/nix-view.js +198 -0
- package/src/lineage/sink-registry.js +10 -1
- package/src/lineage/source-registry.js +3 -0
- package/src/lineage/transform-catalog.js +12 -0
- package/src/llm-validator/index.js +16 -2
- package/src/lsp/server.js +69 -9
- package/src/mcp/CLAUDE.md +7 -0
- package/src/mcp/tools.js +26 -3
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/accuracy-scorecard.js +51 -0
- package/src/posture/aibom.js +61 -3
- package/src/posture/artifact-registry.js +12 -0
- package/src/posture/auditor-walkthrough.js +41 -0
- package/src/posture/clustering.js +2 -2
- package/src/posture/evidence-bundle.js +7 -1
- package/src/posture/execution-proof.js +15 -0
- package/src/posture/falsification.js +7 -0
- package/src/posture/family-resolve.js +7 -0
- package/src/posture/fix-history.js +40 -0
- package/src/posture/fix-verify.js +15 -3
- package/src/posture/provenance/git-evidence.js +24 -0
- package/src/posture/provenance/origin-resolver.js +14 -2
- package/src/posture/provenance/predicate-replay.js +11 -2
- package/src/posture/provenance/sca-origin.js +20 -3
- package/src/posture/sbom-diff.js +7 -6
- package/src/posture/sbom.js +85 -32
- package/src/posture/sca-policy.js +10 -1
- package/src/posture/sca-verdict.js +8 -1
- package/src/posture/secret-history.js +62 -18
- package/src/posture/stable-id.js +21 -2
- package/src/posture/state-dir.js +21 -0
- package/src/posture/threat-model-auto.js +4 -4
- package/src/posture/triage.js +18 -1
- package/src/posture/watch-mode.js +34 -3
- package/src/pr-delta.js +9 -4
- package/src/report/index.js +35 -9
- package/src/runScan.js +56 -5
- package/src/sast/code-injection-multilang.js +15 -0
- package/src/sast/go-extended.js +11 -0
- package/src/sast/pipeline.js +34 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
e460f63891e70f53f1a0d6061a5393cc92c5ade0ddde80bd8bfaf8eb00dbb849 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.154.0",
|
|
4
4
|
"description": "Scanner engine for the agentic-security Claude Code plugin — SAST, SCA (function-level reachability + CISA KEV), secrets, IaC, prompt-injection, MCP/agent-tool audit, auth/authZ deep analysis, attack chains, PoC generation, business logic, toxic-combinations scoring, SBOM, pipeline integrity, compliance attestation, and more.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"z3-solver": "^5.2.0"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
|
-
"@types/node": "^26.
|
|
55
|
+
"@types/node": "^26.6.4",
|
|
56
56
|
"@vercel/ncc": "^0.45.0",
|
|
57
57
|
"java-parser": "^3.0.1"
|
|
58
58
|
},
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
"prepare": "node scripts/prepare.mjs",
|
|
66
66
|
"prepublishOnly": "npm run build && node ../scripts/sync-scanner-changelog.mjs && node ../scripts/release-check.mjs",
|
|
67
67
|
"test": "node ../scripts/run-unit-tests.mjs && 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",
|
|
68
|
+
"loop": "node ../scripts/loop-engineering/run.mjs",
|
|
69
|
+
"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-runbook.test.js",
|
|
68
70
|
"test:python": "node ../scripts/run-python-tests.mjs",
|
|
69
71
|
"test:smoke": "node --test test/smoke.test.js test/demo-app.test.js",
|
|
70
72
|
"test:ollama-e2e": "AGENTIC_SECURITY_OLLAMA_E2E=1 node --test test/ollama-e2e.test.js",
|
|
@@ -77,6 +79,13 @@
|
|
|
77
79
|
"test:lineage": "node --test test/lineage/schema.test.js test/lineage/ids.test.js test/lineage/protection.test.js test/lineage/classification.test.js test/lineage/validate.test.js test/lineage/json-schema-parity.test.js test/lineage/flagship-fixture.test.js test/lineage/flagship-fixture-semantics.test.js test/lineage/field-identity.test.js test/lineage/engine-expr-resolver.test.js test/lineage/engine-walker.test.js test/lineage/engine-integration.test.js test/lineage/engine-receiver-identity.test.js test/lineage/summaries.test.js test/lineage/driver.test.js test/lineage/engine-provenance.test.js test/lineage/engine-provenance-interprocedural.test.js test/lineage/path-store.test.js test/lineage/path-query.test.js test/lineage/flow-grade.test.js test/lineage/source-registry.test.js test/lineage/sink-registry.test.js test/lineage/transform-catalog.test.js test/lineage/handling-analyzer.test.js test/lineage/registry-real-code.test.js test/lineage/source-seeding.test.js test/lineage/graph-builder.test.js test/lineage/coverage.test.js test/lineage/resolve-destination.test.js test/lineage/queue-detail.test.js test/lineage/index.test.js test/lineage/obligation-mapping.test.js test/lineage/obligation-predicates.test.js test/lineage/obligation-predicates-walkthrough.test.js test/lineage/transit-protection.test.js test/lineage/at-rest-protection.test.js test/lineage/protection-summary.test.js test/lineage/ac01-multi-sink.test.js test/lineage/policy-verdict.test.js test/lineage/governance-refs.test.js test/lineage/export-privacy.test.js test/lineage/export-json.test.js test/lineage/export-csv.test.js test/lineage/decision-story.test.js test/lineage/export-briefing.test.js test/lineage/graph-snapshot.test.js test/lineage/graph-diff.test.js test/lineage/drift-policy.test.js test/lineage/recipient-profile.test.js test/lineage/recipient-registry.test.js test/lineage/recipient-wiring.test.js test/lineage/redact-graph.test.js test/lineage-scan-wiring.test.js test/lineage-artifact-write.test.js test/bench-data-lineage-runner.test.js test/bundle-frontend.test.js test/bundle-frontend-golden.test.js test/generate-html-report.test.js test/export-image.test.js test/lineage/scenario.test.js test/lineage/scenario-engine.test.js test/lineage/scenario-diff.test.js test/lineage/scenario-no-obligation-wiring.test.js test/lineage/impact-assessment.test.js test/lineage/impact-engine.test.js test/lineage/impact-no-obligation-wiring.test.js test/lineage/governance-edit.test.js test/lineage/remediation.test.js test/lineage/runtime-observation.test.js test/lineage/observation-correlation.test.js test/lineage/observation-adapters.test.js test/lineage/observation-store.test.js test/lineage/runtime-corroboration-wiring.test.js test/lineage/cross-repo-link.test.js test/lineage/federation-loader.test.js test/lineage/cross-repo-link-wiring.test.js",
|
|
78
80
|
"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",
|
|
79
81
|
"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",
|
|
82
|
+
"test:haskell": "node --test test/haskell/haskell-injection.test.js test/haskell/haskell-ir.test.js test/haskell/haskell-manifests.test.js test/haskell/haskell-parser.test.js test/haskell/haskell-remediation.test.js test/haskell/haskell-resolved-graph.test.js test/haskell/haskell-sca.test.js test/haskell/haskell-security-rules.test.js test/haskell/haskell-taint.test.js",
|
|
83
|
+
"test:nix": "node --test test/nix/nix-build-trust.test.js test/nix/nix-eval-isolation.test.js test/nix/nix-input-inventory.test.js test/nix/nix-parser-ir.test.js test/nix/nix-remediation.test.js test/nix/nix-resolved-closure.test.js test/nix/nix-sca-patches.test.js test/nix/nix-script-taint.test.js test/nix/nix-secrets-store.test.js test/nix/nixos-hardening.test.js test/nix/nixos-module-resolution.test.js",
|
|
84
|
+
"test:language": "node --test test/language/capability-ledger.test.js test/language/language-accuracy-regression.test.js test/language/language-aibom.test.js test/language/language-assurance.test.js test/language/language-bom.test.js test/language/language-compliance.test.js test/language/language-contracts.test.js test/language/language-corpora-integrity.test.js test/language/language-discovery.test.js test/language/language-doc-coverage.test.js test/language/language-evidence-posture.test.js test/language/language-explorer.test.js test/language/language-governance.test.js test/language/language-integrations.test.js test/language/language-llm-agent.test.js test/language/language-local-ai.test.js test/language/language-polyglot-bridges.test.js test/language/language-privacy-lineage.test.js test/language/language-proof-remediation.test.js test/language/language-report-formats.test.js test/language/language-secrets.test.js",
|
|
85
|
+
"test:language-stress": "node --test --test-concurrency=1 test/language/language-stress-offline.test.js",
|
|
86
|
+
"test:language-tools": "node --test test/haskell/haskell-web-auth.test.js test/haskell/haskell-support-gate.test.js",
|
|
87
|
+
"bench:language-support:check": "node ../bench/language-support/check.mjs",
|
|
88
|
+
"bench:language-support:perf": "node ../bench/language-support/perf.mjs",
|
|
80
89
|
"test:lifecycle": "node --test 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/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",
|
|
81
90
|
"test:eval": "node --test test/independent-eval.test.js test/realworld-recall.test.js",
|
|
82
91
|
"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",
|
|
@@ -187,7 +196,13 @@
|
|
|
187
196
|
"bench:independent:merge": "node ../bench/independent/merge-chunks.mjs",
|
|
188
197
|
"bench:independent:agent-delta": "node ../bench/independent/agent-boundary-delta.mjs",
|
|
189
198
|
"bench:independent:deep": "node ../bench/independent/runner.mjs --deep",
|
|
190
|
-
"test:ci-parity": "CI=true GITHUB_ACTIONS=true node --test test/backward-default.test.js test/ci-parity.test.js test/container-taint.test.js test/deep-taint.test.js test/engine-recall.test.js test/engine-reconnect.test.js test/gate-verdict-cache.test.js test/ignore-pragma.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/py-annotation-sources.test.js test/runscan-deep-option.test.js test/sanitizer-typed-flow.test.js"
|
|
199
|
+
"test:ci-parity": "CI=true GITHUB_ACTIONS=true node --test test/backward-default.test.js test/ci-parity.test.js test/container-taint.test.js test/deep-taint.test.js test/engine-recall.test.js test/engine-reconnect.test.js test/gate-verdict-cache.test.js test/ignore-pragma.test.js test/java-taint-flow.test.js test/kt-taint-flow.test.js test/py-annotation-sources.test.js test/runscan-deep-option.test.js test/sanitizer-typed-flow.test.js",
|
|
200
|
+
"docs:render-language": "node ../scripts/render-language-docs.mjs",
|
|
201
|
+
"docs:capture-language": "node ../scripts/capture-language-examples.mjs",
|
|
202
|
+
"docs:check-language": "node ../scripts/render-language-docs.mjs --check && node ../scripts/capture-language-examples.mjs --check",
|
|
203
|
+
"test:nixos-host": "node --test test/nix/nixos-host-runtime.test.js",
|
|
204
|
+
"test:language-gates": "node --test --test-concurrency=1 test/language/language-benchmark-gates.test.js",
|
|
205
|
+
"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"
|
|
191
206
|
},
|
|
192
207
|
"author": "Ross Young <ross@clearcapabilities.com>",
|
|
193
208
|
"license": "PolyForm-Internal-Use-1.0.0",
|
package/src/dataflow/CLAUDE.md
CHANGED
|
@@ -98,3 +98,12 @@ Layer-2 taint engine. Walks the Layer-1 IR (`../ir/`) with field-sensitive forwa
|
|
|
98
98
|
- **`AGENTIC_SECURITY_BLIND_BENCH=1` does NOT disable the deep engine.** It gates the bench-shape detectors; `_deepRequested` in `engine.js` keys on `AGENTIC_SECURITY_DEEP === '1'` alone, so deep mode runs regardless. (This bullet previously claimed the opposite — verified false while building `bench/layer-recall/`, where setting `BLIND_BENCH=1` left taint recall unchanged at 20/210.) To turn the taint engine off, set `AGENTIC_SECURITY_DEEP=0`.
|
|
99
99
|
- **The corpus barely exercises this engine.** `bench/cve-replay` enables deep mode only for its 6 `deep`-tier entries, and with the deep engine off entirely, corpus detection falls only 210 → 204. Taint quality is measured by `npm run bench:layer-recall` (which forces deep on for all 210), not by the corpus gate — see `docs/METRICS.md`.
|
|
100
100
|
- **Resolved: `stub-aware-filter.js` no longer mutates `severity`.** It used to (`f.severity = downgrade[f.severity]`, opt-in via `AGENTIC_SECURITY_TYPE_STUBS=1`), which violated the recall-preserving convention every other precision annotator in this directory follows. Flagged by documentation audit, then fixed on maintainer decision: it now demotes `confidence` (× 0.4, floor 0.01) + `confidenceTier` + `exploitabilityTier` by one step each, mirroring `proof-gate.js`'s own demotion shape exactly (`_confidenceBeforeStubFilter` records the pre-demotion value, a distinct field from `proof-gate.js`'s `_confidenceBeforeProofGate` so the two compose if both fire on the same finding). Severity is never touched.
|
|
101
|
+
|
|
102
|
+
## Haskell catalog
|
|
103
|
+
|
|
104
|
+
`catalog-haskell.js` is **generated** from `language/haskell-models.js` (one registry of sources, sinks and sanitizers; entries
|
|
105
|
+
match import-qualified callees, so a user function with the same name cannot match). Two extra sources are annotation- and
|
|
106
|
+
member-based: `hs-src-caller-controlled-param` (a text parameter of an exported function nothing in the module calls) and
|
|
107
|
+
`hs-lin-src-record-parameter` (the lineage form of a record parameter). `engine.js` skips the literal-skeleton filter for Haskell
|
|
108
|
+
catalog entries and requires a markup literal for `htmlSkeleton` sinks. Pinned counts in the lineage registry tests change
|
|
109
|
+
deliberately when the model registry grows. Details: `scanner/src/language/CLAUDE.md`.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// Haskell taint catalog, GENERATED from the model registry (language/haskell-models.js) so there is
|
|
2
|
+
// exactly one list of Haskell sources, sinks and sanitizers. Every entry matches the IMPORT-QUALIFIED
|
|
3
|
+
// callee the Haskell IR produces (`System.Process.callCommand`), never a bare name, so a user's own
|
|
4
|
+
// function with the same name resolves to its own module and cannot match.
|
|
5
|
+
|
|
6
|
+
import { HS_SOURCES, HS_SINKS, HS_SANITIZERS } from '../language/haskell-models.js';
|
|
7
|
+
|
|
8
|
+
const idOf = (kind, e, extra = '') => `hs-${kind}-${e.module}.${e.name}${extra}`.replace(/[^A-Za-z0-9.\-_]/g, '-').toLowerCase();
|
|
9
|
+
|
|
10
|
+
export const HASKELL_CATALOG = Object.freeze([
|
|
11
|
+
...HS_SOURCES.map((e) => ({
|
|
12
|
+
kind: 'source', id: idOf('src', e), language: 'hs', framework: e.framework || 'haskell',
|
|
13
|
+
match: { type: 'call', callee: `${e.module}.${e.name}` }, label: e.label, provenance: e.provenance,
|
|
14
|
+
})),
|
|
15
|
+
...HS_SINKS.map((e) => ({
|
|
16
|
+
kind: 'sink', id: idOf('sink', e, `.${e.cwe}.${e.argIndex}`), language: 'hs', framework: e.framework || 'haskell',
|
|
17
|
+
match: { type: 'call', callee: `${e.module}.${e.name}` }, argIndex: e.argIndex,
|
|
18
|
+
vuln: { name: e.vuln, severity: e.severity, cwe: e.cwe, remediation: e.remediation },
|
|
19
|
+
hs: { family: e.family, shell: e.shell === true, argv: e.argv === true, package: e.package, htmlSkeleton: e.htmlSkeleton === true },
|
|
20
|
+
})),
|
|
21
|
+
// LINEAGE form of every source: a member read on the synthetic root `$hs` (see lineage/haskell-view.js, which
|
|
22
|
+
// rewrites a CLONE of each Haskell function so a monadic source read becomes a seedable member read). The
|
|
23
|
+
// taint engine never sees this form: the shared IR keeps the call form above.
|
|
24
|
+
...HS_SOURCES.map((e) => ({
|
|
25
|
+
kind: 'source', id: idOf('lin-src', e), language: 'hs', framework: e.framework || 'haskell',
|
|
26
|
+
match: { type: 'member', object: '$hs', prop: `${e.module}.${e.name}` }, label: e.label, provenance: e.provenance,
|
|
27
|
+
})),
|
|
28
|
+
// A text-typed parameter of an exported function (see language/haskell-ir.js `callerControlledParams`). The label says what
|
|
29
|
+
// it is: the caller supplies the value and nothing in the module proves it trusted.
|
|
30
|
+
// LINEAGE form of a record parameter of an exported function (see lineage/haskell-view.js)
|
|
31
|
+
{ kind: 'source', id: 'hs-lin-src-record-parameter', language: 'hs', framework: 'haskell', match: { type: 'member', object: '$hs', prop: 'hs:record-parameter' }, label: 'record parameter of an exported function (caller-supplied customer data)' },
|
|
32
|
+
{ kind: 'source', id: 'hs-src-caller-controlled-param', language: 'hs', framework: 'haskell', match: { type: 'annotation', name: 'hs:caller-controlled' }, label: 'text parameter of an exported function (caller-controlled)' },
|
|
33
|
+
...HS_SANITIZERS.map((e) => ({
|
|
34
|
+
kind: 'sanitizer', id: idOf('san', e), language: 'hs',
|
|
35
|
+
match: { type: 'call', callee: `${e.module}.${e.name}` }, effect: 'strip', appliesTo: e.appliesTo, note: e.note,
|
|
36
|
+
})),
|
|
37
|
+
]);
|
package/src/dataflow/catalog.js
CHANGED
|
@@ -2174,6 +2174,7 @@ export const CATALOG = [
|
|
|
2174
2174
|
// Lives in catalog-expanded.js to keep the diff reviewable. Merged into
|
|
2175
2175
|
// the main CATALOG below so the indexer treats them identically.
|
|
2176
2176
|
import { EXPANDED_SANITIZERS } from './catalog-expanded.js';
|
|
2177
|
+
import { HASKELL_CATALOG } from './catalog-haskell.js';
|
|
2177
2178
|
import { cppExtRe } from '../ir/parser-cpp.js';
|
|
2178
2179
|
|
|
2179
2180
|
// Language-scope guard. `file` is optional — when absent, behavior is
|
|
@@ -2200,6 +2201,7 @@ const _LANG_EXT = {
|
|
|
2200
2201
|
rb: /\.rb$/i,
|
|
2201
2202
|
java: /\.java$/i,
|
|
2202
2203
|
rs: /\.rs$/i,
|
|
2204
|
+
hs: /\.(?:hs|lhs)$/i,
|
|
2203
2205
|
};
|
|
2204
2206
|
|
|
2205
2207
|
// cpp delegates to cppExtRe() rather than duplicating a literal set, so it
|
|
@@ -2395,6 +2397,8 @@ function _receiverTypeConfirms(entry, receiverType) {
|
|
|
2395
2397
|
_ids.add(k);
|
|
2396
2398
|
CATALOG.push(e);
|
|
2397
2399
|
}
|
|
2400
|
+
// Haskell models (generated from language/haskell-models.js). Entries are import-qualified.
|
|
2401
|
+
for (const e of HASKELL_CATALOG) { const k = String(e.id).toLowerCase(); if (_ids.has(k)) continue; _ids.add(k); CATALOG.push(e); }
|
|
2398
2402
|
}
|
|
2399
2403
|
|
|
2400
2404
|
// Provenance defaults (Sentinel-parity audit P1-10):
|
package/src/dataflow/engine.js
CHANGED
|
@@ -1073,7 +1073,11 @@ function _sinkFindingsForCall(calleeExpr, argExprs, cat, argTaints, state, callC
|
|
|
1073
1073
|
? argTaints.findIndex(Boolean) : e.argIndex;
|
|
1074
1074
|
const taintedArgExpr = (argExprs || [])[taintedArgIdx];
|
|
1075
1075
|
// String content analysis: skip if literal skeleton doesn't match injection family
|
|
1076
|
-
|
|
1076
|
+
// Haskell entries are import-qualified, API-specific sinks (`preEscapedToHtml`, `callCommand`), never a generic
|
|
1077
|
+
// `.query()`-style call that may carry an unrelated string, so the literal-skeleton filter does not apply to them.
|
|
1078
|
+
if (e.vuln && taintedArgExpr && !e.hs && !literalSkeletonMatchesFamily(taintedArgExpr, e.vuln.cwe)) continue;
|
|
1079
|
+
// A text-output sink that is an XSS sink ONLY when the string being assembled contains markup (`putStrLn`).
|
|
1080
|
+
if (e.hs && e.hs.htmlSkeleton && !(taintedArgExpr && /<[A-Za-z!\/]/.test(_literalPartsOfExpr(taintedArgExpr).join(' ')))) continue;
|
|
1077
1081
|
// Taint-recall PRD (80%): `match.requireLiteralArg` — a precision
|
|
1078
1082
|
// gate for sinks whose danger depends on a SIBLING argument's
|
|
1079
1083
|
// literal value, not the tainted one. Go's `exec.Command(path,
|
|
@@ -1141,6 +1145,9 @@ function _sinkFindingsForCall(calleeExpr, argExprs, cat, argTaints, state, callC
|
|
|
1141
1145
|
findings.push({
|
|
1142
1146
|
...(_sanNames.size ? { _sanitizersOnPath: [..._sanNames] } : {}),
|
|
1143
1147
|
...(_unsanNames.size ? { _unsanitizersOnPath: [..._unsanNames] } : {}),
|
|
1148
|
+
// Haskell: two sinks of the same kind on one line (`if b then f (clean x) else f x`) are distinct
|
|
1149
|
+
// sites; without a site key they share an id and one silently replaces the other.
|
|
1150
|
+
...(/\.(?:hs|lhs)$/i.test(_currentFile || '') ? { siteKey: _siteKey(taintedArgExpr) } : {}),
|
|
1144
1151
|
kind: 'taint',
|
|
1145
1152
|
sinkId: e.id,
|
|
1146
1153
|
vuln: e.vuln?.name || 'Tainted Sink',
|
|
@@ -1742,6 +1749,18 @@ function step(node, stateIn, callContext) {
|
|
|
1742
1749
|
findings.push(..._sinkFindingsForCall(
|
|
1743
1750
|
node.value.callee, node.value.args, cat, argTaints, state, callContext, node.line, node.value.kwargs).findings);
|
|
1744
1751
|
findings.push(..._nestedSinkFindings(node.value.args, state, callContext, node.line));
|
|
1752
|
+
} else if (node.value && node.value.kind === 'union' && /\.(?:hs|lhs)$/i.test(_currentFile || '')) {
|
|
1753
|
+
// Haskell: `if c then A else B` and guards return a union of the branch ACTIONS, each of which
|
|
1754
|
+
// runs when its branch is taken, so every branch call is a sink site in its own right. (Scoped
|
|
1755
|
+
// to Haskell: other IRs lower conditional expressions differently and are unchanged.)
|
|
1756
|
+
const leaves = [];
|
|
1757
|
+
const walk = (v, d) => { if (!v || d > 40) return; if (v.kind === 'union') (v.branches || []).forEach((b) => walk(b, d + 1)); else if (v.kind === 'call') leaves.push(v); };
|
|
1758
|
+
walk(node.value, 0);
|
|
1759
|
+
for (const c of leaves) {
|
|
1760
|
+
const { cat, argTaints } = _matchCallCatalog(c.callee, c.args, state, callContext);
|
|
1761
|
+
findings.push(..._sinkFindingsForCall(c.callee, c.args, cat, argTaints, state, callContext, c.line || node.line, c.kwargs).findings);
|
|
1762
|
+
findings.push(..._nestedSinkFindings(c.args, state, callContext, c.line || node.line));
|
|
1763
|
+
}
|
|
1745
1764
|
}
|
|
1746
1765
|
if (exprTaint(node.value, state, callContext)) {
|
|
1747
1766
|
callContext._returnTainted = true;
|
|
@@ -1764,6 +1783,14 @@ function step(node, stateIn, callContext) {
|
|
|
1764
1783
|
// state, never into the SummaryCache key (that stays exactly what the caller
|
|
1765
1784
|
// supplied). Returns the ORIGINAL Set unchanged when there's nothing to add,
|
|
1766
1785
|
// so callers that never touch annotation-shaped params pay zero extra cost.
|
|
1786
|
+
// Short stable fingerprint of an argument expression (ignores line numbers and IR bookkeeping).
|
|
1787
|
+
function _siteKey(expr) {
|
|
1788
|
+
let h = 5381;
|
|
1789
|
+
const str = JSON.stringify(expr, (k, v) => (k === 'line' || k === 'hs' ? undefined : v)) || '';
|
|
1790
|
+
for (let i = 0; i < str.length; i++) h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
|
|
1791
|
+
return h.toString(16);
|
|
1792
|
+
}
|
|
1793
|
+
|
|
1767
1794
|
function _unionAnnotationTaint(fn, entrySet) {
|
|
1768
1795
|
if (!fn.paramAnnotations || !fn.paramAnnotations.length) return entrySet;
|
|
1769
1796
|
const extra = matchAnnotationParams(fn.paramAnnotations, fn.file);
|
|
@@ -2541,6 +2568,8 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
2541
2568
|
if (!fn) continue;
|
|
2542
2569
|
if (calledQids.has(f._funcQid)) continue;
|
|
2543
2570
|
if (fn.name === '<module>' || /handler|route|controller|middleware|endpoint/i.test(fn.name || '')) continue;
|
|
2571
|
+
// Haskell: an exported function or `main` is reachable from outside the analysed files.
|
|
2572
|
+
if (fn.hs && (fn.hs.exported || /\.main$/.test(fn.name || ''))) continue;
|
|
2544
2573
|
f._inDeadCode = true;
|
|
2545
2574
|
const dg = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
|
|
2546
2575
|
if (dg[f.severity]) f.severity = dg[f.severity];
|
|
@@ -2572,12 +2601,13 @@ export function runTaintEngine(perFileIR, callGraph, opts = {}) {
|
|
|
2572
2601
|
// every path pushing into `_findings` sets `file` explicitly as of
|
|
2573
2602
|
// this fix.
|
|
2574
2603
|
const file = f.file || attributedFn.file;
|
|
2575
|
-
const key = `${f.sinkId}:${file}:${f.line}`;
|
|
2604
|
+
const key = `${f.sinkId}:${file}:${f.line}${f.siteKey ? ':' + f.siteKey : ''}`;
|
|
2576
2605
|
if (seen.has(key)) continue;
|
|
2577
2606
|
seen.add(key);
|
|
2578
2607
|
const fn = attributedFn;
|
|
2579
2608
|
all.push({
|
|
2580
|
-
id: `ir-taint:${file}:${f.line}:${f.sinkId}`,
|
|
2609
|
+
id: `ir-taint:${file}:${f.line}:${f.sinkId}${f.siteKey ? ':' + f.siteKey : ''}`,
|
|
2610
|
+
...(f.siteKey ? { siteKey: f.siteKey } : {}),
|
|
2581
2611
|
file,
|
|
2582
2612
|
line: f.line,
|
|
2583
2613
|
vuln: f.vuln,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
// Haskell privacy sinks (X-004), kept SEPARATE from the security sinks of haskell-models.js: a field that is
|
|
2
|
+
// legitimately written to a database or sent to an API is not a vulnerability, it is a privacy-relevant flow
|
|
3
|
+
// that the Data Flow Explorer classifies. Entries use the same shape as privacy-catalog.js and match the
|
|
4
|
+
// IMPORT-QUALIFIED callee the Haskell IR produces (`Database.PostgreSQL.Simple.execute`), so a user's own
|
|
5
|
+
// function of the same name cannot match.
|
|
6
|
+
//
|
|
7
|
+
// Package/function names are curated from each library's documented API; `argIndex` is the position of the
|
|
8
|
+
// DATA argument (the row/parameter tuple, the message, the body), never the query text or a connection.
|
|
9
|
+
|
|
10
|
+
import { HS_LOG_SINKS } from '../language/haskell-models.js';
|
|
11
|
+
|
|
12
|
+
const mk = (category, id, module, names, argIndex, framework, vuln, severity = 'high') => names.map((name) => ({
|
|
13
|
+
kind: 'sink', id: `privacy-hs-${id}-${name}`.toLowerCase(), language: 'hs', framework, category,
|
|
14
|
+
match: { type: 'call', callee: `${module}.${name}` }, argIndex,
|
|
15
|
+
vuln: { name: vuln, severity, cwe: 'CWE-359', remediation: 'Regulated data (PII/PHI/PCI) reaching this destination needs a documented purpose, retention and protection; redact, hash or drop the field first.' },
|
|
16
|
+
}));
|
|
17
|
+
|
|
18
|
+
export const HASKELL_PRIVACY_SINKS = Object.freeze([
|
|
19
|
+
// log: derived from the same table the sensitive-logging rule reads
|
|
20
|
+
...HS_LOG_SINKS.map((e) => ({
|
|
21
|
+
kind: 'sink', id: `privacy-hs-log-${e.module}.${e.name}`.toLowerCase(), language: 'hs', framework: 'haskell', category: 'log',
|
|
22
|
+
match: { type: 'call', callee: `${e.module}.${e.name}` }, argIndex: e.argIndex,
|
|
23
|
+
vuln: { name: `Privacy Leak (${e.channel} log)`, severity: 'medium', cwe: 'CWE-359', remediation: 'Do not log regulated data (PII/PHI/PCI). Redact or hash the field before logging.' },
|
|
24
|
+
})),
|
|
25
|
+
// response
|
|
26
|
+
...mk('response', 'scotty', 'Web.Scotty', ['text', 'html', 'json', 'raw'], 0, 'scotty', 'Privacy Leak (response body)'),
|
|
27
|
+
...mk('response', 'scotty-trans', 'Web.Scotty.Trans', ['text', 'html', 'json', 'raw'], 0, 'scotty', 'Privacy Leak (response body)'),
|
|
28
|
+
...mk('response', 'wai', 'Network.Wai', ['responseLBS'], 2, 'wai', 'Privacy Leak (response body)'),
|
|
29
|
+
// outbound HTTP (the request body is the data carrier)
|
|
30
|
+
...mk('outboundHttp', 'http-simple', 'Network.HTTP.Simple', ['setRequestBodyJSON', 'setRequestBodyLBS', 'setRequestBodyURLEncoded'], 0, 'http-conduit', 'Privacy Leak (outbound HTTP request)'),
|
|
31
|
+
...mk('outboundHttp', 'wreq', 'Network.Wreq', ['post', 'put'], 1, 'wreq', 'Privacy Leak (outbound HTTP request)'),
|
|
32
|
+
...mk('outboundHttp', 'wreq-with', 'Network.Wreq', ['postWith', 'putWith'], 2, 'wreq', 'Privacy Leak (outbound HTTP request)'),
|
|
33
|
+
// storage
|
|
34
|
+
...mk('storage', 'pgsimple', 'Database.PostgreSQL.Simple', ['execute', 'executeMany', 'query', 'returning'], 2, 'postgresql-simple', 'Privacy Leak (database write)', 'medium'),
|
|
35
|
+
...mk('storage', 'sqlite', 'Database.SQLite.Simple', ['execute', 'executeMany', 'query', 'executeNamed'], 2, 'sqlite-simple', 'Privacy Leak (database write)', 'medium'),
|
|
36
|
+
...mk('storage', 'mysql', 'Database.MySQL.Simple', ['execute', 'executeMany', 'query'], 2, 'mysql-simple', 'Privacy Leak (database write)', 'medium'),
|
|
37
|
+
...mk('storage', 'persistent', 'Database.Persist', ['insert', 'insert_', 'insertMany', 'insertMany_', 'repsert', 'replace', 'upsert'], 0, 'persistent', 'Privacy Leak (database write)', 'medium'),
|
|
38
|
+
...mk('storage', 'persistent-sql', 'Database.Persist.Sql', ['insert', 'insert_', 'insertMany', 'insertMany_', 'repsert', 'replace', 'upsert'], 0, 'persistent', 'Privacy Leak (database write)', 'medium'),
|
|
39
|
+
// file write
|
|
40
|
+
...mk('fileWrite', 'prelude', 'Prelude', ['writeFile', 'appendFile'], 1, 'base', 'Privacy Leak (file write)', 'medium'),
|
|
41
|
+
...mk('fileWrite', 'system-io', 'System.IO', ['writeFile', 'appendFile'], 1, 'base', 'Privacy Leak (file write)', 'medium'),
|
|
42
|
+
...mk('fileWrite', 'text-io', 'Data.Text.IO', ['writeFile', 'appendFile'], 1, 'text', 'Privacy Leak (file write)', 'medium'),
|
|
43
|
+
...mk('fileWrite', 'bytestring', 'Data.ByteString', ['writeFile', 'appendFile'], 1, 'bytestring', 'Privacy Leak (file write)', 'medium'),
|
|
44
|
+
// object storage / email / queues
|
|
45
|
+
...mk('s3Upload', 'amazonka', 'Amazonka.S3', ['newPutObject'], 2, 'amazonka', 'Privacy Leak (object storage upload)'),
|
|
46
|
+
...mk('emailSend', 'smtp', 'Network.Mail.SMTP', ['sendMail', 'simpleMail', 'plainTextPart', 'htmlPart'], 0, 'smtp', 'Privacy Leak (outbound email)', 'medium'),
|
|
47
|
+
...mk('emailSend', 'mime', 'Network.Mail.Mime', ['renderSendMail'], 0, 'mime-mail', 'Privacy Leak (outbound email)', 'medium'),
|
|
48
|
+
...mk('queue', 'amqp', 'Network.AMQP', ['publishMsg'], 2, 'amqp', 'Privacy Leak (queue publish)', 'medium'),
|
|
49
|
+
...mk('queue', 'kafka', 'Kafka.Producer', ['produceMessage'], 1, 'hw-kafka-client', 'Privacy Leak (queue publish)', 'medium'),
|
|
50
|
+
]);
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
import { classifyFieldAgainst } from './privacy-taxonomy.js';
|
|
38
38
|
import { _languageFamilyExtensions } from './catalog.js';
|
|
39
|
+
import { HASKELL_PRIVACY_SINKS } from './privacy-catalog-haskell.js';
|
|
39
40
|
|
|
40
41
|
export const PRIVACY_SINK_CATALOG = [
|
|
41
42
|
// ── log ──────────────────────────────────────────────────────────────────
|
|
@@ -129,6 +130,7 @@ export const PRIVACY_SINK_CATALOG = [
|
|
|
129
130
|
remediation: 'Regulated data (PII/PHI/PCI) placed on a queue is readable by every consumer with queue access — document who can subscribe before sending it.' } },
|
|
130
131
|
];
|
|
131
132
|
|
|
133
|
+
|
|
132
134
|
// All 9 sink categories FR-403 names (7 already covered by privacy-taint.js's
|
|
133
135
|
// own SINK_PATTERNS, plus storage/queues) — used by this module's own tests
|
|
134
136
|
// to assert coverage is genuinely complete, not just "some entries exist."
|
|
@@ -242,7 +244,9 @@ export function matchPrivacySink(calleeExpr, file, receiverType) {
|
|
|
242
244
|
if (!calleeExpr) return null;
|
|
243
245
|
const { last, full } = _privacyCalleeNames(calleeExpr);
|
|
244
246
|
if (!last && !full) return null;
|
|
245
|
-
|
|
247
|
+
// Haskell privacy sinks live in their own table (privacy-catalog-haskell.js): the pinned catalog above is the
|
|
248
|
+
// JS/Python/... surface other suites count, and a new language does not silently change those counts.
|
|
249
|
+
const hits = [...PRIVACY_SINK_CATALOG, ...HASKELL_PRIVACY_SINKS].filter((e) => {
|
|
246
250
|
if (!e || e.kind !== 'sink') return false;
|
|
247
251
|
const cName = e.match && e.match.callee;
|
|
248
252
|
if (!cName) return false;
|
package/src/discovery/CLAUDE.md
CHANGED
|
@@ -46,3 +46,9 @@ panel · `judge.js` shapes findings and dedupes against the prior scan ·
|
|
|
46
46
|
it was not examined, and the report says so.
|
|
47
47
|
- **Coverage is reported.** Degraded runs and their reasons appear in every
|
|
48
48
|
report. A half-failed pass must never read as a clean one.
|
|
49
|
+
|
|
50
|
+
## Nix partitioning
|
|
51
|
+
|
|
52
|
+
`partition.js` `partitionNixFiles` groups `.nix` files by their import structure (a configuration and the modules it imports are
|
|
53
|
+
one area), merged areas are capped, and hunter prompts for Haskell and Nix are redacted (split literals joined first) before they
|
|
54
|
+
leave the process. `hunt` includes `.hs`, `.lhs` and `.nix`. With no endpoint every run is degraded and says so.
|
package/src/discovery/index.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
// planned versus hunted and how many runs degraded, with reasons. A discovery
|
|
8
8
|
// pass that half failed and reports "no findings" is indistinguishable from a
|
|
9
9
|
// clean codebase unless it says so.
|
|
10
|
-
import { partitionCallGraph } from './partition.js';
|
|
10
|
+
import { partitionCallGraph, partitionNixFiles } from './partition.js';
|
|
11
11
|
import { LENSES, lensByKey } from './lenses.js';
|
|
12
12
|
import { runHunter } from './hunter.js';
|
|
13
13
|
import { confirmAll } from './confirm.js';
|
|
@@ -131,7 +131,16 @@ export function makeBudget(opts = {}, now = Date.now) {
|
|
|
131
131
|
}
|
|
132
132
|
|
|
133
133
|
export async function runDiscovery(ctx = {}, opts = {}) {
|
|
134
|
-
|
|
134
|
+
// Call-graph areas (every language the IR covers, Haskell included) plus Nix configuration areas, which have no call
|
|
135
|
+
// graph. Together they respect the same ceiling.
|
|
136
|
+
const maxAreas = opts.maxAreas ?? 8;
|
|
137
|
+
let areas = [...partitionCallGraph(ctx.callGraph, { maxAreas }), ...partitionNixFiles(ctx.fileContents, { maxAreas })];
|
|
138
|
+
if (areas.length > maxAreas) {
|
|
139
|
+
const kept = areas.slice(0, maxAreas - 1); const tail = areas.slice(maxAreas - 1);
|
|
140
|
+
const files = [...new Set(tail.flatMap((a) => a.files))].sort();
|
|
141
|
+
kept.push({ id: tail.map((a) => a.id).join('+').slice(0, 64), label: 'misc', files, functions: tail.flatMap((a) => a.functions), size: tail.reduce((n, a) => n + a.size, 0) });
|
|
142
|
+
areas = kept;
|
|
143
|
+
}
|
|
135
144
|
|
|
136
145
|
const reasons = [];
|
|
137
146
|
const budget = makeBudget(opts);
|
|
@@ -169,7 +178,7 @@ export async function runDiscovery(ctx = {}, opts = {}) {
|
|
|
169
178
|
for (const area of areas) {
|
|
170
179
|
let areaDegradedCount = 0;
|
|
171
180
|
for (const lens of lenses) {
|
|
172
|
-
const run = await runHunter(area, lens, { fileContents: ctx.fileContents || {} }, { llmInvoke, scanRoot: opts.scanRoot });
|
|
181
|
+
const run = await runHunter(area, lens, { fileContents: ctx.fileContents || {}, scanRoot: opts.scanRoot }, { llmInvoke, scanRoot: opts.scanRoot });
|
|
173
182
|
runs.push({ focusAreaId: run.focusAreaId, lens: run.lens, degraded: run.degraded, reason: run.reason, candidateCount: run.candidates.length });
|
|
174
183
|
if (run.degraded && run.reason) reasons.push(`${area.label} × ${lens.key}: ${run.reason}`);
|
|
175
184
|
if (run.degraded) areaDegradedCount += 1;
|
package/src/discovery/lenses.js
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// same code than one told to look at crypto, so the union covers failure modes
|
|
8
8
|
// no single prompt reaches. `wildcard` exists because a fixed taxonomy is a
|
|
9
9
|
// ceiling, and the classes worth finding are the ones not on the list.
|
|
10
|
+
import { redactPayload } from '../egress/redact.js';
|
|
10
11
|
export const LENSES = Object.freeze([
|
|
11
12
|
{ key: 'injection', title: 'Injection', family: 'injection', cwe: 'CWE-74',
|
|
12
13
|
brief: 'Untrusted input reaching an interpreter: SQL, shell, template, XPath, LDAP, or deserialization. Follow the value, not the function name.' },
|
|
@@ -39,7 +40,9 @@ export function buildHunterPrompt(focusArea, lens, ctx = {}) {
|
|
|
39
40
|
let budget = maxChars;
|
|
40
41
|
const blocks = [];
|
|
41
42
|
for (const f of files) {
|
|
42
|
-
|
|
43
|
+
// Everything that leaves the machine is redacted first (secrets, PII/PHI/PCI-shaped fields, operator-declared
|
|
44
|
+
// customer data, and whole proprietary paths), by the same choke point the validator and the proposers use.
|
|
45
|
+
const src = redactPayload({ text: contents[f], filePath: f, scanRoot: ctx.scanRoot || null }).text;
|
|
43
46
|
const slice = src.length > budget ? src.slice(0, Math.max(0, budget)) : src;
|
|
44
47
|
const truncated = slice.length < src.length;
|
|
45
48
|
blocks.push(`--- SOURCE FILE (untrusted data): ${f}${truncated ? ' (truncated)' : ''} ---\n${slice}\n--- END SOURCE FILE: ${f} ---`);
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// components share a file they are merged, otherwise the same source lands in
|
|
13
13
|
// two hunters' context and the convergence this module exists to prevent
|
|
14
14
|
// comes straight back.
|
|
15
|
+
import { buildImportGraph } from '../language/discovery.js';
|
|
15
16
|
import * as crypto from 'node:crypto';
|
|
16
17
|
|
|
17
18
|
export function focusAreaId(files) {
|
|
@@ -90,3 +91,30 @@ export function partitionCallGraph(callGraph, opts = {}) {
|
|
|
90
91
|
}
|
|
91
92
|
return areas;
|
|
92
93
|
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Focus areas for Nix configuration, which has no call graph: files joined by `imports` / path references form one
|
|
97
|
+
* area, so a NixOS entry point and the modules that decide its options are hunted together. Same shape as a call-graph
|
|
98
|
+
* area (`functions` is empty: a Nix file has none).
|
|
99
|
+
*/
|
|
100
|
+
export function partitionNixFiles(fileContents, opts = {}) {
|
|
101
|
+
const maxAreas = Number.isInteger(opts.maxAreas) && opts.maxAreas > 0 ? opts.maxAreas : 8;
|
|
102
|
+
const nix = {};
|
|
103
|
+
for (const [f, t] of Object.entries(fileContents || {})) if (/\.nix$/i.test(f) && typeof t === 'string') nix[f] = t;
|
|
104
|
+
const files = Object.keys(nix).sort();
|
|
105
|
+
if (!files.length) return [];
|
|
106
|
+
const { graph } = buildImportGraph(nix);
|
|
107
|
+
const dsu = makeDSU();
|
|
108
|
+
for (const f of files) dsu.find(f);
|
|
109
|
+
for (const [f, deps] of graph) for (const d of deps) dsu.union(f, d);
|
|
110
|
+
const groups = new Map();
|
|
111
|
+
for (const f of files) { const r = dsu.find(f); if (!groups.has(r)) groups.set(r, []); groups.get(r).push(f); }
|
|
112
|
+
let areas = [...groups.values()].map((g) => { const sorted = [...g].sort(); return { id: focusAreaId(sorted), label: labelFor(sorted), files: sorted, functions: [], size: sorted.length, language: 'nix' }; });
|
|
113
|
+
areas.sort((a, b) => b.size - a.size || (a.id < b.id ? -1 : 1));
|
|
114
|
+
if (areas.length > maxAreas) {
|
|
115
|
+
const kept = areas.slice(0, maxAreas - 1); const tail = areas.slice(maxAreas - 1).flatMap((a) => a.files).sort();
|
|
116
|
+
kept.push({ id: focusAreaId(tail), label: 'misc', files: tail, functions: [], size: tail.length, language: 'nix' });
|
|
117
|
+
areas = kept;
|
|
118
|
+
}
|
|
119
|
+
return areas;
|
|
120
|
+
}
|
package/src/egress/redact.js
CHANGED
|
@@ -43,6 +43,7 @@
|
|
|
43
43
|
// HOW MUCH was removed per category (FR-604's audit metadata wants exactly
|
|
44
44
|
// this: counts, not content) without retaining what was actually removed.
|
|
45
45
|
|
|
46
|
+
import { redactLanguageSecrets } from '../language/secrets.js';
|
|
46
47
|
import { loadPolicyConfig } from './policy.js';
|
|
47
48
|
import { redactSecrets } from '../llm-validator/redact.js';
|
|
48
49
|
import { loadPrivacyTaxonomy } from '../dataflow/privacy-taxonomy.js';
|
|
@@ -159,6 +160,13 @@ export function redactPayload({ text, filePath = null, scanRoot = null, taxonomy
|
|
|
159
160
|
const secretResult = redactSecrets(out, { filePath });
|
|
160
161
|
out = secretResult.text;
|
|
161
162
|
categories.secrets = secretResult.redactions;
|
|
163
|
+
// Haskell and Nix: a credential split across string literals (`"sk_live_" ++ "..."`) is invisible to a
|
|
164
|
+
// single-literal pattern, so those files get a token-based pass as well.
|
|
165
|
+
if (filePath && /\.(?:l?hs|hs-boot|hsc|nix)$/i.test(filePath)) {
|
|
166
|
+
const lang = redactLanguageSecrets(filePath, out);
|
|
167
|
+
out = lang.text;
|
|
168
|
+
categories.secrets += lang.redactions;
|
|
169
|
+
}
|
|
162
170
|
|
|
163
171
|
const piiEnabled = cfg.redactPii !== false; // default ON — this category has a safe built-in default, unlike customerDataPatterns
|
|
164
172
|
if (piiEnabled) {
|