@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.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 +555 -0
- package/bin/agentic-security.js +770 -49
- package/dist/1.index.js +223 -0
- package/dist/113.index.js +117 -18
- package/dist/144.index.js +163 -0
- package/dist/178.index.js +24 -6
- package/dist/238.index.js +3 -2
- package/dist/265.index.js +191 -0
- package/dist/271.index.js +165 -0
- package/dist/384.index.js +1 -1
- package/dist/435.index.js +187 -52
- package/dist/444.index.js +11 -2
- package/dist/449.index.js +76 -12
- package/dist/526.index.js +117 -18
- package/dist/552.index.js +97 -0
- package/dist/637.index.js +27 -5
- package/dist/730.index.js +311 -0
- package/dist/736.index.js +301 -0
- package/dist/824.index.js +7 -0
- package/dist/905.index.js +88 -22
- package/dist/920.index.js +491 -0
- package/dist/970.index.js +173 -0
- package/dist/agentic-security.mjs +13 -13
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/calibration-seed.json +2 -0
- package/package.json +23 -9
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/index.js +18 -0
- package/src/dataflow/privacy-catalog.js +290 -0
- package/src/dataflow/privacy-deep-walker.js +515 -0
- package/src/dataflow/privacy-governance.js +126 -0
- package/src/dataflow/privacy-inventory.js +154 -0
- package/src/dataflow/privacy-sink-policy.js +125 -0
- package/src/dataflow/privacy-taint.js +115 -54
- package/src/dataflow/privacy-taxonomy.js +233 -0
- package/src/discovery/disprove.js +7 -3
- package/src/discovery/hunter.js +9 -5
- package/src/discovery/index.js +2 -2
- package/src/discovery/llm-invoke.js +69 -13
- package/src/egress/audit.js +147 -0
- package/src/egress/policy.js +313 -0
- package/src/egress/redact.js +180 -0
- package/src/engine.js +1048 -302
- package/src/fix/apply-fix-service.js +404 -0
- package/src/fix/approver-registry.js +157 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +1 -1
- package/src/llm-validator/index.js +86 -9
- package/src/llm-validator/model-status.js +66 -0
- package/src/lsp/server.js +49 -2
- package/src/mcp/tools.js +177 -50
- package/src/pipeline/analyzer-supervisor.js +93 -0
- package/src/pipeline/analyzer-worker.js +26 -0
- package/src/pipeline/annotator-runner.js +33 -0
- package/src/pipeline/assurance-mode.js +154 -0
- package/src/pipeline/cascade-worker-pool.js +172 -0
- package/src/pipeline/cascade-worker.js +43 -0
- package/src/pipeline/coverage-ledger.js +0 -0
- package/src/pipeline/detector-runner.js +51 -0
- package/src/pipeline/enrichment-completion.js +58 -0
- package/src/pipeline/evidence-provenance.js +91 -0
- package/src/pipeline/finding-schema.js +108 -0
- package/src/pipeline/legacy-compat.js +101 -0
- package/src/pipeline/producer-collector.js +48 -0
- package/src/pipeline/producer-registry.js +112 -0
- package/src/pipeline/scan-health.js +144 -0
- package/src/posture/CLAUDE.md +123 -0
- package/src/posture/accuracy-scorecard.js +156 -1
- package/src/posture/adversary-agent.js +15 -3
- package/src/posture/artifact-registry.js +241 -0
- package/src/posture/auditor-walkthrough.js +186 -21
- package/src/posture/calibration-feedback.js +201 -0
- package/src/posture/calibration-seed.json +2 -0
- package/src/posture/calibration.js +25 -0
- package/src/posture/compliance-evidence-signing.js +131 -0
- package/src/posture/compliance-policy.js +324 -17
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/custom-rules.js +36 -0
- package/src/posture/deterministic.js +8 -1
- package/src/posture/encryption-provider.js +205 -0
- package/src/posture/evidence-grade-wording.js +71 -0
- package/src/posture/fix-history.js +137 -20
- package/src/posture/fix-honesty-gate.js +47 -6
- package/src/posture/fix-verify.js +65 -8
- package/src/posture/fleet.js +0 -0
- package/src/posture/flow-narration.js +7 -2
- package/src/posture/git-history.js +13 -5
- package/src/posture/legal-hold.js +140 -0
- package/src/posture/llm-redteam.js +10 -1
- package/src/posture/material-change.js +111 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/policy-bundle.js +274 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +47 -6
- package/src/posture/production-feedback.js +179 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/retention-policy.js +132 -0
- package/src/posture/risk-dollars.js +216 -26
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/scan-checkpoint.js +176 -31
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +64 -5
- package/src/posture/state-lifecycle-report.js +77 -0
- package/src/posture/suppressions.js +59 -3
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/privacy/ir-adapter.js +380 -0
- package/src/report/index.js +248 -5
- package/src/report/oscal.js +7 -2
- package/src/runScan.js +34 -5
- package/src/sast/cpp.js +3 -14
- package/src/sast/rate-limit.js +33 -3
- package/src/sca/llm-function-extract.js +6 -0
- package/src/util/git-hardening.js +128 -0
package/src/pr-delta.js
CHANGED
|
@@ -24,18 +24,27 @@
|
|
|
24
24
|
// comment) can transform without re-walking IR.
|
|
25
25
|
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
|
+
import { hardenGitArgs, hardenGitEnv } from './util/git-hardening.js';
|
|
27
28
|
import { runFullScan } from './engine.js';
|
|
28
29
|
|
|
29
30
|
const FILE_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|java|cs|kt|go|rb|php|sol|swift|rs|tf|yml|yaml|json|toml|md)$/i;
|
|
30
31
|
const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
|
|
31
32
|
|
|
33
|
+
// `root` is the PR's repository — a scan target, not this project's own
|
|
34
|
+
// trusted checkout — hardened per FR-PROV-024 / the second Finding
|
|
35
|
+
// Provenance PRD audit (same exposure class as
|
|
36
|
+
// provenance/git-evidence.js's `_run`).
|
|
32
37
|
function _git(root, args) {
|
|
33
|
-
const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
|
|
38
|
+
const r = spawnSync('git', hardenGitArgs(args), { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: hardenGitEnv() });
|
|
34
39
|
return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '' };
|
|
35
40
|
}
|
|
36
41
|
|
|
37
42
|
function _readFileAtRef(root, ref, file) {
|
|
38
|
-
|
|
43
|
+
// `--no-textconv`: this blob-cat form of `show` (`<ref>:<file>`, not a
|
|
44
|
+
// diff) was verified NOT reachable via a hostile textconv driver in
|
|
45
|
+
// current git, same as git-evidence.js's getBlobAtCommit — kept for
|
|
46
|
+
// defense-in-depth/uniformity.
|
|
47
|
+
const r = _git(root, ['show', '--no-textconv', `${ref}:${file}`]);
|
|
39
48
|
return r.ok ? r.stdout : null;
|
|
40
49
|
}
|
|
41
50
|
|
|
@@ -56,7 +65,12 @@ async function _scanAtRef(root, ref) {
|
|
|
56
65
|
const c = _readFileAtRef(root, ref, f);
|
|
57
66
|
if (c != null) fileContents[f] = c;
|
|
58
67
|
}
|
|
59
|
-
|
|
68
|
+
// `provenance:false` — a base-ref snapshot, not the current working state.
|
|
69
|
+
// Beyond the wasted git walks, updateLifecycle marks every open stableId
|
|
70
|
+
// absent from the finding set it is handed as `remediated`; running the PR
|
|
71
|
+
// delta gate would silently rewrite the project's lifecycle store from the
|
|
72
|
+
// base ref's findings.
|
|
73
|
+
return runFullScan({ fileContents, scanRoot: root, provenance: false }, () => {});
|
|
60
74
|
}
|
|
61
75
|
|
|
62
76
|
function _summary(findings) {
|
|
@@ -71,7 +85,14 @@ function _summary(findings) {
|
|
|
71
85
|
}
|
|
72
86
|
|
|
73
87
|
function _changedFiles(root, baseRef, headRef) {
|
|
74
|
-
|
|
88
|
+
// `--no-ext-diff`: found during the second-audit-remediation sweep — this
|
|
89
|
+
// is a `git diff` call site (`--name-only`, no content rendered, so not
|
|
90
|
+
// itself a verified exploit path today, same as runScan.js's
|
|
91
|
+
// `changedSince`), but every `diff` invocation in this codebase gets it
|
|
92
|
+
// uniformly per the live exploit VERIFIED in material-change.js's
|
|
93
|
+
// classifyGitDiff (external diff drivers fire on `git diff` even with
|
|
94
|
+
// `--no-textconv`, which is a surface `--no-textconv` alone does not close).
|
|
95
|
+
const r = _git(root, ['diff', '--name-only', '--no-ext-diff', `${baseRef}...${headRef}`]);
|
|
75
96
|
if (!r.ok) return new Set();
|
|
76
97
|
return new Set(r.stdout.trim().split('\n').filter(Boolean));
|
|
77
98
|
}
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
// Privacy IR adapter (assurance-hardening PRD, Milestone 2, FR-401).
|
|
2
|
+
//
|
|
3
|
+
// dataflow/privacy-taint.js's annotatePrivacyTaint() expects a per-file map
|
|
4
|
+
// of `{ _content, decls: [{name, line, type}], calls: [{line, fullPath|callee,
|
|
5
|
+
// args: [{text}]}] }` — a FLAT, per-file shape. The real Layer-1 IR
|
|
6
|
+
// (ir/index.js#buildProjectIR) does not produce that shape at all: it is
|
|
7
|
+
// per-FUNCTION (`functions: [{params, cfg: {nodes: {...}}}]`), and CFG node
|
|
8
|
+
// arguments are structured expression trees (`{kind:'ident', name}`,
|
|
9
|
+
// `{kind:'member', object, prop}`, ...), not `{text: string}` objects. This
|
|
10
|
+
// was the actual reason privacy analysis was wired with hardcoded
|
|
11
|
+
// `decls:[], calls:[]` (A-06) — there was no adapter reconciling the two
|
|
12
|
+
// shapes, not a missing capability in the underlying IR/taint engine (see
|
|
13
|
+
// docs/implementation/assurance-hardening-decisions.md D-0003's pattern:
|
|
14
|
+
// verify before assuming something is unbuilt).
|
|
15
|
+
//
|
|
16
|
+
// This module is that adapter. It does NOT reimplement privacy analysis —
|
|
17
|
+
// it only reshapes the real IR into exactly what annotatePrivacyTaint()
|
|
18
|
+
// already knows how to consume.
|
|
19
|
+
//
|
|
20
|
+
// Declarations: sourced from every function's `params` (a parameter is a
|
|
21
|
+
// declaration) and every `assign`-kind CFG node's `target` (an assignment
|
|
22
|
+
// declares/rebinds a name). Both are what a real codebase would call PII
|
|
23
|
+
// "at the point it enters a function or gets bound to a local".
|
|
24
|
+
//
|
|
25
|
+
// Calls: sourced from every `call`-kind CFG node, AND from `assign`-kind
|
|
26
|
+
// nodes whose `source` is itself a call expression (`const x = fetch(...)`)
|
|
27
|
+
// — privacy-taint.js only walks a flat `ir.calls` list, so a call used as
|
|
28
|
+
// an assignment's right-hand side needs to be surfaced there too or it's
|
|
29
|
+
// invisible to the sink-matching pass entirely.
|
|
30
|
+
//
|
|
31
|
+
// Argument "text": privacy-taint.js's sink-matching does a literal
|
|
32
|
+
// \b<name>\b regex test against the joined text of a call's argument
|
|
33
|
+
// expressions — it does not need faithful re-rendered source code, only
|
|
34
|
+
// text that CONTAINS every identifier name referenced (directly or nested)
|
|
35
|
+
// in that argument, so a tainted variable's name is found wherever it
|
|
36
|
+
// appears. `_identifiersIn` recursively collects identifier names (and
|
|
37
|
+
// literal values, for completeness) rather than attempting full expression
|
|
38
|
+
// pretty-printing, which is simpler and sufficient for this consumer's
|
|
39
|
+
// actual matching logic.
|
|
40
|
+
//
|
|
41
|
+
// Storage: this codebase already has a separate stored-taint subsystem
|
|
42
|
+
// (engine.js's STORED_TAINT_FIELD_PATTERNS / buildStoredTaintRegistry) that
|
|
43
|
+
// tracks which ORM/model fields are written with unsanitized request input,
|
|
44
|
+
// for its own cross-file write-then-render correlation (crossStoredTaint).
|
|
45
|
+
// `_storageForFile` reindexes that ALREADY-COMPUTED, whole-project registry
|
|
46
|
+
// down to one file's writes rather than recomputing anything — the engine
|
|
47
|
+
// builds it once per scan regardless of whether privacy analysis runs at all.
|
|
48
|
+
//
|
|
49
|
+
// Types: the underlying Layer-1 IR (ir/index.js#buildProjectIR) tracks no
|
|
50
|
+
// type-annotation/inference data for any language — genuine cross-language
|
|
51
|
+
// type inference would mean extending every language parser, a separate,
|
|
52
|
+
// substantially larger project deliberately out of scope here. What IS in
|
|
53
|
+
// scope, and implemented below, is a narrow, honest, TypeScript-only sliver:
|
|
54
|
+
// `_extractTsTypes` does a SEPARATE, read-only `@babel/core` `parseSync` pass
|
|
55
|
+
// over `.ts`/`.tsx` source (never touching ir/parser-js.js's own transform
|
|
56
|
+
// pipeline or its shared, heavily-relied-on visitor plugin) and walks the
|
|
57
|
+
// resulting AST for `TSTypeAnnotation` nodes on function parameters and
|
|
58
|
+
// `let`/`const`/`var` declarators. Babel's `parseSync` only PARSES — it never
|
|
59
|
+
// runs `@babel/preset-typescript`'s stripping transform, so a `.ts` file's
|
|
60
|
+
// type annotations are fully intact on the AST it returns, no plugin-ordering
|
|
61
|
+
// trick required. Matching this SEPARATE parse's declarations back to the
|
|
62
|
+
// real IR's decls is done by `(line, name)` — the same real source line and
|
|
63
|
+
// identifier name a given parameter or declarator sits on in both parses,
|
|
64
|
+
// since both parse the identical source text. A `.js` file, a type Babel
|
|
65
|
+
// cannot express as a plain name (destructured/computed types, deeply
|
|
66
|
+
// generic types past a small recursion depth), or any parse/traverse failure
|
|
67
|
+
// all degrade to `type: null` — never a guess, matching every other
|
|
68
|
+
// optional field in this module.
|
|
69
|
+
|
|
70
|
+
import { parseSync as babelParseSync, traverse as babelTraverse } from '@babel/core';
|
|
71
|
+
import presetTypescript from '@babel/preset-typescript';
|
|
72
|
+
|
|
73
|
+
const MAX_EXPR_DEPTH = 40;
|
|
74
|
+
const MAX_TS_TYPE_DEPTH = 6;
|
|
75
|
+
|
|
76
|
+
function _calleeToString(callee, depth = 0) {
|
|
77
|
+
if (typeof callee === 'string') return callee;
|
|
78
|
+
if (!callee || typeof callee !== 'object' || depth > MAX_EXPR_DEPTH) return '';
|
|
79
|
+
switch (callee.kind) {
|
|
80
|
+
case 'ident': return callee.name || '';
|
|
81
|
+
case 'member': {
|
|
82
|
+
const obj = _calleeToString(callee.object, depth + 1);
|
|
83
|
+
return obj ? `${obj}.${callee.prop || ''}` : String(callee.prop || '');
|
|
84
|
+
}
|
|
85
|
+
case 'call': return _calleeToString(callee.callee, depth + 1);
|
|
86
|
+
default: return '';
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function _identifiersIn(expr, out, depth = 0) {
|
|
91
|
+
if (!expr || typeof expr !== 'object' || depth > MAX_EXPR_DEPTH) return;
|
|
92
|
+
switch (expr.kind) {
|
|
93
|
+
case 'ident':
|
|
94
|
+
if (expr.name) out.push(expr.name);
|
|
95
|
+
return;
|
|
96
|
+
case 'literal':
|
|
97
|
+
if (expr.value != null) out.push(String(expr.value));
|
|
98
|
+
return;
|
|
99
|
+
case 'member':
|
|
100
|
+
_identifiersIn(expr.object, out, depth + 1);
|
|
101
|
+
if (expr.prop) out.push(String(expr.prop));
|
|
102
|
+
return;
|
|
103
|
+
case 'binary':
|
|
104
|
+
case 'logical':
|
|
105
|
+
_identifiersIn(expr.left, out, depth + 1);
|
|
106
|
+
_identifiersIn(expr.right, out, depth + 1);
|
|
107
|
+
return;
|
|
108
|
+
case 'tpl':
|
|
109
|
+
for (const p of expr.parts || []) _identifiersIn(p, out, depth + 1);
|
|
110
|
+
return;
|
|
111
|
+
case 'call':
|
|
112
|
+
_identifiersIn(expr.callee, out, depth + 1);
|
|
113
|
+
for (const a of expr.args || []) _identifiersIn(a, out, depth + 1);
|
|
114
|
+
return;
|
|
115
|
+
case 'array':
|
|
116
|
+
for (const e of expr.elements || []) _identifiersIn(e, out, depth + 1);
|
|
117
|
+
return;
|
|
118
|
+
case 'object':
|
|
119
|
+
for (const p of expr.props || []) _identifiersIn(p.value, out, depth + 1);
|
|
120
|
+
return;
|
|
121
|
+
case 'union':
|
|
122
|
+
for (const b of expr.branches || []) _identifiersIn(b, out, depth + 1);
|
|
123
|
+
return;
|
|
124
|
+
default:
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function _argsToTextArgs(args) {
|
|
130
|
+
return (args || []).map(a => {
|
|
131
|
+
const names = [];
|
|
132
|
+
_identifiersIn(a, names);
|
|
133
|
+
return { text: names.join(' ') };
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// A TSQualifiedName is `A.B.C` (e.g. `Foo.Bar`) — dot-join it the same way
|
|
138
|
+
// _calleeToString dot-joins a member expression above.
|
|
139
|
+
function _qualifiedTypeName(node, depth = 0) {
|
|
140
|
+
if (!node || depth > MAX_TS_TYPE_DEPTH) return null;
|
|
141
|
+
if (node.type === 'Identifier') return node.name || null;
|
|
142
|
+
if (node.type === 'TSQualifiedName') {
|
|
143
|
+
const left = _qualifiedTypeName(node.left, depth + 1);
|
|
144
|
+
const right = node.right?.name || null;
|
|
145
|
+
if (!left || !right) return left || right;
|
|
146
|
+
return `${left}.${right}`;
|
|
147
|
+
}
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Renders a TSType AST node to a short, human-readable string — never a
|
|
153
|
+
* best-effort re-render of the whole type, just enough to be a real,
|
|
154
|
+
* non-fabricated signal. Anything not recognized (mapped types, conditional
|
|
155
|
+
* types, indexed access, deeply nested generics past the depth guard) is
|
|
156
|
+
* `null`, exactly like every other "don't know" in this module — no type
|
|
157
|
+
* string is ever invented for a shape this doesn't genuinely understand.
|
|
158
|
+
*/
|
|
159
|
+
function _tsTypeToString(node, depth = 0) {
|
|
160
|
+
if (!node || depth > MAX_TS_TYPE_DEPTH) return null;
|
|
161
|
+
switch (node.type) {
|
|
162
|
+
case 'TSStringKeyword': return 'string';
|
|
163
|
+
case 'TSNumberKeyword': return 'number';
|
|
164
|
+
case 'TSBooleanKeyword': return 'boolean';
|
|
165
|
+
case 'TSAnyKeyword': return 'any';
|
|
166
|
+
case 'TSUnknownKeyword': return 'unknown';
|
|
167
|
+
case 'TSVoidKeyword': return 'void';
|
|
168
|
+
case 'TSNullKeyword': return 'null';
|
|
169
|
+
case 'TSUndefinedKeyword': return 'undefined';
|
|
170
|
+
case 'TSNeverKeyword': return 'never';
|
|
171
|
+
case 'TSObjectKeyword': return 'object';
|
|
172
|
+
case 'TSBigIntKeyword': return 'bigint';
|
|
173
|
+
case 'TSSymbolKeyword': return 'symbol';
|
|
174
|
+
case 'TSTypeReference': {
|
|
175
|
+
const name = node.typeName?.type === 'Identifier'
|
|
176
|
+
? node.typeName.name
|
|
177
|
+
: _qualifiedTypeName(node.typeName, depth + 1);
|
|
178
|
+
if (!name) return null;
|
|
179
|
+
// Babel's current AST names this `typeArguments`; `typeParameters` was
|
|
180
|
+
// the property on older @babel/parser versions for the same node —
|
|
181
|
+
// checking both means this doesn't silently go generic-blind on a
|
|
182
|
+
// dependency bump either direction.
|
|
183
|
+
const paramNodes = (node.typeArguments || node.typeParameters)?.params || [];
|
|
184
|
+
const args = paramNodes.map(p => _tsTypeToString(p, depth + 1)).filter(Boolean);
|
|
185
|
+
return (args.length && args.length === paramNodes.length) ? `${name}<${args.join(', ')}>` : name;
|
|
186
|
+
}
|
|
187
|
+
case 'TSArrayType': {
|
|
188
|
+
const el = _tsTypeToString(node.elementType, depth + 1);
|
|
189
|
+
return el ? `${el}[]` : null;
|
|
190
|
+
}
|
|
191
|
+
case 'TSUnionType': {
|
|
192
|
+
const parts = (node.types || []).map(t => _tsTypeToString(t, depth + 1)).filter(Boolean);
|
|
193
|
+
return parts.length === (node.types || []).length ? parts.join(' | ') : null;
|
|
194
|
+
}
|
|
195
|
+
case 'TSLiteralType': {
|
|
196
|
+
const lit = node.literal;
|
|
197
|
+
if (lit?.type === 'StringLiteral') return JSON.stringify(lit.value);
|
|
198
|
+
if (lit?.type === 'NumericLiteral') return String(lit.value);
|
|
199
|
+
if (lit?.type === 'BooleanLiteral') return String(lit.value);
|
|
200
|
+
return null;
|
|
201
|
+
}
|
|
202
|
+
default: return null;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* FR-401's "types" element, TypeScript-only. A separate, read-only parse of
|
|
208
|
+
* the real source — never the transformed AST ir/parser-js.js builds its IR
|
|
209
|
+
* from, and never a mutation of anything that module owns. `parseSync` only
|
|
210
|
+
* parses; `@babel/preset-typescript`'s stripping transform never runs, so
|
|
211
|
+
* `.ts`/`.tsx` type annotations are fully intact on the returned AST.
|
|
212
|
+
*
|
|
213
|
+
* Keyed by the real source line a parameter or declarator's IDENTIFIER sits
|
|
214
|
+
* on (matching how ir/parser-js.js's own IR already records a parameter's
|
|
215
|
+
* line as its enclosing function's start line, and an assignment's line as
|
|
216
|
+
* its own CFG node's line) so `_adaptFile` can look a type up by the exact
|
|
217
|
+
* (line, name) pair it already has, without needing any cross-parse node
|
|
218
|
+
* identity.
|
|
219
|
+
*
|
|
220
|
+
* @returns {Map<number, Map<string,string>>} line -> (name -> type string)
|
|
221
|
+
*/
|
|
222
|
+
function _extractTsTypes(file, content) {
|
|
223
|
+
const out = new Map();
|
|
224
|
+
if (!/\.tsx?$/i.test(file) || typeof content !== 'string' || content.length > 500_000) return out;
|
|
225
|
+
const record = (name, line, typeAnnotationNode) => {
|
|
226
|
+
if (!name || !line || !typeAnnotationNode) return;
|
|
227
|
+
const t = _tsTypeToString(typeAnnotationNode.typeAnnotation);
|
|
228
|
+
if (!t) return;
|
|
229
|
+
if (!out.has(line)) out.set(line, new Map());
|
|
230
|
+
out.get(line).set(name, t);
|
|
231
|
+
};
|
|
232
|
+
let ast;
|
|
233
|
+
try {
|
|
234
|
+
ast = babelParseSync(content, {
|
|
235
|
+
filename: file,
|
|
236
|
+
presets: [[presetTypescript, { ignoreExtensions: true }]],
|
|
237
|
+
// Mirrors ir/parser-js.js's own parserOpts exactly — accepting the same
|
|
238
|
+
// decorator syntax it accepts, so this separate parse does not reject
|
|
239
|
+
// (and silently lose types from) a file the real IR parser accepts.
|
|
240
|
+
parserOpts: { plugins: ['decorators-legacy', 'decoratorAutoAccessors'] },
|
|
241
|
+
babelrc: false, configFile: false, ast: true, code: false,
|
|
242
|
+
});
|
|
243
|
+
} catch { return out; }
|
|
244
|
+
if (!ast) return out;
|
|
245
|
+
try {
|
|
246
|
+
babelTraverse(ast, {
|
|
247
|
+
Function(path) {
|
|
248
|
+
for (const p of path.node.params || []) {
|
|
249
|
+
const resolved = p.type === 'AssignmentPattern' ? p.left : p;
|
|
250
|
+
if (resolved?.type === 'Identifier' && resolved.typeAnnotation) {
|
|
251
|
+
record(resolved.name, path.node.loc?.start?.line, resolved.typeAnnotation);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
},
|
|
255
|
+
VariableDeclarator(path) {
|
|
256
|
+
const id = path.node.id;
|
|
257
|
+
if (id?.type === 'Identifier' && id.typeAnnotation) {
|
|
258
|
+
record(id.name, path.node.loc?.start?.line, id.typeAnnotation);
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
} catch { /* degrade to whatever was recorded before the failure */ }
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* FR-401's "storage" element. Reindexes the whole-project, field-name-keyed
|
|
268
|
+
* stored-taint registry (engine.js#buildStoredTaintRegistry — already
|
|
269
|
+
* computed once per scan for crossStoredTaint's own cross-file correlation,
|
|
270
|
+
* not recomputed here) down to just the writes that happened in THIS file.
|
|
271
|
+
* `storedRegistry` is `{ [fieldName]: [{file, line, snippet, named}, ...] }`;
|
|
272
|
+
* this returns the flattened, per-file subset as
|
|
273
|
+
* `[{field, line, snippet, named}, ...]`.
|
|
274
|
+
*/
|
|
275
|
+
function _storageForFile(file, storedRegistry) {
|
|
276
|
+
const out = [];
|
|
277
|
+
if (!storedRegistry || typeof storedRegistry !== 'object') return out;
|
|
278
|
+
for (const [field, writes] of Object.entries(storedRegistry)) {
|
|
279
|
+
for (const w of writes || []) {
|
|
280
|
+
if (w && w.file === file) out.push({ field, line: w.line ?? 0, snippet: w.snippet || '', named: !!w.named });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return out;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* @param {object} fileIR - one entry from ir/index.js#buildProjectIR's `perFile` map: {file, functions, topLevel}
|
|
288
|
+
* @param {Map<number, Map<string,string>>|null} [tsTypes] - `_extractTsTypes`'s
|
|
289
|
+
* output for this same file. Optional: omitted (or no match at a given
|
|
290
|
+
* (line, name)) leaves `type: null`, exactly as before this existed.
|
|
291
|
+
* @returns {{decls: Array, calls: Array}}
|
|
292
|
+
*/
|
|
293
|
+
function _adaptFile(fileIR, tsTypes) {
|
|
294
|
+
const decls = [];
|
|
295
|
+
const calls = [];
|
|
296
|
+
const returns = [];
|
|
297
|
+
const typeAt = (line, name) => tsTypes?.get(line)?.get(name) ?? null;
|
|
298
|
+
for (const fn of fileIR?.functions || []) {
|
|
299
|
+
for (const paramName of fn.params || []) {
|
|
300
|
+
if (typeof paramName === 'string' && paramName) {
|
|
301
|
+
// `kind` genuinely distinguishes a parameter from a later
|
|
302
|
+
// assignment (FR-401's "parameters" vs "assignments" elements) —
|
|
303
|
+
// annotatePrivacyTaint() ignores unknown fields, so this is purely
|
|
304
|
+
// additive for that consumer.
|
|
305
|
+
decls.push({ name: paramName, line: fn.line ?? 0, type: typeAt(fn.line ?? 0, paramName), kind: 'parameter' });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const nodes = fn.cfg?.nodes || {};
|
|
309
|
+
for (const node of Object.values(nodes)) {
|
|
310
|
+
if (!node) continue;
|
|
311
|
+
if (node.kind === 'assign' && node.target) {
|
|
312
|
+
const line = node.line ?? fn.line ?? 0;
|
|
313
|
+
decls.push({ name: node.target, line, type: typeAt(line, node.target), kind: 'assignment' });
|
|
314
|
+
}
|
|
315
|
+
if (node.kind === 'call') {
|
|
316
|
+
calls.push({
|
|
317
|
+
line: node.line ?? fn.line ?? 0,
|
|
318
|
+
fullPath: _calleeToString(node.callee),
|
|
319
|
+
args: _argsToTextArgs(node.args),
|
|
320
|
+
});
|
|
321
|
+
} else if (node.kind === 'assign' && node.source && node.source.kind === 'call') {
|
|
322
|
+
calls.push({
|
|
323
|
+
line: node.line ?? fn.line ?? 0,
|
|
324
|
+
fullPath: _calleeToString(node.source.callee),
|
|
325
|
+
args: _argsToTextArgs(node.source.args),
|
|
326
|
+
});
|
|
327
|
+
} else if (node.kind === 'return' && node.value) {
|
|
328
|
+
// FR-401's "returns" element: every CFG language parser genuinely
|
|
329
|
+
// emits return-kind nodes (confirmed across parser-js/py/java/go/
|
|
330
|
+
// rb/php/cs/kt/cpp), so this is real IR data, not a stub — surfaced
|
|
331
|
+
// here even though annotatePrivacyTaint() (the shallow walker) does
|
|
332
|
+
// not consume it today, matching the adapter's stated job of
|
|
333
|
+
// supplying what the real IR has, not just what today's one
|
|
334
|
+
// consumer happens to read (see D-0052's evidence for the honest
|
|
335
|
+
// caveat: no current caller reads this field yet).
|
|
336
|
+
const names = [];
|
|
337
|
+
_identifiersIn(node.value, names);
|
|
338
|
+
returns.push({ line: node.line ?? fn.line ?? 0, names });
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return { decls, calls, returns };
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Build the flat, per-file map annotatePrivacyTaint() expects, from the real
|
|
347
|
+
* Layer-1 IR plus the raw file contents (for the `_content` field it uses
|
|
348
|
+
* for snippet extraction — the IR itself carries no raw source text).
|
|
349
|
+
*
|
|
350
|
+
* @param {Record<string,object>} perFileIR - ir/index.js#buildProjectIR's `perFile` (or buildProjectIRAsync's)
|
|
351
|
+
* @param {Record<string,string>} fileContents
|
|
352
|
+
* @param {object|null} [storedRegistry] - engine.js#buildStoredTaintRegistry's
|
|
353
|
+
* output, for FR-401's "storage" element. Optional and additive: omitted
|
|
354
|
+
* (or not an object) yields `storage: []` for every file, same
|
|
355
|
+
* degrade-gracefully convention as every other field here — this is not a
|
|
356
|
+
* second copy of the registry's own computation, just a per-file view of
|
|
357
|
+
* the one the engine already builds once per scan.
|
|
358
|
+
* @returns {Map<string, {_content:string, decls:Array, calls:Array, returns:Array, storage:Array}>}
|
|
359
|
+
*/
|
|
360
|
+
export function adaptIRForPrivacyTaint(perFileIR, fileContents, storedRegistry) {
|
|
361
|
+
const out = new Map();
|
|
362
|
+
const fc = fileContents || {};
|
|
363
|
+
for (const [file, content] of Object.entries(fc)) {
|
|
364
|
+
if (typeof content !== 'string') continue;
|
|
365
|
+
const fileIR = perFileIR ? perFileIR[file] : null;
|
|
366
|
+
// FR-401's "types" element: a real, TypeScript-only signal via a
|
|
367
|
+
// separate parse (see _extractTsTypes's own header) — a no-op Map for
|
|
368
|
+
// every non-.ts/.tsx file, so this costs nothing on the common path.
|
|
369
|
+
const tsTypes = _extractTsTypes(file, content);
|
|
370
|
+
const { decls, calls, returns } = fileIR ? _adaptFile(fileIR, tsTypes) : { decls: [], calls: [], returns: [] };
|
|
371
|
+
const storage = _storageForFile(file, storedRegistry);
|
|
372
|
+
out.set(file, { _content: content, decls, calls, returns, storage });
|
|
373
|
+
}
|
|
374
|
+
return out;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
export const _internals = {
|
|
378
|
+
_calleeToString, _identifiersIn, _argsToTextArgs, _adaptFile, _storageForFile,
|
|
379
|
+
_tsTypeToString, _qualifiedTypeName, _extractTsTypes,
|
|
380
|
+
};
|