@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5
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 +266 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +779 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +2 -2
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/{271.index.js → 2271.index.js} +2 -2
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +379 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +5 -5
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +5 -5
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +2 -2
- package/dist/5051.index.js +841 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +3 -3
- package/dist/{449.index.js → 5830.index.js} +2 -2
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +3 -3
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +560 -189
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/dist/frontend/index.html +21 -0
- package/dist/frontend/src/app.js +176 -0
- package/dist/frontend/src/components/evidence-inspector.js +141 -0
- package/dist/frontend/src/components/filter-rail.js +119 -0
- package/dist/frontend/src/components/query-bar.js +126 -0
- package/dist/frontend/src/data/flagship-graph.js +1460 -0
- package/dist/frontend/src/export-entry.js +36 -0
- package/dist/frontend/src/lib/api-client.js +92 -0
- package/dist/frontend/src/lib/contrast.js +34 -0
- package/dist/frontend/src/lib/dom.js +24 -0
- package/dist/frontend/src/lib/escape-html.js +16 -0
- package/dist/frontend/src/lib/flow-path.js +40 -0
- package/dist/frontend/src/lib/focus-controls.js +149 -0
- package/dist/frontend/src/lib/protection-visual.js +46 -0
- package/dist/frontend/src/lib/query-language.js +240 -0
- package/dist/frontend/src/lib/row-filters.js +43 -0
- package/dist/frontend/src/lib/state.js +84 -0
- package/dist/frontend/src/main.js +83 -0
- package/dist/frontend/src/shell.js +184 -0
- package/dist/frontend/src/views/architecture-view.js +798 -0
- package/dist/frontend/src/views/inventory-view.js +292 -0
- package/dist/frontend/src/views/privacy-view.js +172 -0
- package/dist/frontend/src/views/trace-view.js +206 -0
- package/dist/frontend/styles/architecture-view.css +93 -0
- package/dist/frontend/styles/filter-rail.css +34 -0
- package/dist/frontend/styles/inspector.css +69 -0
- package/dist/frontend/styles/inventory-view.css +74 -0
- package/dist/frontend/styles/privacy-view.css +86 -0
- package/dist/frontend/styles/query-bar.css +107 -0
- package/dist/frontend/styles/shell.css +155 -0
- package/dist/frontend/styles/tokens.css +128 -0
- package/dist/frontend/styles/trace-view.css +95 -0
- package/package.json +13 -6
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +92 -1
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +19 -0
- package/src/posture/artifact-registry.js +52 -0
- package/src/posture/auditor-walkthrough.js +76 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +144 -0
- package/src/shared/frontend-root.js +52 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
|
@@ -0,0 +1,427 @@
|
|
|
1
|
+
//
|
|
2
|
+
// sink-registry.js — Data Flow Explorer, Sub-project D, increment D3.
|
|
3
|
+
//
|
|
4
|
+
// Reclassifies TWO separate catalogs into DataFlowGraph v1's
|
|
5
|
+
// `SINK_CATEGORIES` / `coverageStatus` vocabulary (`schema.js`):
|
|
6
|
+
// - scanner/src/dataflow/catalog.js's `kind: 'sink'` entries (194 as of
|
|
7
|
+
// this writing — re-derive via `CATALOG.filter(e => e.kind ===
|
|
8
|
+
// 'sink').length` rather than trusting this comment; the
|
|
9
|
+
// dataflow/CLAUDE.md count has already drifted twice), keyed on
|
|
10
|
+
// `vuln.cwe` — reclassifySink(entry).
|
|
11
|
+
// - scanner/src/dataflow/privacy-catalog.js's PRIVACY_SINK_CATALOG (18
|
|
12
|
+
// entries), keyed on `category` — reclassifyPrivacySink(entry).
|
|
13
|
+
//
|
|
14
|
+
// TWO separate functions, deliberately, not a unified dispatcher: the two
|
|
15
|
+
// catalogs key on different fields (DESIGN_REGISTRIES.md §2.1), and
|
|
16
|
+
// privacy-catalog.js is deliberately NOT merged into CATALOG (merging it
|
|
17
|
+
// would make every already-active general source spuriously trigger
|
|
18
|
+
// privacy-leak findings — see that file's own header). Reclassifying the two
|
|
19
|
+
// through separate functions respects that boundary; a unified dispatcher
|
|
20
|
+
// would have to re-merge them internally to work at all.
|
|
21
|
+
//
|
|
22
|
+
// This module is the MECHANICAL implementation of a binding, independently
|
|
23
|
+
// reviewed design — it is not a fresh design decision (except where noted
|
|
24
|
+
// below for FR-203, which the design specifies field-by-field but not as
|
|
25
|
+
// runnable code). Every mapping row traces to `DESIGN_REGISTRIES.md`:
|
|
26
|
+
// §5.1 — CWE_MAP, the primary sink-side mapping (keyed on `vuln.cwe`,
|
|
27
|
+
// 21 rows — 20 from D1, plus CWE-201 added by Sub-project H's
|
|
28
|
+
// AC-07 closure).
|
|
29
|
+
// §5.2 — the CWE-79 `framework` refinement: DOM/React sinks are
|
|
30
|
+
// `client-storage` (partial); every other framework is
|
|
31
|
+
// `http-response` (modeled).
|
|
32
|
+
// §5.3 — PRIVACY_CATEGORY_MAP, the privacy-catalog mapping (9 rows).
|
|
33
|
+
// `thirdPartySdk` is the one entry that DOES split at match time
|
|
34
|
+
// and cannot be resolved by a registry at all — see item 4 below.
|
|
35
|
+
// §6 — the `coverageStatus` decision procedure (modeled/partial/
|
|
36
|
+
// candidate/unsupported — never `manual`, §6.5).
|
|
37
|
+
// §7.1 — CATEGORY_NODE_KIND: sink-side node kind is category-derived,
|
|
38
|
+
// NOT uniformly `'sink'` (unlike D2's uniform `'source'`). A
|
|
39
|
+
// `null` category (`unsupported`) always yields `'process'`, and
|
|
40
|
+
// `kind === 'process'` iff `coverageStatus === 'unsupported'`
|
|
41
|
+
// (D1/3c's biconditional — contingent, per §7.1's own boxed note,
|
|
42
|
+
// on no AI-sink detection existing yet; not this task's problem
|
|
43
|
+
// to fix, but this module's own FR-203 branch below must not
|
|
44
|
+
// accidentally break it).
|
|
45
|
+
// §7.5 — CATEGORY_EXTERNALITY: category-derived, with the null-category
|
|
46
|
+
// (`unsupported`/`process`) fallback hardcoded to `'internal'` —
|
|
47
|
+
// the asymmetric OPPOSITE of source-registry.js's own
|
|
48
|
+
// unsupported fallback (`'unknown'`), per §7.5's own
|
|
49
|
+
// "2026-08-31 implementation note (D2, Task 1)" asymmetry #1: a
|
|
50
|
+
// sink's unsupported call site is still IN this program
|
|
51
|
+
// (genuinely internal); a source's unsupported construct is one
|
|
52
|
+
// we don't even understand (no honest internal claim to make).
|
|
53
|
+
// §9.0 — the shared `{kind, category, coverageStatus, externality,
|
|
54
|
+
// reason}` decision shape. `category` is this module's OWN field
|
|
55
|
+
// name — never a field literally called `subtype` (that's
|
|
56
|
+
// Sub-project E's graph builder's job, writing this decision's
|
|
57
|
+
// `category` onto `node.subtype`).
|
|
58
|
+
// §9's D3 checklist item 3 — the `unsupported` → `process` node with its
|
|
59
|
+
// reason string must be PRESERVED, never dropped (the single
|
|
60
|
+
// easiest way to violate AC-11/FR-201, per that item's own text).
|
|
61
|
+
// §9's D3 checklist item 4 — `thirdPartySdk` is a known, disclosed open
|
|
62
|
+
// item: resolves to `analytics` (the plurality of its receiver
|
|
63
|
+
// alternation) with `coverageStatus: 'partial'`, never a silent,
|
|
64
|
+
// undisclosed guess. A later match-time consumer that knows which
|
|
65
|
+
// receiver actually matched (segment/amplitude/mixpanel/posthog →
|
|
66
|
+
// analytics; sentry/datadog → monitoring; stripe → external-api;
|
|
67
|
+
// intercom/braze → collaboration) can promote `partial` →
|
|
68
|
+
// `modeled` — this registry cannot, since it reclassifies
|
|
69
|
+
// ENTRIES, never match-site receivers.
|
|
70
|
+
// §9's D3 checklist item 5 — FR-203: closed below. See the dedicated
|
|
71
|
+
// comment on `reclassifySink`'s `opts.destinationUnresolved`
|
|
72
|
+
// branch for the full reasoning, including how it is kept
|
|
73
|
+
// DISTINCT from the unrelated, already-shipped §16.7
|
|
74
|
+
// degraded-analysis `unresolved` case (a different module
|
|
75
|
+
// entirely — a future Sub-project E graph builder reading
|
|
76
|
+
// `path-store.js` diagnostics, never this registry).
|
|
77
|
+
// DISCLOSED ASYMMETRY: `reclassifyPrivacySink` has no equivalent
|
|
78
|
+
// `opts.destinationUnresolved` parameter. Item 5's own text names
|
|
79
|
+
// only `reclassifySink`; nothing in the design scopes FR-203 out
|
|
80
|
+
// of the privacy side, and the design's own worked examples
|
|
81
|
+
// (a fetch() call with a computed URL) map onto privacy
|
|
82
|
+
// categories like `outboundHttp`/`thirdPartySdk` just as
|
|
83
|
+
// plausibly as onto a general CATALOG sink. Left unresolved here,
|
|
84
|
+
// not silently — no consumer exists yet to need it (Sub-project E
|
|
85
|
+
// hasn't landed), so there was nothing to prove this signature
|
|
86
|
+
// against; closing it is deferred to whichever increment adds
|
|
87
|
+
// the first real caller.
|
|
88
|
+
//
|
|
89
|
+
// Pure reclassification layer: reads catalog entries as DATA and maps them
|
|
90
|
+
// onto the target vocabulary. It never re-derives what a call site
|
|
91
|
+
// matches — `matchSinkOrSanitizer()` / `matchPrivacySink()` already own
|
|
92
|
+
// that, and duplicating it here would fork a matcher the corpus proves
|
|
93
|
+
// against one that nothing proves (DESIGN_REGISTRIES.md §1).
|
|
94
|
+
//
|
|
95
|
+
// Isolation: imports ONLY `CATALOG` from ../dataflow/catalog.js and the
|
|
96
|
+
// privacy catalog's own exports from ../dataflow/privacy-catalog.js.
|
|
97
|
+
// Never dataflow/engine.js, never dataflow/summaries.js, never matcher
|
|
98
|
+
// internals (this PRD's §18.1 isolation principle, the same basis
|
|
99
|
+
// source-registry.js and Sub-project A/B/C use for their own `dataflow/`
|
|
100
|
+
// reuse).
|
|
101
|
+
|
|
102
|
+
import { CATALOG } from '../dataflow/catalog.js';
|
|
103
|
+
import { PRIVACY_SINK_CATALOG } from '../dataflow/privacy-catalog.js';
|
|
104
|
+
import { ORM_WRITE_CATALOG } from '../dataflow/orm-write-catalog.js';
|
|
105
|
+
|
|
106
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
107
|
+
// §5.1 — CWE_MAP: the primary sink-side mapping, keyed on the entry's
|
|
108
|
+
// `vuln.cwe` field (21 rows). A `null` category is NOT a dropped entry —
|
|
109
|
+
// see §6.4: it is retained with node kind `process` and a stated reason,
|
|
110
|
+
// which is what AC-11 / FR-201 require.
|
|
111
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
112
|
+
|
|
113
|
+
export const CWE_MAP = Object.freeze({
|
|
114
|
+
'CWE-89': Object.freeze({ category: 'database', status: 'modeled', why: 'a SQL query call is unambiguously a database destination' }),
|
|
115
|
+
'CWE-943': Object.freeze({ category: 'database', status: 'modeled', why: 'NoSQL $where — still a database destination' }),
|
|
116
|
+
'CWE-22': Object.freeze({ category: 'file', status: 'modeled', why: 'path traversal sinks are filesystem reads/writes' }),
|
|
117
|
+
'CWE-73': Object.freeze({ category: 'file', status: 'modeled', why: 'arbitrary file write' }),
|
|
118
|
+
'CWE-918': Object.freeze({ category: 'external-api', status: 'modeled', why: 'SSRF sinks are outbound HTTP client calls' }),
|
|
119
|
+
'CWE-601': Object.freeze({ category: 'http-response', status: 'modeled', why: 'a redirect is written as a response header' }),
|
|
120
|
+
'CWE-113': Object.freeze({ category: 'http-response', status: 'modeled', why: 'response splitting — the sink IS the response header writer' }),
|
|
121
|
+
// The sink side's ONE documented refinement (§5.2). `status: 'split'` is
|
|
122
|
+
// an INTERNAL marker only — reclassifySink() intercepts it before it can
|
|
123
|
+
// ever reach a caller as a coverageStatus (mirrors PROVENANCE_MAP's own
|
|
124
|
+
// `agent-tool` row in source-registry.js).
|
|
125
|
+
'CWE-79': Object.freeze({ category: null, status: 'split', why: "REFINED ON `framework`: browser-DOM sinks and server-side response writers are different destinations" }),
|
|
126
|
+
'CWE-90': Object.freeze({ category: 'database', status: 'candidate', why: 'an LDAP directory is a queryable store, structurally like a DB, but FR-201 never names directory services' }),
|
|
127
|
+
// AC-07 closure (Sub-project H): the AI-sink catalog bridge. CWE-201
|
|
128
|
+
// (Insertion of Sensitive Information Into Sent Data) is the general
|
|
129
|
+
// CATALOG's own key for the four OpenAI/Anthropic/Bedrock sink entries.
|
|
130
|
+
// Deliberately NOT CWE-359 — that CWE belongs exclusively to
|
|
131
|
+
// PRIVACY_SINK_CATALOG's "Privacy Leak" family, and this file's own
|
|
132
|
+
// `completeness/1c` test fails the build if CWE_MAP ever maps it.
|
|
133
|
+
'CWE-201': Object.freeze({ category: 'ai-model-provider', status: 'modeled', why: 'a call to a named AI model provider SDK (OpenAI/Anthropic/Bedrock) is unambiguously an AI-model-provider destination' }),
|
|
134
|
+
// Everything below: the data's destination is IN-PROCESS COMPUTATION (an
|
|
135
|
+
// interpreter, parser, template engine, regex engine, loader, or raw
|
|
136
|
+
// memory). FR-201's category list is an EGRESS taxonomy and models none
|
|
137
|
+
// of them — the single largest structural finding of D1 (§3/§7.1).
|
|
138
|
+
'CWE-78': Object.freeze({ category: null, status: 'unsupported', why: 'shell/process execution — no FR-201 category models process execution' }),
|
|
139
|
+
'CWE-95': Object.freeze({ category: null, status: 'unsupported', why: 'code evaluation (eval/Function/exec/compile) — destination is an interpreter' }),
|
|
140
|
+
'CWE-94': Object.freeze({ category: null, status: 'unsupported', why: 'code injection / template compilation — destination is an interpreter or template engine' }),
|
|
141
|
+
'CWE-1336': Object.freeze({ category: null, status: 'unsupported', why: 'SSTI — destination is a template engine, not a data destination' }),
|
|
142
|
+
'CWE-502': Object.freeze({ category: null, status: 'unsupported', why: 'deserialization — destination is a deserializer' }),
|
|
143
|
+
'CWE-611': Object.freeze({ category: null, status: 'unsupported', why: 'XXE — destination is an XML parser' }),
|
|
144
|
+
'CWE-643': Object.freeze({ category: null, status: 'unsupported', why: 'XPath injection — destination is a query engine over an in-memory document' }),
|
|
145
|
+
'CWE-120': Object.freeze({ category: null, status: 'unsupported', why: 'buffer overflow — destination is raw memory' }),
|
|
146
|
+
'CWE-787': Object.freeze({ category: null, status: 'unsupported', why: 'out-of-bounds write — destination is raw memory' }),
|
|
147
|
+
'CWE-1333': Object.freeze({ category: null, status: 'unsupported', why: 'ReDoS — destination is the regex engine' }),
|
|
148
|
+
'CWE-114': Object.freeze({ category: null, status: 'unsupported', why: 'untrusted library load — destination is the dynamic loader' }),
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
// §5.2 — CWE-79's refinement key. FR-201 lumps "browser DOM or client
|
|
152
|
+
// storage" into one SINK_CATEGORIES value (`client-storage`), which
|
|
153
|
+
// correctly targets the PRD's own bullet but under-names the DOM half —
|
|
154
|
+
// disclosed via `status: 'partial'`, never silently upgraded to `modeled`
|
|
155
|
+
// (DESIGN_REGISTRIES.md §7.4(b); not this task's to patch).
|
|
156
|
+
export const DOM_FRAMEWORKS = new Set(['dom', 'react']);
|
|
157
|
+
|
|
158
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
159
|
+
// §7.1 — CATEGORY_NODE_KIND: sink category → node kind. Every sink category
|
|
160
|
+
// any catalog can currently produce, plus the in-process fallback
|
|
161
|
+
// (`null` category → `'process'`, handled directly in the functions below,
|
|
162
|
+
// not via this table).
|
|
163
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
export const CATEGORY_NODE_KIND = Object.freeze({
|
|
166
|
+
'log': 'log', 'stdout': 'log',
|
|
167
|
+
'http-response': 'sink',
|
|
168
|
+
'database': 'store', 'file': 'store', 'object-storage': 'store',
|
|
169
|
+
'cache': 'store', 'client-storage': 'store', 'backup': 'store', 'export': 'store',
|
|
170
|
+
'queue': 'queue',
|
|
171
|
+
'external-api': 'external', 'webhook': 'external', 'email': 'external',
|
|
172
|
+
'sms': 'external', 'push-notification': 'external', 'collaboration': 'external',
|
|
173
|
+
'analytics': 'external', 'monitoring': 'external',
|
|
174
|
+
'ai-model-provider': 'external', 'ai-agent': 'external', 'ai-tool': 'external',
|
|
175
|
+
'ai-vector-store': 'external', 'ai-memory': 'external', 'ai-training': 'external',
|
|
176
|
+
'ai-evaluation': 'external', 'ai-telemetry': 'external',
|
|
177
|
+
'ai-local-model': 'process',
|
|
178
|
+
'declared': 'sink',
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
182
|
+
// §7.5 — CATEGORY_EXTERNALITY: sink category → externality. Fully specified
|
|
183
|
+
// by D1 (unlike source-registry.js's SOURCE_CATEGORY_EXTERNALITY, which D2
|
|
184
|
+
// had to build fresh — see that module's own header for why). The
|
|
185
|
+
// null-category (`unsupported`/`process`) case is handled directly in the
|
|
186
|
+
// functions below as a hardcoded `'internal'`, not via this table — a
|
|
187
|
+
// sink's unsupported call site is still IN this program, genuinely
|
|
188
|
+
// internal, the asymmetric opposite of the source side's `'unknown'`.
|
|
189
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
190
|
+
|
|
191
|
+
export const CATEGORY_EXTERNALITY = Object.freeze({
|
|
192
|
+
'log': 'internal', 'stdout': 'internal', 'http-response': 'internal',
|
|
193
|
+
'file': 'internal', 'client-storage': 'internal',
|
|
194
|
+
'database': 'unknown', 'object-storage': 'unknown', 'cache': 'unknown',
|
|
195
|
+
'queue': 'unknown', 'backup': 'unknown', 'export': 'unknown',
|
|
196
|
+
'external-api': 'external', 'webhook': 'external', 'email': 'external',
|
|
197
|
+
'sms': 'external', 'push-notification': 'external', 'collaboration': 'external',
|
|
198
|
+
'analytics': 'external', 'monitoring': 'external',
|
|
199
|
+
'ai-model-provider': 'external', 'ai-agent': 'external', 'ai-tool': 'external',
|
|
200
|
+
'ai-vector-store': 'external', 'ai-memory': 'external', 'ai-training': 'external',
|
|
201
|
+
'ai-evaluation': 'external', 'ai-telemetry': 'external', 'ai-local-model': 'internal',
|
|
202
|
+
'declared': 'unknown',
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
206
|
+
// §5.3 — PRIVACY_CATEGORY_MAP: the privacy-catalog mapping, keyed on the
|
|
207
|
+
// entry's `category` field (9 rows — confirmed against the live
|
|
208
|
+
// privacy-catalog.js's PRIVACY_SINK_CATEGORIES, NINE, not the eight the
|
|
209
|
+
// scoping doc originally assumed; see this module's own test suite's
|
|
210
|
+
// completeness guard).
|
|
211
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
212
|
+
|
|
213
|
+
export const PRIVACY_CATEGORY_MAP = Object.freeze({
|
|
214
|
+
'log': Object.freeze({ category: 'log', status: 'modeled', why: 'console/logger calls' }),
|
|
215
|
+
'response': Object.freeze({ category: 'http-response', status: 'modeled', why: 'res.send / res.json' }),
|
|
216
|
+
'outboundHttp': Object.freeze({ category: 'external-api', status: 'modeled', why: 'fetch / axios.post' }),
|
|
217
|
+
'fileWrite': Object.freeze({ category: 'file', status: 'modeled', why: 'fs.writeFile / writeFileSync' }),
|
|
218
|
+
's3Upload': Object.freeze({ category: 'object-storage', status: 'modeled', why: 's3.putObject' }),
|
|
219
|
+
'emailSend': Object.freeze({ category: 'email', status: 'modeled', why: 'nodemailer sendMail' }),
|
|
220
|
+
'storage': Object.freeze({ category: 'database', status: 'modeled', why: 'both current entries are mongodb insertOne/insertMany with a collection|db|mongo receiver — the name is broader than its entries, but nothing splits TODAY (D1/4b, proven against real entries)' }),
|
|
221
|
+
'queues': Object.freeze({ category: 'queue', status: 'modeled', why: "sqs sendMessage / sns|kafka publish; FR-201's `queue` covers 'queues, topics, streams, and event buses'" }),
|
|
222
|
+
// §9's D3 checklist item 4: a KNOWN, DISCLOSED open item, never a silent
|
|
223
|
+
// guess. The single receiverTypeIn alternation
|
|
224
|
+
// (stripe|sentry|datadog|segment|amplitude|mixpanel|posthog|braze|
|
|
225
|
+
// intercom|analytics) spans FOUR SINK_CATEGORIES values — analytics
|
|
226
|
+
// (segment/amplitude/mixpanel/posthog), monitoring (sentry/datadog),
|
|
227
|
+
// external-api (stripe), collaboration (intercom/braze) — and which one
|
|
228
|
+
// is correct is only decidable AT MATCH TIME, from the receiver that
|
|
229
|
+
// actually matched: information a registry reclassifying ENTRIES never
|
|
230
|
+
// sees. Resolves to `analytics` (the plurality) with `coverageStatus:
|
|
231
|
+
// 'partial'`, so the ambiguity is visible on every node this produces,
|
|
232
|
+
// never hidden behind a confident `modeled`. A later match-time consumer
|
|
233
|
+
// that knows which receiver fired can promote `partial` → `modeled`.
|
|
234
|
+
'thirdPartySdk': Object.freeze({ category: 'analytics', status: 'partial', why: 'DISCLOSED OPEN ITEM, not a silent guess: the receiverTypeIn alternation also matches sentry|datadog (monitoring), stripe (external-api) and intercom|braze (collaboration) — the true category is only decidable at MATCH TIME from the matched receiver, which a registry reclassifying ENTRIES never sees. A match-time consumer that knows the matched receiver can promote this partial to modeled.' }),
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Reclassify a single `kind: 'sink'` catalog entry (from CATALOG) into
|
|
239
|
+
* DataFlowGraph v1's vocabulary. Total: every entry gets a decision, none
|
|
240
|
+
* throws, none is silently dropped (§6). `kind` is category-derived, NOT
|
|
241
|
+
* uniformly `'sink'` (§7.1) — a `null` category (`unsupported`) always
|
|
242
|
+
* yields `'process'`, never dropped, always carrying a non-empty `reason`
|
|
243
|
+
* (§9's D3 item 3 / AC-11).
|
|
244
|
+
*
|
|
245
|
+
* @param {object} entry a CATALOG entry with `kind === 'sink'`
|
|
246
|
+
* @param {object} [opts]
|
|
247
|
+
* @param {boolean} [opts.destinationUnresolved] FR-203 (§9's D3 item 5):
|
|
248
|
+
* set by a FUTURE per-call-site caller (Sub-project E's graph builder —
|
|
249
|
+
* no catalog entry carries per-call-site destination information, §7.5,
|
|
250
|
+
* so this can never be true from CATALOG data alone today) when the
|
|
251
|
+
* RECOGNIZED sink's actual destination could not be statically resolved
|
|
252
|
+
* (`fetch(url)` with a computed `url`; an SDK client built from config).
|
|
253
|
+
* Ignored when the entry's own category is unmapped (`null`) — see the
|
|
254
|
+
* branch below for why.
|
|
255
|
+
* @param {string} [opts.blockingExpression] the expression that prevented
|
|
256
|
+
* resolution, named in the returned `reason` (FR-203 requires the
|
|
257
|
+
* evidence panel to show it).
|
|
258
|
+
* @returns {{kind: string, category: string|null, coverageStatus: string, externality: string, reason: string}}
|
|
259
|
+
*/
|
|
260
|
+
export function reclassifySink(entry, opts = {}) {
|
|
261
|
+
opts = opts ?? {}; // guard an explicit `null` too, not just `undefined`
|
|
262
|
+
const cwe = entry.vuln?.cwe ?? '<<none>>';
|
|
263
|
+
let row = CWE_MAP[cwe];
|
|
264
|
+
if (!row) {
|
|
265
|
+
// Defensive fallback only — D1/1c-equivalent completeness guard below
|
|
266
|
+
// proves every real CATALOG sink CWE is mapped, so this is unreachable
|
|
267
|
+
// from live data. A future genuinely-new CWE must fail that guard
|
|
268
|
+
// loudly, not this function silently.
|
|
269
|
+
return {
|
|
270
|
+
kind: 'process', category: null, coverageStatus: 'unsupported',
|
|
271
|
+
externality: 'internal', reason: `unmapped CWE ${cwe}`,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
if (row.status === 'split') {
|
|
275
|
+
// The single documented refinement on the sink side (§5.2).
|
|
276
|
+
row = DOM_FRAMEWORKS.has(entry.framework)
|
|
277
|
+
? { category: 'client-storage', status: 'partial', why: "LOSSY: the destination is the rendered browser DOM; schema.js's `client-storage` is its encoding of FR-201's 'browser DOM or client storage' bullet, and under-names the DOM half" }
|
|
278
|
+
: { category: 'http-response', status: 'modeled', why: 'a server-side response writer (res.send / PrintWriter / echo / Fprintf)' };
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const category = row.category;
|
|
282
|
+
const kind = category === null ? 'process' : (CATEGORY_NODE_KIND[category] ?? 'sink');
|
|
283
|
+
const externality = category === null ? 'internal' : (CATEGORY_EXTERNALITY[category] ?? 'unknown');
|
|
284
|
+
const base = { kind, category, coverageStatus: row.status, externality, reason: row.why };
|
|
285
|
+
|
|
286
|
+
// FR-203 (§9's D3 item 5) — CLOSED here, and kept structurally distinct
|
|
287
|
+
// from the UNRELATED, already-shipped §16.7 degraded-analysis
|
|
288
|
+
// `unresolved` case (DESIGN_PATH_PROVENANCE.md §16.7 Finding 2, carried
|
|
289
|
+
// forward as binding on Sub-project D; produced elsewhere entirely — a
|
|
290
|
+
// FUTURE Sub-project E graph builder reading path-store.js's
|
|
291
|
+
// `orphanedPeerSources`/context-cap-degraded diagnostics, never this
|
|
292
|
+
// registry). Both cases share `kind: 'unresolved'` and
|
|
293
|
+
// `externality: 'unknown'`, and it would be a real mistake to conflate
|
|
294
|
+
// them (item 5's own text warns against exactly this):
|
|
295
|
+
// - FR-203 (this branch): the CLASSIFICATION succeeded — the registry
|
|
296
|
+
// knows the sink's category from its CWE/framework — but a SPECIFIC
|
|
297
|
+
// call site's destination expression could not be resolved
|
|
298
|
+
// (`fetch(url)` with a computed `url`). `coverageStatus` is
|
|
299
|
+
// UNCHANGED from whatever the category mapping already assigned
|
|
300
|
+
// (`base.coverageStatus` above) — destination resolution is a
|
|
301
|
+
// DIFFERENT AXIS from classification confidence (this item's own
|
|
302
|
+
// opening sentence), so a `modeled` sink whose destination happens
|
|
303
|
+
// to be dynamic stays `modeled`.
|
|
304
|
+
// - §16.7 (NOT this branch, NOT this module): the ANALYSIS itself is
|
|
305
|
+
// incomplete — a truncation-terminal path node with no catalog entry
|
|
306
|
+
// behind it at all, hence `coverageStatus: 'partial'` UNCONDITIONALLY
|
|
307
|
+
// (§16.7's own text: "the analyzer genuinely observed this flow and
|
|
308
|
+
// lost only its continuation" — §6.2's lossy-but-sure shape, not a
|
|
309
|
+
// category-mapping carry-over, since there is no category mapping to
|
|
310
|
+
// carry over).
|
|
311
|
+
// Guarded ONLY when `category !== null`: an unsupported/process sink has
|
|
312
|
+
// no category to retain (`category retained` is FR-203's own explicit
|
|
313
|
+
// requirement, and there is nothing to retain here), and letting this
|
|
314
|
+
// branch fire on a null-category entry would silently break D1/3c's
|
|
315
|
+
// `kind === 'process'` iff `coverageStatus === 'unsupported'`
|
|
316
|
+
// biconditional (coverageStatus would stay `'unsupported'` while kind
|
|
317
|
+
// became `'unresolved'` instead of `'process'`) — a bug this task must
|
|
318
|
+
// not introduce, even though fixing the biconditional's own documented
|
|
319
|
+
// future fragility (§7.1's boxed note, AI-sink detection) is explicitly
|
|
320
|
+
// not this task's problem.
|
|
321
|
+
if (opts.destinationUnresolved && category !== null) {
|
|
322
|
+
return {
|
|
323
|
+
kind: 'unresolved',
|
|
324
|
+
category,
|
|
325
|
+
coverageStatus: row.status,
|
|
326
|
+
externality: 'unknown',
|
|
327
|
+
reason: opts.blockingExpression
|
|
328
|
+
? `destination could not be statically resolved: ${opts.blockingExpression}`
|
|
329
|
+
: 'destination could not be statically resolved to a fixed value',
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
return base;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Reclassify a single privacy-catalog entry (from PRIVACY_SINK_CATALOG) into
|
|
338
|
+
* DataFlowGraph v1's vocabulary. A SEPARATE function from `reclassifySink`,
|
|
339
|
+
* deliberately — see this module's own header for why (§2.1: the two
|
|
340
|
+
* catalogs key on different fields, and privacy-catalog.js is deliberately
|
|
341
|
+
* not merged into CATALOG).
|
|
342
|
+
*
|
|
343
|
+
* @param {object} entry a PRIVACY_SINK_CATALOG entry
|
|
344
|
+
* @returns {{kind: string, category: string|null, coverageStatus: string, externality: string, reason: string}}
|
|
345
|
+
*/
|
|
346
|
+
export function reclassifyPrivacySink(entry) {
|
|
347
|
+
const row = PRIVACY_CATEGORY_MAP[entry.category];
|
|
348
|
+
if (!row) {
|
|
349
|
+
// Defensive fallback only — the completeness guard in this module's own
|
|
350
|
+
// test suite proves every real privacy-catalog category is mapped
|
|
351
|
+
// (both against the entries actually used AND against the DECLARED
|
|
352
|
+
// PRIVACY_SINK_CATEGORIES vocabulary), so this is unreachable from live
|
|
353
|
+
// data.
|
|
354
|
+
return {
|
|
355
|
+
kind: 'process', category: null, coverageStatus: 'unsupported',
|
|
356
|
+
externality: 'internal', reason: `unmapped privacy category ${entry.category}`,
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
return {
|
|
360
|
+
kind: CATEGORY_NODE_KIND[row.category] ?? 'sink',
|
|
361
|
+
category: row.category,
|
|
362
|
+
coverageStatus: row.status,
|
|
363
|
+
externality: CATEGORY_EXTERNALITY[row.category] ?? 'unknown',
|
|
364
|
+
reason: row.why,
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Reclassify a single `dataflow/orm-write-catalog.js` entry into
|
|
370
|
+
* DataFlowGraph v1's vocabulary. Milestone 2, Sub-project E, increment 1.
|
|
371
|
+
* A THIRD separate function, deliberately, for the identical reason
|
|
372
|
+
* `reclassifySink`/`reclassifyPrivacySink` are already two separate
|
|
373
|
+
* functions rather than one dispatcher (§2.1 above): `ORM_WRITE_CATALOG`
|
|
374
|
+
* keys on `category` exactly like the privacy catalog does, but it is its
|
|
375
|
+
* own isolated catalog (`orm-write-catalog.js`'s own header explains why),
|
|
376
|
+
* and mirroring `reclassifyPrivacySink`'s shape here — rather than
|
|
377
|
+
* threading a third catalog through that function — keeps each
|
|
378
|
+
* reclassifier reading exactly one catalog's own field convention.
|
|
379
|
+
*
|
|
380
|
+
* `coverageStatus` is the literal string `'candidate'`, UNCONDITIONALLY,
|
|
381
|
+
* computed from nothing — never derived from `entry` the way `reclassifySink`
|
|
382
|
+
* derives it from `CWE_MAP`, since this catalog carries no `vuln.cwe` that
|
|
383
|
+
* drives classification at all (its `vuln` block exists for documentation/
|
|
384
|
+
* potential-future-use only, mirroring `reclassifyPrivacySink`'s own
|
|
385
|
+
* "category drives reclassification, not the vuln block" pattern). Even
|
|
386
|
+
* with both of `orm-write-catalog.js`'s own precision signals satisfied,
|
|
387
|
+
* this is real, disclosed uncertainty — an arbitrary capitalized-identifier
|
|
388
|
+
* receiver could still be a non-ORM builder pattern that happens to take an
|
|
389
|
+
* object-literal argument — so this is deliberately never `'modeled'`.
|
|
390
|
+
*
|
|
391
|
+
* `kind`/`externality` are derived the SAME WAY `reclassifyPrivacySink`
|
|
392
|
+
* derives them (`CATEGORY_NODE_KIND`/`CATEGORY_EXTERNALITY`, keyed on
|
|
393
|
+
* `category: 'database'`) — genuinely mirroring that function's shape,
|
|
394
|
+
* not a hand-copied literal, so this stays correct if either table's
|
|
395
|
+
* `'database'` row is ever revised.
|
|
396
|
+
*
|
|
397
|
+
* @param {object} entry an ORM_WRITE_CATALOG entry
|
|
398
|
+
* @returns {{kind: string, category: string, coverageStatus: 'candidate', externality: string, reason: string}}
|
|
399
|
+
*/
|
|
400
|
+
export function reclassifyOrmWrite(entry) {
|
|
401
|
+
const category = entry?.category ?? 'database';
|
|
402
|
+
return {
|
|
403
|
+
kind: CATEGORY_NODE_KIND[category] ?? 'sink',
|
|
404
|
+
category,
|
|
405
|
+
coverageStatus: 'candidate',
|
|
406
|
+
externality: CATEGORY_EXTERNALITY[category] ?? 'unknown',
|
|
407
|
+
reason: 'heuristic ORM-write recognition (create/save/update/upsert on a bare capitalized receiver with an object-literal first argument) — unconfirmed receiver identity, never full-confidence',
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Re-exported for callers that want the raw sink-entry slice without
|
|
412
|
+
// re-filtering CATALOG themselves (the completeness guards in this
|
|
413
|
+
// module's own test suite are the primary consumer) — mirrors
|
|
414
|
+
// source-registry.js's own SOURCE_ENTRIES precedent.
|
|
415
|
+
export const SINK_ENTRIES = Object.freeze(CATALOG.filter((e) => e.kind === 'sink'));
|
|
416
|
+
|
|
417
|
+
// Re-exported for the same reason, on the privacy side — callers that want
|
|
418
|
+
// the privacy sink entries without importing dataflow/privacy-catalog.js
|
|
419
|
+
// directly. Deliberately NOT a merge with SINK_ENTRIES (see this module's
|
|
420
|
+
// own header).
|
|
421
|
+
export const PRIVACY_SINK_ENTRIES = PRIVACY_SINK_CATALOG;
|
|
422
|
+
|
|
423
|
+
// Re-exported for the same reason, on the ORM-write side (Milestone 2,
|
|
424
|
+
// Sub-project E, increment 1) — callers that want the ORM-write sink
|
|
425
|
+
// entries without importing dataflow/orm-write-catalog.js directly.
|
|
426
|
+
// Deliberately NOT a merge with SINK_ENTRIES/PRIVACY_SINK_ENTRIES.
|
|
427
|
+
export const ORM_WRITE_ENTRIES = ORM_WRITE_CATALOG;
|