@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,107 @@
|
|
|
1
|
+
// cross-repo-link.js — M5 deliverable #8 (FR-304's "declared" half only,
|
|
2
|
+
// per the M5 top-level scoping doc's own DFG-025 row and this
|
|
3
|
+
// deliverable's own scoping doc, 2026-09-02). The CrossRepoLink
|
|
4
|
+
// extension contract — a graph-attached array (mirrors
|
|
5
|
+
// `graph.recipientProfiles[]`'s own precedent, `recipient-profile.js`),
|
|
6
|
+
// NEVER a DataFlowGraph v1 core-schema edge: `validate.js`'s
|
|
7
|
+
// `_validateEdge` requires both endpoints of an edge to resolve against
|
|
8
|
+
// the ONE graph's own `nodeIds` set, so a foreign node id from a
|
|
9
|
+
// different repo's build can never pass `validateGraph()` — settling,
|
|
10
|
+
// not merely motivating, the decision that a cross-repo link must be a
|
|
11
|
+
// separate extension record.
|
|
12
|
+
//
|
|
13
|
+
// Mirrors `recipient-profile.js`'s own file shape exactly (pure module,
|
|
14
|
+
// `{valid, errors}` validator, zero graph access at construction time),
|
|
15
|
+
// with the one real, disclosed departure that module's own header also
|
|
16
|
+
// discloses for itself: no per-field `fieldEvidence` map, since every
|
|
17
|
+
// field on a CrossRepoLink is uniformly operator-declared (no
|
|
18
|
+
// code-derived half) — closer to `ObligationMapping`'s single
|
|
19
|
+
// record-level `factType` shape (here, `provenance`) than to
|
|
20
|
+
// `RecipientProfile`'s per-field one.
|
|
21
|
+
//
|
|
22
|
+
// `provenance` reuses `schema.js`'s own `EDGE_PROVENANCE_VALUES` — this
|
|
23
|
+
// deliverable's CLI is the FIRST real producer of `'manual'` anywhere in
|
|
24
|
+
// this codebase (confirmed by the scoping investigation: every shipped
|
|
25
|
+
// edge is `provenance: 'code'`, unconditionally, per Milestone 2
|
|
26
|
+
// Sub-project F increment 1). `'schema'` stays reserved on the SAME
|
|
27
|
+
// field for a future "imported"/auto-correlated producer (FR-304's
|
|
28
|
+
// second flavor — destination/schema-based automatic cross-repo edge
|
|
29
|
+
// correlation) — explicitly out of scope for this deliverable, per the
|
|
30
|
+
// scoping doc's own "The real correction" section.
|
|
31
|
+
|
|
32
|
+
import { EDGE_PROVENANCE_VALUES } from './schema.js';
|
|
33
|
+
|
|
34
|
+
export const CROSS_REPO_LINK_VERSION = '1.0.0';
|
|
35
|
+
|
|
36
|
+
// The operator-config filename this deliverable's CLI reads/writes,
|
|
37
|
+
// resolved via `posture/state-dir.js`'s `statePath()` — mirrors
|
|
38
|
+
// `recipient-registry.js`'s own `RECIPIENT_CONFIG_FILENAME` precedent.
|
|
39
|
+
export const CROSS_REPO_LINKS_FILENAME = 'cross-repo-links.json';
|
|
40
|
+
|
|
41
|
+
// Fixed, single legal value — mirrors `edge.relationship`'s own single
|
|
42
|
+
// legal value ('data_flow', validate.js's `_validateEdge`). No new
|
|
43
|
+
// taxonomy is introduced for this deliverable.
|
|
44
|
+
export const CROSS_REPO_LINK_RELATIONSHIP = 'data_flow';
|
|
45
|
+
|
|
46
|
+
function _isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
|
|
47
|
+
function _isStringOrNull(v) { return v === null || v === undefined || typeof v === 'string'; }
|
|
48
|
+
function _isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
|
|
49
|
+
|
|
50
|
+
// Shared endpoint-shape check for `local`/`remote` — `local` always
|
|
51
|
+
// checks {graphId, graphDigest, nodeId}; `remote` additionally checks
|
|
52
|
+
// {repository, sourceFile} via `extraFields`.
|
|
53
|
+
function _validateEndpoint(endpoint, label, err, extraFields = []) {
|
|
54
|
+
if (!_isPlainObject(endpoint)) {
|
|
55
|
+
err(`$.${label}`, `${label} is required and must be an object`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
if (!_isNonEmptyString(endpoint.graphId)) err(`$.${label}.graphId`, `${label}.graphId is required`);
|
|
59
|
+
if (!_isNonEmptyString(endpoint.graphDigest)) err(`$.${label}.graphDigest`, `${label}.graphDigest is required`);
|
|
60
|
+
if (!_isNonEmptyString(endpoint.nodeId)) err(`$.${label}.nodeId`, `${label}.nodeId is required`);
|
|
61
|
+
for (const field of extraFields) {
|
|
62
|
+
if (!_isNonEmptyString(endpoint[field])) err(`$.${label}.${field}`, `${label}.${field} is required`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Structural validation only — mirrors `validateRecipientProfile`'s/
|
|
68
|
+
* `validateScenario`'s own `{valid, errors}` shape and "never throws"
|
|
69
|
+
* contract. Never confirms `local.nodeId`/`remote.nodeId` actually exist
|
|
70
|
+
* in any real graph — that needs real graph content, which this pure
|
|
71
|
+
* module deliberately has no access to (mirrors `scenario.js`'s own
|
|
72
|
+
* "structural-only, zero graph access" boundary exactly). That check is
|
|
73
|
+
* `federation-loader.js`'s (for the remote side) and the CLI's own
|
|
74
|
+
* `loadSignedGraph` call (for the local side) job, at declare time.
|
|
75
|
+
*
|
|
76
|
+
* @param {object} record
|
|
77
|
+
* @returns {{valid: boolean, errors: Array<{path: string, message: string}>}}
|
|
78
|
+
*/
|
|
79
|
+
export function validateCrossRepoLink(record) {
|
|
80
|
+
const errors = [];
|
|
81
|
+
const err = (p, message) => errors.push({ path: p, message });
|
|
82
|
+
|
|
83
|
+
if (!_isPlainObject(record)) {
|
|
84
|
+
err('$', 'CrossRepoLink record must be an object');
|
|
85
|
+
return { valid: false, errors };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!_isNonEmptyString(record.id) || !record.id.startsWith('crosslink:')) {
|
|
89
|
+
err('$.id', 'id is required and must start with "crosslink:"');
|
|
90
|
+
}
|
|
91
|
+
if (!_isNonEmptyString(record.version)) err('$.version', 'version is required');
|
|
92
|
+
if (!EDGE_PROVENANCE_VALUES.includes(record.provenance)) {
|
|
93
|
+
err('$.provenance', `unrecognized provenance "${record.provenance}" — must be one of ${EDGE_PROVENANCE_VALUES.join('|')}`);
|
|
94
|
+
}
|
|
95
|
+
if (record.relationship !== CROSS_REPO_LINK_RELATIONSHIP) {
|
|
96
|
+
err('$.relationship', `relationship must be "${CROSS_REPO_LINK_RELATIONSHIP}" (got "${record.relationship}")`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_validateEndpoint(record.local, 'local', err);
|
|
100
|
+
_validateEndpoint(record.remote, 'remote', err, ['repository', 'sourceFile']);
|
|
101
|
+
|
|
102
|
+
if (!_isStringOrNull(record.rationale)) err('$.rationale', 'rationale must be a string or null');
|
|
103
|
+
if (!_isNonEmptyString(record.declaredBy)) err('$.declaredBy', 'declaredBy is required');
|
|
104
|
+
if (!_isNonEmptyString(record.declaredAt)) err('$.declaredAt', 'declaredAt is required');
|
|
105
|
+
|
|
106
|
+
return { valid: errors.length === 0, errors };
|
|
107
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://agentic-security.dev/schemas/dataflow-graph/1.0.0.json",
|
|
4
|
+
"title": "DataFlowGraph v1",
|
|
5
|
+
"description": "Canonical application data-lineage graph contract (Data Flow Explorer PRD section 10). Renderer-independent; consumed by Architecture, Privacy, and Trace projections.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["schemaVersion", "graphId", "generatedAt", "scope", "scanHealth", "taxonomy", "nodes", "edges", "dataElements", "transformations", "flows", "controls", "policies", "evidence", "coverage", "limitations", "extensions"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"schemaVersion": { "const": "1.0.0" },
|
|
10
|
+
"graphId": { "type": "string", "pattern": "^dfg:" },
|
|
11
|
+
"generatedAt": { "type": "string", "format": "date-time" },
|
|
12
|
+
"scope": {
|
|
13
|
+
"type": "object",
|
|
14
|
+
"properties": {
|
|
15
|
+
"source": { "type": "string", "enum": ["scan", "fixture"] }
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"scanHealth": { "type": "object" },
|
|
19
|
+
"taxonomy": { "type": "object" },
|
|
20
|
+
"nodes": { "type": "array", "items": { "$ref": "#/$defs/node" } },
|
|
21
|
+
"edges": { "type": "array", "items": { "$ref": "#/$defs/edge" } },
|
|
22
|
+
"dataElements": { "type": "array", "items": { "$ref": "#/$defs/dataElement" } },
|
|
23
|
+
"transformations": { "type": "array", "items": { "$ref": "#/$defs/transformation" } },
|
|
24
|
+
"flows": { "type": "array", "items": { "$ref": "#/$defs/flow" } },
|
|
25
|
+
"controls": { "type": "array" },
|
|
26
|
+
"policies": { "type": "array" },
|
|
27
|
+
"evidence": { "type": "array", "items": { "$ref": "#/$defs/evidence" } },
|
|
28
|
+
"coverage": { "type": "object" },
|
|
29
|
+
"limitations": { "type": "array" },
|
|
30
|
+
"extensions": { "type": "object" }
|
|
31
|
+
},
|
|
32
|
+
"$defs": {
|
|
33
|
+
"node": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["id", "kind", "label", "aliases", "system", "externality", "lifecycleStages", "governanceRefs", "dataElementIds", "evidenceRefs", "confidence", "coverageStatus"],
|
|
36
|
+
"properties": {
|
|
37
|
+
"id": { "type": "string", "pattern": "^node:" },
|
|
38
|
+
"kind": { "type": "string", "enum": ["source", "process", "transform", "api", "store", "queue", "log", "sink", "external", "boundary", "unresolved"] },
|
|
39
|
+
"subtype": { "type": ["string", "null"] },
|
|
40
|
+
"label": { "type": "string" },
|
|
41
|
+
"aliases": { "type": "array", "items": { "type": "string" } },
|
|
42
|
+
"location": { "type": ["object", "null"] },
|
|
43
|
+
"system": { "type": "object" },
|
|
44
|
+
"destination": { "type": ["object", "null"] },
|
|
45
|
+
"externality": {
|
|
46
|
+
"type": "object",
|
|
47
|
+
"properties": {
|
|
48
|
+
"value": { "type": "string", "enum": ["internal", "external", "unknown"] },
|
|
49
|
+
"evidenceRefs": { "type": "array", "items": { "type": "string" } }
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
"lifecycleStages": { "type": "array", "items": { "type": "string" } },
|
|
53
|
+
"governanceRefs": { "type": "object" },
|
|
54
|
+
"dataElementIds": { "type": "array", "items": { "type": "string" } },
|
|
55
|
+
"evidenceRefs": { "type": "array", "items": { "type": "string" } },
|
|
56
|
+
"confidence": { "type": "object" },
|
|
57
|
+
"coverageStatus": { "type": "string", "enum": ["modeled", "partial", "candidate", "unsupported", "manual"] }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"fieldMapping": {
|
|
61
|
+
"type": "object",
|
|
62
|
+
"required": ["fromPath", "toPath", "dataElementIds", "mappingType", "transformationIds"],
|
|
63
|
+
"properties": {
|
|
64
|
+
"fromPath": { "type": "string" },
|
|
65
|
+
"toPath": { "type": "string" },
|
|
66
|
+
"dataElementIds": { "type": "array", "items": { "type": "string" } },
|
|
67
|
+
"mappingType": { "type": "string", "enum": ["identity", "rename", "projection", "serialization", "deserialization", "transformation", "aggregation", "join", "filter", "sort", "conditional", "unknown"] },
|
|
68
|
+
"transformationIds": { "type": "array", "items": { "type": "string" } }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
"protocol": {
|
|
72
|
+
"type": "object",
|
|
73
|
+
"properties": {
|
|
74
|
+
"name": { "type": "string" },
|
|
75
|
+
"destinationResolution": { "type": "string", "enum": ["literal", "resolved_from_constant", "resolved_from_config", "resolved_from_schema", "declared_service", "runtime_corroborated", "dynamic", "unknown"] }
|
|
76
|
+
}
|
|
77
|
+
},
|
|
78
|
+
"protectionDimension": {
|
|
79
|
+
"type": "object",
|
|
80
|
+
"required": ["verdict", "evidenceGrade"],
|
|
81
|
+
"properties": {
|
|
82
|
+
"verdict": { "type": "string", "enum": ["protected", "unprotected", "unknown", "not_applicable", "not_assessed"] },
|
|
83
|
+
"evidenceGrade": { "type": "string", "enum": ["runtime", "code_and_config", "code", "config", "declared", "assumed", "manual", "none"] }
|
|
84
|
+
}
|
|
85
|
+
},
|
|
86
|
+
"edge": {
|
|
87
|
+
"type": "object",
|
|
88
|
+
"required": ["id", "from", "to", "relationship", "fieldMappings", "protocol", "boundaryCrossings", "provenance", "protection", "evidenceRefs", "coverageStatus"],
|
|
89
|
+
"properties": {
|
|
90
|
+
"id": { "type": "string", "pattern": "^edge:" },
|
|
91
|
+
"from": { "type": "string" },
|
|
92
|
+
"to": { "type": "string" },
|
|
93
|
+
"relationship": { "const": "data_flow" },
|
|
94
|
+
"fieldMappings": { "type": "array", "items": { "$ref": "#/$defs/fieldMapping" } },
|
|
95
|
+
"protocol": { "$ref": "#/$defs/protocol" },
|
|
96
|
+
"boundaryCrossings": { "type": "array", "items": { "type": "string" } },
|
|
97
|
+
"provenance": { "type": "string", "enum": ["code", "schema", "manual", "runtime"] },
|
|
98
|
+
"protection": {
|
|
99
|
+
"type": "object",
|
|
100
|
+
"required": ["transit", "atRest", "handling"],
|
|
101
|
+
"properties": {
|
|
102
|
+
"transit": { "$ref": "#/$defs/protectionDimension" },
|
|
103
|
+
"atRest": { "$ref": "#/$defs/protectionDimension" },
|
|
104
|
+
"handling": { "$ref": "#/$defs/protectionDimension" }
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"evidenceRefs": { "type": "array", "items": { "type": "string" } },
|
|
108
|
+
"coverageStatus": { "type": "string", "enum": ["modeled", "partial", "candidate", "unsupported", "manual"] }
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
"dataElement": {
|
|
112
|
+
"type": "object",
|
|
113
|
+
"required": ["id", "name", "aliases", "dataClasses", "aiContexts", "sourceLocations", "classificationEvidence", "manualOverride"],
|
|
114
|
+
"properties": {
|
|
115
|
+
"id": { "type": "string", "pattern": "^data:" },
|
|
116
|
+
"name": { "type": "string" },
|
|
117
|
+
"aliases": { "type": "array", "items": { "type": "string" } },
|
|
118
|
+
"declaredType": { "type": ["string", "null"] },
|
|
119
|
+
"dataClasses": { "type": "array", "items": { "type": "string", "enum": ["PII", "PHI", "PCI", "FIN", "CREDENTIALS", "GEOLOCATION", "DEVICE_ID", "CONFIDENTIAL"] } },
|
|
120
|
+
"aiContexts": { "type": "array", "items": { "type": "string", "enum": ["ai.system_prompt", "ai.user_prompt", "ai.model_input", "ai.model_output", "ai.rag_context", "ai.embedding", "ai.vector_store", "ai.memory", "ai.tool_argument", "ai.tool_result", "ai.training_data", "ai.fine_tuning_data", "ai.evaluation_data", "ai.telemetry", "ai.model_artifact"] } },
|
|
121
|
+
"sourceLocations": { "type": "array" },
|
|
122
|
+
"dataSubjectCategory": { "type": ["string", "null"] },
|
|
123
|
+
"classificationEvidence": { "type": "array" },
|
|
124
|
+
"manualOverride": { "type": "boolean" },
|
|
125
|
+
"firstSeenProvenance": { "type": ["object", "null"] }
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
"transformation": {
|
|
129
|
+
"type": "object",
|
|
130
|
+
"required": ["id", "kind", "reversibility"],
|
|
131
|
+
"properties": {
|
|
132
|
+
"id": { "type": "string", "pattern": "^transform:" },
|
|
133
|
+
"inputPath": { "type": "string" },
|
|
134
|
+
"outputPath": { "type": "string" },
|
|
135
|
+
"callee": { "type": "string" },
|
|
136
|
+
"location": { "type": ["object", "null"] },
|
|
137
|
+
"kind": { "type": "string", "enum": ["mask", "redact", "tokenize", "hash", "encrypt", "decrypt", "encode", "decode", "aggregate", "truncate", "normalize", "custom", "unknown"] },
|
|
138
|
+
"reversibility": { "type": "string", "enum": ["reversible", "irreversible", "unknown"] },
|
|
139
|
+
"algorithm": { "type": ["string", "null"] },
|
|
140
|
+
"appliesToAllPaths": { "type": ["boolean", "null"] },
|
|
141
|
+
"controlCredit": { "type": ["boolean", "null"] },
|
|
142
|
+
"controlCreditReason": { "type": ["string", "null"] }
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
"flow": {
|
|
146
|
+
"type": "object",
|
|
147
|
+
"required": ["id", "dataElementIds", "source", "sink", "edgeIds", "policyVerdict", "protectionSummary", "evidenceRefs", "coverageStatus"],
|
|
148
|
+
"properties": {
|
|
149
|
+
"id": { "type": "string", "pattern": "^flow:" },
|
|
150
|
+
"dataElementIds": { "type": "array", "items": { "type": "string" } },
|
|
151
|
+
"source": { "type": "string" },
|
|
152
|
+
"sink": { "type": "string" },
|
|
153
|
+
"edgeIds": { "type": "array", "items": { "type": "string" } },
|
|
154
|
+
"transformationIds": { "type": "array", "items": { "type": "string" } },
|
|
155
|
+
"alternatePathCount": { "type": "integer", "minimum": 0 },
|
|
156
|
+
"policyVerdict": { "type": "string", "enum": ["prohibited", "permitted", "conditionally_permitted", "manual_review_required", "not_evaluated"] },
|
|
157
|
+
"protectionSummary": { "type": "string", "enum": ["protected", "unprotected", "mixed", "unknown", "not_assessed"] },
|
|
158
|
+
"evidenceRefs": { "type": "array", "items": { "type": "string" } },
|
|
159
|
+
"confidence": { "type": "object" },
|
|
160
|
+
"coverageStatus": { "type": "string", "enum": ["modeled", "partial", "candidate", "unsupported", "manual"] },
|
|
161
|
+
"findingRefs": { "type": "array" },
|
|
162
|
+
"governanceRefs": { "type": "object" },
|
|
163
|
+
"limitations": { "type": "array" }
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
"evidence": {
|
|
167
|
+
"type": "object",
|
|
168
|
+
"required": ["id", "claim", "evidenceType"],
|
|
169
|
+
"properties": {
|
|
170
|
+
"id": { "type": "string", "pattern": "^evidence:" },
|
|
171
|
+
"claim": { "type": "string" },
|
|
172
|
+
"evidenceType": { "type": "string", "enum": ["code", "ir", "configuration", "iac", "schema", "service_declaration", "policy", "manual", "runtime"] },
|
|
173
|
+
"location": { "type": ["object", "null"] },
|
|
174
|
+
"producer": { "type": ["string", "null"] },
|
|
175
|
+
"confidenceTier": { "type": ["string", "null"] },
|
|
176
|
+
"snippet": { "type": ["string", "null"] },
|
|
177
|
+
"timestamp": { "type": ["string", "null"] },
|
|
178
|
+
"commit": { "type": ["string", "null"] },
|
|
179
|
+
"limitations": { "type": "array" },
|
|
180
|
+
"conflict": { "type": ["boolean", "null"] }
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
// decision-story.js — M4 deliverable #7 (FR-501 §14, DFG-035): the
|
|
2
|
+
// DecisionStory extension contract (§10.10) + a transparent 9-factor
|
|
3
|
+
// ranking engine over real flows.
|
|
4
|
+
//
|
|
5
|
+
// Mirrors obligation-mapping.js's own shape exactly: a record is
|
|
6
|
+
// explicitly NOT a DataFlowGraph v1 entity (§10.10 — "associated with,
|
|
7
|
+
// but not required inside, the immutable base graph"), never added to
|
|
8
|
+
// dataflow-graph.schema.json, never routed through validate.js.
|
|
9
|
+
//
|
|
10
|
+
// Ranking-factor availability, grounded in real investigation (see this
|
|
11
|
+
// sub-project's own scoping doc): 7 of 9 factors are direct reads off
|
|
12
|
+
// the graph, 1 (breadth) is a small new aggregation defined here, and 2
|
|
13
|
+
// (recipientJurisdiction, changeRecency) are honestly `available: false`
|
|
14
|
+
// on every flow — never fabricated, never silently dropped from the
|
|
15
|
+
// factor list. recipientJurisdiction needs a RecipientProfile extension
|
|
16
|
+
// (capability #6, not yet built); changeRecency needs GraphSnapshot/
|
|
17
|
+
// GraphDiff (capability #3, Data-Flow Time Machine, not yet built).
|
|
18
|
+
//
|
|
19
|
+
// PRD's own binding constraint: "never represents an uncalibrated score
|
|
20
|
+
// as expected financial loss or breach probability." This module NEVER
|
|
21
|
+
// blends factors into a single opaque float — rankFlows performs a
|
|
22
|
+
// documented, transparent, CONFIGURABLE (opts.factorOrder) multi-key
|
|
23
|
+
// sort over each factor's own ordinal tier. Every factor stays
|
|
24
|
+
// individually inspectable on the returned record.
|
|
25
|
+
|
|
26
|
+
import { SEVERITY_RANK, DEFAULT_TAXONOMY } from '../dataflow/privacy-taxonomy.js';
|
|
27
|
+
|
|
28
|
+
// The sink-registry categories (sink-registry.js's own CATEGORY_NODE_KIND
|
|
29
|
+
// vocabulary) that count as an AI destination for the aiUse factor.
|
|
30
|
+
// Exported so export-briefing.js's own "AI providers" chapter-3 listing
|
|
31
|
+
// reads the SAME vocabulary rather than keeping a second, independently
|
|
32
|
+
// drifting copy (Task 2 review finding, fixed).
|
|
33
|
+
export const AI_SINK_SUBTYPES = Object.freeze(['ai-model-provider', 'ai-agent', 'ai-tool']);
|
|
34
|
+
|
|
35
|
+
export const RANKING_FACTORS = Object.freeze([
|
|
36
|
+
'sensitivity', 'externality', 'controlVerdict', 'recipientJurisdiction',
|
|
37
|
+
'aiUse', 'breadth', 'evidenceConfidence', 'policyState', 'changeRecency',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// Ordinal tier ranks, worst-first, used both to score a flow's own
|
|
41
|
+
// factor and to compare two flows during rankFlows's own sort. Every
|
|
42
|
+
// factor uses a SMALL, real, disclosed vocabulary — never a blended
|
|
43
|
+
// number.
|
|
44
|
+
const _TIER_RANK = {
|
|
45
|
+
sensitivity: { critical: 4, high: 3, medium: 2, low: 1, none: 0 },
|
|
46
|
+
externality: { external: 2, unknown: 1, internal: 0 },
|
|
47
|
+
controlVerdict: { unprotected: 4, mixed: 3, unknown: 2, not_assessed: 1, protected: 0, not_applicable: 0 },
|
|
48
|
+
recipientJurisdiction: { unknown: 0 },
|
|
49
|
+
aiUse: { ai_destination: 1, none: 0 },
|
|
50
|
+
breadth: { high: 2, medium: 1, low: 0 },
|
|
51
|
+
// Lower confidence is MORE attention-worthy (an uncertain flow needs
|
|
52
|
+
// review), so the tier rank is inverted relative to the raw score —
|
|
53
|
+
// disclosed here, not left implicit.
|
|
54
|
+
evidenceConfidence: { low: 2, medium: 1, high: 0 },
|
|
55
|
+
policyState: { prohibited: 4, manual_review_required: 3, conditionally_permitted: 2, not_evaluated: 1, permitted: 0 },
|
|
56
|
+
changeRecency: { unknown: 0 },
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
function _isNonEmptyString(v) { return typeof v === 'string' && v.length > 0; }
|
|
60
|
+
function _isPlainObject(v) { return v !== null && typeof v === 'object' && !Array.isArray(v); }
|
|
61
|
+
function _isArray(v) { return Array.isArray(v); }
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Structural validation only — mirrors
|
|
65
|
+
* obligation-mapping.js#validateObligationMapping's own {valid, errors}
|
|
66
|
+
* shape and "never throws" contract.
|
|
67
|
+
*/
|
|
68
|
+
export function validateDecisionStory(record) {
|
|
69
|
+
const errors = [];
|
|
70
|
+
const err = (path, message) => errors.push({ path, message });
|
|
71
|
+
|
|
72
|
+
if (!record || typeof record !== 'object' || Array.isArray(record)) {
|
|
73
|
+
err('$', 'DecisionStory record must be an object');
|
|
74
|
+
return { valid: false, errors };
|
|
75
|
+
}
|
|
76
|
+
if (!_isNonEmptyString(record.id) || !record.id.startsWith('story:')) {
|
|
77
|
+
err('$.id', 'id is required and must start with "story:"');
|
|
78
|
+
}
|
|
79
|
+
if (!_isNonEmptyString(record.version)) err('$.version', 'version is required');
|
|
80
|
+
if (!_isNonEmptyString(record.audienceMode)) err('$.audienceMode', 'audienceMode is required');
|
|
81
|
+
if (!_isPlainObject(record.scopeQuery)) err('$.scopeQuery', 'scopeQuery is required and must be an object');
|
|
82
|
+
if (!_isArray(record.chapters)) err('$.chapters', 'chapters must be an array');
|
|
83
|
+
if (!_isArray(record.contributingGraphIds)) err('$.contributingGraphIds', 'contributingGraphIds must be an array');
|
|
84
|
+
if (!_isArray(record.rankingFactors)) err('$.rankingFactors', 'rankingFactors must be an array');
|
|
85
|
+
if (!_isNonEmptyString(record.evidenceGrade)) err('$.evidenceGrade', 'evidenceGrade is required');
|
|
86
|
+
if (!_isPlainObject(record.coverage)) err('$.coverage', 'coverage is required and must be an object');
|
|
87
|
+
if (!_isArray(record.decisions)) err('$.decisions', 'decisions must be an array');
|
|
88
|
+
if (!_isNonEmptyString(record.generatedAt)) err('$.generatedAt', 'generatedAt is required');
|
|
89
|
+
if (!_isNonEmptyString(record.graphDigest)) err('$.graphDigest', 'graphDigest is required');
|
|
90
|
+
|
|
91
|
+
return { valid: errors.length === 0, errors };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function _sensitivityFactor(flow, graph) {
|
|
95
|
+
const de = graph.dataElements.find((d) => flow.dataElementIds.includes(d.id));
|
|
96
|
+
const classes = de?.dataClasses ?? [];
|
|
97
|
+
if (classes.length === 0) return { available: true, tier: 'none', evidence: [] };
|
|
98
|
+
let worst = 'low';
|
|
99
|
+
for (const cls of classes) {
|
|
100
|
+
const sev = DEFAULT_TAXONOMY[cls]?.severity ?? 'medium';
|
|
101
|
+
if ((SEVERITY_RANK[sev] ?? 0) > (SEVERITY_RANK[worst] ?? 0)) worst = sev;
|
|
102
|
+
}
|
|
103
|
+
return { available: true, tier: worst, evidence: [de.id] };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function _externalityFactor(flow, nodesById) {
|
|
107
|
+
const sink = nodesById.get(flow.sink);
|
|
108
|
+
const value = sink?.externality?.value ?? 'unknown';
|
|
109
|
+
return { available: true, tier: value, evidence: [flow.sink] };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function _controlVerdictFactor(flow) {
|
|
113
|
+
return { available: true, tier: flow.protectionSummary ?? 'not_assessed', evidence: flow.edgeIds ?? [] };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function _recipientJurisdictionFactor() {
|
|
117
|
+
// Honest gap — see this file's own header. Never fabricated.
|
|
118
|
+
return { available: false, tier: 'unknown', evidence: [], unavailableReason: 'RecipientProfile extension not yet built (capability #6, Third-Party/Cross-Border Intelligence)' };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function _aiUseFactor(flow, nodesById) {
|
|
122
|
+
const sink = nodesById.get(flow.sink);
|
|
123
|
+
const isAi = AI_SINK_SUBTYPES.includes(sink?.subtype);
|
|
124
|
+
return { available: true, tier: isAi ? 'ai_destination' : 'none', evidence: isAi ? [flow.sink] : [] };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* New small aggregation (the scoping doc's own "smallest real addition"
|
|
129
|
+
* for breadth/blast-radius): how many OTHER flows in the same graph
|
|
130
|
+
* share this flow's own sink node or dataElement. A crude but real,
|
|
131
|
+
* non-fabricated proxy for "how widely does this exposure reach" —
|
|
132
|
+
* never presented as a calibrated blast-radius count, just an ordinal
|
|
133
|
+
* tier over a real, disclosed count.
|
|
134
|
+
*/
|
|
135
|
+
function _breadthFactor(flow, graph) {
|
|
136
|
+
let sharedCount = 0;
|
|
137
|
+
for (const other of graph.flows) {
|
|
138
|
+
if (other.id === flow.id) continue;
|
|
139
|
+
const sharesSink = other.sink === flow.sink;
|
|
140
|
+
const sharesData = other.dataElementIds.some((id) => flow.dataElementIds.includes(id));
|
|
141
|
+
if (sharesSink || sharesData) sharedCount++;
|
|
142
|
+
}
|
|
143
|
+
const tier = sharedCount >= 5 ? 'high' : sharedCount >= 1 ? 'medium' : 'low';
|
|
144
|
+
return { available: true, tier, evidence: [], sharedFlowCount: sharedCount };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function _evidenceConfidenceFactor(flow) {
|
|
148
|
+
const tier = flow.confidence?.tier ?? 'medium';
|
|
149
|
+
return { available: true, tier, evidence: [] };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function _policyStateFactor(flow) {
|
|
153
|
+
return { available: true, tier: flow.policyVerdict ?? 'not_evaluated', evidence: flow.evidenceRefs ?? [] };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function _changeRecencyFactor() {
|
|
157
|
+
// Honest gap — see this file's own header. Never fabricated.
|
|
158
|
+
return { available: false, tier: 'unknown', evidence: [], unavailableReason: 'GraphSnapshot/GraphDiff not yet built (capability #3, Data-Flow Time Machine)' };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Score ONE flow on all 9 factors. Never throws on a well-formed graph. */
|
|
162
|
+
export function scoreFlow(flow, graph, nodesById) {
|
|
163
|
+
return {
|
|
164
|
+
flowId: flow.id,
|
|
165
|
+
factors: {
|
|
166
|
+
sensitivity: _sensitivityFactor(flow, graph),
|
|
167
|
+
externality: _externalityFactor(flow, nodesById),
|
|
168
|
+
controlVerdict: _controlVerdictFactor(flow),
|
|
169
|
+
recipientJurisdiction: _recipientJurisdictionFactor(),
|
|
170
|
+
aiUse: _aiUseFactor(flow, nodesById),
|
|
171
|
+
breadth: _breadthFactor(flow, graph),
|
|
172
|
+
evidenceConfidence: _evidenceConfidenceFactor(flow),
|
|
173
|
+
policyState: _policyStateFactor(flow),
|
|
174
|
+
changeRecency: _changeRecencyFactor(),
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Score and rank every flow in the graph. opts.factorOrder (default
|
|
181
|
+
* RANKING_FACTORS) is the PRD's own "transparent configurable factors"
|
|
182
|
+
* requirement, made real: a lexicographic multi-key sort over each
|
|
183
|
+
* factor's own ordinal tier rank, in the given priority order — NEVER a
|
|
184
|
+
* blended single score. Ties within all factors preserve original flow
|
|
185
|
+
* order (stable sort).
|
|
186
|
+
*/
|
|
187
|
+
export function rankFlows(graph, opts = {}) {
|
|
188
|
+
const factorOrder = opts.factorOrder ?? RANKING_FACTORS;
|
|
189
|
+
const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
190
|
+
const scored = graph.flows.map((flow) => ({
|
|
191
|
+
flow,
|
|
192
|
+
...scoreFlow(flow, graph, nodesById),
|
|
193
|
+
factorOrderUsed: factorOrder,
|
|
194
|
+
}));
|
|
195
|
+
const rankOf = (scoredFlow, factor) => {
|
|
196
|
+
const f = scoredFlow.factors[factor];
|
|
197
|
+
return _TIER_RANK[factor]?.[f.tier] ?? 0;
|
|
198
|
+
};
|
|
199
|
+
return scored.sort((a, b) => {
|
|
200
|
+
for (const factor of factorOrder) {
|
|
201
|
+
const diff = rankOf(b, factor) - rankOf(a, factor);
|
|
202
|
+
if (diff !== 0) return diff;
|
|
203
|
+
}
|
|
204
|
+
return 0;
|
|
205
|
+
});
|
|
206
|
+
}
|