@clear-capabilities/agentic-security-scanner 0.172.0 → 0.173.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 +29 -0
- package/bin/agentic-security.js +37 -0
- package/dist/119.index.js +197 -0
- package/dist/128.index.js +129 -0
- package/dist/1293.index.js +5689 -0
- package/dist/1336.index.js +101 -101
- package/dist/1351.index.js +133 -0
- package/dist/1387.index.js +271 -0
- package/dist/1437.index.js +23 -0
- package/dist/1445.index.js +110 -0
- package/dist/1505.index.js +151 -0
- package/dist/1557.index.js +293 -0
- package/dist/1621.index.js +311 -0
- package/dist/1751.index.js +160 -0
- package/dist/1765.index.js +727 -0
- package/dist/1861.index.js +690 -0
- package/dist/1898.index.js +116 -0
- package/dist/1906.index.js +433 -0
- package/dist/1920.index.js +1 -291
- package/dist/208.index.js +1631 -0
- package/dist/2199.index.js +0 -239
- package/dist/2223.index.js +159 -0
- package/dist/2530.index.js +152 -0
- package/dist/2565.index.js +116 -0
- package/dist/2574.index.js +127 -0
- package/dist/2620.index.js +345 -0
- package/dist/2773.index.js +98 -0
- package/dist/2775.index.js +32 -0
- package/dist/2966.index.js +171 -0
- package/dist/2972.index.js +87 -0
- package/dist/2991.index.js +131 -0
- package/dist/3063.index.js +833 -0
- package/dist/3075.index.js +227 -0
- package/dist/3121.index.js +297 -0
- package/dist/3314.index.js +263 -0
- package/dist/3480.index.js +1822 -0
- package/dist/3481.index.js +24 -0
- package/dist/3506.index.js +268 -0
- package/dist/3533.index.js +591 -0
- package/dist/3653.index.js +450 -0
- package/dist/3822.index.js +131 -0
- package/dist/3868.index.js +292 -0
- package/dist/3917.index.js +152 -0
- package/dist/3918.index.js +192 -0
- package/dist/4090.index.js +288 -0
- package/dist/4121.index.js +1190 -0
- package/dist/4142.index.js +0 -239
- package/dist/4336.index.js +436 -0
- package/dist/4387.index.js +532 -0
- package/dist/4413.index.js +302 -0
- package/dist/4476.index.js +4510 -0
- package/dist/4491.index.js +133 -0
- package/dist/4538.index.js +185 -0
- package/dist/4684.index.js +319 -0
- package/dist/4740.index.js +165 -0
- package/dist/4780.index.js +209 -0
- package/dist/4802.index.js +225 -0
- package/dist/4812.index.js +379 -0
- package/dist/4835.index.js +126 -0
- package/dist/494.index.js +226 -0
- package/dist/4991.index.js +549 -0
- package/dist/5199.index.js +305 -0
- package/dist/5216.index.js +109 -0
- package/dist/5439.index.js +301 -0
- package/dist/5443.index.js +97 -0
- package/dist/5496.index.js +377 -0
- package/dist/550.index.js +2 -1
- package/dist/5801.index.js +588 -0
- package/dist/5858.index.js +223 -0
- package/dist/5933.index.js +143 -0
- package/dist/601.index.js +23 -0
- package/dist/6173.index.js +273 -0
- package/dist/6291.index.js +1871 -0
- package/dist/6312.index.js +666 -0
- package/dist/634.index.js +16182 -0
- package/dist/644.index.js +87 -0
- package/dist/6508.index.js +283 -0
- package/dist/6560.index.js +645 -0
- package/dist/6576.index.js +422 -0
- package/dist/6579.index.js +271 -0
- package/dist/6745.index.js +305 -0
- package/dist/6818.index.js +21642 -0
- package/dist/6972.index.js +167 -0
- package/dist/6995.index.js +163 -0
- package/dist/7249.index.js +191 -0
- package/dist/7372.index.js +1197 -0
- package/dist/7405.index.js +23 -6
- package/dist/7546.index.js +541 -0
- package/dist/76.index.js +448 -0
- package/dist/7879.index.js +163 -0
- package/dist/8025.index.js +130 -0
- package/dist/8054.index.js +248 -0
- package/dist/8097.index.js +201 -0
- package/dist/8126.index.js +191 -0
- package/dist/8178.index.js +78 -0
- package/dist/818.index.js +212 -0
- package/dist/8382.index.js +93 -0
- package/dist/8408.index.js +261 -0
- package/dist/8540.index.js +174 -0
- package/dist/8635.index.js +802 -0
- package/dist/8688.index.js +410 -0
- package/dist/8699.index.js +779 -0
- package/dist/8891.index.js +457 -0
- package/dist/8916.index.js +885 -0
- package/dist/9146.index.js +170 -0
- package/dist/9168.index.js +841 -0
- package/dist/9292.index.js +369 -0
- package/dist/9382.index.js +338 -48
- package/dist/9422.index.js +866 -0
- package/dist/9440.index.js +453 -0
- package/dist/9479.index.js +152 -0
- package/dist/9481.index.js +220 -0
- package/dist/9539.index.js +100 -0
- package/dist/9547.index.js +103 -0
- package/dist/9560.index.js +41 -61
- package/dist/9692.index.js +90 -0
- package/dist/9800.index.js +213 -1
- package/dist/9899.index.js +138 -0
- package/dist/9981.index.js +206 -0
- package/dist/999.index.js +117 -0
- package/dist/agentic-security.mjs +28 -28
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/package.json +31 -6
- package/src/context-intelligence/CLAUDE.md +4 -0
- package/src/context-intelligence/adapters/haskell-hie.js +28 -1
- package/src/context-intelligence/candidates.js +2 -1
- package/src/context-intelligence/canonical.js +25 -0
- package/src/context-intelligence/capabilities.js +2 -1
- package/src/context-intelligence/cli.js +16 -10
- package/src/context-intelligence/config.js +4 -1
- package/src/context-intelligence/controls.js +108 -10
- package/src/context-intelligence/counterexample.js +200 -0
- package/src/context-intelligence/cross-route.js +111 -0
- package/src/context-intelligence/entrypoints.js +2 -2
- package/src/context-intelligence/evaluation/index.js +11 -4
- package/src/context-intelligence/evaluation/scripted-reader.js +12 -2
- package/src/context-intelligence/findings.js +166 -0
- package/src/context-intelligence/invariants.js +197 -0
- package/src/context-intelligence/queue.js +6 -1
- package/src/context-intelligence/reachability.js +10 -4
- package/src/context-intelligence/review.js +129 -16
- package/src/dataflow/access-paths.js +11 -0
- package/src/dataflow/analysis-limits.js +66 -0
- package/src/dataflow/engine.js +272 -20
- package/src/dataflow/service-edges.js +174 -0
- package/src/dataflow/summaries.js +7 -1
- package/src/discovery/judge.js +2 -1
- package/src/engine.js +82 -14
- package/src/fix/apply-fix-service.js +55 -8
- package/src/fix/batch-journal.js +84 -0
- package/src/fix/exploit-replay.js +51 -0
- package/src/fix/footprint.js +65 -0
- package/src/fix/patch-policy.js +49 -0
- package/src/fix/secret-persistence.js +39 -0
- package/src/fix/test-commands.js +84 -0
- package/src/fix/verification-adapters.js +97 -0
- package/src/fix/verification-record.js +243 -0
- package/src/fix/verify-patch.js +89 -0
- package/src/ir/CLAUDE.md +5 -0
- package/src/ir/callgraph.js +23 -1
- package/src/ir/module-resolver.js +315 -0
- package/src/ir/parser-js.js +21 -2
- package/src/ir/semantic-graph.js +245 -0
- package/src/lineage/build-attested-correlation.js +63 -0
- package/src/lineage/deployment-identity.js +76 -0
- package/src/mcp/tools.js +13 -34
- package/src/pipeline/canonical-output.js +64 -0
- package/src/pipeline/detector-runner.js +7 -0
- package/src/pipeline/scan-health.js +14 -0
- package/src/pipeline/supervised-analyzers.js +125 -0
- package/src/pipeline/supervised-child.js +17 -0
- package/src/pipeline/telemetry.js +79 -0
- package/src/posture/accuracy-scorecard.js +98 -9
- package/src/posture/artifact-registry.js +5 -0
- package/src/posture/assurance/evidence-registry.js +215 -0
- package/src/posture/assurance/release-package-verify.js +135 -0
- package/src/posture/assurance/release-package.js +118 -0
- package/src/posture/assurance/release-verdict.js +94 -0
- package/src/posture/assurance/waiver.js +200 -0
- package/src/posture/compliance-truth.js +100 -0
- package/src/posture/evaluation/scan-child.js +3 -1
- package/src/posture/evaluation/why-missed.js +3 -1
- package/src/posture/exploitability-evidence.js +141 -0
- package/src/posture/fix-verify.js +4 -2
- package/src/posture/fleet/exposure-ranking.js +115 -0
- package/src/posture/fleet/service-chain-graph.js +150 -0
- package/src/posture/graph-reconciliation.js +154 -0
- package/src/posture/provenance/coverage.js +103 -0
- package/src/posture/provenance/git-evidence.js +84 -0
- package/src/posture/provenance/origin-resolver.js +7 -1
- package/src/posture/provenance/predicate-replay.js +14 -4
- package/src/posture/runtime-correlation.js +61 -15
- package/src/posture/runtime-explanation.js +116 -0
- package/src/posture/runtime-trace-ingest.js +194 -0
- package/src/posture/scan-checkpoint.js +38 -10
- package/src/posture/waf-ingest.js +7 -2
- package/src/report/index.js +4 -0
- package/src/report/oscal.js +38 -0
- package/src/report/perspectives.js +150 -0
- package/src/sast/_comment-strip.js +8 -1
- package/src/sast/ruby.js +26 -0
- package/src/util/string-memo.js +38 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
export const id = 3075;
|
|
2
|
+
export const ids = [3075];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 53075:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
9
|
+
/* harmony export */ computeDelta: () => (/* binding */ computeDelta),
|
|
10
|
+
/* harmony export */ persistStatus: () => (/* binding */ persistStatus),
|
|
11
|
+
/* harmony export */ renderStatusLine: () => (/* binding */ renderStatusLine),
|
|
12
|
+
/* harmony export */ watchProject: () => (/* binding */ watchProject)
|
|
13
|
+
/* harmony export */ });
|
|
14
|
+
/* unused harmony exports readStatus, _internals */
|
|
15
|
+
/* harmony import */ var node_fs_promises__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(51455);
|
|
16
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(73024);
|
|
17
|
+
/* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(76760);
|
|
18
|
+
/* harmony import */ var _state_dir_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(69259);
|
|
19
|
+
/* harmony import */ var _language_discovery_js__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(41928);
|
|
20
|
+
// Watch mode — continuous incremental scan as the developer edits.
|
|
21
|
+
//
|
|
22
|
+
// Spawns a long-running scan watcher that:
|
|
23
|
+
// 1. Subscribes to file-system events under the project root
|
|
24
|
+
// 2. When a file matching the scan glob changes, re-scans incrementally
|
|
25
|
+
// using dataflow/incremental-cache.js (per-file cache hits avoid the
|
|
26
|
+
// full IR build)
|
|
27
|
+
// 3. Diffs against the prior scan to compute a "risk delta" string
|
|
28
|
+
// (added/removed/changed criticals + highs)
|
|
29
|
+
// 4. Writes the delta to .agentic-security/watch-status.{md,json}
|
|
30
|
+
//
|
|
31
|
+
// The Claude Code statusline / a chat command can poll watch-status.md
|
|
32
|
+
// (cheap file read) to surface the delta inline without re-running anything.
|
|
33
|
+
//
|
|
34
|
+
// Implementation is deliberately node-only — no chokidar dep, uses
|
|
35
|
+
// fs.promises.watch (Node ≥ 20). Debounces to 350ms.
|
|
36
|
+
//
|
|
37
|
+
// Lifecycle: start() returns an AbortController-like handle. The caller
|
|
38
|
+
// (a /watch slash command or a long-running daemon spawn) is responsible
|
|
39
|
+
// for keeping the process alive; this module is the pure logic.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
const STATUS_MD = 'watch-status.md';
|
|
48
|
+
const STATUS_JSON = 'watch-status.json';
|
|
49
|
+
const DEBOUNCE_MS = 350;
|
|
50
|
+
const MAX_BURST = 50; // ignore beyond this # of rapid events
|
|
51
|
+
|
|
52
|
+
const SCAN_EXT_RE = /\.(?:[jt]sx?|mjs|cjs|py|java|kt|go|rb|php|cs|c|cc|cpp|h|hpp|rs|sol|vy|swift|dart|toml|yml|yaml|json|tf|tfvars|bicep)$/i;
|
|
53
|
+
const IGNORE_DIR_RE = /(?:^|\/)(?:\.git|node_modules|\.bench-cache|dist|build|\.next|coverage|\.agentic-security)(?:$|\/)/;
|
|
54
|
+
|
|
55
|
+
function _isScanable(rel) {
|
|
56
|
+
if (!rel) return false;
|
|
57
|
+
// An explicit export is evidence even though it lives in a build directory.
|
|
58
|
+
if ((0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isExplicitExport */ .Zh)(rel)) return true;
|
|
59
|
+
if (IGNORE_DIR_RE.test(rel)) return false;
|
|
60
|
+
if ((0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageSource */ .Sv)(rel) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageManifest */ .mD)(rel)) return !(0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageExcludedPath */ .EG)(rel);
|
|
61
|
+
return SCAN_EXT_RE.test(rel);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Widen a batch of changed absolute paths to every file whose results the
|
|
66
|
+
* change can stale: dependents through imports, and all Haskell/Nix sources for
|
|
67
|
+
* a manifest, lockfile or export change. Batches with no Haskell/Nix path are
|
|
68
|
+
* returned untouched, so other languages never pay for a tree read.
|
|
69
|
+
*/
|
|
70
|
+
async function expandChangedBatch(scanRoot, batch) {
|
|
71
|
+
const rels = batch.map((p) => node_path__WEBPACK_IMPORTED_MODULE_2__.relative(scanRoot, p).split(node_path__WEBPACK_IMPORTED_MODULE_2__.sep).join('/'));
|
|
72
|
+
if (!rels.some((r) => (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageSource */ .Sv)(r) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageManifest */ .mD)(r) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isExplicitExport */ .Zh)(r))) return batch;
|
|
73
|
+
try {
|
|
74
|
+
const { readTree } = await Promise.resolve(/* import() */).then(__webpack_require__.bind(__webpack_require__, 58307));
|
|
75
|
+
const { fileContents, depFileContents } = await readTree(scanRoot);
|
|
76
|
+
const hit = (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .impactedFiles */ .nn)(fileContents, depFileContents, rels);
|
|
77
|
+
const out = new Set(batch);
|
|
78
|
+
for (const r of hit) out.add(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, r));
|
|
79
|
+
return [...out];
|
|
80
|
+
} catch {
|
|
81
|
+
return batch;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function _readJsonSafe(fp) {
|
|
86
|
+
try { return JSON.parse(fsSync.readFileSync(fp, 'utf8')); } catch { return null; }
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function _sevRank(s) { return ['info', 'low', 'medium', 'high', 'critical'].indexOf(s) + 1; }
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Pure delta computation between two finding arrays. Same logic
|
|
93
|
+
* baseline-compare uses, surfaced as a single ASCII status line.
|
|
94
|
+
*/
|
|
95
|
+
function computeDelta(prevFindings, currFindings) {
|
|
96
|
+
const key = (f) => `${f.file || ''}::${f.line || 0}::${f.family || f.parser || ''}`;
|
|
97
|
+
const prev = new Map();
|
|
98
|
+
for (const f of prevFindings || []) prev.set(key(f), f);
|
|
99
|
+
const cur = new Map();
|
|
100
|
+
for (const f of currFindings || []) cur.set(key(f), f);
|
|
101
|
+
const added = [], removed = [];
|
|
102
|
+
for (const [k, f] of cur) if (!prev.has(k)) added.push(f);
|
|
103
|
+
for (const [k, f] of prev) if (!cur.has(k)) removed.push(f);
|
|
104
|
+
const newCrit = added.filter(f => f.severity === 'critical').length;
|
|
105
|
+
const newHigh = added.filter(f => f.severity === 'high').length;
|
|
106
|
+
const fixedCrit = removed.filter(f => f.severity === 'critical').length;
|
|
107
|
+
const fixedHigh = removed.filter(f => f.severity === 'high').length;
|
|
108
|
+
return {
|
|
109
|
+
addedCount: added.length, removedCount: removed.length,
|
|
110
|
+
newCritical: newCrit, newHigh, fixedCritical: fixedCrit, fixedHigh,
|
|
111
|
+
added, removed,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Render a one-line status string for the Claude Code statusline.
|
|
117
|
+
*/
|
|
118
|
+
function renderStatusLine(delta) {
|
|
119
|
+
const parts = [];
|
|
120
|
+
if (delta.newCritical) parts.push(`🛑 +${delta.newCritical} crit`);
|
|
121
|
+
if (delta.newHigh) parts.push(`⚠️ +${delta.newHigh} high`);
|
|
122
|
+
if (delta.fixedCritical) parts.push(`✅ -${delta.fixedCritical} crit`);
|
|
123
|
+
if (delta.fixedHigh) parts.push(`✅ -${delta.fixedHigh} high`);
|
|
124
|
+
if (!parts.length && (delta.addedCount + delta.removedCount) === 0) return 'agentic-security: clean';
|
|
125
|
+
if (!parts.length) return `agentic-security: +${delta.addedCount} / -${delta.removedCount}`;
|
|
126
|
+
return 'agentic-security: ' + parts.join(' · ');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Persist watch-status.{md,json}. Cheap atomic write (write tmp, rename).
|
|
131
|
+
*/
|
|
132
|
+
function persistStatus(scanRoot, delta) {
|
|
133
|
+
const dir = (0,_state_dir_js__WEBPACK_IMPORTED_MODULE_3__/* .stateDir */ .Pn)(scanRoot);
|
|
134
|
+
if (!(0,_state_dir_js__WEBPACK_IMPORTED_MODULE_3__.stateWritesEnabled)()) return;
|
|
135
|
+
try { node_fs__WEBPACK_IMPORTED_MODULE_1__.mkdirSync(dir, { recursive: true }); } catch {}
|
|
136
|
+
const status = {
|
|
137
|
+
ts: new Date().toISOString(),
|
|
138
|
+
line: renderStatusLine(delta),
|
|
139
|
+
delta: {
|
|
140
|
+
addedCount: delta.addedCount, removedCount: delta.removedCount,
|
|
141
|
+
newCritical: delta.newCritical, newHigh: delta.newHigh,
|
|
142
|
+
fixedCritical: delta.fixedCritical, fixedHigh: delta.fixedHigh,
|
|
143
|
+
},
|
|
144
|
+
addedTop5: (delta.added || []).slice(0, 5).map(f => ({
|
|
145
|
+
file: f.file, line: f.line, family: f.family, severity: f.severity, vuln: f.vuln,
|
|
146
|
+
})),
|
|
147
|
+
};
|
|
148
|
+
const jsonPath = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, STATUS_JSON);
|
|
149
|
+
const mdPath = node_path__WEBPACK_IMPORTED_MODULE_2__.join(dir, STATUS_MD);
|
|
150
|
+
try { node_fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(jsonPath, JSON.stringify(status, null, 2)); } catch {}
|
|
151
|
+
const md = [
|
|
152
|
+
`# Watch status — ${status.ts.slice(11, 19)} UTC`,
|
|
153
|
+
'',
|
|
154
|
+
status.line,
|
|
155
|
+
'',
|
|
156
|
+
];
|
|
157
|
+
if (status.addedTop5.length) {
|
|
158
|
+
md.push('## New findings');
|
|
159
|
+
for (const f of status.addedTop5) {
|
|
160
|
+
md.push(`- **[${(f.severity || '?').toUpperCase()}]** ${f.vuln || f.family || 'finding'} — \`${f.file}:${f.line}\``);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
try { node_fs__WEBPACK_IMPORTED_MODULE_1__.writeFileSync(mdPath, md.join('\n')); } catch {}
|
|
164
|
+
return status;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Read the latest watch-status (returns null if none).
|
|
169
|
+
*/
|
|
170
|
+
function readStatus(scanRoot) {
|
|
171
|
+
return _readJsonSafe(statePath(scanRoot, STATUS_JSON));
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Subscribe to FS events and call onChange(absPath, eventType) for each
|
|
176
|
+
* matching event. Debounces bursts. Returns a controller with stop().
|
|
177
|
+
*
|
|
178
|
+
* The actual incremental scan call lives in the caller — this module
|
|
179
|
+
* stays pure for testability.
|
|
180
|
+
*/
|
|
181
|
+
async function watchProject(scanRoot, onChange, opts = {}) {
|
|
182
|
+
if (process.env.AGENTIC_SECURITY_NO_WATCH === '1') return { stop: async () => {}, _disabled: true };
|
|
183
|
+
const recursive = opts.recursive !== false;
|
|
184
|
+
const ac = new AbortController();
|
|
185
|
+
let timer = null;
|
|
186
|
+
const pending = new Set();
|
|
187
|
+
const flush = () => {
|
|
188
|
+
timer = null;
|
|
189
|
+
if (pending.size > MAX_BURST) { pending.clear(); return; }
|
|
190
|
+
const batch = Array.from(pending);
|
|
191
|
+
pending.clear();
|
|
192
|
+
const needsExpansion = batch.some((p) => {
|
|
193
|
+
const r = node_path__WEBPACK_IMPORTED_MODULE_2__.relative(scanRoot, p).split(node_path__WEBPACK_IMPORTED_MODULE_2__.sep).join('/');
|
|
194
|
+
return (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageSource */ .Sv)(r) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isLanguageManifest */ .mD)(r) || (0,_language_discovery_js__WEBPACK_IMPORTED_MODULE_4__/* .isExplicitExport */ .Zh)(r);
|
|
195
|
+
});
|
|
196
|
+
if (!needsExpansion) { try { onChange(batch); } catch {} return; }
|
|
197
|
+
expandChangedBatch(scanRoot, batch).then((b) => { try { onChange(b); } catch {} });
|
|
198
|
+
};
|
|
199
|
+
let stopped = false;
|
|
200
|
+
(async () => {
|
|
201
|
+
try {
|
|
202
|
+
for await (const evt of node_fs_promises__WEBPACK_IMPORTED_MODULE_0__.watch(scanRoot, { recursive, signal: ac.signal })) {
|
|
203
|
+
if (stopped) break;
|
|
204
|
+
const rel = String(evt.filename || '').replace(/\\/g, '/');
|
|
205
|
+
if (!_isScanable(rel)) continue;
|
|
206
|
+
pending.add(node_path__WEBPACK_IMPORTED_MODULE_2__.join(scanRoot, rel));
|
|
207
|
+
if (timer) clearTimeout(timer);
|
|
208
|
+
timer = setTimeout(flush, DEBOUNCE_MS);
|
|
209
|
+
}
|
|
210
|
+
} catch (e) {
|
|
211
|
+
if (e && e.name !== 'AbortError') {
|
|
212
|
+
// Surface but don't crash — caller decides how to handle.
|
|
213
|
+
try { onChange([], e); } catch {}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
})();
|
|
217
|
+
return {
|
|
218
|
+
stop: async () => { stopped = true; ac.abort(); if (timer) clearTimeout(timer); },
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const _internals = { _isScanable, expandChangedBatch, SCAN_EXT_RE, IGNORE_DIR_RE };
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
/***/ })
|
|
226
|
+
|
|
227
|
+
};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
export const id = 3121;
|
|
2
|
+
export const ids = [3121];
|
|
3
|
+
export const modules = {
|
|
4
|
+
|
|
5
|
+
/***/ 83121:
|
|
6
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
|
+
|
|
8
|
+
__webpack_require__.r(__webpack_exports__);
|
|
9
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
10
|
+
/* harmony export */ evaluateDriftPolicies: () => (/* binding */ evaluateDriftPolicies),
|
|
11
|
+
/* harmony export */ loadDriftPolicies: () => (/* binding */ loadDriftPolicies)
|
|
12
|
+
/* harmony export */ });
|
|
13
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(73024);
|
|
14
|
+
// drift-policy.js — M4 deliverable #8, sub-project 8b, Task 2: a
|
|
15
|
+
// before/after drift-policy DSL evaluated against a GraphDiff (Task 1's
|
|
16
|
+
// own output, see graph-diff.js).
|
|
17
|
+
//
|
|
18
|
+
// Mirrors dataflow/privacy-sink-policy.js's fail-closed axis-matching
|
|
19
|
+
// STYLE exactly — the `_matchesEnvironment`/`_matchesDestination`
|
|
20
|
+
// precedent (an unset rule field is unconstrained; a set field the
|
|
21
|
+
// current context has no comparable value for does NOT match) — never
|
|
22
|
+
// its literal single-graph-state shape. That module evaluates a static
|
|
23
|
+
// state ("is this (class, sink) pair permitted right now"); this module
|
|
24
|
+
// evaluates a TRANSITION ("did this (class, sink) pair newly start
|
|
25
|
+
// flowing, or did this flow's own verdict regress"), which is why its
|
|
26
|
+
// rule shape is `{trigger, ...}`-keyed rather than `{allow: [...]}`.
|
|
27
|
+
//
|
|
28
|
+
// ── Design judgment calls made in this file (disclosed, not hidden) ──
|
|
29
|
+
//
|
|
30
|
+
// 1. `loadDriftPolicies(policyFilePath)` takes a literal file path, not
|
|
31
|
+
// a scanRoot — a deliberate, task-brief-directed divergence from
|
|
32
|
+
// `loadPrivacySinkPolicy(scanRoot)`'s own scanRoot-relative
|
|
33
|
+
// convention (`statePath(scanRoot, 'privacy-policy.json')`). Drift
|
|
34
|
+
// policies are a new artifact with no established scanRoot-relative
|
|
35
|
+
// home yet (Task 3's CLI wiring is the only real caller, and it can
|
|
36
|
+
// resolve whatever path convention it wants — e.g. a `--policy`
|
|
37
|
+
// flag, or a default under `.agentic-security/` — without this
|
|
38
|
+
// module forcing that choice). Keeping the signature a plain path
|
|
39
|
+
// keeps this module honest about being a pure evaluator with no
|
|
40
|
+
// opinion on where its config file lives.
|
|
41
|
+
//
|
|
42
|
+
// 2. The rule vocabulary generalizes `fromPolicyVerdict`/
|
|
43
|
+
// `toPolicyVerdict` to a second, structurally identical pair,
|
|
44
|
+
// `fromProtectionSummary`/`toProtectionSummary`, rather than adding a
|
|
45
|
+
// generic `field`/`from`/`to` triple. The task brief's own worked
|
|
46
|
+
// examples name `protectionSummary`'s "protected -> unknown/
|
|
47
|
+
// unprotected" transition as "an equivalent from/to pair for
|
|
48
|
+
// protectionSummary, generalizing" the policyVerdict pair — read
|
|
49
|
+
// literally, that is an instruction to add the SAME shape for a
|
|
50
|
+
// second watched field, not to invent a fully generic DSL over every
|
|
51
|
+
// WATCHED_FLOW_FIELDS entry. A generic `field`/`from`/`to` triple was
|
|
52
|
+
// considered and rejected: nothing in the brief's worked examples
|
|
53
|
+
// needs a THIRD field's transition, and a generic form would need its
|
|
54
|
+
// own validation against `WATCHED_FLOW_FIELDS` this task's tests
|
|
55
|
+
// don't exercise. Extending to a fully generic form remains easy
|
|
56
|
+
// later (both matchers already share one small helper,
|
|
57
|
+
// `_matchesVerdictTransition`) without a rule-shape migration for
|
|
58
|
+
// what's shipped today.
|
|
59
|
+
//
|
|
60
|
+
// 3. `dataClass`/`sinkCategory` are read as ADDITIONAL, optional
|
|
61
|
+
// constraints on a `changed_flow` rule too, not just `new_flow`
|
|
62
|
+
// rules — the brief's own rule shape lists them as top-level fields
|
|
63
|
+
// (not scoped textually to `new_flow` alone), and a changed flow's
|
|
64
|
+
// id is guaranteed present in `graphAfter` by construction (a
|
|
65
|
+
// `changed.flows` entry only exists when the SAME flow id survived
|
|
66
|
+
// into the after-graph — see graph-diff.js's own `computeGraphDiff`
|
|
67
|
+
// header), so the same `_resolveFlowContext` resolution that
|
|
68
|
+
// `new_flow` rules use applies unchanged. This lets an operator write
|
|
69
|
+
// "a PHI flow that regresses from protected" as one rule instead of
|
|
70
|
+
// two, and costs nothing when a rule doesn't set either field (both
|
|
71
|
+
// stay unconstrained, per the fail-closed style above).
|
|
72
|
+
//
|
|
73
|
+
// 4. A `changed_flow` rule that sets none of
|
|
74
|
+
// dataClass/sinkCategory/fromPolicyVerdict/toPolicyVerdict/
|
|
75
|
+
// fromProtectionSummary/toProtectionSummary matches EVERY
|
|
76
|
+
// `changed.flows` entry — every axis is independently unconstrained,
|
|
77
|
+
// so a rule with no axes set is the fully unconstrained case, exactly
|
|
78
|
+
// as an `allow` rule with neither `class` nor `sink` would be in
|
|
79
|
+
// `privacy-sink-policy.js` (it doesn't have that case in practice
|
|
80
|
+
// because `sink` is required there; this module has no required axis
|
|
81
|
+
// besides `trigger`, so the maximally-open rule is reachable and
|
|
82
|
+
// intentional — "alert on ANY change to a flow matching these
|
|
83
|
+
// constraints" is a real, useful policy shape).
|
|
84
|
+
//
|
|
85
|
+
// 5. The task brief's PRD-quoted worked examples also name "PII -> AI"
|
|
86
|
+
// ,"PCI -> log" (both plain `new_flow` shapes, already covered by
|
|
87
|
+
// dataClass/sinkCategory matching — no separate mechanism needed) and
|
|
88
|
+
// "new unresolved recipient" (a `governanceRefs.recipient`-shaped
|
|
89
|
+
// transition). The brief's own "Tests to write" section does not
|
|
90
|
+
// require a test for the recipient example, and it does not reduce
|
|
91
|
+
// cleanly to either matcher this task implements (a "new unresolved
|
|
92
|
+
// recipient" is a value becoming a specific sentinel, not a flow
|
|
93
|
+
// newly existing or an already-modeled verdict field transitioning)
|
|
94
|
+
// — left out rather than guessed at. Judgment call #2's rejected
|
|
95
|
+
// generic `field`/`from`/`to` form is the natural place to add it
|
|
96
|
+
// later, against a real governanceRefs.recipient fixture.
|
|
97
|
+
//
|
|
98
|
+
// 6. Fix round 1, Important 1 (see graph-diff.js's own judgment call #4):
|
|
99
|
+
// a `new_flow` rule must skip an added flow entry whose
|
|
100
|
+
// `causeClassification === 'reidentified'` — that flow isn't actually
|
|
101
|
+
// new, it's the SAME real-world (source, sink, dataElementIds) flow as
|
|
102
|
+
// a flow that existed before, just minted under a new flowId because
|
|
103
|
+
// the engine's own confidence/shape (evidenceGrade/transformationIds)
|
|
104
|
+
// changed. Firing `new_flow` on it would false-fire `--fail-on-drift`
|
|
105
|
+
// on a non-event, exactly what this fix round exists to close.
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
const POLICY_TRIGGERS = Object.freeze(['new_flow', 'changed_flow']);
|
|
110
|
+
|
|
111
|
+
function _isValidRuleShape(p) {
|
|
112
|
+
return !!p && typeof p === 'object' && !Array.isArray(p) && POLICY_TRIGGERS.includes(p.trigger);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Load an operator's drift-policy rules from a literal JSON file path
|
|
117
|
+
* (see judgment call #1 above — never a scanRoot). Never throws — a
|
|
118
|
+
* missing file (ENOENT) is "no policies configured," matching
|
|
119
|
+
* `loadPrivacySinkPolicy`'s own precedent; a malformed file logs a
|
|
120
|
+
* warning and degrades to no policies. Each entry is validated loosely:
|
|
121
|
+
* an entry that isn't a plain object with a recognized `trigger` is
|
|
122
|
+
* skipped (with a warning naming how many were dropped), never crashing
|
|
123
|
+
* the whole load over one bad rule.
|
|
124
|
+
*/
|
|
125
|
+
function loadDriftPolicies(policyFilePath) {
|
|
126
|
+
const EMPTY = { policies: [] };
|
|
127
|
+
if (!policyFilePath) return EMPTY;
|
|
128
|
+
|
|
129
|
+
let raw = null;
|
|
130
|
+
try {
|
|
131
|
+
raw = JSON.parse(node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(policyFilePath, 'utf8'));
|
|
132
|
+
} catch (e) {
|
|
133
|
+
if (e.code !== 'ENOENT') {
|
|
134
|
+
console.error(`agentic-security: bad JSON in drift policy file (${policyFilePath}) — falling back to no policies (${e.message})`);
|
|
135
|
+
}
|
|
136
|
+
return EMPTY;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!Array.isArray(raw?.policies)) {
|
|
140
|
+
console.error(`agentic-security: drift policy file ${policyFilePath} has no "policies" array — falling back to no policies (expected {"policies":[...]})`);
|
|
141
|
+
}
|
|
142
|
+
const rawPolicies = Array.isArray(raw?.policies) ? raw.policies : [];
|
|
143
|
+
const policies = rawPolicies.filter(_isValidRuleShape);
|
|
144
|
+
const skipped = rawPolicies.length - policies.length;
|
|
145
|
+
if (skipped > 0) {
|
|
146
|
+
console.error(`agentic-security: skipped ${skipped} malformed drift-policy rule(s) in ${policyFilePath} (each must be an object with trigger: 'new_flow' | 'changed_flow')`);
|
|
147
|
+
}
|
|
148
|
+
return { policies };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Fail-closed, matching privacy-sink-policy.js's own
|
|
152
|
+
// _matchesEnvironment/_matchesDestination precedent exactly: an unset
|
|
153
|
+
// rule value is unconstrained (matches anything); a set rule value with
|
|
154
|
+
// no comparable current value does NOT match.
|
|
155
|
+
function _matchesDataClass(ruleDataClass, dataClasses) {
|
|
156
|
+
if (ruleDataClass == null) return true;
|
|
157
|
+
if (!Array.isArray(dataClasses) || !dataClasses.length) return false;
|
|
158
|
+
return dataClasses.includes(ruleDataClass);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function _matchesSinkCategory(ruleSinkCategory, sinkCategory) {
|
|
162
|
+
if (ruleSinkCategory == null) return true;
|
|
163
|
+
if (!sinkCategory) return false;
|
|
164
|
+
return ruleSinkCategory === sinkCategory;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Matches a rule's {fromX, toX} pair against the {before, after} of the
|
|
168
|
+
// changes[] entry for field X (or undefined, when that field didn't
|
|
169
|
+
// change on this flow). Unset fromX/toX is unconstrained. A set fromX/
|
|
170
|
+
// toX with no changes[] entry for that field at all (the flow changed,
|
|
171
|
+
// but not on this axis) does NOT match — fails closed, same as an
|
|
172
|
+
// environment-scoped rule against a caller who supplied no environment.
|
|
173
|
+
function _matchesVerdictTransition(ruleFrom, ruleTo, changeEntry) {
|
|
174
|
+
if (ruleFrom == null && ruleTo == null) return true;
|
|
175
|
+
if (!changeEntry) return false;
|
|
176
|
+
if (ruleFrom != null && changeEntry.before !== ruleFrom) return false;
|
|
177
|
+
if (ruleTo != null && changeEntry.after !== ruleTo) return false;
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// Resolves the real dataClasses/sinkCategory/sinkNode/flow for a flow id
|
|
182
|
+
// against graphAfter — shared by both new_flow (added.flows entries) and
|
|
183
|
+
// changed_flow (changed.flows entries) matching, since a changed.flows
|
|
184
|
+
// entry's id is, by construction, present in graphAfter too (see
|
|
185
|
+
// graph-diff.js's own computeGraphDiff: a changed entry only exists for
|
|
186
|
+
// a flow id surviving into the after-graph). Returns null when the id
|
|
187
|
+
// can't be resolved (should not happen for a well-formed GraphDiff
|
|
188
|
+
// produced by computeGraphDiff against its own graphAfter, but a rule
|
|
189
|
+
// must never crash the whole evaluation over a caller-supplied
|
|
190
|
+
// diff/graph pairing that doesn't actually match).
|
|
191
|
+
function _resolveFlowContext(flowId, graphAfter) {
|
|
192
|
+
const flow = (graphAfter?.flows ?? []).find((f) => f.id === flowId);
|
|
193
|
+
if (!flow) return null;
|
|
194
|
+
|
|
195
|
+
const dataClasses = [];
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
for (const deId of flow.dataElementIds ?? []) {
|
|
198
|
+
const de = (graphAfter.dataElements ?? []).find((d) => d.id === deId);
|
|
199
|
+
if (!de) continue;
|
|
200
|
+
for (const cls of de.dataClasses ?? []) {
|
|
201
|
+
if (!seen.has(cls)) { seen.add(cls); dataClasses.push(cls); }
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
const dataElementNames = (flow.dataElementIds ?? [])
|
|
205
|
+
.map((id) => (graphAfter.dataElements ?? []).find((d) => d.id === id)?.name)
|
|
206
|
+
.filter((n) => typeof n === 'string' && n.length > 0);
|
|
207
|
+
|
|
208
|
+
const sinkNode = (graphAfter.nodes ?? []).find((n) => n.id === flow.sink) ?? null;
|
|
209
|
+
const sinkCategory = sinkNode?.subtype ?? sinkNode?.kind ?? null;
|
|
210
|
+
|
|
211
|
+
return { flow, dataClasses, dataElementNames, sinkNode, sinkCategory };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function _describeTransition(changeEntry) {
|
|
215
|
+
if (!changeEntry) return null;
|
|
216
|
+
return `${changeEntry.field} ${changeEntry.before} -> ${changeEntry.after}`;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function _buildViolation(rule, trigger, ctx, transitionDescriptions) {
|
|
220
|
+
const base = trigger === 'new_flow'
|
|
221
|
+
? `New flow ${ctx.flow.id} (${ctx.dataClasses.join('/') || 'unclassified data'}) newly reaches sink ${ctx.sinkCategory ?? ctx.sinkNode?.id ?? 'unknown'}`
|
|
222
|
+
: `Flow ${ctx.flow.id} changed: ${transitionDescriptions.length ? transitionDescriptions.join(', ') : 'matched changed_flow rule (no policyVerdict/protectionSummary transition named by this rule)'}`;
|
|
223
|
+
const reason = rule.reason ? `${base} (${rule.reason})` : base;
|
|
224
|
+
|
|
225
|
+
return {
|
|
226
|
+
flowId: ctx.flow.id,
|
|
227
|
+
dataElementNames: ctx.dataElementNames,
|
|
228
|
+
sinkCategory: ctx.sinkCategory,
|
|
229
|
+
sinkNodeId: ctx.sinkNode?.id ?? ctx.flow.sink,
|
|
230
|
+
trigger,
|
|
231
|
+
rule,
|
|
232
|
+
reason,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Evaluates operator-configured drift policies against a GraphDiff.
|
|
238
|
+
* `policies` is `{policies: [...]}` (loadDriftPolicies' own return
|
|
239
|
+
* shape). `graphAfter` is the AFTER graph the diff was computed against
|
|
240
|
+
* — needed because an `added.flows`/`changed.flows` entry carries only
|
|
241
|
+
* `{id, causeClassification, ...}` (see graph-diff.js's own header),
|
|
242
|
+
* never dataElement/sink info directly.
|
|
243
|
+
*
|
|
244
|
+
* Returns `{violations: [...]}` — never a bare boolean per flow; every
|
|
245
|
+
* violation names the real triggering flow (id, data element names,
|
|
246
|
+
* sink), the rule that fired, and a human-readable `reason` string.
|
|
247
|
+
*/
|
|
248
|
+
function evaluateDriftPolicies(diff, policies, graphAfter) {
|
|
249
|
+
const violations = [];
|
|
250
|
+
const rules = Array.isArray(policies?.policies) ? policies.policies : [];
|
|
251
|
+
if (!rules.length) return { violations };
|
|
252
|
+
|
|
253
|
+
for (const rule of rules) {
|
|
254
|
+
if (rule.trigger === 'new_flow') {
|
|
255
|
+
for (const addedEntry of diff?.added?.flows ?? []) {
|
|
256
|
+
// See judgment call #6 above / graph-diff.js's own judgment call
|
|
257
|
+
// #4: a reidentified added flow is not actually new.
|
|
258
|
+
if (addedEntry.causeClassification === 'reidentified') continue;
|
|
259
|
+
const ctx = _resolveFlowContext(addedEntry.id, graphAfter);
|
|
260
|
+
if (!ctx) continue;
|
|
261
|
+
if (!_matchesDataClass(rule.dataClass, ctx.dataClasses)) continue;
|
|
262
|
+
if (!_matchesSinkCategory(rule.sinkCategory, ctx.sinkCategory)) continue;
|
|
263
|
+
violations.push(_buildViolation(rule, 'new_flow', ctx, []));
|
|
264
|
+
}
|
|
265
|
+
} else if (rule.trigger === 'changed_flow') {
|
|
266
|
+
for (const changedEntry of diff?.changed?.flows ?? []) {
|
|
267
|
+
const ctx = _resolveFlowContext(changedEntry.id, graphAfter);
|
|
268
|
+
if (!ctx) continue;
|
|
269
|
+
if (!_matchesDataClass(rule.dataClass, ctx.dataClasses)) continue;
|
|
270
|
+
if (!_matchesSinkCategory(rule.sinkCategory, ctx.sinkCategory)) continue;
|
|
271
|
+
|
|
272
|
+
const changes = Array.isArray(changedEntry.changes) ? changedEntry.changes : [];
|
|
273
|
+
const policyChange = changes.find((c) => c.field === 'policyVerdict');
|
|
274
|
+
if (!_matchesVerdictTransition(rule.fromPolicyVerdict, rule.toPolicyVerdict, policyChange)) continue;
|
|
275
|
+
|
|
276
|
+
const protectionChange = changes.find((c) => c.field === 'protectionSummary');
|
|
277
|
+
if (!_matchesVerdictTransition(rule.fromProtectionSummary, rule.toProtectionSummary, protectionChange)) continue;
|
|
278
|
+
|
|
279
|
+
const transitionDescriptions = [policyChange, protectionChange].map(_describeTransition).filter(Boolean);
|
|
280
|
+
violations.push(_buildViolation(rule, 'changed_flow', ctx, transitionDescriptions));
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
// An unrecognized trigger was already filtered out by
|
|
284
|
+
// loadDriftPolicies' own _isValidRuleShape when loaded from disk; a
|
|
285
|
+
// directly hand-built `policies` object (as tests do) with an
|
|
286
|
+
// unrecognized trigger simply matches nothing, silently — the same
|
|
287
|
+
// "match nothing rather than throw" discipline as every other
|
|
288
|
+
// unresolvable axis in this file.
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
return { violations };
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
/***/ })
|
|
296
|
+
|
|
297
|
+
};
|