@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.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 +139 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +702 -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 +307 -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 +770 -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/package.json +12 -5
- 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 +139 -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,143 @@
|
|
|
1
|
+
# DESIGN_STORE_DETAIL.md — Sub-project E's binding design record (increment 2)
|
|
2
|
+
|
|
3
|
+
**Status:** landed as Milestone 2, Sub-project E, increment **2** — a
|
|
4
|
+
small, focused slice, per
|
|
5
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-e2-plan.md`.
|
|
6
|
+
Binding on later Sub-project E increments the same way `DESIGN_
|
|
7
|
+
DESTINATION_RESOLVER.md` binds Sub-project A — but this record covers its
|
|
8
|
+
own, unrelated field: `node.storeDetail` is specific to database-category
|
|
9
|
+
ORM-write sink nodes, never to be conflated with `node.destination`
|
|
10
|
+
(Sub-project A's own field, for external-api-category nodes).
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## 1. What this increment actually is
|
|
15
|
+
|
|
16
|
+
Increment 1 (`8e733fe9`) shipped ORM-write SITE RECOGNITION only: a call
|
|
17
|
+
site like `User.create({ email: x })` is recognized as an ORM-write sink
|
|
18
|
+
candidate, but nothing about the call is extracted or retained beyond that
|
|
19
|
+
boolean fact — no table name, no operation, no column list. This increment
|
|
20
|
+
extracts FR-204's structured facts from a site increment 1 already
|
|
21
|
+
recognizes, and attaches them to the sink node as a new field,
|
|
22
|
+
`node.storeDetail`.
|
|
23
|
+
|
|
24
|
+
**PRD text (FR-204, verbatim):** *"When supported evidence exists, a
|
|
25
|
+
database sink must include provider, host/config reference, database,
|
|
26
|
+
schema, table/collection, operation, column/field mapping. Unknown portions
|
|
27
|
+
remain `unknown`; the entire store must not be omitted."*
|
|
28
|
+
|
|
29
|
+
No new detection. Every fact extracted here was already in scope at the
|
|
30
|
+
recognized call site — `resolveOrmWriteAtCallSite` already has, in scope,
|
|
31
|
+
the callee expression (whose `object.name` is the model identifier), the
|
|
32
|
+
matched catalog entry's own `callee` (`create`/`save`/`update`/`upsert`),
|
|
33
|
+
and the confirmed object-literal first argument. This increment reads three
|
|
34
|
+
of those facts it was already holding and never recording.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 2. The `storeDetail` object shape
|
|
39
|
+
|
|
40
|
+
```
|
|
41
|
+
{
|
|
42
|
+
provider: string | null, // deferred to a later increment — always null in E2
|
|
43
|
+
host: string | null, // deferred — always null in E2
|
|
44
|
+
database: string | null, // deferred — always null in E2 (DB/cluster name, not the table)
|
|
45
|
+
schema: string | null, // deferred — always null in E2 (SQL schema/namespace)
|
|
46
|
+
table: string | null, // the model/receiver identifier name (e.g. 'User'), or null if unavailable
|
|
47
|
+
operation: string | null, // one of schema.js's STORE_OPERATION_VALUES, or null if unavailable
|
|
48
|
+
columns: string[], // real, non-'*' property key names from the object-literal argument; [] if none
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`table`/`operation`/`columns` are the only fields this increment ever
|
|
53
|
+
populates, and only from JS/TS ORM-write sites recognized by increment 1's
|
|
54
|
+
own catalog. `provider`/`host`/`database`/`schema` stay `null`
|
|
55
|
+
unconditionally in every case this increment produces — this is FR-204's
|
|
56
|
+
own "unknown portions remain unknown" clause in action, not a gap silently
|
|
57
|
+
left to be filled later without saying so.
|
|
58
|
+
|
|
59
|
+
## 3. `table` — the receiver identifier
|
|
60
|
+
|
|
61
|
+
`calleeExpr.object.name`, already confirmed a string by
|
|
62
|
+
`_ormReceiverIsCapitalizedIdent`'s own check inside `orm-write-catalog.js`
|
|
63
|
+
before a site is even recognized. Re-verified defensively at the extraction
|
|
64
|
+
site rather than assumed to have survived unchanged — a `typeof` guard, not
|
|
65
|
+
a re-implementation of that check.
|
|
66
|
+
|
|
67
|
+
## 4. `operation` — the method-name mapping
|
|
68
|
+
|
|
69
|
+
The mapping lives in `graph-builder.js`, not `schema.js` — `schema.js`
|
|
70
|
+
holds enums, not mapping logic, per this package's own established
|
|
71
|
+
separation (the same split `sink-registry.js`'s `CWE_MAP` vs.
|
|
72
|
+
`schema.js`'s `SINK_CATEGORIES` already establishes).
|
|
73
|
+
|
|
74
|
+
| catalog `callee` | `operation` |
|
|
75
|
+
|---|---|
|
|
76
|
+
| `create` | `'create'` |
|
|
77
|
+
| `update` | `'update'` |
|
|
78
|
+
| `upsert` | `'upsert'` |
|
|
79
|
+
| `save` | `'upsert'` |
|
|
80
|
+
|
|
81
|
+
**`save` → `'upsert'`, not `'create'` — a deliberate, disclosed judgment
|
|
82
|
+
call.** Mongoose's `.save()` performs an INSERT on a new document but an
|
|
83
|
+
UPDATE on one loaded from the database, which is genuinely undecidable
|
|
84
|
+
statically from the call site alone; `'upsert'` is the honest umbrella
|
|
85
|
+
covering both, not a guess at which one it is. Any catalog `callee` outside
|
|
86
|
+
this table (there are none today — `ORM_WRITE_CATALOG` has exactly these
|
|
87
|
+
four entries) maps to `null`, never a fabricated guess.
|
|
88
|
+
|
|
89
|
+
## 5. `columns` — the object-literal property keys
|
|
90
|
+
|
|
91
|
+
`arg0.props`, filtered and mapped:
|
|
92
|
+
|
|
93
|
+
```js
|
|
94
|
+
[...new Set(
|
|
95
|
+
arg0.props
|
|
96
|
+
.filter((p) => !p.spread && typeof p.key === 'string' && p.key !== '*')
|
|
97
|
+
.map((p) => p.key),
|
|
98
|
+
)]
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Two exclusions, both load-bearing (per `parser-js.js`'s real
|
|
102
|
+
`ObjectExpression` lowering, confirmed by direct read, ~line 116-159):
|
|
103
|
+
|
|
104
|
+
- **A spread entry** (`{spread: true, value: <expr>}`, no `key` field at
|
|
105
|
+
all — `{...extra}`) has no key to report. Reporting one would be a
|
|
106
|
+
fabrication.
|
|
107
|
+
- **A `'*'`-keyed entry** (a non-literal computed key, `resolveObjectKey`'s
|
|
108
|
+
established convention — `{[dynamicKey]: x}`) is a genuinely UNKNOWN
|
|
109
|
+
column name, not a literal column named `"*"`. Reporting `'*'` as a real
|
|
110
|
+
column name would be the exact fabrication `field-identity.js`'s own
|
|
111
|
+
`object` case already refuses to commit one level up (see
|
|
112
|
+
`scanner/src/lineage/CLAUDE.md`'s "Round 5" note).
|
|
113
|
+
|
|
114
|
+
Deduplicated via `[...new Set(...)]` since two distinct-value properties
|
|
115
|
+
can share a key in real (if unusual) source — `{email: a, email: b}` — and
|
|
116
|
+
without dedup that would report `'email'` twice, a differently-shaped
|
|
117
|
+
version of the same over-counting bug increment 1's own precision work
|
|
118
|
+
elsewhere in this package has repeatedly guarded against.
|
|
119
|
+
|
|
120
|
+
## 6. Wiring — mirrors `destination`'s own precedent exactly
|
|
121
|
+
|
|
122
|
+
`mintNode` gains an optional `storeDetail` param (default `null`).
|
|
123
|
+
`sinkNodeFor` passes `site.storeDetail ?? null` through — the exact wiring
|
|
124
|
+
point `destination` already established. `storeDetail` is set once, at node
|
|
125
|
+
MINT time, and is deliberately **NOT** part of the node identity
|
|
126
|
+
discriminator (`ids.nodeId`'s inputs are unchanged) — the same disclosed
|
|
127
|
+
coarsening `destination` already accepted: two sites colliding onto one
|
|
128
|
+
registry-decision node (same `kind`/`subtypeKey`/`coverageStatus`/
|
|
129
|
+
`externality`) still collide onto one node, and that node's `storeDetail`
|
|
130
|
+
is whichever site's resolution landed first, not a set/union of every
|
|
131
|
+
site's own facts.
|
|
132
|
+
|
|
133
|
+
## 7. Explicitly deferred (named, not silently skipped)
|
|
134
|
+
|
|
135
|
+
- **`provider`/`host`/`database`/`schema` extraction** — needs config-chain
|
|
136
|
+
resolution (Sub-project A's own still-open scope) or a schema-correlation
|
|
137
|
+
mechanism neither this increment nor any prior one builds.
|
|
138
|
+
- **Table/column extraction for raw-SQL `database` sinks** — needs SQL
|
|
139
|
+
parsing, out of scope per the E-scoping doc's own Finding 2. This
|
|
140
|
+
increment is JS/TS ORM-write-site-only.
|
|
141
|
+
- **Python/Java/Go/Ruby/PHP ORM shapes** — increment 1's own JS/TS-only
|
|
142
|
+
scope boundary, unchanged.
|
|
143
|
+
- **Queue/topic mapping** — Sub-project E, increment 3.
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# DESIGN_TRANSIT_PROTECTION.md — Sub-project B's binding design record (increments 1-2)
|
|
2
|
+
|
|
3
|
+
**Status:** increment **1** (plumbing skeleton) and increment **2** (real
|
|
4
|
+
`edge.protection.transit` verdicts, closing AC-03/AC-04) are both landed.
|
|
5
|
+
Increment 1 shipped per
|
|
6
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-b1-plan.md`
|
|
7
|
+
and the scoping correction at
|
|
8
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-b-scoping.md`.
|
|
9
|
+
Increment 2 shipped per
|
|
10
|
+
`docs/superpowers/plans/2026-08-31-data-flow-explorer-m2-subproject-b2-plan.md`
|
|
11
|
+
and that same scoping doc's own "Correction (post-B1, before B2)" section —
|
|
12
|
+
which overrides §2 below's "separate post-pass, touches neither `coverage.js`
|
|
13
|
+
nor `graph-builder.js`" framing for the VERDICT-COMPUTATION job specifically
|
|
14
|
+
(§2 remains correct and unchanged for increment 1's own plumbing job — see
|
|
15
|
+
§6 for exactly what changed and why). Binding on later Sub-project B
|
|
16
|
+
increments (B3, if anything remains once B2 lands) the same way `DESIGN_
|
|
17
|
+
DESTINATION_RESOLVER.md` binds Sub-project A.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## 1. What this increment actually is
|
|
22
|
+
|
|
23
|
+
FR-401 needs a real, computed `edge.protection.transit` verdict for
|
|
24
|
+
network-outbound edges. Before any verdict logic can run, two things had to
|
|
25
|
+
exist that didn't: (1) a way to run `crypto-protocol.js`'s already-proven
|
|
26
|
+
TLS/cipher pattern recognition over the lineage engine's own input, and (2)
|
|
27
|
+
a way for that raw file text to reach the lineage build pipeline at all —
|
|
28
|
+
neither `coverage.js` nor `graph-builder.js` accepts, stores, or threads a
|
|
29
|
+
file's raw source string anywhere in their current signatures (confirmed by
|
|
30
|
+
direct read; see the scoping doc's Finding 2). This increment builds ONLY
|
|
31
|
+
those two things — the plumbing — and proves it end to end. No verdict
|
|
32
|
+
logic. `edge.protection.transit` is not written to anywhere in this
|
|
33
|
+
increment; every edge still gets `emptyProtection()`'s
|
|
34
|
+
`{verdict: 'not_assessed', evidenceGrade: 'none'}` exactly as before.
|
|
35
|
+
|
|
36
|
+
## 2. The isolation decision (Finding 2's Option 2, confirmed buildable and taken)
|
|
37
|
+
|
|
38
|
+
A **separate post-pass module**, `transit-protection.js`, consuming
|
|
39
|
+
`fileContents` directly — never a change to `coverage.js`/`graph-builder.js`
|
|
40
|
+
(the six-increment-old, real, tested pipeline this whole session has been
|
|
41
|
+
careful never to destabilize). `scanTransitEvidence(fileContents)` runs
|
|
42
|
+
`scanCryptoProtocol` once per file and returns a `Map<file, findings[]>` —
|
|
43
|
+
genuinely reusable here (unlike, say, an ORM-write catalog reuse case)
|
|
44
|
+
because the signal needed is coarser: "was TLS verification disabled
|
|
45
|
+
ANYWHERE in this file" is a defensible question for a whole-file scanner to
|
|
46
|
+
answer, the same reuse shape `posture/network-policy-import.js` already
|
|
47
|
+
established for correlating a whole-file finding to a specific location by
|
|
48
|
+
`(file, line)` proximity — not by re-deriving structural call-site identity.
|
|
49
|
+
|
|
50
|
+
Option 1 (threading raw text into `coverage.js`/`graph-builder.js` directly)
|
|
51
|
+
was rejected for the same reason `index.js` itself already wraps
|
|
52
|
+
`coverage.js` for scan-facing concerns one layer further out: it would
|
|
53
|
+
couple a whole-file regex scanner's raw-text dependency into a pipeline that
|
|
54
|
+
today only ever sees already-parsed IR, for no benefit this increment needs.
|
|
55
|
+
|
|
56
|
+
## 3. The `fileContents` plumbing path
|
|
57
|
+
|
|
58
|
+
`runFullScan`'s own signature already carries `fileContents` (the real
|
|
59
|
+
`{path: rawSourceString}` map, used pervasively elsewhere in that function
|
|
60
|
+
for `dropGuardedFindings`/`_isInlineSuppressed`/etc.) as a live local in the
|
|
61
|
+
exact scope that calls `buildLineageGraph(callGraph, {...})`. This
|
|
62
|
+
increment adds one field to that call site's opts object —
|
|
63
|
+
`fileContents` — and one corresponding optional parameter,
|
|
64
|
+
`opts.fileContents`, to `buildLineageGraph` itself, mirroring the existing
|
|
65
|
+
`opts.perFile`/`opts.parseFailures` passthrough pattern. `buildLineageGraph`
|
|
66
|
+
calls `scanTransitEvidence(opts.fileContents ?? {})` and attaches the
|
|
67
|
+
result to its own returned status object as a new field, `transitEvidence`
|
|
68
|
+
— a `Map`, alongside the existing `{status, graph, failure, elapsedMs}`
|
|
69
|
+
shape. It is a `Map`, not `Object.fromEntries(...)`, because every consumer
|
|
70
|
+
proven so far (this increment's own test, and B2's future join logic) reads
|
|
71
|
+
it in-process, never serializes it — `bin/agentic-security.js`'s
|
|
72
|
+
JSON-persistence path (`.agentic-security/lineage-graph.json`) only ever
|
|
73
|
+
persists `scan.lineageGraph` (the `graph` field), never the status object
|
|
74
|
+
this field lives on, so no serialization boundary exists yet to force a
|
|
75
|
+
plain-object shape. A future increment that needs to serialize
|
|
76
|
+
`transitEvidence` can convert at that boundary then, not here.
|
|
77
|
+
|
|
78
|
+
**Load-bearing invariant, proven by this increment's own test**: `graph`
|
|
79
|
+
itself is byte-identical whether or not `opts.fileContents` is supplied —
|
|
80
|
+
the same "byte-identical when a hook is omitted" proof every additive hook
|
|
81
|
+
this session has shipped (`opts.resolveSiteDecision`, `opts.resolveDestination`)
|
|
82
|
+
already carries. `scanTransitEvidence`'s own result is attached
|
|
83
|
+
ALONGSIDE `graph`, never folded into it.
|
|
84
|
+
|
|
85
|
+
## 4. The candidate "network" category list (named, not yet used)
|
|
86
|
+
|
|
87
|
+
`sink-registry.js`'s `CATEGORY_NODE_KIND` maps a `SINK_CATEGORIES` value to
|
|
88
|
+
a node kind of `'external'` for the following categories: `external-api`,
|
|
89
|
+
`webhook`, `email`, `sms`, `push-notification`, `collaboration`,
|
|
90
|
+
`analytics`, `monitoring`, and every `ai-*` category except
|
|
91
|
+
`ai-local-model` (which maps to `'process'`, correctly — a local model is
|
|
92
|
+
not a network call). Of these, `external-api` is the unambiguous case: an
|
|
93
|
+
HTTP/RPC call to an external service. The others are all named here as
|
|
94
|
+
candidates a future increment must decide on, not decided now:
|
|
95
|
+
|
|
96
|
+
- `webhook`, `email`, `sms`, `push-notification` — all genuinely
|
|
97
|
+
network-outbound in every real implementation (an email/SMS/push send is
|
|
98
|
+
itself an API call to a provider); plausibly all "network,
|
|
99
|
+
transit-relevant" the same way `external-api` is.
|
|
100
|
+
- `analytics`, `monitoring`, `collaboration` — also network-outbound in
|
|
101
|
+
practice (a SaaS analytics/monitoring/chat-integration SDK call), but
|
|
102
|
+
named separately here because their `CATEGORY_EXTERNALITY` entry
|
|
103
|
+
(`'external'`) and node kind (`'external'`) are the same signal
|
|
104
|
+
`external-api` carries, so nothing in the registry currently
|
|
105
|
+
distinguishes "this external call matters for transit-protection" from
|
|
106
|
+
"this external call happens to be classified `analytics`" — a future
|
|
107
|
+
increment may find a reason to exclude some of these (e.g. a
|
|
108
|
+
fire-and-forget beacon with no response path) that this increment has no
|
|
109
|
+
basis to decide.
|
|
110
|
+
- Every `ai-*` category except `ai-local-model` — an AI SDK call
|
|
111
|
+
(`ai-model-provider`, `ai-agent`, `ai-tool`, `ai-vector-store`,
|
|
112
|
+
`ai-memory`, `ai-training`, `ai-evaluation`, `ai-telemetry`) is a network
|
|
113
|
+
call to an external service by construction, per Sub-project A's own
|
|
114
|
+
literal-URL AI-provider resolution work — a strong candidate for
|
|
115
|
+
inclusion.
|
|
116
|
+
|
|
117
|
+
**Not** candidates: everything `CATEGORY_NODE_KIND` maps to `'log'`,
|
|
118
|
+
`'sink'`, `'store'`, `'queue'`, or `'process'` — none of those describe a
|
|
119
|
+
genuine outbound network call in the transit-protection sense (a `store`
|
|
120
|
+
category like `database`/`file`/`object-storage` may itself cross a
|
|
121
|
+
network in a real deployment, e.g. a managed database over TLS, but that is
|
|
122
|
+
Sub-project C's at-rest/connection concern, not this one, per the scoping
|
|
123
|
+
doc's own AC-06 exclusion).
|
|
124
|
+
|
|
125
|
+
This list is deliberately a naming exercise only in increment 1 — increment
|
|
126
|
+
2 (§6 below) decides the filter for THIS increment (`external-api` only) and
|
|
127
|
+
names the rest of this list as still-deferred widening, not silently
|
|
128
|
+
resolved.
|
|
129
|
+
|
|
130
|
+
## 5. Explicitly deferred as of increment 1 (superseded in part by §6)
|
|
131
|
+
|
|
132
|
+
- ~~The file+line correlation join~~ — **closed by increment 2**, §6 below.
|
|
133
|
+
- ~~Any write to `edge.protection.transit`~~ — **closed by increment 2**,
|
|
134
|
+
for `external-api` sites only; every other category's edges still get
|
|
135
|
+
`emptyProtection()`'s own `not_assessed`/`none` defaults, unchanged.
|
|
136
|
+
- **The final candidate "network category" list's actual USE in filtering
|
|
137
|
+
which edges matter** — increment 2 uses only `external-api` (the
|
|
138
|
+
unambiguous case named in §4); the rest of §4's list remains named,
|
|
139
|
+
deferred widening for a future increment (see §6.2).
|
|
140
|
+
- **AC-03/AC-04 fixtures** — **closed by increment 2**, §6 below.
|
|
141
|
+
|
|
142
|
+
## 6. Increment 2: the corrected hook point, the verdict logic, and what's still deferred
|
|
143
|
+
|
|
144
|
+
### 6.1 The correction this increment implements
|
|
145
|
+
|
|
146
|
+
§2 above (increment 1's own design) chose a separate post-pass — a real,
|
|
147
|
+
correct, buildable design for the PLUMBING job (getting raw file text into
|
|
148
|
+
the pipeline). Re-reading `graph-builder.js` directly while scoping the
|
|
149
|
+
verdict-computation job itself found that this does NOT extend to the
|
|
150
|
+
verdict job: `DESIGN_GRAPH_BUILDER.md` §6.1's own rule — "a node is a
|
|
151
|
+
REGISTRY DECISION, not a provenance node and not a call site" — means a
|
|
152
|
+
node's own `location` is unconditionally `null`, and many distinct call
|
|
153
|
+
sites (different files, different lines) can collide onto ONE network sink
|
|
154
|
+
node. The per-site `file`/`line` this correlation needs is available ONLY
|
|
155
|
+
on the `site` object `enumerateSinkSites` builds, and that object is
|
|
156
|
+
consumed and discarded INSIDE `graph-builder.js`'s own edge-construction
|
|
157
|
+
loop — never surfaced onto `graph.edges[]`.
|
|
158
|
+
|
|
159
|
+
**Corrected design, implemented exactly as specified:** a new
|
|
160
|
+
`opts.resolveTransitProtection(site) -> {verdict, evidenceGrade} |
|
|
161
|
+
undefined` hook on `buildDataFlowGraph` (`graph-builder.js`), applied at
|
|
162
|
+
the exact same block that already reads `site.destination` for
|
|
163
|
+
`protocol.destinationResolution` — the same point `opts.resolveDestination`
|
|
164
|
+
(Sub-project A) hooks — composing into
|
|
165
|
+
`protection: { ...emptyProtection(), transit: resolved ??
|
|
166
|
+
emptyProtection().transit }`. `coverage.js`'s `buildGraphWithCoverage`
|
|
167
|
+
gains a new `opts.transitEvidenceByFile` parameter and wires in a DEFAULT
|
|
168
|
+
`resolveTransitProtection` closing over it, composing with a
|
|
169
|
+
caller-supplied override exactly the way `resolveSiteDecision`/
|
|
170
|
+
`resolveDestination` already do (`opts.resolveTransitProtection ?? <default
|
|
171
|
+
built from transitEvidenceByFile>`).
|
|
172
|
+
|
|
173
|
+
### 6.2 The single-computation requirement
|
|
174
|
+
|
|
175
|
+
`index.js`'s own `buildLineageGraph` is now the ONLY place
|
|
176
|
+
`scanTransitEvidence` is ever called. Its result — a `Map<file,
|
|
177
|
+
findings[]>` — feeds BOTH the existing, unchanged `transitEvidence` return
|
|
178
|
+
field AND the new `opts.transitEvidenceByFile` passed to
|
|
179
|
+
`buildGraphWithCoverage`, the SAME `Map` reference, computed once.
|
|
180
|
+
`coverage.js`'s default hook never calls `scanTransitEvidence`/
|
|
181
|
+
`scanCryptoProtocol` itself — it only reads the pre-computed Map handed to
|
|
182
|
+
it. This is load-bearing, not a nice-to-have: before this change,
|
|
183
|
+
`index.js`'s own `buildGraphWithCoverage(callGraph, {...})` call passed no
|
|
184
|
+
`fileContents` at all, so `scanTransitEvidence` ran exactly once (for the
|
|
185
|
+
`transitEvidence` field alone); a second, independent call inside
|
|
186
|
+
`coverage.js`'s own default hook (re-deriving the Map from raw
|
|
187
|
+
`fileContents` a second time) would have silently doubled the per-file
|
|
188
|
+
regex-scan cost of every scan that supplies `fileContents`. A live test
|
|
189
|
+
(`transit-protection.test.js`, the call-count instrumentation case) proves
|
|
190
|
+
`scanCryptoProtocol` is invoked exactly once per file per
|
|
191
|
+
`buildLineageGraph` call, not merely architected that way.
|
|
192
|
+
|
|
193
|
+
### 6.3 The verdict logic (`resolveTransitProtectionForSite`)
|
|
194
|
+
|
|
195
|
+
Lives in `transit-protection.js`, exported alongside `scanTransitEvidence`.
|
|
196
|
+
`TRANSIT_PROTECTION_WINDOW_LINES = 10` — a named, disclosed constant, a
|
|
197
|
+
real tuning choice and NOT a calibrated one (no real fixture corpus exists
|
|
198
|
+
yet to tune against): a TLS-config object (`{ rejectUnauthorized: false }`)
|
|
199
|
+
is often on the same line as, or a few lines before, the network call it
|
|
200
|
+
configures. This is a NEW, independently-chosen value for THIS
|
|
201
|
+
correlation, following this codebase's own established line-window
|
|
202
|
+
correlation precedent (`engine.js`'s `dropGuardedFindings` and several
|
|
203
|
+
other detectors use their own independently-chosen windows — measured
|
|
204
|
+
directly at `-2/+3`, `-2/+4`, `+10` lines depending on the detector) — not
|
|
205
|
+
copied from any one existing example.
|
|
206
|
+
|
|
207
|
+
`category !== 'external-api'` is a deliberate, NARROW first slice.
|
|
208
|
+
`webhook`/`email`/`sms`/`push-notification`/`analytics`/`monitoring`/
|
|
209
|
+
`collaboration`/`ai-*` (§4's own candidate list) are all real, plausible
|
|
210
|
+
"also network" categories — widening the filter to include them is
|
|
211
|
+
separate, deliberate scope for a later increment, named here, not silently
|
|
212
|
+
included or silently excluded.
|
|
213
|
+
|
|
214
|
+
Decision table, in order:
|
|
215
|
+
|
|
216
|
+
| # | Condition | Result |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| 1 | `site.decision.category !== 'external-api'` | `undefined` (not assessed) |
|
|
219
|
+
| 2 | Literal destination starts with `http://` | `{verdict: 'unprotected', evidenceGrade: 'code'}` — the scheme alone is sufficient evidence |
|
|
220
|
+
| 3 | A `crypto-tls-no-verify`/`crypto-tls-version` finding in the site's file within `TRANSIT_PROTECTION_WINDOW_LINES` lines of the site's line | `{verdict: 'unprotected', evidenceGrade: 'code'}` — overrides a literal `https://` scheme; a plain scheme is never sufficient once a nearby finding says verification was disabled (AC-04's own core property: the UI must not award protection based on the scheme alone) |
|
|
221
|
+
| 4 | Literal destination starts with `https://`, no nearby finding | `{verdict: 'protected', evidenceGrade: 'code'}` |
|
|
222
|
+
| 5 | Anything else (dynamic/unresolved destination, or no scheme opinion) | `undefined` — the honest answer; `emptyProtection()`'s own default (`not_assessed`/`none`) already means exactly that, so this function declines to overwrite it rather than manufacturing a fabricated verdict |
|
|
223
|
+
|
|
224
|
+
Returning `undefined` (never a fabricated `unknown`/`none` string) is the
|
|
225
|
+
same discipline `resolveDestination`/`resolveSiteDecision` already
|
|
226
|
+
established: a function that didn't really analyze something must never
|
|
227
|
+
manufacture a verdict that implies it did.
|
|
228
|
+
|
|
229
|
+
### 6.4 Still explicitly deferred after increment 2
|
|
230
|
+
|
|
231
|
+
- Widening the network-category filter beyond `external-api` (§4's own
|
|
232
|
+
named list) — a later increment's job.
|
|
233
|
+
- AC-05's own dynamic-destination clause beyond "stays `not_assessed`" (the
|
|
234
|
+
PRD's AC-05 wording is about the Unresolved-outbound-destination NODE
|
|
235
|
+
existing, already shipped by Sub-project A/E4 — this increment's own
|
|
236
|
+
transit-verdict contribution to that scenario is exactly "stays
|
|
237
|
+
`not_assessed`," proven by a dedicated test, not more).
|
|
238
|
+
- AC-06 (database encryption / at-rest — Sub-project C, an entirely
|
|
239
|
+
different protection dimension).
|
|
240
|
+
- AC-12's aggregate "mixed" verdict (needs an aggregation rule no increment
|
|
241
|
+
has built yet — this increment populates one INPUT to that future
|
|
242
|
+
aggregation, not the aggregation itself).
|
|
243
|
+
- `atRest`/`handling` protection dimensions — untouched by this increment.
|
|
244
|
+
- Any language beyond JS/TS.
|
|
245
|
+
- `runtime` evidence grade (Milestone 5).
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Data classification model (Data Flow Explorer PRD section 9). Reuses
|
|
3
|
+
// scanner/src/dataflow/privacy-taxonomy.js for the PII/PHI/PCI/FIN/
|
|
4
|
+
// CREDENTIALS/GEOLOCATION/DEVICE_ID classes and their versioned,
|
|
5
|
+
// operator-extensible pattern config — PRD section 3 names this module
|
|
6
|
+
// explicitly as reusable, not something to re-implement. This file adds
|
|
7
|
+
// only what privacy-taxonomy.js does not already have: the CONFIDENTIAL
|
|
8
|
+
// class (PRD 9.1's 8th built-in class — proprietary/business-confidential
|
|
9
|
+
// data has no reliable field-NAME pattern, unlike PII/PHI/PCI, so it
|
|
10
|
+
// ships with zero default patterns and is populated entirely through the
|
|
11
|
+
// same operator-config extension mechanism privacy-taxonomy.js already
|
|
12
|
+
// supports) and the AI processing context enum (PRD 9.2), which is
|
|
13
|
+
// DELIBERATELY ORTHOGONAL to data class — see the PRD's explicit warning
|
|
14
|
+
// against modeling AI as a mutually-exclusive label.
|
|
15
|
+
|
|
16
|
+
import { DEFAULT_TAXONOMY, classifyFieldAgainst, compileTaxonomy } from '../dataflow/privacy-taxonomy.js';
|
|
17
|
+
|
|
18
|
+
// PRD section 9.2 — all 15 supported AI processing contexts. "AI" as a
|
|
19
|
+
// filter means "matches ANY of these", never a single flag.
|
|
20
|
+
export const AI_PROCESSING_CONTEXTS = Object.freeze([
|
|
21
|
+
'ai.system_prompt', 'ai.user_prompt', 'ai.model_input', 'ai.model_output',
|
|
22
|
+
'ai.rag_context', 'ai.embedding', 'ai.vector_store', 'ai.memory',
|
|
23
|
+
'ai.tool_argument', 'ai.tool_result', 'ai.training_data',
|
|
24
|
+
'ai.fine_tuning_data', 'ai.evaluation_data', 'ai.telemetry', 'ai.model_artifact',
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
// CONFIDENTIAL ships with no default patterns on purpose — "confidential
|
|
28
|
+
// business data" has no reliable field-name regex the way "ssn" or
|
|
29
|
+
// "diagnosis" does. An operator adds patterns via the SAME
|
|
30
|
+
// .agentic-security/privacy-taxonomy.json extension mechanism
|
|
31
|
+
// privacy-taxonomy.js already documents (a class name not already in
|
|
32
|
+
// DEFAULT_TAXONOMY is accepted as a brand-new organization-defined class).
|
|
33
|
+
const _CONFIDENTIAL_EXTRA = Object.freeze({ severity: 'medium', patterns: [] });
|
|
34
|
+
|
|
35
|
+
export const LINEAGE_DATA_CLASSES = Object.freeze([...Object.keys(DEFAULT_TAXONOMY), 'CONFIDENTIAL']);
|
|
36
|
+
|
|
37
|
+
const _COMPILED_WITH_CONFIDENTIAL = compileTaxonomy({ ...DEFAULT_TAXONOMY, CONFIDENTIAL: _CONFIDENTIAL_EXTRA });
|
|
38
|
+
|
|
39
|
+
export function isAiContext(value) {
|
|
40
|
+
return AI_PROCESSING_CONTEXTS.includes(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Classify a data element's canonical/declared name against the
|
|
45
|
+
* (privacy-taxonomy-plus-CONFIDENTIAL) class list. Returns
|
|
46
|
+
* `{classes: string[], aiContexts: []}` — aiContexts is ALWAYS empty from
|
|
47
|
+
* this function: a name alone can never prove a field reaches an AI
|
|
48
|
+
* processing context (PRD 10.5/FR-205 — that requires actual lineage
|
|
49
|
+
* evidence connecting the field to a model input/prompt/embedding/etc.,
|
|
50
|
+
* which is Milestone 1 scope). Callers must not skip that proof step by
|
|
51
|
+
* reading a non-empty aiContexts here; it is shaped this way specifically
|
|
52
|
+
* so there is nothing to accidentally read.
|
|
53
|
+
*/
|
|
54
|
+
export function classifyDataElementName(name, compiled = _COMPILED_WITH_CONFIDENTIAL) {
|
|
55
|
+
return { classes: classifyFieldAgainst(name, compiled), aiContexts: [] };
|
|
56
|
+
}
|