@clear-capabilities/agentic-security-scanner 0.145.0 → 0.147.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +266 -0
- package/bin/agentic-security.js +3508 -69
- package/dist/1122.index.js +779 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +2 -2
- package/dist/{660.index.js → 1660.index.js} +2 -2
- package/dist/{700.index.js → 1700.index.js} +2 -2
- package/dist/{905.index.js → 1905.index.js} +3 -3
- package/dist/{920.index.js → 1920.index.js} +3 -3
- package/dist/{238.index.js → 2238.index.js} +3 -3
- package/dist/{271.index.js → 2271.index.js} +2 -2
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +379 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +5 -5
- package/dist/{736.index.js → 3736.index.js} +4 -4
- package/dist/{839.index.js → 3839.index.js} +4 -4
- package/dist/{113.index.js → 4113.index.js} +5 -5
- package/dist/{265.index.js → 4265.index.js} +2 -2
- package/dist/{384.index.js → 4384.index.js} +3 -3
- package/dist/4547.index.js +268 -0
- package/dist/4863.index.js +422 -0
- package/dist/{970.index.js → 4970.index.js} +2 -2
- package/dist/5051.index.js +841 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +3 -3
- package/dist/{449.index.js → 5830.index.js} +2 -2
- package/dist/6626.index.js +532 -0
- package/dist/6662.index.js +297 -0
- package/dist/{675.index.js → 6675.index.js} +5 -5
- package/dist/{730.index.js → 6730.index.js} +6 -6
- package/dist/6829.index.js +225 -0
- package/dist/6944.index.js +130 -0
- package/dist/{178.index.js → 7178.index.js} +3 -3
- package/dist/{227.index.js → 7227.index.js} +2 -2
- package/dist/7310.index.js +520 -0
- package/dist/{552.index.js → 7552.index.js} +4 -4
- package/dist/7709.index.js +78 -0
- package/dist/8218.index.js +160 -0
- package/dist/{476.index.js → 8476.index.js} +4 -4
- package/dist/{513.index.js → 8513.index.js} +5 -5
- package/dist/{520.index.js → 8520.index.js} +2 -2
- package/dist/{718.index.js → 8718.index.js} +2 -2
- package/dist/{752.index.js → 8752.index.js} +2 -2
- package/dist/8846.index.js +100 -0
- package/dist/{435.index.js → 9091.index.js} +560 -189
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/dist/frontend/index.html +21 -0
- package/dist/frontend/src/app.js +176 -0
- package/dist/frontend/src/components/evidence-inspector.js +141 -0
- package/dist/frontend/src/components/filter-rail.js +119 -0
- package/dist/frontend/src/components/query-bar.js +126 -0
- package/dist/frontend/src/data/flagship-graph.js +1460 -0
- package/dist/frontend/src/export-entry.js +36 -0
- package/dist/frontend/src/lib/api-client.js +92 -0
- package/dist/frontend/src/lib/contrast.js +34 -0
- package/dist/frontend/src/lib/dom.js +24 -0
- package/dist/frontend/src/lib/escape-html.js +16 -0
- package/dist/frontend/src/lib/flow-path.js +40 -0
- package/dist/frontend/src/lib/focus-controls.js +149 -0
- package/dist/frontend/src/lib/protection-visual.js +46 -0
- package/dist/frontend/src/lib/query-language.js +240 -0
- package/dist/frontend/src/lib/row-filters.js +43 -0
- package/dist/frontend/src/lib/state.js +84 -0
- package/dist/frontend/src/main.js +83 -0
- package/dist/frontend/src/shell.js +184 -0
- package/dist/frontend/src/views/architecture-view.js +798 -0
- package/dist/frontend/src/views/inventory-view.js +292 -0
- package/dist/frontend/src/views/privacy-view.js +172 -0
- package/dist/frontend/src/views/trace-view.js +206 -0
- package/dist/frontend/styles/architecture-view.css +93 -0
- package/dist/frontend/styles/filter-rail.css +34 -0
- package/dist/frontend/styles/inspector.css +69 -0
- package/dist/frontend/styles/inventory-view.css +74 -0
- package/dist/frontend/styles/privacy-view.css +86 -0
- package/dist/frontend/styles/query-bar.css +107 -0
- package/dist/frontend/styles/shell.css +155 -0
- package/dist/frontend/styles/tokens.css +128 -0
- package/dist/frontend/styles/trace-view.css +95 -0
- package/package.json +13 -6
- package/src/dataflow/CLAUDE.md +1 -1
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +92 -1
- package/src/ir/CLAUDE.md +1 -0
- package/src/ir/chrome-probe.mjs +150 -0
- package/src/ir/parser-js.js +94 -7
- package/src/lineage/CLAUDE.md +1203 -0
- package/src/lineage/DESIGN_DESTINATION_RESOLVER.md +156 -0
- package/src/lineage/DESIGN_GRAPH_BUILDER.md +938 -0
- package/src/lineage/DESIGN_HANDLING_ANALYZER.md +355 -0
- package/src/lineage/DESIGN_INTRAPROCEDURAL.md +628 -0
- package/src/lineage/DESIGN_PATH_PROVENANCE.md +3451 -0
- package/src/lineage/DESIGN_QUEUE_DETAIL.md +120 -0
- package/src/lineage/DESIGN_REGISTRIES.md +880 -0
- package/src/lineage/DESIGN_STORE_DETAIL.md +143 -0
- package/src/lineage/DESIGN_TRANSIT_PROTECTION.md +245 -0
- package/src/lineage/classification.js +56 -0
- package/src/lineage/coverage.js +658 -0
- package/src/lineage/cross-repo-link.js +107 -0
- package/src/lineage/dataflow-graph.schema.json +184 -0
- package/src/lineage/decision-story.js +206 -0
- package/src/lineage/drift-policy.js +279 -0
- package/src/lineage/driver.js +135 -0
- package/src/lineage/engine.js +992 -0
- package/src/lineage/export-briefing.js +628 -0
- package/src/lineage/export-csv.js +62 -0
- package/src/lineage/export-json.js +238 -0
- package/src/lineage/export-privacy.js +258 -0
- package/src/lineage/federation-loader.js +111 -0
- package/src/lineage/field-identity.js +78 -0
- package/src/lineage/fixtures/build-flagship-fixture.mjs +272 -0
- package/src/lineage/fixtures/flagship-graph.json +1453 -0
- package/src/lineage/flow-grade.js +221 -0
- package/src/lineage/governance-edit.js +169 -0
- package/src/lineage/graph-builder.js +1114 -0
- package/src/lineage/graph-diff.js +431 -0
- package/src/lineage/graph-snapshot.js +180 -0
- package/src/lineage/handling-analyzer.js +168 -0
- package/src/lineage/ids.js +349 -0
- package/src/lineage/impact-assessment.js +76 -0
- package/src/lineage/impact-engine.js +268 -0
- package/src/lineage/index.js +281 -0
- package/src/lineage/language-coverage-tiers.js +58 -0
- package/src/lineage/obligation-mapping.js +126 -0
- package/src/lineage/obligation-predicates.js +235 -0
- package/src/lineage/observation-adapters.js +282 -0
- package/src/lineage/observation-correlation.js +622 -0
- package/src/lineage/observation-store.js +497 -0
- package/src/lineage/path-query.js +410 -0
- package/src/lineage/path-store.js +400 -0
- package/src/lineage/protection.js +53 -0
- package/src/lineage/recipient-profile.js +192 -0
- package/src/lineage/recipient-registry.js +394 -0
- package/src/lineage/redact-graph.js +224 -0
- package/src/lineage/remediation.js +417 -0
- package/src/lineage/resolve-destination.js +91 -0
- package/src/lineage/runtime-observation.js +464 -0
- package/src/lineage/scenario-diff.js +84 -0
- package/src/lineage/scenario-engine.js +251 -0
- package/src/lineage/scenario.js +101 -0
- package/src/lineage/schema.js +167 -0
- package/src/lineage/sink-registry.js +427 -0
- package/src/lineage/source-registry.js +357 -0
- package/src/lineage/source-seeding.js +212 -0
- package/src/lineage/summaries.js +590 -0
- package/src/lineage/transform-catalog.js +397 -0
- package/src/lineage/transit-protection.js +150 -0
- package/src/lineage/validate.js +285 -0
- package/src/mcp/CLAUDE.md +7 -1
- package/src/mcp/dataflow-tools.js +160 -0
- package/src/mcp/server.js +1 -1
- package/src/mcp/tools.js +2 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +19 -0
- package/src/posture/artifact-registry.js +52 -0
- package/src/posture/auditor-walkthrough.js +76 -0
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +144 -0
- package/src/shared/frontend-root.js +52 -0
- package/dist/11.index.js +0 -353
- package/dist/259.index.js +0 -975
- package/dist/317.index.js +0 -300
- package/dist/609.index.js +0 -741
- package/dist/838.index.js +0 -152
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
export const id =
|
|
2
|
-
export const ids = [
|
|
1
|
+
export const id = 9091;
|
|
2
|
+
export const ids = [9091,8218,8752];
|
|
3
3
|
export const modules = {
|
|
4
4
|
|
|
5
|
-
/***/
|
|
5
|
+
/***/ 9091:
|
|
6
6
|
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
7
7
|
|
|
8
8
|
|
|
@@ -102,113 +102,182 @@ var integrity = __webpack_require__(1130);
|
|
|
102
102
|
var state_dir = __webpack_require__(1174);
|
|
103
103
|
// EXTERNAL MODULE: ./src/posture/cache-economics.js
|
|
104
104
|
var cache_economics = __webpack_require__(8752);
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
//
|
|
105
|
+
// EXTERNAL MODULE: ./src/mcp/redact.js
|
|
106
|
+
var redact = __webpack_require__(3468);
|
|
107
|
+
// EXTERNAL MODULE: ./src/report/index.js + 3 modules
|
|
108
|
+
var report = __webpack_require__(457);
|
|
109
|
+
// EXTERNAL MODULE: ./src/posture/provenance/schema.js
|
|
110
|
+
var schema = __webpack_require__(4594);
|
|
111
|
+
// EXTERNAL MODULE: ./src/server/graph-loader.js
|
|
112
|
+
var graph_loader = __webpack_require__(8218);
|
|
113
|
+
// EXTERNAL MODULE: ./src/server/routes.js
|
|
114
|
+
var routes = __webpack_require__(4268);
|
|
115
|
+
// EXTERNAL MODULE: ./src/lineage/redact-graph.js
|
|
116
|
+
var redact_graph = __webpack_require__(334);
|
|
117
|
+
// EXTERNAL MODULE: ./src/lineage/export-json.js
|
|
118
|
+
var export_json = __webpack_require__(859);
|
|
119
|
+
;// CONCATENATED MODULE: ./src/mcp/dataflow-tools.js
|
|
120
|
+
// dataflow-tools.js — Milestone 4, sub-project MCP tools.
|
|
118
121
|
//
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
[/sk-ant-[A-Za-z0-9_-]{20,}/g, 'anthropic-key'],
|
|
129
|
-
[/sk-proj-[A-Za-z0-9_-]{20,}/g, 'openai-project-key'],
|
|
130
|
-
[/sk-[A-Za-z0-9]{32,}/g, 'openai-or-stripe-key'],
|
|
131
|
-
[/sk_(?:live|test)_[A-Za-z0-9]{20,}/g, 'stripe-key'],
|
|
132
|
-
[/rk_(?:live|test)_[A-Za-z0-9]{20,}/g, 'stripe-restricted-key'],
|
|
133
|
-
[/SG\.[A-Za-z0-9_-]{22}\.[A-Za-z0-9_-]{43}/g, 'sendgrid-key'],
|
|
134
|
-
[/AIza[0-9A-Za-z_-]{35}/g, 'google-api-key'],
|
|
135
|
-
// Stage 4 correctness audit (coverage breadth, AI security): this list
|
|
136
|
-
// only covered a small subset of what the scanner's OWN credential
|
|
137
|
-
// detector (engine.js's CREDENTIAL_PATTERNS, 40+ provider shapes) finds
|
|
138
|
-
// — a Shopify/Telegram/Twilio/Discord-webhook/Square/Google-OAuth/JDBC
|
|
139
|
-
// secret detected and reported by a scan reached explain_finding's
|
|
140
|
-
// output completely unredacted, because none of those shapes were in
|
|
141
|
-
// THIS separate, narrower list. Reusing the same regex bodies as
|
|
142
|
-
// engine.js's CREDENTIAL_PATTERNS for the shapes verified to leak
|
|
143
|
-
// (rather than importing engine.js itself, which would pull its entire
|
|
144
|
-
// multi-thousand-line module graph into the MCP server's dependency
|
|
145
|
-
// surface for a handful of consts).
|
|
146
|
-
[/ya29\.[0-9A-Za-z_-]{20,}/g, 'google-oauth-token'],
|
|
147
|
-
[/shp(?:at|ss|ca|pa)_[a-fA-F0-9]{32}/g, 'shopify-token'],
|
|
148
|
-
[/(?<![0-9])[0-9]{8,10}:AA[0-9A-Za-z_-]{33}(?![A-Za-z0-9_])/g, 'telegram-bot-token'],
|
|
149
|
-
[/twilio.{0,20}SK[0-9a-fA-F]{32}/gi, 'twilio-api-key'],
|
|
150
|
-
[/sq0atp-[0-9A-Za-z_-]{22}/g, 'square-access-token'],
|
|
151
|
-
[/sq0csp-[0-9A-Za-z_-]{43}/g, 'square-oauth-secret'],
|
|
152
|
-
[/access_token\$production\$[0-9a-z]{16}\$[0-9a-f]{32}/g, 'paypal-braintree-token'],
|
|
153
|
-
[/https:\/\/(?:discordapp|discord)\.com\/api\/webhooks\/[0-9]+\/[A-Za-z0-9_-]+/g, 'discord-webhook'],
|
|
154
|
-
[/https:\/\/hooks\.slack\.com\/services\/T[a-zA-Z0-9_]{8}\/B[a-zA-Z0-9_]{8,12}\/[a-zA-Z0-9_]{24}/g, 'slack-webhook'],
|
|
155
|
-
[/https:\/\/outlook\.office\.com\/webhook\/[A-Za-z0-9\-@]+\/IncomingWebhook\/[A-Za-z0-9-]+\/[A-Za-z0-9-]+/g, 'teams-webhook'],
|
|
156
|
-
[/https:\/\/(?:www\.)?hooks\.zapier\.com\/hooks\/catch\/[A-Za-z0-9]+\/[A-Za-z0-9]+\//g, 'zapier-webhook'],
|
|
157
|
-
// JDBC connection string carrying a password: only redact when password
|
|
158
|
-
// evidence is actually on the line (matches engine.js's own ctx gate),
|
|
159
|
-
// so a credential-free JDBC URL in docs isn't needlessly mangled.
|
|
160
|
-
[/jdbc:[a-z:]+:\/\/[A-Za-z0-9.\-_:;=/@?,&]*(?:@|password=|passwd=|pwd=)[A-Za-z0-9.\-_:;=/@?,&]*/gi, 'jdbc-connection-string'],
|
|
161
|
-
// JWT — three dot-separated b64url segments starting with eyJ
|
|
162
|
-
[/eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g, 'jwt'],
|
|
163
|
-
// PEM-encoded private keys
|
|
164
|
-
[/-----BEGIN (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |DSA |EC |OPENSSH |PGP )?PRIVATE KEY-----/g, 'private-key-block'],
|
|
165
|
-
// Authorization headers — common copy-paste shape
|
|
166
|
-
[/(?:Authorization|authorization)\s*:\s*Bearer\s+[A-Za-z0-9._~+/-]{20,}={0,2}/g, 'bearer-token'],
|
|
167
|
-
// Hardcoded password literals — assignment shape with quoted value
|
|
168
|
-
[/(password|passwd|secret|api[_-]?key|access[_-]?token)\s*[:=]\s*["'][^"'\n]{6,}["']/gi, 'hardcoded-credential'],
|
|
169
|
-
];
|
|
122
|
+
// Thin, read-only MCP adapter over the DataFlowGraph v1 artifact. Every
|
|
123
|
+
// piece of actual graph-loading and graph-query logic here is REUSED,
|
|
124
|
+
// unmodified, from scanner/src/server/ (built for the `explore` HTTP
|
|
125
|
+
// server, Milestone 3): loadSignedGraph does the signed-artifact
|
|
126
|
+
// load+verify, the four handleX functions do the lookups. This module
|
|
127
|
+
// adds nothing but MCP tool shape (name/description/inputSchema/handler)
|
|
128
|
+
// and MCP-appropriate error handling — no new graph-query logic is
|
|
129
|
+
// written here, on purpose (see this sub-project's own scoping doc).
|
|
130
|
+
|
|
170
131
|
|
|
171
|
-
const SNIPPET_MAX = 2000;
|
|
172
|
-
// OWASP A03 — cap input before running 14 regex patterns over it. A forged
|
|
173
|
-
// last-scan.json could plant a 50MB description string; without this cap a
|
|
174
|
-
// single explain_finding/query_taint call would peg CPU. After truncation
|
|
175
|
-
// the snippet still gets the final SNIPPET_MAX trim downstream.
|
|
176
|
-
const INPUT_MAX = 100_000;
|
|
177
|
-
|
|
178
|
-
function redactString(s) {
|
|
179
|
-
if (typeof s !== 'string') return s;
|
|
180
|
-
let out = s;
|
|
181
|
-
if (out.length > INPUT_MAX) out = out.slice(0, INPUT_MAX) + `…(+${out.length - INPUT_MAX})`;
|
|
182
|
-
for (const [re, kind] of PATTERNS) {
|
|
183
|
-
out = out.replace(re, `[REDACTED:${kind}]`);
|
|
184
|
-
}
|
|
185
|
-
if (out.length > SNIPPET_MAX) out = out.slice(0, SNIPPET_MAX) + `…(+${out.length - SNIPPET_MAX})`;
|
|
186
|
-
return out;
|
|
187
|
-
}
|
|
188
132
|
|
|
189
|
-
// Deep-redact every string in a finding-like object (mutates returned copy).
|
|
190
|
-
function redactFinding(f) {
|
|
191
|
-
if (!f || typeof f !== 'object') return f;
|
|
192
|
-
const out = { ...f };
|
|
193
|
-
for (const k of ['snippet', 'description', 'remediation', 'title', 'vuln', 'message']) {
|
|
194
|
-
if (typeof out[k] === 'string') out[k] = redactString(out[k]);
|
|
195
|
-
}
|
|
196
|
-
if (out.trace) {
|
|
197
|
-
try { out.trace = JSON.parse(redactString(JSON.stringify(out.trace))); }
|
|
198
|
-
catch { /* keep as-is if not round-trippable */ }
|
|
199
|
-
}
|
|
200
|
-
return out;
|
|
201
|
-
}
|
|
202
133
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
const META = { source: 'agentic-security-mcp', untrusted_excerpts: true };
|
|
137
|
+
|
|
138
|
+
function _loadOrFailure(sessionRoot) {
|
|
139
|
+
const loaded = (0,graph_loader.loadSignedGraph)(sessionRoot);
|
|
140
|
+
if (loaded.ok) return { graph: loaded.graph };
|
|
141
|
+
return {
|
|
142
|
+
failure: {
|
|
143
|
+
_meta: META,
|
|
144
|
+
hasResult: false,
|
|
145
|
+
reason: loaded.reason,
|
|
146
|
+
message: loaded.message,
|
|
147
|
+
},
|
|
148
|
+
};
|
|
206
149
|
}
|
|
207
150
|
|
|
208
|
-
//
|
|
209
|
-
|
|
210
|
-
//
|
|
211
|
-
|
|
151
|
+
// Milestone 5, large-graph pagination: an optional `filter` input narrows the
|
|
152
|
+
// returned graph via the exact same `validateFilterShape`/`_filterGraph`
|
|
153
|
+
// pair the CLI's own `--filter` and the `explore` server's new
|
|
154
|
+
// `POST /api/v1/query` endpoint both already use — one real, shared
|
|
155
|
+
// primitive, not a third drifting copy. KNOWN, DISCLOSED GAP (still open):
|
|
156
|
+
// an OMITTED filter still returns the whole graph inline, with the same
|
|
157
|
+
// stdio.js MAX_LINE_BYTES (4MB) risk on a very large, unfiltered scan as
|
|
158
|
+
// before this change — this increment adds an opt-in capability for a
|
|
159
|
+
// caller that supplies a filter, it does not add a forced fallback/offload
|
|
160
|
+
// for a caller that doesn't. That remains a follow-up increment.
|
|
161
|
+
// Final whole-branch review finding: `filter: {}` is NOT the same as
|
|
162
|
+
// omitting `filter` — `_filterGraph` treats an empty (but well-formed)
|
|
163
|
+
// filter object as "narrow to nothing" (empty nodeIds/edgeIds Sets), so
|
|
164
|
+
// `filter: {}` returns an EMPTY graph (zero nodes/edges/flows), not the
|
|
165
|
+
// whole one. Called out explicitly in this tool's own `description` below
|
|
166
|
+
// so an agent reaching for "no restriction" reaches for OMITTING the
|
|
167
|
+
// argument, never for `{}`.
|
|
168
|
+
const dataflow_get_graph = {
|
|
169
|
+
name: 'dataflow_get_graph',
|
|
170
|
+
description: 'Return the DataFlowGraph v1 artifact from the last signed, verified deep-mode scan: nodes, edges, flows, scope, coverage, and limitations. Requires a prior `AGENTIC_SECURITY_LINEAGE_DEEP=1 agentic-security scan`. Optional `filter: {nodeIds, edgeIds}` narrows the returned nodes/edges/flows/dataElements (same primitive as the CLI\'s `--filter` and the `explore` server\'s `POST /api/v1/query`). IMPORTANT: omit `filter` entirely for the whole graph — passing `filter: {}` returns an EMPTY graph (zero nodes/edges/flows), not the whole one, since an empty filter narrows to nothing rather than meaning "no restriction". KNOWN GAP: an OMITTED filter still returns the whole graph inline with no pagination/offload — may exceed the stdio transport line cap on a very large, unfiltered graph; supply a real, non-empty filter to narrow the response.',
|
|
171
|
+
inputSchema: {
|
|
172
|
+
type: 'object',
|
|
173
|
+
additionalProperties: false,
|
|
174
|
+
properties: {
|
|
175
|
+
filter: {
|
|
176
|
+
type: 'object',
|
|
177
|
+
additionalProperties: false,
|
|
178
|
+
properties: {
|
|
179
|
+
nodeIds: { type: 'array', items: { type: 'string' } },
|
|
180
|
+
edgeIds: { type: 'array', items: { type: 'string' } },
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
async handler(args, ctx) {
|
|
186
|
+
const { graph, failure } = _loadOrFailure(ctx.sessionRoot);
|
|
187
|
+
if (failure) return failure;
|
|
188
|
+
// Milestone 5, large-graph pagination: reuses the exact same
|
|
189
|
+
// validateFilterShape/_filterGraph pair the new POST /api/v1/query
|
|
190
|
+
// server endpoint and the CLI's own --filter both use — one real,
|
|
191
|
+
// shared primitive, not a third drifting copy.
|
|
192
|
+
const filterCheck = (0,export_json.validateFilterShape)(args?.filter);
|
|
193
|
+
if (!filterCheck.valid) {
|
|
194
|
+
return { _meta: META, hasResult: false, reason: 'invalid-filter', message: filterCheck.error };
|
|
195
|
+
}
|
|
196
|
+
const { status, body } = (0,routes/* handleGraph */.fn)(graph);
|
|
197
|
+
return {
|
|
198
|
+
_meta: META,
|
|
199
|
+
hasResult: true,
|
|
200
|
+
status,
|
|
201
|
+
data: (0,redact_graph/* _redactGraph */.zl)(args?.filter ? (0,export_json/* _filterGraph */.e)(body.data, args.filter) : body.data),
|
|
202
|
+
digest: body.digest,
|
|
203
|
+
schemaVersion: body.schemaVersion,
|
|
204
|
+
extensions: body.extensions,
|
|
205
|
+
scope: body.scope,
|
|
206
|
+
coverage: body.coverage,
|
|
207
|
+
limitations: body.limitations,
|
|
208
|
+
};
|
|
209
|
+
},
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
const dataflow_get_node = {
|
|
213
|
+
name: 'dataflow_get_node',
|
|
214
|
+
description: 'Look up one node by canonical id in the DataFlowGraph v1 artifact.',
|
|
215
|
+
inputSchema: {
|
|
216
|
+
type: 'object',
|
|
217
|
+
additionalProperties: false,
|
|
218
|
+
properties: { id: { type: 'string', minLength: 1, maxLength: 512 } },
|
|
219
|
+
required: ['id'],
|
|
220
|
+
},
|
|
221
|
+
async handler({ id }, ctx) {
|
|
222
|
+
const { graph, failure } = _loadOrFailure(ctx.sessionRoot);
|
|
223
|
+
if (failure) return failure;
|
|
224
|
+
const { status, body } = (0,routes/* handleNode */.d5)(graph, id);
|
|
225
|
+
return {
|
|
226
|
+
_meta: META,
|
|
227
|
+
hasResult: true,
|
|
228
|
+
notFound: status === 404,
|
|
229
|
+
data: (0,redact_graph/* _redactNode */.T2)(body.data),
|
|
230
|
+
canonicalIds: body.canonicalIds,
|
|
231
|
+
};
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const dataflow_get_edge = {
|
|
236
|
+
name: 'dataflow_get_edge',
|
|
237
|
+
description: 'Look up one edge by canonical id in the DataFlowGraph v1 artifact.',
|
|
238
|
+
inputSchema: {
|
|
239
|
+
type: 'object',
|
|
240
|
+
additionalProperties: false,
|
|
241
|
+
properties: { id: { type: 'string', minLength: 1, maxLength: 512 } },
|
|
242
|
+
required: ['id'],
|
|
243
|
+
},
|
|
244
|
+
async handler({ id }, ctx) {
|
|
245
|
+
const { graph, failure } = _loadOrFailure(ctx.sessionRoot);
|
|
246
|
+
if (failure) return failure;
|
|
247
|
+
const { status, body } = (0,routes/* handleEdge */.Yu)(graph, id);
|
|
248
|
+
return {
|
|
249
|
+
_meta: META,
|
|
250
|
+
hasResult: true,
|
|
251
|
+
notFound: status === 404,
|
|
252
|
+
data: body.data,
|
|
253
|
+
canonicalIds: body.canonicalIds,
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const dataflow_get_flow = {
|
|
259
|
+
name: 'dataflow_get_flow',
|
|
260
|
+
description: 'Look up one flow by canonical id in the DataFlowGraph v1 artifact, including its contributing node/edge canonical ids.',
|
|
261
|
+
inputSchema: {
|
|
262
|
+
type: 'object',
|
|
263
|
+
additionalProperties: false,
|
|
264
|
+
properties: { id: { type: 'string', minLength: 1, maxLength: 512 } },
|
|
265
|
+
required: ['id'],
|
|
266
|
+
},
|
|
267
|
+
async handler({ id }, ctx) {
|
|
268
|
+
const { graph, failure } = _loadOrFailure(ctx.sessionRoot);
|
|
269
|
+
if (failure) return failure;
|
|
270
|
+
const { status, body } = (0,routes/* handleFlow */.jg)(graph, id);
|
|
271
|
+
return {
|
|
272
|
+
_meta: META,
|
|
273
|
+
hasResult: true,
|
|
274
|
+
notFound: status === 404,
|
|
275
|
+
data: body.data,
|
|
276
|
+
canonicalIds: body.canonicalIds,
|
|
277
|
+
};
|
|
278
|
+
},
|
|
279
|
+
};
|
|
280
|
+
|
|
212
281
|
;// CONCATENATED MODULE: ./src/posture/agents-memory.js
|
|
213
282
|
// AGENTS.md — writable continual-learning memory (harness-anatomy #2).
|
|
214
283
|
//
|
|
@@ -515,6 +584,7 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
|
|
|
515
584
|
// (Sigstore/SLSA attestation) — see report/index.js's import comment.
|
|
516
585
|
|
|
517
586
|
|
|
587
|
+
|
|
518
588
|
// Lazy-loaded: these transitively pull in npm packages (@babel/core and
|
|
519
589
|
// friends) that aren't available in the plugin-cache install path
|
|
520
590
|
// (no node_modules). Deferring keeps the MCP server bootable everywhere;
|
|
@@ -526,14 +596,14 @@ async function getRunScan() {
|
|
|
526
596
|
}
|
|
527
597
|
let _verifyFixCore;
|
|
528
598
|
async function getVerifyFixCore() {
|
|
529
|
-
if (!_verifyFixCore) _verifyFixCore = (await __webpack_require__.e(/* import() */
|
|
599
|
+
if (!_verifyFixCore) _verifyFixCore = (await __webpack_require__.e(/* import() */ 3526).then(__webpack_require__.bind(__webpack_require__, 3526))).verifyFix;
|
|
530
600
|
return _verifyFixCore;
|
|
531
601
|
}
|
|
532
602
|
|
|
533
603
|
const MAX_FILES_PER_SCAN = 1024;
|
|
534
604
|
const MAX_FILE_BYTES = 500_000;
|
|
535
605
|
const MAX_TOTAL_SCAN_BYTES = 50_000_000;
|
|
536
|
-
const
|
|
606
|
+
const tools_META = { source: 'agentic-security-mcp', untrusted_excerpts: true };
|
|
537
607
|
|
|
538
608
|
// OWASP A01 — refuse writes to paths that could subvert the security tool
|
|
539
609
|
// itself or the host's source-control / dependency state. A forged finding
|
|
@@ -913,7 +983,7 @@ const scan_diff = {
|
|
|
913
983
|
// scanning), left as-is rather than silently claimed fixed here.
|
|
914
984
|
const findings = (0,report.normalizeFindings)(result.scan)
|
|
915
985
|
.filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
|
|
916
|
-
.map(f => redactFinding({
|
|
986
|
+
.map(f => (0,redact/* redactFinding */.lE)({
|
|
917
987
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
918
988
|
title: f.vuln, cwe: f.cwe,
|
|
919
989
|
description: f.description, remediation: f.remediation,
|
|
@@ -925,7 +995,7 @@ const scan_diff = {
|
|
|
925
995
|
const off = _maybeOffload(sessionRoot, 'scan_diff', findings);
|
|
926
996
|
if (off.offloaded) {
|
|
927
997
|
return {
|
|
928
|
-
_meta:
|
|
998
|
+
_meta: tools_META,
|
|
929
999
|
scannedFiles: Object.keys(fileContents).length,
|
|
930
1000
|
findingCount: off.total,
|
|
931
1001
|
offloaded: true,
|
|
@@ -935,7 +1005,7 @@ const scan_diff = {
|
|
|
935
1005
|
};
|
|
936
1006
|
}
|
|
937
1007
|
return {
|
|
938
|
-
_meta:
|
|
1008
|
+
_meta: tools_META,
|
|
939
1009
|
scannedFiles: Object.keys(fileContents).length,
|
|
940
1010
|
findingCount: findings.length,
|
|
941
1011
|
findings,
|
|
@@ -961,7 +1031,7 @@ const query_taint = {
|
|
|
961
1031
|
async handler({ source, sink, limit, offset }, ctx) {
|
|
962
1032
|
const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: true });
|
|
963
1033
|
if (!scan) {
|
|
964
|
-
return { _meta:
|
|
1034
|
+
return { _meta: tools_META, hasResult: false, status, message: `No usable scan state (${status}).` };
|
|
965
1035
|
}
|
|
966
1036
|
const lim = Number.isInteger(limit) ? Math.min(50, Math.max(1, limit)) : 10;
|
|
967
1037
|
const off = Number.isInteger(offset) ? Math.max(0, offset) : 0;
|
|
@@ -976,13 +1046,13 @@ const query_taint = {
|
|
|
976
1046
|
const hay = [f.description, f.title, f.vuln, f.snippet, JSON.stringify(f.trace || '')].join(' ').toLowerCase();
|
|
977
1047
|
return hay.includes(srcL) && hay.includes(sinkL);
|
|
978
1048
|
});
|
|
979
|
-
const page = all.slice(off, off + lim).map(f => redactFinding({
|
|
1049
|
+
const page = all.slice(off, off + lim).map(f => (0,redact/* redactFinding */.lE)({
|
|
980
1050
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
981
1051
|
title: f.title || f.vuln, description: f.description,
|
|
982
1052
|
trace: f.trace || null,
|
|
983
1053
|
}));
|
|
984
1054
|
return {
|
|
985
|
-
_meta:
|
|
1055
|
+
_meta: tools_META,
|
|
986
1056
|
hasResult: true,
|
|
987
1057
|
integrity: status,
|
|
988
1058
|
scanStartedAt: scan.startedAt || scan.meta?.startedAt || null,
|
|
@@ -1014,7 +1084,7 @@ const explain_finding = {
|
|
|
1014
1084
|
if (!scan) throw new Error(`No usable scan state (${status}).`);
|
|
1015
1085
|
const f = _findById(scan, finding_id);
|
|
1016
1086
|
if (!f) throw new Error(`Finding not found: ${finding_id}`);
|
|
1017
|
-
const redacted = redactFinding({
|
|
1087
|
+
const redacted = (0,redact/* redactFinding */.lE)({
|
|
1018
1088
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
1019
1089
|
title: f.title || f.vuln, cwe: f.cwe,
|
|
1020
1090
|
description: f.description, remediation: f.remediation,
|
|
@@ -1039,7 +1109,7 @@ const explain_finding = {
|
|
|
1039
1109
|
}
|
|
1040
1110
|
}
|
|
1041
1111
|
return {
|
|
1042
|
-
_meta:
|
|
1112
|
+
_meta: tools_META,
|
|
1043
1113
|
...redacted,
|
|
1044
1114
|
trace: traceTrimmed,
|
|
1045
1115
|
traceOffload: traceMeta,
|
|
@@ -1150,16 +1220,16 @@ const apply_fix = {
|
|
|
1150
1220
|
},
|
|
1151
1221
|
async handler({ finding_id, confirm, dry_run = false, patch = null, fixMeta = null }, ctx) {
|
|
1152
1222
|
if (confirm !== true) {
|
|
1153
|
-
return { _meta:
|
|
1223
|
+
return { _meta: tools_META, applied: false, reason: 'apply_fix requires confirm: true.' };
|
|
1154
1224
|
}
|
|
1155
1225
|
const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
|
|
1156
1226
|
if (!scan) {
|
|
1157
|
-
return { _meta:
|
|
1227
|
+
return { _meta: tools_META, applied: false, reason: `last-scan.json failed integrity check: ${status}. Run a fresh scan.` };
|
|
1158
1228
|
}
|
|
1159
1229
|
const f = _findById(scan, finding_id);
|
|
1160
|
-
if (!f) return { _meta:
|
|
1230
|
+
if (!f) return { _meta: tools_META, applied: false, reason: `Finding not found: ${finding_id}` };
|
|
1161
1231
|
if (f._shadow === true) {
|
|
1162
|
-
return { _meta:
|
|
1232
|
+
return { _meta: tools_META, applied: false, reason: 'shadow findings cannot be auto-applied' };
|
|
1163
1233
|
}
|
|
1164
1234
|
|
|
1165
1235
|
// #3 — verifier-approved patch path. When the caller supplies `patch` (a
|
|
@@ -1174,15 +1244,15 @@ const apply_fix = {
|
|
|
1174
1244
|
// slip through — there is no token to replay, the verify runs here and now.
|
|
1175
1245
|
if (patch && typeof patch === 'object' && Object.keys(patch).length) {
|
|
1176
1246
|
if (!f.stableId) {
|
|
1177
|
-
return { _meta:
|
|
1247
|
+
return { _meta: tools_META, applied: false, reason: 'finding has no stableId — cannot verify a patch against it' };
|
|
1178
1248
|
}
|
|
1179
1249
|
const confinedAbs = {};
|
|
1180
1250
|
for (const [rel, content] of Object.entries(patch)) {
|
|
1181
1251
|
let abs;
|
|
1182
1252
|
try { abs = _confine(ctx.sessionRoot, rel, 'patch key'); }
|
|
1183
|
-
catch (e) { return { _meta:
|
|
1253
|
+
catch (e) { return { _meta: tools_META, applied: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1184
1254
|
if (_isReservedWritePath(ctx.sessionRoot, abs)) {
|
|
1185
|
-
return { _meta:
|
|
1255
|
+
return { _meta: tools_META, applied: false, reason: `reserved path refused: ${rel}` };
|
|
1186
1256
|
}
|
|
1187
1257
|
confinedAbs[rel] = { abs, content: String(content) };
|
|
1188
1258
|
}
|
|
@@ -1195,7 +1265,7 @@ const apply_fix = {
|
|
|
1195
1265
|
// suite as a fourth verification leg (scan + lint + tests). Opt-in
|
|
1196
1266
|
// because many repos have no runner and we must not fail-closed by
|
|
1197
1267
|
// default. Normalized to the scan+lint verdict shape used below.
|
|
1198
|
-
const { verifyFixWithTests } = await __webpack_require__.e(/* import() */
|
|
1268
|
+
const { verifyFixWithTests } = await __webpack_require__.e(/* import() */ 4113).then(__webpack_require__.bind(__webpack_require__, 4113));
|
|
1199
1269
|
const t = await verifyFixWithTests({ scanRoot: ctx.sessionRoot, originalFindingStableId: f.stableId, files: _files });
|
|
1200
1270
|
verdict = { ok: t.ok, summary: t.summary, rescan: t.legs?.scan?.detail, lint: t.legs?.lint?.detail, tests: t.legs?.tests, testVerdict: t.verdict };
|
|
1201
1271
|
} else {
|
|
@@ -1208,11 +1278,11 @@ const apply_fix = {
|
|
|
1208
1278
|
});
|
|
1209
1279
|
}
|
|
1210
1280
|
} catch (e) {
|
|
1211
|
-
return { _meta:
|
|
1281
|
+
return { _meta: tools_META, applied: false, reason: `patch verification failed: ${e.message}` };
|
|
1212
1282
|
}
|
|
1213
1283
|
if (!verdict.ok) {
|
|
1214
1284
|
return {
|
|
1215
|
-
_meta:
|
|
1285
|
+
_meta: tools_META, applied: false,
|
|
1216
1286
|
reason: `patch rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
|
|
1217
1287
|
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
|
|
1218
1288
|
};
|
|
@@ -1235,7 +1305,7 @@ const apply_fix = {
|
|
|
1235
1305
|
}
|
|
1236
1306
|
const materialClassification = (0,material_change/* classifyFixMaterialRisk */.kz)(filesForMaterialClassification);
|
|
1237
1307
|
if (dry_run) {
|
|
1238
|
-
return { _meta:
|
|
1308
|
+
return { _meta: tools_META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
|
|
1239
1309
|
}
|
|
1240
1310
|
if (materialClassification.highImpactCategories.length) {
|
|
1241
1311
|
const approval = fixMeta && typeof fixMeta === 'object' ? fixMeta.approval : null;
|
|
@@ -1244,7 +1314,7 @@ const apply_fix = {
|
|
|
1244
1314
|
typeof approval.reason === 'string' && approval.reason.trim().length > 0);
|
|
1245
1315
|
if (!hasApprovalEvidence) {
|
|
1246
1316
|
return {
|
|
1247
|
-
_meta:
|
|
1317
|
+
_meta: tools_META, applied: false,
|
|
1248
1318
|
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) requires approval evidence — pass fixMeta.approval: {approvedBy, reason} — before it can be applied`,
|
|
1249
1319
|
materialClassification,
|
|
1250
1320
|
};
|
|
@@ -1254,7 +1324,7 @@ const apply_fix = {
|
|
|
1254
1324
|
const identityCheck = (0,approver_registry/* verifyApprover */.$N)(approverRegistry, approval.approvedBy, requiredRoles);
|
|
1255
1325
|
if (!identityCheck.verified) {
|
|
1256
1326
|
return {
|
|
1257
|
-
_meta:
|
|
1327
|
+
_meta: tools_META, applied: false,
|
|
1258
1328
|
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${identityCheck.reason}`,
|
|
1259
1329
|
materialClassification,
|
|
1260
1330
|
};
|
|
@@ -1264,7 +1334,7 @@ const apply_fix = {
|
|
|
1264
1334
|
const sodCheck = (0,approver_registry/* checkSeparationOfDuties */.I0)(approverRegistry, fixMeta?.author, approval.approvedBy);
|
|
1265
1335
|
if (!sodCheck.ok) {
|
|
1266
1336
|
return {
|
|
1267
|
-
_meta:
|
|
1337
|
+
_meta: tools_META, applied: false,
|
|
1268
1338
|
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${sodCheck.reason}`,
|
|
1269
1339
|
materialClassification,
|
|
1270
1340
|
};
|
|
@@ -1291,20 +1361,20 @@ const apply_fix = {
|
|
|
1291
1361
|
try { await (0,fix_history/* revertEntryById */.rJ)(ctx.sessionRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
|
|
1292
1362
|
}
|
|
1293
1363
|
if (e && e.name === 'FixAttemptBudgetExceededError') {
|
|
1294
|
-
return { _meta:
|
|
1364
|
+
return { _meta: tools_META, applied: false, reason: `budget-exceeded: ${e.message}`, budgetExceeded: true, attempts: e.attempts, maxAttempts: e.max, key: e.key };
|
|
1295
1365
|
}
|
|
1296
1366
|
throw e;
|
|
1297
1367
|
}
|
|
1298
1368
|
let acceptance = null;
|
|
1299
1369
|
try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* best-effort */ }
|
|
1300
|
-
return { _meta:
|
|
1370
|
+
return { _meta: tools_META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
|
|
1301
1371
|
}
|
|
1302
1372
|
|
|
1303
1373
|
if (typeof f.fix?.replacement !== 'string') {
|
|
1304
1374
|
// Premortem #2: templates are patch-shaped text. Same reasoning as
|
|
1305
1375
|
// the replacement path — do NOT pass through redactString here.
|
|
1306
1376
|
return {
|
|
1307
|
-
_meta:
|
|
1377
|
+
_meta: tools_META, applied: false,
|
|
1308
1378
|
reason: 'No full replacement available — only a template. Apply the template manually.',
|
|
1309
1379
|
template: f.fix?.code || '',
|
|
1310
1380
|
file: f.file, line: f.line,
|
|
@@ -1313,19 +1383,19 @@ const apply_fix = {
|
|
|
1313
1383
|
let absFile;
|
|
1314
1384
|
try { absFile = _confine(ctx.sessionRoot, f.file, 'finding.file'); }
|
|
1315
1385
|
catch (e) {
|
|
1316
|
-
return { _meta:
|
|
1386
|
+
return { _meta: tools_META, applied: false, reason: `path-escape refused: ${e.message}` };
|
|
1317
1387
|
}
|
|
1318
1388
|
if (_isReservedWritePath(ctx.sessionRoot, absFile)) {
|
|
1319
|
-
return { _meta:
|
|
1389
|
+
return { _meta: tools_META, applied: false, reason: `reserved path refused: writes to .git/, .agentic-security/, or node_modules/ are not permitted via apply_fix` };
|
|
1320
1390
|
}
|
|
1321
1391
|
if (!external_node_fs_.existsSync(absFile)) {
|
|
1322
|
-
return { _meta:
|
|
1392
|
+
return { _meta: tools_META, applied: false, reason: `File not found: ${absFile}` };
|
|
1323
1393
|
}
|
|
1324
1394
|
const originalContent = await promises_.readFile(absFile, 'utf8');
|
|
1325
1395
|
|
|
1326
1396
|
if (dry_run) {
|
|
1327
1397
|
return {
|
|
1328
|
-
_meta:
|
|
1398
|
+
_meta: tools_META,
|
|
1329
1399
|
applied: false, dryRun: true,
|
|
1330
1400
|
file: f.file,
|
|
1331
1401
|
originalSize: originalContent.length,
|
|
@@ -1347,7 +1417,7 @@ const apply_fix = {
|
|
|
1347
1417
|
// size-diff shape) but the load-bearing addition is the verification
|
|
1348
1418
|
// gate before the write.
|
|
1349
1419
|
if (!f.stableId) {
|
|
1350
|
-
return { _meta:
|
|
1420
|
+
return { _meta: tools_META, applied: false, reason: 'finding has no stableId — cannot verify a stored fix against it' };
|
|
1351
1421
|
}
|
|
1352
1422
|
const result = await (0,apply_fix_service/* applyVerifiedFix */.On)({
|
|
1353
1423
|
scanRoot: ctx.sessionRoot,
|
|
@@ -1357,9 +1427,9 @@ const apply_fix = {
|
|
|
1357
1427
|
});
|
|
1358
1428
|
if (!result.ok) {
|
|
1359
1429
|
if (result.budgetExceeded) {
|
|
1360
|
-
return { _meta:
|
|
1430
|
+
return { _meta: tools_META, applied: false, reason: result.reason, budgetExceeded: true, attempts: result.attempts, maxAttempts: result.maxAttempts, key: result.key };
|
|
1361
1431
|
}
|
|
1362
|
-
return { _meta:
|
|
1432
|
+
return { _meta: tools_META, applied: false, reason: result.reason, verify: result.verify || null };
|
|
1363
1433
|
}
|
|
1364
1434
|
// R25 (PRD §5): surface the running auto-fix acceptance rate after each
|
|
1365
1435
|
// applied fix, so the closed loop reports its own success metric.
|
|
@@ -1370,7 +1440,7 @@ const apply_fix = {
|
|
|
1370
1440
|
// FR-305: verifiedFull distinguishes "every required leg (lint, tests)
|
|
1371
1441
|
// genuinely ran and passed" from "passed, but a required leg was
|
|
1372
1442
|
// skipped or unavailable" — verified:true alone conflates them.
|
|
1373
|
-
_meta:
|
|
1443
|
+
_meta: tools_META, applied: true, verified: true, verifiedFull: result.verifiedFull,
|
|
1374
1444
|
historyId: entry.historyId, file: entry.file, backupPath: entry.backupPath,
|
|
1375
1445
|
integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance,
|
|
1376
1446
|
verify: result.verify,
|
|
@@ -1439,7 +1509,7 @@ const verify_fix = {
|
|
|
1439
1509
|
try {
|
|
1440
1510
|
_confine(ctx.sessionRoot, relPath, 'files key');
|
|
1441
1511
|
} catch (e) {
|
|
1442
|
-
return { _meta:
|
|
1512
|
+
return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` };
|
|
1443
1513
|
}
|
|
1444
1514
|
confined[relPath] = String(content);
|
|
1445
1515
|
}
|
|
@@ -1471,10 +1541,10 @@ const verify_fix = {
|
|
|
1471
1541
|
fixMeta,
|
|
1472
1542
|
});
|
|
1473
1543
|
return {
|
|
1474
|
-
_meta:
|
|
1544
|
+
_meta: tools_META,
|
|
1475
1545
|
ok: r.ok,
|
|
1476
1546
|
rescan: { ok: r.rescan.ok, reason: r.rescan.reason, introduced: r.rescan.introduced || [] },
|
|
1477
|
-
lint: { runner: r.lint.runner, ok: r.lint.ok, skipped: r.lint.skipped || false, output: redactString(r.lint.output || '').slice(0, 1500) },
|
|
1547
|
+
lint: { runner: r.lint.runner, ok: r.lint.ok, skipped: r.lint.skipped || false, output: (0,redact/* redactString */.rd)(r.lint.output || '').slice(0, 1500) },
|
|
1478
1548
|
// verifyFix computes five legs, not two — tests/honesty/poc were
|
|
1479
1549
|
// being silently dropped here, leaving an agent with no structured
|
|
1480
1550
|
// way to see WHY verification failed when the failure was in one
|
|
@@ -1485,11 +1555,11 @@ const verify_fix = {
|
|
|
1485
1555
|
// it can echo proof-harness detail derived from scanned source.
|
|
1486
1556
|
tests: r.tests,
|
|
1487
1557
|
honesty: r.honesty,
|
|
1488
|
-
poc: r.poc ? { ...r.poc, reason: r.poc.reason ? redactString(r.poc.reason) : r.poc.reason } : r.poc,
|
|
1558
|
+
poc: r.poc ? { ...r.poc, reason: r.poc.reason ? (0,redact/* redactString */.rd)(r.poc.reason) : r.poc.reason } : r.poc,
|
|
1489
1559
|
summary: r.summary,
|
|
1490
1560
|
};
|
|
1491
1561
|
} catch (e) {
|
|
1492
|
-
return { _meta:
|
|
1562
|
+
return { _meta: tools_META, ok: false, reason: `verify_fix failed: ${e.message}` };
|
|
1493
1563
|
}
|
|
1494
1564
|
},
|
|
1495
1565
|
};
|
|
@@ -1512,11 +1582,11 @@ const synthesize_fix = {
|
|
|
1512
1582
|
async handler({ finding_id }, ctx) {
|
|
1513
1583
|
const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
|
|
1514
1584
|
if (!scan) {
|
|
1515
|
-
return { _meta:
|
|
1585
|
+
return { _meta: tools_META, ok: false, reason: `last-scan.json failed integrity check: ${status}` };
|
|
1516
1586
|
}
|
|
1517
1587
|
const f = _findById(scan, finding_id);
|
|
1518
|
-
if (!f) return { _meta:
|
|
1519
|
-
if (f._shadow === true) return { _meta:
|
|
1588
|
+
if (!f) return { _meta: tools_META, ok: false, reason: `Finding not found: ${finding_id}` };
|
|
1589
|
+
if (f._shadow === true) return { _meta: tools_META, ok: false, reason: 'shadow findings have no synthesized fix' };
|
|
1520
1590
|
const fix = f.fix || {};
|
|
1521
1591
|
const hasReplacement = typeof fix.replacement === 'string' && fix.replacement.length > 0;
|
|
1522
1592
|
// Patch bounds: count files touched + LoC delta.
|
|
@@ -1552,7 +1622,7 @@ const synthesize_fix = {
|
|
|
1552
1622
|
// verbatim. Snippet/description/etc. continue to be redacted in
|
|
1553
1623
|
// explain_finding / scan_diff — that's the right surface for redaction.
|
|
1554
1624
|
return {
|
|
1555
|
-
_meta:
|
|
1625
|
+
_meta: tools_META,
|
|
1556
1626
|
ok: true,
|
|
1557
1627
|
stable_id: f.stableId || null,
|
|
1558
1628
|
file: f.file, line: f.line,
|
|
@@ -1602,14 +1672,14 @@ const find_rule_module = {
|
|
|
1602
1672
|
},
|
|
1603
1673
|
async handler({ cwe, family }, ctx) {
|
|
1604
1674
|
if (!cwe && !family) {
|
|
1605
|
-
return { _meta:
|
|
1675
|
+
return { _meta: tools_META, ok: false, reason: 'provide cwe (e.g. "CWE-89") or family (e.g. "sql-injection")' };
|
|
1606
1676
|
}
|
|
1607
1677
|
// Pattern enforcement — the mini-schema validator doesn't do `pattern`.
|
|
1608
1678
|
if (cwe && !/^CWE-\d+$/.test(cwe)) {
|
|
1609
|
-
return { _meta:
|
|
1679
|
+
return { _meta: tools_META, ok: false, reason: 'cwe must match /^CWE-\\d+$/ (e.g. "CWE-89")' };
|
|
1610
1680
|
}
|
|
1611
1681
|
if (family && !/^[a-z][a-z0-9-]+$/.test(family)) {
|
|
1612
|
-
return { _meta:
|
|
1682
|
+
return { _meta: tools_META, ok: false, reason: 'family must match /^[a-z][a-z0-9-]+$/ (e.g. "sql-injection")' };
|
|
1613
1683
|
}
|
|
1614
1684
|
const sessionRoot = ctx.sessionRoot;
|
|
1615
1685
|
const roots = [
|
|
@@ -1663,7 +1733,7 @@ const find_rule_module = {
|
|
|
1663
1733
|
if (hits.length >= 20) break;
|
|
1664
1734
|
}
|
|
1665
1735
|
return {
|
|
1666
|
-
_meta:
|
|
1736
|
+
_meta: tools_META,
|
|
1667
1737
|
ok: true,
|
|
1668
1738
|
query: { cwe: cwe || null, family: family || null },
|
|
1669
1739
|
hitCount: hits.length,
|
|
@@ -1706,14 +1776,14 @@ const append_scratchpad = {
|
|
|
1706
1776
|
},
|
|
1707
1777
|
async handler({ path: relPath, content }, ctx) {
|
|
1708
1778
|
const v = _validateScratchpadPath(relPath);
|
|
1709
|
-
if (!v.ok) return { _meta:
|
|
1779
|
+
if (!v.ok) return { _meta: tools_META, ok: false, reason: v.reason };
|
|
1710
1780
|
let abs;
|
|
1711
1781
|
try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
|
|
1712
|
-
catch (e) { return { _meta:
|
|
1782
|
+
catch (e) { return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1713
1783
|
const total = _scratchpadTotalBytes(ctx.sessionRoot);
|
|
1714
1784
|
if (total + content.length > SCRATCHPAD_MAX_TOTAL_BYTES) {
|
|
1715
1785
|
return {
|
|
1716
|
-
_meta:
|
|
1786
|
+
_meta: tools_META, ok: false,
|
|
1717
1787
|
reason: `scratchpad-total-exceeded: ${total} + ${content.length} > ${SCRATCHPAD_MAX_TOTAL_BYTES}. Clean up via "rm -rf .agentic-security/agent-scratchpad" or rotate sessions.`,
|
|
1718
1788
|
};
|
|
1719
1789
|
}
|
|
@@ -1721,7 +1791,7 @@ const append_scratchpad = {
|
|
|
1721
1791
|
try { if (external_node_fs_.existsSync(abs)) existing = external_node_fs_.statSync(abs).size; } catch {}
|
|
1722
1792
|
if (existing + content.length > SCRATCHPAD_MAX_FILE_BYTES) {
|
|
1723
1793
|
return {
|
|
1724
|
-
_meta:
|
|
1794
|
+
_meta: tools_META, ok: false,
|
|
1725
1795
|
reason: `scratchpad-file-exceeded: ${existing} + ${content.length} > ${SCRATCHPAD_MAX_FILE_BYTES}. Start a new file.`,
|
|
1726
1796
|
};
|
|
1727
1797
|
}
|
|
@@ -1729,12 +1799,12 @@ const append_scratchpad = {
|
|
|
1729
1799
|
external_node_fs_.mkdirSync(external_node_path_.dirname(abs), { recursive: true });
|
|
1730
1800
|
external_node_fs_.appendFileSync(abs, content);
|
|
1731
1801
|
return {
|
|
1732
|
-
_meta:
|
|
1802
|
+
_meta: tools_META, ok: true,
|
|
1733
1803
|
path: relPath, bytesWritten: content.length, fileSize: existing + content.length,
|
|
1734
1804
|
scratchpadTotal: total + content.length,
|
|
1735
1805
|
};
|
|
1736
1806
|
} catch (e) {
|
|
1737
|
-
return { _meta:
|
|
1807
|
+
return { _meta: tools_META, ok: false, reason: `write-failed: ${e.message}` };
|
|
1738
1808
|
}
|
|
1739
1809
|
},
|
|
1740
1810
|
};
|
|
@@ -1754,14 +1824,14 @@ const read_scratchpad = {
|
|
|
1754
1824
|
},
|
|
1755
1825
|
async handler({ path: relPath, offset, limit }, ctx) {
|
|
1756
1826
|
const v = _validateScratchpadPath(relPath);
|
|
1757
|
-
if (!v.ok) return { _meta:
|
|
1827
|
+
if (!v.ok) return { _meta: tools_META, ok: false, reason: v.reason };
|
|
1758
1828
|
let abs;
|
|
1759
1829
|
try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
|
|
1760
|
-
catch (e) { return { _meta:
|
|
1761
|
-
if (!external_node_fs_.existsSync(abs)) return { _meta:
|
|
1830
|
+
catch (e) { return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1831
|
+
if (!external_node_fs_.existsSync(abs)) return { _meta: tools_META, ok: false, reason: 'not-found' };
|
|
1762
1832
|
let stat;
|
|
1763
|
-
try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta:
|
|
1764
|
-
if (!stat.isFile()) return { _meta:
|
|
1833
|
+
try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta: tools_META, ok: false, reason: `stat-failed: ${e.message}` }; }
|
|
1834
|
+
if (!stat.isFile()) return { _meta: tools_META, ok: false, reason: 'not-a-file' };
|
|
1765
1835
|
const off = Number.isInteger(offset) ? Math.max(0, offset) : 0;
|
|
1766
1836
|
const lim = Number.isInteger(limit) ? Math.min(64 * 1024, Math.max(1, limit)) : 4096;
|
|
1767
1837
|
let buf;
|
|
@@ -1771,10 +1841,10 @@ const read_scratchpad = {
|
|
|
1771
1841
|
const read = external_node_fs_.readSync(fd, tmp, 0, lim, off);
|
|
1772
1842
|
external_node_fs_.closeSync(fd);
|
|
1773
1843
|
buf = tmp.slice(0, read);
|
|
1774
|
-
} catch (e) { return { _meta:
|
|
1844
|
+
} catch (e) { return { _meta: tools_META, ok: false, reason: `read-failed: ${e.message}` }; }
|
|
1775
1845
|
const text = buf.toString('utf8');
|
|
1776
1846
|
return {
|
|
1777
|
-
_meta:
|
|
1847
|
+
_meta: tools_META, ok: true,
|
|
1778
1848
|
path: relPath,
|
|
1779
1849
|
offset: off, limit: lim, bytesRead: buf.length,
|
|
1780
1850
|
totalSize: stat.size,
|
|
@@ -1806,7 +1876,7 @@ const append_agents_memory = {
|
|
|
1806
1876
|
},
|
|
1807
1877
|
async handler({ agent, body }, ctx) {
|
|
1808
1878
|
const r = appendAgentsMemory(ctx.sessionRoot, { agent, body });
|
|
1809
|
-
return { _meta:
|
|
1879
|
+
return { _meta: tools_META, ...r };
|
|
1810
1880
|
},
|
|
1811
1881
|
};
|
|
1812
1882
|
|
|
@@ -1822,16 +1892,16 @@ const read_agents_memory = {
|
|
|
1822
1892
|
},
|
|
1823
1893
|
async handler({ full }, ctx) {
|
|
1824
1894
|
const body = readAgentsMemory(ctx.sessionRoot);
|
|
1825
|
-
if (!body) return { _meta:
|
|
1826
|
-
if (full) return { _meta:
|
|
1895
|
+
if (!body) return { _meta: tools_META, present: false };
|
|
1896
|
+
if (full) return { _meta: tools_META, present: true, length: body.length, content: body };
|
|
1827
1897
|
// Tail-only — same logic as summarizeForSession but inlined to avoid a
|
|
1828
1898
|
// second import surface.
|
|
1829
1899
|
const limit = 6 * 1024;
|
|
1830
|
-
if (body.length <= limit) return { _meta:
|
|
1900
|
+
if (body.length <= limit) return { _meta: tools_META, present: true, length: body.length, content: body };
|
|
1831
1901
|
const tail = body.slice(-limit);
|
|
1832
1902
|
const firstSection = tail.indexOf('\n## ');
|
|
1833
1903
|
const slice = firstSection >= 0 ? tail.slice(firstSection) : tail;
|
|
1834
|
-
return { _meta:
|
|
1904
|
+
return { _meta: tools_META, present: true, length: body.length, truncated: true, content: slice };
|
|
1835
1905
|
},
|
|
1836
1906
|
};
|
|
1837
1907
|
|
|
@@ -1865,10 +1935,10 @@ const query_triage_memory = {
|
|
|
1865
1935
|
// they landed in, rather than hardcoding a field allowlist that could
|
|
1866
1936
|
// miss one.
|
|
1867
1937
|
let results;
|
|
1868
|
-
try { results = JSON.parse(redactString(JSON.stringify(raw))); }
|
|
1938
|
+
try { results = JSON.parse((0,redact/* redactString */.rd)(JSON.stringify(raw))); }
|
|
1869
1939
|
catch { results = raw; }
|
|
1870
1940
|
return {
|
|
1871
|
-
_meta:
|
|
1941
|
+
_meta: tools_META,
|
|
1872
1942
|
count: results.length,
|
|
1873
1943
|
results,
|
|
1874
1944
|
};
|
|
@@ -1893,7 +1963,7 @@ const query_findings_memory = {
|
|
|
1893
1963
|
required: ['query'],
|
|
1894
1964
|
},
|
|
1895
1965
|
async handler({ query }, ctx) {
|
|
1896
|
-
const { queryFindingsMemory } = await __webpack_require__.e(/* import() */
|
|
1966
|
+
const { queryFindingsMemory } = await __webpack_require__.e(/* import() */ 3839).then(__webpack_require__.bind(__webpack_require__, 3839));
|
|
1897
1967
|
const raw = queryFindingsMemory(ctx.sessionRoot, query || '');
|
|
1898
1968
|
// Stage 6 correctness audit — same redaction gap and same fix as
|
|
1899
1969
|
// query_triage_memory just above: this mixes four differently-shaped
|
|
@@ -1901,9 +1971,9 @@ const query_findings_memory = {
|
|
|
1901
1971
|
// whole-structure redactString round-trip is applied rather than a
|
|
1902
1972
|
// per-field allowlist that could miss one of the four shapes.
|
|
1903
1973
|
let body;
|
|
1904
|
-
try { body = JSON.parse(redactString(JSON.stringify(raw))); }
|
|
1974
|
+
try { body = JSON.parse((0,redact/* redactString */.rd)(JSON.stringify(raw))); }
|
|
1905
1975
|
catch { body = raw; }
|
|
1906
|
-
return { _meta:
|
|
1976
|
+
return { _meta: tools_META, ...body };
|
|
1907
1977
|
},
|
|
1908
1978
|
};
|
|
1909
1979
|
|
|
@@ -1924,7 +1994,7 @@ const lookup_cve = {
|
|
|
1924
1994
|
},
|
|
1925
1995
|
async handler({ cve }, _ctx) {
|
|
1926
1996
|
const r = lookupCve(cve);
|
|
1927
|
-
return { _meta:
|
|
1997
|
+
return { _meta: tools_META, ...r };
|
|
1928
1998
|
},
|
|
1929
1999
|
};
|
|
1930
2000
|
|
|
@@ -1942,9 +2012,9 @@ const query_cache_telemetry = {
|
|
|
1942
2012
|
},
|
|
1943
2013
|
async handler({ transcript_path } = {}, ctx) {
|
|
1944
2014
|
const result = (0,cache_economics.analyzeTranscript)({ transcriptPath: transcript_path, projectDir: ctx?.sessionRoot || process.cwd() });
|
|
1945
|
-
if (!result.ok) return { _meta:
|
|
2015
|
+
if (!result.ok) return { _meta: tools_META, ok: false, reason: result.reason };
|
|
1946
2016
|
return {
|
|
1947
|
-
_meta:
|
|
2017
|
+
_meta: tools_META,
|
|
1948
2018
|
ok: true,
|
|
1949
2019
|
metrics: result.metrics,
|
|
1950
2020
|
leaks: result.leaks,
|
|
@@ -1960,7 +2030,7 @@ const query_cache_telemetry = {
|
|
|
1960
2030
|
// ecosystem's native --dry-run command. Safe to call any number of times.
|
|
1961
2031
|
let _scaUpgrade;
|
|
1962
2032
|
async function _getScaUpgrade() {
|
|
1963
|
-
if (!_scaUpgrade) _scaUpgrade = await __webpack_require__.e(/* import() */
|
|
2033
|
+
if (!_scaUpgrade) _scaUpgrade = await __webpack_require__.e(/* import() */ 5333).then(__webpack_require__.bind(__webpack_require__, 5333));
|
|
1964
2034
|
return _scaUpgrade;
|
|
1965
2035
|
}
|
|
1966
2036
|
const synthesize_sca_upgrade = {
|
|
@@ -1980,11 +2050,11 @@ const synthesize_sca_upgrade = {
|
|
|
1980
2050
|
const f = _findById(scan, finding_id);
|
|
1981
2051
|
if (!f) throw new Error(`Finding not found: ${finding_id}`);
|
|
1982
2052
|
if (f.type !== 'vulnerable_dep') {
|
|
1983
|
-
return { _meta:
|
|
2053
|
+
return { _meta: tools_META, ok: false, reason: 'finding is not an SCA vulnerable_dep — use synthesize_fix for SAST findings' };
|
|
1984
2054
|
}
|
|
1985
2055
|
const { planScaUpgrade } = await _getScaUpgrade();
|
|
1986
2056
|
const plan = await planScaUpgrade({ scanRoot: ctx.sessionRoot, finding: f });
|
|
1987
|
-
return { _meta:
|
|
2057
|
+
return { _meta: tools_META, ...plan };
|
|
1988
2058
|
},
|
|
1989
2059
|
};
|
|
1990
2060
|
|
|
@@ -2011,24 +2081,24 @@ const apply_sca_upgrade = {
|
|
|
2011
2081
|
},
|
|
2012
2082
|
async handler({ finding_id, confirm, run_tests = true }, ctx) {
|
|
2013
2083
|
if (confirm !== true) {
|
|
2014
|
-
return { _meta:
|
|
2084
|
+
return { _meta: tools_META, applied: false, reason: 'apply_sca_upgrade requires confirm: true.' };
|
|
2015
2085
|
}
|
|
2016
2086
|
const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
|
|
2017
2087
|
if (!scan) {
|
|
2018
|
-
return { _meta:
|
|
2088
|
+
return { _meta: tools_META, applied: false, reason: `last-scan.json failed integrity check: ${status}. Run a fresh scan.` };
|
|
2019
2089
|
}
|
|
2020
2090
|
const f = _findById(scan, finding_id);
|
|
2021
|
-
if (!f) return { _meta:
|
|
2091
|
+
if (!f) return { _meta: tools_META, applied: false, reason: `Finding not found: ${finding_id}` };
|
|
2022
2092
|
if (f.type !== 'vulnerable_dep') {
|
|
2023
|
-
return { _meta:
|
|
2093
|
+
return { _meta: tools_META, applied: false, reason: 'finding is not an SCA vulnerable_dep — use apply_fix for SAST findings' };
|
|
2024
2094
|
}
|
|
2025
2095
|
const { applyScaUpgrade } = await _getScaUpgrade();
|
|
2026
2096
|
const result = await applyScaUpgrade({ scanRoot: ctx.sessionRoot, finding: f, runTests: run_tests });
|
|
2027
|
-
return { _meta:
|
|
2097
|
+
return { _meta: tools_META, ...result };
|
|
2028
2098
|
},
|
|
2029
2099
|
};
|
|
2030
2100
|
|
|
2031
|
-
const ALL_TOOLS = [scan_diff, query_taint, explain_finding, apply_fix, verify_fix, synthesize_fix, find_rule_module, append_scratchpad, read_scratchpad, append_agents_memory, read_agents_memory, lookup_cve, synthesize_sca_upgrade, apply_sca_upgrade, query_triage_memory, query_findings_memory, query_cache_telemetry];
|
|
2101
|
+
const ALL_TOOLS = [scan_diff, query_taint, explain_finding, apply_fix, verify_fix, synthesize_fix, find_rule_module, append_scratchpad, read_scratchpad, append_agents_memory, read_agents_memory, lookup_cve, synthesize_sca_upgrade, apply_sca_upgrade, query_triage_memory, query_findings_memory, query_cache_telemetry, dataflow_get_graph, dataflow_get_node, dataflow_get_edge, dataflow_get_flow];
|
|
2032
2102
|
|
|
2033
2103
|
;// CONCATENATED MODULE: ./src/mcp/validate.js
|
|
2034
2104
|
// Minimal JSON Schema validator — just the subset our tool schemas use.
|
|
@@ -2126,7 +2196,7 @@ const SESSION_ID = `${process.pid}-${Date.now().toString(36).slice(-6)}`;
|
|
|
2126
2196
|
function _summarize(args) {
|
|
2127
2197
|
let s;
|
|
2128
2198
|
try { s = JSON.stringify(args); } catch { s = '<unserializable>'; }
|
|
2129
|
-
s = redactArgsBlob(s);
|
|
2199
|
+
s = (0,redact/* redactArgsBlob */.MC)(s);
|
|
2130
2200
|
if (s.length > MAX_ARG_BYTES) s = s.slice(0, MAX_ARG_BYTES) + `…(+${s.length - MAX_ARG_BYTES})`;
|
|
2131
2201
|
return s;
|
|
2132
2202
|
}
|
|
@@ -2271,7 +2341,7 @@ const TOOLS_BY_NAME = Object.fromEntries(ALL_TOOLS.map(t => [t.name, t]));
|
|
|
2271
2341
|
function _codeFingerprint() {
|
|
2272
2342
|
try {
|
|
2273
2343
|
const here = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
|
|
2274
|
-
const files = ['server.js', 'tools.js', 'stdio.js', 'audit.js', 'validate.js', 'redact.js'];
|
|
2344
|
+
const files = ['server.js', 'tools.js', 'dataflow-tools.js', 'stdio.js', 'audit.js', 'validate.js', 'redact.js'];
|
|
2275
2345
|
const h = external_node_crypto_.createHash('sha256');
|
|
2276
2346
|
for (const f of files) {
|
|
2277
2347
|
try { h.update(f); h.update(external_node_fs_.readFileSync(external_node_path_.join(here, f))); } catch {}
|
|
@@ -2759,6 +2829,307 @@ const _internal = {
|
|
|
2759
2829
|
};
|
|
2760
2830
|
|
|
2761
2831
|
|
|
2832
|
+
/***/ }),
|
|
2833
|
+
|
|
2834
|
+
/***/ 8218:
|
|
2835
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
2836
|
+
|
|
2837
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2838
|
+
/* harmony export */ loadFreshLineageGraph: () => (/* binding */ loadFreshLineageGraph),
|
|
2839
|
+
/* harmony export */ loadSignedGraph: () => (/* binding */ loadSignedGraph)
|
|
2840
|
+
/* harmony export */ });
|
|
2841
|
+
/* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(3024);
|
|
2842
|
+
/* harmony import */ var _posture_state_dir_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(1174);
|
|
2843
|
+
/* harmony import */ var _posture_integrity_js__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(1130);
|
|
2844
|
+
// graph-loader.js — Milestone 3, sub-project Server, increment 1.
|
|
2845
|
+
//
|
|
2846
|
+
// Reads and VERIFIES the `.agentic-security/lineage-graph.json` artifact
|
|
2847
|
+
// before `explore` is allowed to serve a single byte of it. Reuses
|
|
2848
|
+
// `posture/integrity.js`'s `verifyLastScan` DIRECTLY (per the plan and the
|
|
2849
|
+
// root CLAUDE.md's own instruction) — this module does not implement any
|
|
2850
|
+
// signature comparison of its own. `verifyLastScan` already uses
|
|
2851
|
+
// `crypto.timingSafeEqual` internally.
|
|
2852
|
+
//
|
|
2853
|
+
// Loaded ONCE at server startup (see bin/agentic-security.js's cmdExplore)
|
|
2854
|
+
// and held in memory for the life of the process — this is a read-only,
|
|
2855
|
+
// single-scan-snapshot server; a change to the graph on disk mid-session is
|
|
2856
|
+
// out of scope for this increment (threat-model doc's own "P0 is
|
|
2857
|
+
// read-only" framing).
|
|
2858
|
+
|
|
2859
|
+
|
|
2860
|
+
|
|
2861
|
+
|
|
2862
|
+
|
|
2863
|
+
/**
|
|
2864
|
+
* @param {string} scanRoot
|
|
2865
|
+
* @returns {{ok:true, graph:object} | {ok:false, reason:'missing'|'unsigned'|'tampered'|'malformed', message:string}}
|
|
2866
|
+
*
|
|
2867
|
+
* Four, and only four, distinct failure reasons — each with its own clear
|
|
2868
|
+
* message so an operator knows exactly what to do next:
|
|
2869
|
+
* - 'missing' — no lineage-graph.json at all. Run a scan with
|
|
2870
|
+
* AGENTIC_SECURITY_LINEAGE_DEEP=1 first.
|
|
2871
|
+
* - 'unsigned' — the graph exists but its .sig sibling does not
|
|
2872
|
+
* (verifyLastScan returns null). Refuse to serve an
|
|
2873
|
+
* unverifiable graph.
|
|
2874
|
+
* - 'tampered' — the graph exists and has a .sig, but the signature does
|
|
2875
|
+
* not match the body (verifyLastScan returns false). The
|
|
2876
|
+
* file was modified after signing, or signed under a
|
|
2877
|
+
* different install key.
|
|
2878
|
+
* - 'malformed' — the body passed signature verification but is not
|
|
2879
|
+
* valid JSON. Should not happen from a normal scan; the
|
|
2880
|
+
* file may be corrupted on disk after signing.
|
|
2881
|
+
*/
|
|
2882
|
+
function loadSignedGraph(scanRoot) {
|
|
2883
|
+
const graphPath = (0,_posture_state_dir_js__WEBPACK_IMPORTED_MODULE_1__/* .statePath */ .BQ)(scanRoot, 'lineage-graph.json');
|
|
2884
|
+
const sigPath = graphPath + '.sig';
|
|
2885
|
+
|
|
2886
|
+
if (!node_fs__WEBPACK_IMPORTED_MODULE_0__.existsSync(graphPath)) {
|
|
2887
|
+
return {
|
|
2888
|
+
ok: false,
|
|
2889
|
+
reason: 'missing',
|
|
2890
|
+
message: `No lineage graph found at ${graphPath}. Run a scan with AGENTIC_SECURITY_LINEAGE_DEEP=1 first (e.g. \`AGENTIC_SECURITY_LINEAGE_DEEP=1 agentic-security scan\`), then re-run \`agentic-security explore\`.`,
|
|
2891
|
+
};
|
|
2892
|
+
}
|
|
2893
|
+
|
|
2894
|
+
let body;
|
|
2895
|
+
try {
|
|
2896
|
+
body = node_fs__WEBPACK_IMPORTED_MODULE_0__.readFileSync(graphPath, 'utf8');
|
|
2897
|
+
} catch (e) {
|
|
2898
|
+
return {
|
|
2899
|
+
ok: false,
|
|
2900
|
+
reason: 'missing',
|
|
2901
|
+
message: `Lineage graph found at ${graphPath} but could not be read: ${e && e.message ? e.message : e}.`,
|
|
2902
|
+
};
|
|
2903
|
+
}
|
|
2904
|
+
|
|
2905
|
+
const verified = (0,_posture_integrity_js__WEBPACK_IMPORTED_MODULE_2__/* .verifyLastScan */ .Ef)(body, sigPath);
|
|
2906
|
+
if (verified === null) {
|
|
2907
|
+
return {
|
|
2908
|
+
ok: false,
|
|
2909
|
+
reason: 'unsigned',
|
|
2910
|
+
message: `Lineage graph at ${graphPath} has no signature file (${sigPath} is missing). Refusing to serve an unverifiable graph. Re-run the scan (AGENTIC_SECURITY_LINEAGE_DEEP=1) to regenerate both files together.`,
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
if (verified === false) {
|
|
2914
|
+
return {
|
|
2915
|
+
ok: false,
|
|
2916
|
+
reason: 'tampered',
|
|
2917
|
+
message: `Lineage graph at ${graphPath} FAILED signature verification — its contents do not match ${sigPath}. The file may have been modified after the scan, or signed under a different install key. Refusing to serve a tampered graph. Re-run the scan to regenerate it.`,
|
|
2918
|
+
};
|
|
2919
|
+
}
|
|
2920
|
+
|
|
2921
|
+
let graph;
|
|
2922
|
+
try {
|
|
2923
|
+
graph = JSON.parse(body);
|
|
2924
|
+
} catch (e) {
|
|
2925
|
+
return {
|
|
2926
|
+
ok: false,
|
|
2927
|
+
reason: 'malformed',
|
|
2928
|
+
message: `Lineage graph at ${graphPath} passed signature verification but is not valid JSON (${e && e.message ? e.message : e}). This should not happen from a normal scan — the file may be corrupted. Re-run the scan to regenerate it.`,
|
|
2929
|
+
};
|
|
2930
|
+
}
|
|
2931
|
+
|
|
2932
|
+
return { ok: true, graph };
|
|
2933
|
+
}
|
|
2934
|
+
|
|
2935
|
+
/**
|
|
2936
|
+
* Load .agentic-security/lineage-graph.json ONLY when it is genuinely
|
|
2937
|
+
* fresh for THIS scan — never merely because a file happens to exist on
|
|
2938
|
+
* disk. Shared by every caller that signs or narrates a graph:-derived
|
|
2939
|
+
* compliance claim (M4 sub-project 6c's final whole-branch review found
|
|
2940
|
+
* the identical staleness gap independently reachable from
|
|
2941
|
+
* `attest --obligations` AND `compliance --walkthrough`, and required
|
|
2942
|
+
* this predicate to live in exactly one place rather than being
|
|
2943
|
+
* copy-pasted per caller — a safety check that drifts between two
|
|
2944
|
+
* near-identical inline copies is worse than one shared bug).
|
|
2945
|
+
*
|
|
2946
|
+
* `.agentic-security/lineage-graph.json` is only rewritten when a scan
|
|
2947
|
+
* actually finishes building a graph (`if (scan.lineageGraph)` in
|
|
2948
|
+
* bin/agentic-security.js's persistence code) — an ordinary non-deep
|
|
2949
|
+
* rescan, or a deep scan whose lineage build fails, leaves whatever file
|
|
2950
|
+
* was there from an earlier successful deep scan untouched. Loading that
|
|
2951
|
+
* stale graph and joining it to the CURRENT scan's other data would let a
|
|
2952
|
+
* caller assert a graph-derived fact (e.g. "transit protected") about
|
|
2953
|
+
* code that has since changed.
|
|
2954
|
+
*
|
|
2955
|
+
* `enabled: true` in `scan.scanHealth.lineageAnalysis` does NOT by itself
|
|
2956
|
+
* mean the build succeeded — engine.js sets it the moment
|
|
2957
|
+
* AGENTIC_SECURITY_LINEAGE_DEEP=1 is read, before the build even starts,
|
|
2958
|
+
* and leaves it `true` even when the build later throws (only `failure`
|
|
2959
|
+
* gets set in that case). `requested && enabled` alone therefore still
|
|
2960
|
+
* accepts a stale graph after a failed rebuild — reproduced live via the
|
|
2961
|
+
* scan's own already-shipped fault-injection fixture
|
|
2962
|
+
* (test/lineage-fault-injection.test.js) before this `failure === null`
|
|
2963
|
+
* check was added.
|
|
2964
|
+
*
|
|
2965
|
+
* @param {string} scanRoot
|
|
2966
|
+
* @param {object} scan - the parsed last-scan.json for the CURRENT scan
|
|
2967
|
+
* @returns {{graph:object|null, fresh:boolean, loaded:ReturnType<typeof loadSignedGraph>}}
|
|
2968
|
+
* `fresh` is true only when a signed graph loaded successfully AND this
|
|
2969
|
+
* scan's own scanHealth confirms lineage analysis was requested,
|
|
2970
|
+
* enabled, and did not fail. `graph` is `loaded.graph` when fresh, else
|
|
2971
|
+
* `null` — never the stale file, even when one exists on disk.
|
|
2972
|
+
* `loaded` is the raw `loadSignedGraph` result, so a caller can still
|
|
2973
|
+
* distinguish "no file at all" from "a file exists but isn't fresh" for
|
|
2974
|
+
* its own disclosure message.
|
|
2975
|
+
*/
|
|
2976
|
+
function loadFreshLineageGraph(scanRoot, scan) {
|
|
2977
|
+
const la = scan?.scanHealth?.lineageAnalysis;
|
|
2978
|
+
const requested = la?.requested === true;
|
|
2979
|
+
const enabled = la?.enabled === true;
|
|
2980
|
+
const failure = la?.failure ?? null;
|
|
2981
|
+
const loaded = loadSignedGraph(scanRoot);
|
|
2982
|
+
const fresh = loaded.ok && requested && enabled && failure === null;
|
|
2983
|
+
return { graph: fresh ? loaded.graph : null, fresh, loaded };
|
|
2984
|
+
}
|
|
2985
|
+
|
|
2986
|
+
|
|
2987
|
+
/***/ }),
|
|
2988
|
+
|
|
2989
|
+
/***/ 4268:
|
|
2990
|
+
/***/ ((__unused_webpack___webpack_module__, __webpack_exports__, __webpack_require__) => {
|
|
2991
|
+
|
|
2992
|
+
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
2993
|
+
/* harmony export */ Yo: () => (/* binding */ handleScan),
|
|
2994
|
+
/* harmony export */ Yu: () => (/* binding */ handleEdge),
|
|
2995
|
+
/* harmony export */ d5: () => (/* binding */ handleNode),
|
|
2996
|
+
/* harmony export */ fn: () => (/* binding */ handleGraph),
|
|
2997
|
+
/* harmony export */ jg: () => (/* binding */ handleFlow),
|
|
2998
|
+
/* harmony export */ rR: () => (/* binding */ handleQuery)
|
|
2999
|
+
/* harmony export */ });
|
|
3000
|
+
/* unused harmony export wrapResponse */
|
|
3001
|
+
/* harmony import */ var _lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(859);
|
|
3002
|
+
// routes.js — Milestone 3, sub-project Server, increment 1.
|
|
3003
|
+
//
|
|
3004
|
+
// Five pure GET-endpoint handlers, each `(graph, ...) -> {status, body}`.
|
|
3005
|
+
// No req/res access anywhere in this file — that is what makes these
|
|
3006
|
+
// handlers unit-testable without an HTTP layer at all. http-server.js is
|
|
3007
|
+
// the only module that touches node:http and calls into these.
|
|
3008
|
+
//
|
|
3009
|
+
// Every response body is wrapped in `wrapResponse`, which adds the exact
|
|
3010
|
+
// envelope fields PRD line 1326 names (quoted in the implementation plan):
|
|
3011
|
+
// "base graph/snapshot digest, schema/extension versions, scope, coverage,
|
|
3012
|
+
// limitations, and contributing canonical IDs."
|
|
3013
|
+
|
|
3014
|
+
|
|
3015
|
+
|
|
3016
|
+
/**
|
|
3017
|
+
* Shared response envelope. Maps PRD line 1326's required fields onto the
|
|
3018
|
+
* graph's own real fields:
|
|
3019
|
+
* - digest -> graph.graphId (the base graph/snapshot digest)
|
|
3020
|
+
* - schemaVersion -> graph.schemaVersion
|
|
3021
|
+
* - extensions -> graph.extensions (schema/extension versions —
|
|
3022
|
+
* today always `{}`; see schema.js)
|
|
3023
|
+
* - scope -> graph.scope
|
|
3024
|
+
* - coverage -> graph.coverage
|
|
3025
|
+
* - limitations -> graph.limitations
|
|
3026
|
+
* - canonicalIds -> see the design note below
|
|
3027
|
+
*
|
|
3028
|
+
* "contributing canonical IDs" design decision (disclosed per the plan):
|
|
3029
|
+
* for `handleScan`/`handleGraph`, which describe the WHOLE graph rather
|
|
3030
|
+
* than one entity, `canonicalIds` is `null` — the response body for
|
|
3031
|
+
* `handleGraph` already IS the full nodes/edges/flows arrays, so echoing
|
|
3032
|
+
* every id again here would be pure duplication with no informational
|
|
3033
|
+
* gain, and for a large graph would materially bloat the response for
|
|
3034
|
+
* zero benefit. For `handleNode`/`handleEdge`, `canonicalIds` is the
|
|
3035
|
+
* single id the response is about. For `handleFlow`, `canonicalIds` is
|
|
3036
|
+
* the flow's own id PLUS the node/edge ids that flow's evidence draws
|
|
3037
|
+
* from (source, sink, edgeIds) — a flow is a derived record referencing
|
|
3038
|
+
* several underlying entities, and naming all of them here is genuinely
|
|
3039
|
+
* useful metadata a client would otherwise have to re-derive from the
|
|
3040
|
+
* flow body itself.
|
|
3041
|
+
*/
|
|
3042
|
+
function wrapResponse(data, graph, { canonicalIds = null } = {}) {
|
|
3043
|
+
return {
|
|
3044
|
+
digest: graph?.graphId ?? null,
|
|
3045
|
+
schemaVersion: graph?.schemaVersion ?? null,
|
|
3046
|
+
extensions: graph?.extensions ?? {},
|
|
3047
|
+
scope: graph?.scope ?? null,
|
|
3048
|
+
coverage: graph?.coverage ?? null,
|
|
3049
|
+
limitations: graph?.limitations ?? [],
|
|
3050
|
+
canonicalIds,
|
|
3051
|
+
data,
|
|
3052
|
+
};
|
|
3053
|
+
}
|
|
3054
|
+
|
|
3055
|
+
function _findById(list, id) {
|
|
3056
|
+
if (!Array.isArray(list)) return null;
|
|
3057
|
+
return list.find((item) => item && item.id === id) ?? null;
|
|
3058
|
+
}
|
|
3059
|
+
|
|
3060
|
+
/** Scan/graph metadata — NOT the full node/edge arrays. */
|
|
3061
|
+
function handleScan(graph) {
|
|
3062
|
+
const data = {
|
|
3063
|
+
schemaVersion: graph?.schemaVersion ?? null,
|
|
3064
|
+
graphId: graph?.graphId ?? null,
|
|
3065
|
+
generatedAt: graph?.generatedAt ?? null,
|
|
3066
|
+
scope: graph?.scope ?? null,
|
|
3067
|
+
scanHealth: graph?.scanHealth ?? null,
|
|
3068
|
+
coverage: graph?.coverage ?? null,
|
|
3069
|
+
};
|
|
3070
|
+
return { status: 200, body: wrapResponse(data, graph, { canonicalIds: null }) };
|
|
3071
|
+
}
|
|
3072
|
+
|
|
3073
|
+
/** The full graph document, unfiltered. For a scoped/narrowed projection, use `handleQuery` (`POST /api/v1/query`, Milestone 5) below instead. */
|
|
3074
|
+
function handleGraph(graph) {
|
|
3075
|
+
return { status: 200, body: wrapResponse(graph, graph, { canonicalIds: null }) };
|
|
3076
|
+
}
|
|
3077
|
+
|
|
3078
|
+
/**
|
|
3079
|
+
* A deterministic typed projection query — Milestone 5's own
|
|
3080
|
+
* `POST /api/v1/query`, the S2 endpoint `handleGraph`'s own header
|
|
3081
|
+
* comment named and deferred. `filter` is the exact `{nodeIds, edgeIds}`
|
|
3082
|
+
* shape `dataflow export --filter`/`exportGraphJSON` already use — reused
|
|
3083
|
+
* via `_filterGraph`, never reimplemented. Final whole-branch review
|
|
3084
|
+
* finding: `undefined` (filter omitted entirely) returns the WHOLE graph,
|
|
3085
|
+
* identical to `handleGraph` — but `{}` (an empty, well-formed filter
|
|
3086
|
+
* object) is NOT the same thing, and does NOT mean "no restriction": both
|
|
3087
|
+
* `nodeIds`/`edgeIds` default to empty Sets inside `_filterGraph`, so `{}`
|
|
3088
|
+
* narrows the graph down to EMPTY node/edge/flow/dataElement arrays. A
|
|
3089
|
+
* caller that wants the whole graph must omit `filter` entirely, never
|
|
3090
|
+
* pass `{}` meaning "everything." A malformed filter is a 400, never a
|
|
3091
|
+
* thrown exception reaching the caller.
|
|
3092
|
+
*/
|
|
3093
|
+
function handleQuery(graph, filter) {
|
|
3094
|
+
const check = (0,_lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__.validateFilterShape)(filter);
|
|
3095
|
+
if (!check.valid) {
|
|
3096
|
+
return { status: 400, body: { error: check.error } };
|
|
3097
|
+
}
|
|
3098
|
+
return { status: 200, body: wrapResponse((0,_lineage_export_json_js__WEBPACK_IMPORTED_MODULE_0__/* ._filterGraph */ .e)(graph, filter), graph, { canonicalIds: null }) };
|
|
3099
|
+
}
|
|
3100
|
+
|
|
3101
|
+
/** Look up one node by id. 404 with a clear body if not found. */
|
|
3102
|
+
function handleNode(graph, id) {
|
|
3103
|
+
const node = _findById(graph?.nodes, id);
|
|
3104
|
+
if (!node) {
|
|
3105
|
+
return { status: 404, body: wrapResponse({ error: `node not found: ${id}` }, graph, { canonicalIds: [] }) };
|
|
3106
|
+
}
|
|
3107
|
+
return { status: 200, body: wrapResponse(node, graph, { canonicalIds: [id] }) };
|
|
3108
|
+
}
|
|
3109
|
+
|
|
3110
|
+
/** Look up one edge by id. 404 with a clear body if not found. */
|
|
3111
|
+
function handleEdge(graph, id) {
|
|
3112
|
+
const edge = _findById(graph?.edges, id);
|
|
3113
|
+
if (!edge) {
|
|
3114
|
+
return { status: 404, body: wrapResponse({ error: `edge not found: ${id}` }, graph, { canonicalIds: [] }) };
|
|
3115
|
+
}
|
|
3116
|
+
return { status: 200, body: wrapResponse(edge, graph, { canonicalIds: [id] }) };
|
|
3117
|
+
}
|
|
3118
|
+
|
|
3119
|
+
/** Look up one flow by id. 404 with a clear body if not found. */
|
|
3120
|
+
function handleFlow(graph, id) {
|
|
3121
|
+
const flow = _findById(graph?.flows, id);
|
|
3122
|
+
if (!flow) {
|
|
3123
|
+
return { status: 404, body: wrapResponse({ error: `flow not found: ${id}` }, graph, { canonicalIds: [] }) };
|
|
3124
|
+
}
|
|
3125
|
+
const contributing = new Set([id]);
|
|
3126
|
+
if (flow.source) contributing.add(flow.source);
|
|
3127
|
+
if (flow.sink) contributing.add(flow.sink);
|
|
3128
|
+
for (const eid of (flow.edgeIds || [])) contributing.add(eid);
|
|
3129
|
+
return { status: 200, body: wrapResponse(flow, graph, { canonicalIds: [...contributing] }) };
|
|
3130
|
+
}
|
|
3131
|
+
|
|
3132
|
+
|
|
2762
3133
|
/***/ })
|
|
2763
3134
|
|
|
2764
3135
|
};
|