@clear-capabilities/agentic-security-scanner 0.144.0 → 0.147.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +390 -0
- package/bin/agentic-security.js +3813 -83
- package/dist/1122.index.js +702 -0
- package/dist/{301.index.js → 1301.index.js} +2 -2
- package/dist/1379.index.js +591 -0
- package/dist/{444.index.js → 1444.index.js} +13 -4
- 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/2271.index.js +165 -0
- package/dist/{985.index.js → 2376.index.js} +1260 -340
- package/dist/2432.index.js +793 -0
- package/dist/2659.index.js +93 -0
- package/dist/{826.index.js → 2826.index.js} +2 -2
- package/dist/{830.index.js → 2830.index.js} +2 -2
- package/dist/2923.index.js +298 -0
- package/dist/{1.index.js → 3001.index.js} +5 -5
- package/dist/{117.index.js → 3117.index.js} +3 -3
- package/dist/3180.index.js +307 -0
- package/dist/3276.index.js +117 -0
- package/dist/{415.index.js → 3415.index.js} +2 -2
- package/dist/{499.index.js → 3499.index.js} +2 -2
- package/dist/3518.index.js +450 -0
- package/dist/{526.index.js → 3526.index.js} +14 -6
- 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} +14 -6
- 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} +67 -3
- package/dist/5051.index.js +770 -0
- package/dist/{144.index.js → 5144.index.js} +5 -5
- package/dist/{333.index.js → 5333.index.js} +3 -3
- package/dist/5343.index.js +185 -0
- package/dist/5350.index.js +866 -0
- package/dist/5561.index.js +436 -0
- package/dist/{637.index.js → 5637.index.js} +29 -7
- package/dist/{449.index.js → 5830.index.js} +78 -14
- 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} +26 -8
- 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} +580 -187
- package/dist/{207.index.js → 9207.index.js} +2 -2
- package/dist/{220.index.js → 9220.index.js} +2 -2
- package/dist/9390.index.js +163 -0
- package/dist/{503.index.js → 9503.index.js} +2 -2
- package/dist/{801.index.js → 9801.index.js} +2 -2
- package/dist/{824.index.js → 9824.index.js} +2 -2
- package/dist/agentic-security.mjs +16 -16
- package/dist/agentic-security.mjs.sha256 +1 -1
- package/dist/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/package.json +23 -10
- package/src/compare.js +6 -1
- package/src/dataflow/CLAUDE.md +2 -2
- package/src/dataflow/catalog.js +42 -0
- package/src/dataflow/orm-write-catalog.js +175 -0
- package/src/engine.js +580 -30
- package/src/fix/apply-fix-service.js +1 -0
- package/src/history-scan.js +22 -5
- package/src/ir/CLAUDE.md +2 -1
- 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/lsp/server.js +49 -2
- 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 +22 -1
- package/src/pipeline/assurance-mode.js +64 -1
- package/src/pipeline/finding-schema.js +8 -1
- package/src/pipeline/scan-health.js +19 -1
- package/src/posture/CLAUDE.md +140 -0
- package/src/posture/accuracy-scorecard.js +60 -0
- package/src/posture/artifact-registry.js +76 -0
- package/src/posture/auditor-walkthrough.js +192 -13
- package/src/posture/compliance-frameworks/hipaa-security-rule.json +3 -2
- package/src/posture/compliance-policy.js +12 -2
- package/src/posture/cross-repo-memory.js +7 -2
- package/src/posture/fix-history.js +25 -2
- package/src/posture/fix-verify.js +9 -1
- package/src/posture/fleet.js +0 -0
- package/src/posture/git-history.js +13 -5
- package/src/posture/material-change.js +21 -2
- package/src/posture/mttr.js +75 -12
- package/src/posture/obligation-evidence-pack.js +202 -0
- package/src/posture/pre-incident-archaeology.js +39 -7
- package/src/posture/privacy-framework.js +14 -0
- package/src/posture/provenance/ai-authorship.js +68 -0
- package/src/posture/provenance/branch-entry.js +80 -0
- package/src/posture/provenance/cache.js +143 -0
- package/src/posture/provenance/confidence.js +36 -0
- package/src/posture/provenance/coordinator.js +786 -0
- package/src/posture/provenance/dag-walk.js +249 -0
- package/src/posture/provenance/evidence-attribution.js +59 -0
- package/src/posture/provenance/git-evidence.js +310 -0
- package/src/posture/provenance/lifecycle.js +208 -0
- package/src/posture/provenance/missing-control-resolver.js +137 -0
- package/src/posture/provenance/origin-resolver.js +342 -0
- package/src/posture/provenance/predicate-replay.js +133 -0
- package/src/posture/provenance/providers/config.js +39 -0
- package/src/posture/provenance/providers/github.js +62 -0
- package/src/posture/provenance/providers/gitlab.js +58 -0
- package/src/posture/provenance/repo-lineage.js +74 -0
- package/src/posture/provenance/sca-origin.js +139 -0
- package/src/posture/provenance/schema.js +255 -0
- package/src/posture/provenance/transitive-sca.js +147 -0
- package/src/posture/provenance/validate.js +30 -0
- package/src/posture/provenance-evidence-bundle.js +144 -0
- package/src/posture/remediation-ledger.js +337 -0
- package/src/posture/sbom-diff.js +15 -2
- package/src/posture/secret-history.js +10 -2
- package/src/posture/state-dir.js +38 -14
- package/src/posture/vuln-archaeology.js +8 -2
- package/src/pr-delta.js +25 -4
- package/src/report/index.js +197 -3
- package/src/runScan.js +34 -5
- package/src/sast/rate-limit.js +33 -3
- package/src/server/CLAUDE.md +47 -0
- package/src/server/graph-loader.js +141 -0
- package/src/server/http-server.js +325 -0
- package/src/server/routes.js +129 -0
- package/src/server/security.js +111 -0
- package/src/server/static-assets.js +139 -0
- package/src/util/git-hardening.js +128 -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,111 +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
|
-
|
|
129
|
-
|
|
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
|
+
|
|
131
|
+
|
|
132
|
+
|
|
170
133
|
|
|
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
134
|
|
|
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
135
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
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
|
-
|
|
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
|
+
|
|
210
281
|
;// CONCATENATED MODULE: ./src/posture/agents-memory.js
|
|
211
282
|
// AGENTS.md — writable continual-learning memory (harness-anatomy #2).
|
|
212
283
|
//
|
|
@@ -508,6 +579,11 @@ const cve_lookup_internals = { CACHE_DIR, CVE_RE, _stalenessTier };
|
|
|
508
579
|
|
|
509
580
|
|
|
510
581
|
|
|
582
|
+
// Git-origin provenance (Finding Provenance M0/M1). Distinct from
|
|
583
|
+
// `finding.provenance` (AI-authorship) and from an SCA entry's `provenance`
|
|
584
|
+
// (Sigstore/SLSA attestation) — see report/index.js's import comment.
|
|
585
|
+
|
|
586
|
+
|
|
511
587
|
|
|
512
588
|
// Lazy-loaded: these transitively pull in npm packages (@babel/core and
|
|
513
589
|
// friends) that aren't available in the plugin-cache install path
|
|
@@ -520,14 +596,14 @@ async function getRunScan() {
|
|
|
520
596
|
}
|
|
521
597
|
let _verifyFixCore;
|
|
522
598
|
async function getVerifyFixCore() {
|
|
523
|
-
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;
|
|
524
600
|
return _verifyFixCore;
|
|
525
601
|
}
|
|
526
602
|
|
|
527
603
|
const MAX_FILES_PER_SCAN = 1024;
|
|
528
604
|
const MAX_FILE_BYTES = 500_000;
|
|
529
605
|
const MAX_TOTAL_SCAN_BYTES = 50_000_000;
|
|
530
|
-
const
|
|
606
|
+
const tools_META = { source: 'agentic-security-mcp', untrusted_excerpts: true };
|
|
531
607
|
|
|
532
608
|
// OWASP A01 — refuse writes to paths that could subvert the security tool
|
|
533
609
|
// itself or the host's source-control / dependency state. A forged finding
|
|
@@ -907,7 +983,7 @@ const scan_diff = {
|
|
|
907
983
|
// scanning), left as-is rather than silently claimed fixed here.
|
|
908
984
|
const findings = (0,report.normalizeFindings)(result.scan)
|
|
909
985
|
.filter(f => wantSet.has(String(f.file || '').replace(/\\/g, '/')) && (sevRank[f.severity] ?? 0) >= min)
|
|
910
|
-
.map(f => redactFinding({
|
|
986
|
+
.map(f => (0,redact/* redactFinding */.lE)({
|
|
911
987
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
912
988
|
title: f.vuln, cwe: f.cwe,
|
|
913
989
|
description: f.description, remediation: f.remediation,
|
|
@@ -919,7 +995,7 @@ const scan_diff = {
|
|
|
919
995
|
const off = _maybeOffload(sessionRoot, 'scan_diff', findings);
|
|
920
996
|
if (off.offloaded) {
|
|
921
997
|
return {
|
|
922
|
-
_meta:
|
|
998
|
+
_meta: tools_META,
|
|
923
999
|
scannedFiles: Object.keys(fileContents).length,
|
|
924
1000
|
findingCount: off.total,
|
|
925
1001
|
offloaded: true,
|
|
@@ -929,7 +1005,7 @@ const scan_diff = {
|
|
|
929
1005
|
};
|
|
930
1006
|
}
|
|
931
1007
|
return {
|
|
932
|
-
_meta:
|
|
1008
|
+
_meta: tools_META,
|
|
933
1009
|
scannedFiles: Object.keys(fileContents).length,
|
|
934
1010
|
findingCount: findings.length,
|
|
935
1011
|
findings,
|
|
@@ -955,7 +1031,7 @@ const query_taint = {
|
|
|
955
1031
|
async handler({ source, sink, limit, offset }, ctx) {
|
|
956
1032
|
const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: true });
|
|
957
1033
|
if (!scan) {
|
|
958
|
-
return { _meta:
|
|
1034
|
+
return { _meta: tools_META, hasResult: false, status, message: `No usable scan state (${status}).` };
|
|
959
1035
|
}
|
|
960
1036
|
const lim = Number.isInteger(limit) ? Math.min(50, Math.max(1, limit)) : 10;
|
|
961
1037
|
const off = Number.isInteger(offset) ? Math.max(0, offset) : 0;
|
|
@@ -970,13 +1046,13 @@ const query_taint = {
|
|
|
970
1046
|
const hay = [f.description, f.title, f.vuln, f.snippet, JSON.stringify(f.trace || '')].join(' ').toLowerCase();
|
|
971
1047
|
return hay.includes(srcL) && hay.includes(sinkL);
|
|
972
1048
|
});
|
|
973
|
-
const page = all.slice(off, off + lim).map(f => redactFinding({
|
|
1049
|
+
const page = all.slice(off, off + lim).map(f => (0,redact/* redactFinding */.lE)({
|
|
974
1050
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
975
1051
|
title: f.title || f.vuln, description: f.description,
|
|
976
1052
|
trace: f.trace || null,
|
|
977
1053
|
}));
|
|
978
1054
|
return {
|
|
979
|
-
_meta:
|
|
1055
|
+
_meta: tools_META,
|
|
980
1056
|
hasResult: true,
|
|
981
1057
|
integrity: status,
|
|
982
1058
|
scanStartedAt: scan.startedAt || scan.meta?.startedAt || null,
|
|
@@ -1008,7 +1084,7 @@ const explain_finding = {
|
|
|
1008
1084
|
if (!scan) throw new Error(`No usable scan state (${status}).`);
|
|
1009
1085
|
const f = _findById(scan, finding_id);
|
|
1010
1086
|
if (!f) throw new Error(`Finding not found: ${finding_id}`);
|
|
1011
|
-
const redacted = redactFinding({
|
|
1087
|
+
const redacted = (0,redact/* redactFinding */.lE)({
|
|
1012
1088
|
id: f.id, severity: f.severity, file: f.file, line: f.line,
|
|
1013
1089
|
title: f.title || f.vuln, cwe: f.cwe,
|
|
1014
1090
|
description: f.description, remediation: f.remediation,
|
|
@@ -1033,7 +1109,7 @@ const explain_finding = {
|
|
|
1033
1109
|
}
|
|
1034
1110
|
}
|
|
1035
1111
|
return {
|
|
1036
|
-
_meta:
|
|
1112
|
+
_meta: tools_META,
|
|
1037
1113
|
...redacted,
|
|
1038
1114
|
trace: traceTrimmed,
|
|
1039
1115
|
traceOffload: traceMeta,
|
|
@@ -1053,6 +1129,21 @@ const explain_finding = {
|
|
|
1053
1129
|
epssScore: typeof f.epssScore === 'number' ? f.epssScore : null,
|
|
1054
1130
|
epssPercentile: typeof f.epssPercentile === 'number' ? f.epssPercentile : null,
|
|
1055
1131
|
exploitedNow: !!f.exploitedNow,
|
|
1132
|
+
// Which commit introduced this finding. `includeEmail` stays at its
|
|
1133
|
+
// DEFAULT (false) unconditionally — unlike the JSON report there is no
|
|
1134
|
+
// operator-set env escape for it here, because the consumer is an
|
|
1135
|
+
// agent that has no business receiving a committer's email address.
|
|
1136
|
+
// `pseudonymize`, by contrast, IS read back from the same env var
|
|
1137
|
+
// report/index.js's `_normalizedProvenance` reads
|
|
1138
|
+
// (AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS=1 / --pseudonymize-authors) —
|
|
1139
|
+
// fix-round item 4: an operator who set that policy was still getting
|
|
1140
|
+
// raw committer names (and, via providerEnrichment, raw reviewer
|
|
1141
|
+
// logins/CODEOWNERS lines) through this MCP surface because this call
|
|
1142
|
+
// passed no options object at all, silently defeating their policy at
|
|
1143
|
+
// this one output boundary while report/index.js honoured it.
|
|
1144
|
+
findingProvenance: f.findingProvenance ? (0,schema/* redactFindingProvenance */.As)(f.findingProvenance, {
|
|
1145
|
+
pseudonymize: process.env.AGENTIC_SECURITY_PSEUDONYMIZE_AUTHORS === '1',
|
|
1146
|
+
}) : null,
|
|
1056
1147
|
};
|
|
1057
1148
|
},
|
|
1058
1149
|
};
|
|
@@ -1129,16 +1220,16 @@ const apply_fix = {
|
|
|
1129
1220
|
},
|
|
1130
1221
|
async handler({ finding_id, confirm, dry_run = false, patch = null, fixMeta = null }, ctx) {
|
|
1131
1222
|
if (confirm !== true) {
|
|
1132
|
-
return { _meta:
|
|
1223
|
+
return { _meta: tools_META, applied: false, reason: 'apply_fix requires confirm: true.' };
|
|
1133
1224
|
}
|
|
1134
1225
|
const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
|
|
1135
1226
|
if (!scan) {
|
|
1136
|
-
return { _meta:
|
|
1227
|
+
return { _meta: tools_META, applied: false, reason: `last-scan.json failed integrity check: ${status}. Run a fresh scan.` };
|
|
1137
1228
|
}
|
|
1138
1229
|
const f = _findById(scan, finding_id);
|
|
1139
|
-
if (!f) return { _meta:
|
|
1230
|
+
if (!f) return { _meta: tools_META, applied: false, reason: `Finding not found: ${finding_id}` };
|
|
1140
1231
|
if (f._shadow === true) {
|
|
1141
|
-
return { _meta:
|
|
1232
|
+
return { _meta: tools_META, applied: false, reason: 'shadow findings cannot be auto-applied' };
|
|
1142
1233
|
}
|
|
1143
1234
|
|
|
1144
1235
|
// #3 — verifier-approved patch path. When the caller supplies `patch` (a
|
|
@@ -1153,15 +1244,15 @@ const apply_fix = {
|
|
|
1153
1244
|
// slip through — there is no token to replay, the verify runs here and now.
|
|
1154
1245
|
if (patch && typeof patch === 'object' && Object.keys(patch).length) {
|
|
1155
1246
|
if (!f.stableId) {
|
|
1156
|
-
return { _meta:
|
|
1247
|
+
return { _meta: tools_META, applied: false, reason: 'finding has no stableId — cannot verify a patch against it' };
|
|
1157
1248
|
}
|
|
1158
1249
|
const confinedAbs = {};
|
|
1159
1250
|
for (const [rel, content] of Object.entries(patch)) {
|
|
1160
1251
|
let abs;
|
|
1161
1252
|
try { abs = _confine(ctx.sessionRoot, rel, 'patch key'); }
|
|
1162
|
-
catch (e) { return { _meta:
|
|
1253
|
+
catch (e) { return { _meta: tools_META, applied: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1163
1254
|
if (_isReservedWritePath(ctx.sessionRoot, abs)) {
|
|
1164
|
-
return { _meta:
|
|
1255
|
+
return { _meta: tools_META, applied: false, reason: `reserved path refused: ${rel}` };
|
|
1165
1256
|
}
|
|
1166
1257
|
confinedAbs[rel] = { abs, content: String(content) };
|
|
1167
1258
|
}
|
|
@@ -1174,7 +1265,7 @@ const apply_fix = {
|
|
|
1174
1265
|
// suite as a fourth verification leg (scan + lint + tests). Opt-in
|
|
1175
1266
|
// because many repos have no runner and we must not fail-closed by
|
|
1176
1267
|
// default. Normalized to the scan+lint verdict shape used below.
|
|
1177
|
-
const { verifyFixWithTests } = await __webpack_require__.e(/* import() */
|
|
1268
|
+
const { verifyFixWithTests } = await __webpack_require__.e(/* import() */ 4113).then(__webpack_require__.bind(__webpack_require__, 4113));
|
|
1178
1269
|
const t = await verifyFixWithTests({ scanRoot: ctx.sessionRoot, originalFindingStableId: f.stableId, files: _files });
|
|
1179
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 };
|
|
1180
1271
|
} else {
|
|
@@ -1187,11 +1278,11 @@ const apply_fix = {
|
|
|
1187
1278
|
});
|
|
1188
1279
|
}
|
|
1189
1280
|
} catch (e) {
|
|
1190
|
-
return { _meta:
|
|
1281
|
+
return { _meta: tools_META, applied: false, reason: `patch verification failed: ${e.message}` };
|
|
1191
1282
|
}
|
|
1192
1283
|
if (!verdict.ok) {
|
|
1193
1284
|
return {
|
|
1194
|
-
_meta:
|
|
1285
|
+
_meta: tools_META, applied: false,
|
|
1195
1286
|
reason: `patch rejected by verifier: ${verdict.summary || verdict.rescan?.reason || 'did not verify'}`,
|
|
1196
1287
|
verify: { rescan: verdict.rescan, lint: { runner: verdict.lint?.runner, ok: verdict.lint?.ok }, honesty: verdict.honesty || null },
|
|
1197
1288
|
};
|
|
@@ -1214,7 +1305,7 @@ const apply_fix = {
|
|
|
1214
1305
|
}
|
|
1215
1306
|
const materialClassification = (0,material_change/* classifyFixMaterialRisk */.kz)(filesForMaterialClassification);
|
|
1216
1307
|
if (dry_run) {
|
|
1217
|
-
return { _meta:
|
|
1308
|
+
return { _meta: tools_META, applied: false, dryRun: true, verified: true, files: Object.keys(confinedAbs), summary: verdict.summary, materialClassification };
|
|
1218
1309
|
}
|
|
1219
1310
|
if (materialClassification.highImpactCategories.length) {
|
|
1220
1311
|
const approval = fixMeta && typeof fixMeta === 'object' ? fixMeta.approval : null;
|
|
@@ -1223,7 +1314,7 @@ const apply_fix = {
|
|
|
1223
1314
|
typeof approval.reason === 'string' && approval.reason.trim().length > 0);
|
|
1224
1315
|
if (!hasApprovalEvidence) {
|
|
1225
1316
|
return {
|
|
1226
|
-
_meta:
|
|
1317
|
+
_meta: tools_META, applied: false,
|
|
1227
1318
|
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) requires approval evidence — pass fixMeta.approval: {approvedBy, reason} — before it can be applied`,
|
|
1228
1319
|
materialClassification,
|
|
1229
1320
|
};
|
|
@@ -1233,7 +1324,7 @@ const apply_fix = {
|
|
|
1233
1324
|
const identityCheck = (0,approver_registry/* verifyApprover */.$N)(approverRegistry, approval.approvedBy, requiredRoles);
|
|
1234
1325
|
if (!identityCheck.verified) {
|
|
1235
1326
|
return {
|
|
1236
|
-
_meta:
|
|
1327
|
+
_meta: tools_META, applied: false,
|
|
1237
1328
|
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${identityCheck.reason}`,
|
|
1238
1329
|
materialClassification,
|
|
1239
1330
|
};
|
|
@@ -1243,7 +1334,7 @@ const apply_fix = {
|
|
|
1243
1334
|
const sodCheck = (0,approver_registry/* checkSeparationOfDuties */.I0)(approverRegistry, fixMeta?.author, approval.approvedBy);
|
|
1244
1335
|
if (!sodCheck.ok) {
|
|
1245
1336
|
return {
|
|
1246
|
-
_meta:
|
|
1337
|
+
_meta: tools_META, applied: false,
|
|
1247
1338
|
reason: `high-impact change (${materialClassification.highImpactCategories.join(', ')}) approval rejected: ${sodCheck.reason}`,
|
|
1248
1339
|
materialClassification,
|
|
1249
1340
|
};
|
|
@@ -1257,6 +1348,7 @@ const apply_fix = {
|
|
|
1257
1348
|
const entry = await (0,fix_history/* applyFix */.oM)({
|
|
1258
1349
|
scanRoot: ctx.sessionRoot, file: rel, originalContent, newContent: v.content, fileExisted,
|
|
1259
1350
|
findingId: f.id, stableId: f.stableId, ruleId: f.ruleId || f.cwe || f.family || null, vuln: f.vuln || f.title || null,
|
|
1351
|
+
findingProvenance: f.findingProvenance || null,
|
|
1260
1352
|
});
|
|
1261
1353
|
written.push({ file: rel, historyId: entry.id, backupPath: entry.backupPath });
|
|
1262
1354
|
}
|
|
@@ -1269,20 +1361,20 @@ const apply_fix = {
|
|
|
1269
1361
|
try { await (0,fix_history/* revertEntryById */.rJ)(ctx.sessionRoot, w.historyId); } catch { /* best-effort; original error still propagates below */ }
|
|
1270
1362
|
}
|
|
1271
1363
|
if (e && e.name === 'FixAttemptBudgetExceededError') {
|
|
1272
|
-
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 };
|
|
1273
1365
|
}
|
|
1274
1366
|
throw e;
|
|
1275
1367
|
}
|
|
1276
1368
|
let acceptance = null;
|
|
1277
1369
|
try { acceptance = (0,fix_history/* fixAcceptanceRate */.XR)(ctx.sessionRoot); } catch { /* best-effort */ }
|
|
1278
|
-
return { _meta:
|
|
1370
|
+
return { _meta: tools_META, applied: true, verified: true, patched: written, integrity: status, verify: { summary: verdict.summary }, acceptance, materialClassification };
|
|
1279
1371
|
}
|
|
1280
1372
|
|
|
1281
1373
|
if (typeof f.fix?.replacement !== 'string') {
|
|
1282
1374
|
// Premortem #2: templates are patch-shaped text. Same reasoning as
|
|
1283
1375
|
// the replacement path — do NOT pass through redactString here.
|
|
1284
1376
|
return {
|
|
1285
|
-
_meta:
|
|
1377
|
+
_meta: tools_META, applied: false,
|
|
1286
1378
|
reason: 'No full replacement available — only a template. Apply the template manually.',
|
|
1287
1379
|
template: f.fix?.code || '',
|
|
1288
1380
|
file: f.file, line: f.line,
|
|
@@ -1291,19 +1383,19 @@ const apply_fix = {
|
|
|
1291
1383
|
let absFile;
|
|
1292
1384
|
try { absFile = _confine(ctx.sessionRoot, f.file, 'finding.file'); }
|
|
1293
1385
|
catch (e) {
|
|
1294
|
-
return { _meta:
|
|
1386
|
+
return { _meta: tools_META, applied: false, reason: `path-escape refused: ${e.message}` };
|
|
1295
1387
|
}
|
|
1296
1388
|
if (_isReservedWritePath(ctx.sessionRoot, absFile)) {
|
|
1297
|
-
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` };
|
|
1298
1390
|
}
|
|
1299
1391
|
if (!external_node_fs_.existsSync(absFile)) {
|
|
1300
|
-
return { _meta:
|
|
1392
|
+
return { _meta: tools_META, applied: false, reason: `File not found: ${absFile}` };
|
|
1301
1393
|
}
|
|
1302
1394
|
const originalContent = await promises_.readFile(absFile, 'utf8');
|
|
1303
1395
|
|
|
1304
1396
|
if (dry_run) {
|
|
1305
1397
|
return {
|
|
1306
|
-
_meta:
|
|
1398
|
+
_meta: tools_META,
|
|
1307
1399
|
applied: false, dryRun: true,
|
|
1308
1400
|
file: f.file,
|
|
1309
1401
|
originalSize: originalContent.length,
|
|
@@ -1325,7 +1417,7 @@ const apply_fix = {
|
|
|
1325
1417
|
// size-diff shape) but the load-bearing addition is the verification
|
|
1326
1418
|
// gate before the write.
|
|
1327
1419
|
if (!f.stableId) {
|
|
1328
|
-
return { _meta:
|
|
1420
|
+
return { _meta: tools_META, applied: false, reason: 'finding has no stableId — cannot verify a stored fix against it' };
|
|
1329
1421
|
}
|
|
1330
1422
|
const result = await (0,apply_fix_service/* applyVerifiedFix */.On)({
|
|
1331
1423
|
scanRoot: ctx.sessionRoot,
|
|
@@ -1335,9 +1427,9 @@ const apply_fix = {
|
|
|
1335
1427
|
});
|
|
1336
1428
|
if (!result.ok) {
|
|
1337
1429
|
if (result.budgetExceeded) {
|
|
1338
|
-
return { _meta:
|
|
1430
|
+
return { _meta: tools_META, applied: false, reason: result.reason, budgetExceeded: true, attempts: result.attempts, maxAttempts: result.maxAttempts, key: result.key };
|
|
1339
1431
|
}
|
|
1340
|
-
return { _meta:
|
|
1432
|
+
return { _meta: tools_META, applied: false, reason: result.reason, verify: result.verify || null };
|
|
1341
1433
|
}
|
|
1342
1434
|
// R25 (PRD §5): surface the running auto-fix acceptance rate after each
|
|
1343
1435
|
// applied fix, so the closed loop reports its own success metric.
|
|
@@ -1348,7 +1440,7 @@ const apply_fix = {
|
|
|
1348
1440
|
// FR-305: verifiedFull distinguishes "every required leg (lint, tests)
|
|
1349
1441
|
// genuinely ran and passed" from "passed, but a required leg was
|
|
1350
1442
|
// skipped or unavailable" — verified:true alone conflates them.
|
|
1351
|
-
_meta:
|
|
1443
|
+
_meta: tools_META, applied: true, verified: true, verifiedFull: result.verifiedFull,
|
|
1352
1444
|
historyId: entry.historyId, file: entry.file, backupPath: entry.backupPath,
|
|
1353
1445
|
integrity: status, attemptOrdinal: entry.attemptOrdinal, acceptance,
|
|
1354
1446
|
verify: result.verify,
|
|
@@ -1417,7 +1509,7 @@ const verify_fix = {
|
|
|
1417
1509
|
try {
|
|
1418
1510
|
_confine(ctx.sessionRoot, relPath, 'files key');
|
|
1419
1511
|
} catch (e) {
|
|
1420
|
-
return { _meta:
|
|
1512
|
+
return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` };
|
|
1421
1513
|
}
|
|
1422
1514
|
confined[relPath] = String(content);
|
|
1423
1515
|
}
|
|
@@ -1449,10 +1541,10 @@ const verify_fix = {
|
|
|
1449
1541
|
fixMeta,
|
|
1450
1542
|
});
|
|
1451
1543
|
return {
|
|
1452
|
-
_meta:
|
|
1544
|
+
_meta: tools_META,
|
|
1453
1545
|
ok: r.ok,
|
|
1454
1546
|
rescan: { ok: r.rescan.ok, reason: r.rescan.reason, introduced: r.rescan.introduced || [] },
|
|
1455
|
-
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) },
|
|
1456
1548
|
// verifyFix computes five legs, not two — tests/honesty/poc were
|
|
1457
1549
|
// being silently dropped here, leaving an agent with no structured
|
|
1458
1550
|
// way to see WHY verification failed when the failure was in one
|
|
@@ -1463,11 +1555,11 @@ const verify_fix = {
|
|
|
1463
1555
|
// it can echo proof-harness detail derived from scanned source.
|
|
1464
1556
|
tests: r.tests,
|
|
1465
1557
|
honesty: r.honesty,
|
|
1466
|
-
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,
|
|
1467
1559
|
summary: r.summary,
|
|
1468
1560
|
};
|
|
1469
1561
|
} catch (e) {
|
|
1470
|
-
return { _meta:
|
|
1562
|
+
return { _meta: tools_META, ok: false, reason: `verify_fix failed: ${e.message}` };
|
|
1471
1563
|
}
|
|
1472
1564
|
},
|
|
1473
1565
|
};
|
|
@@ -1490,11 +1582,11 @@ const synthesize_fix = {
|
|
|
1490
1582
|
async handler({ finding_id }, ctx) {
|
|
1491
1583
|
const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
|
|
1492
1584
|
if (!scan) {
|
|
1493
|
-
return { _meta:
|
|
1585
|
+
return { _meta: tools_META, ok: false, reason: `last-scan.json failed integrity check: ${status}` };
|
|
1494
1586
|
}
|
|
1495
1587
|
const f = _findById(scan, finding_id);
|
|
1496
|
-
if (!f) return { _meta:
|
|
1497
|
-
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' };
|
|
1498
1590
|
const fix = f.fix || {};
|
|
1499
1591
|
const hasReplacement = typeof fix.replacement === 'string' && fix.replacement.length > 0;
|
|
1500
1592
|
// Patch bounds: count files touched + LoC delta.
|
|
@@ -1530,7 +1622,7 @@ const synthesize_fix = {
|
|
|
1530
1622
|
// verbatim. Snippet/description/etc. continue to be redacted in
|
|
1531
1623
|
// explain_finding / scan_diff — that's the right surface for redaction.
|
|
1532
1624
|
return {
|
|
1533
|
-
_meta:
|
|
1625
|
+
_meta: tools_META,
|
|
1534
1626
|
ok: true,
|
|
1535
1627
|
stable_id: f.stableId || null,
|
|
1536
1628
|
file: f.file, line: f.line,
|
|
@@ -1580,14 +1672,14 @@ const find_rule_module = {
|
|
|
1580
1672
|
},
|
|
1581
1673
|
async handler({ cwe, family }, ctx) {
|
|
1582
1674
|
if (!cwe && !family) {
|
|
1583
|
-
return { _meta:
|
|
1675
|
+
return { _meta: tools_META, ok: false, reason: 'provide cwe (e.g. "CWE-89") or family (e.g. "sql-injection")' };
|
|
1584
1676
|
}
|
|
1585
1677
|
// Pattern enforcement — the mini-schema validator doesn't do `pattern`.
|
|
1586
1678
|
if (cwe && !/^CWE-\d+$/.test(cwe)) {
|
|
1587
|
-
return { _meta:
|
|
1679
|
+
return { _meta: tools_META, ok: false, reason: 'cwe must match /^CWE-\\d+$/ (e.g. "CWE-89")' };
|
|
1588
1680
|
}
|
|
1589
1681
|
if (family && !/^[a-z][a-z0-9-]+$/.test(family)) {
|
|
1590
|
-
return { _meta:
|
|
1682
|
+
return { _meta: tools_META, ok: false, reason: 'family must match /^[a-z][a-z0-9-]+$/ (e.g. "sql-injection")' };
|
|
1591
1683
|
}
|
|
1592
1684
|
const sessionRoot = ctx.sessionRoot;
|
|
1593
1685
|
const roots = [
|
|
@@ -1641,7 +1733,7 @@ const find_rule_module = {
|
|
|
1641
1733
|
if (hits.length >= 20) break;
|
|
1642
1734
|
}
|
|
1643
1735
|
return {
|
|
1644
|
-
_meta:
|
|
1736
|
+
_meta: tools_META,
|
|
1645
1737
|
ok: true,
|
|
1646
1738
|
query: { cwe: cwe || null, family: family || null },
|
|
1647
1739
|
hitCount: hits.length,
|
|
@@ -1684,14 +1776,14 @@ const append_scratchpad = {
|
|
|
1684
1776
|
},
|
|
1685
1777
|
async handler({ path: relPath, content }, ctx) {
|
|
1686
1778
|
const v = _validateScratchpadPath(relPath);
|
|
1687
|
-
if (!v.ok) return { _meta:
|
|
1779
|
+
if (!v.ok) return { _meta: tools_META, ok: false, reason: v.reason };
|
|
1688
1780
|
let abs;
|
|
1689
1781
|
try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
|
|
1690
|
-
catch (e) { return { _meta:
|
|
1782
|
+
catch (e) { return { _meta: tools_META, ok: false, reason: `path-escape refused: ${e.message}` }; }
|
|
1691
1783
|
const total = _scratchpadTotalBytes(ctx.sessionRoot);
|
|
1692
1784
|
if (total + content.length > SCRATCHPAD_MAX_TOTAL_BYTES) {
|
|
1693
1785
|
return {
|
|
1694
|
-
_meta:
|
|
1786
|
+
_meta: tools_META, ok: false,
|
|
1695
1787
|
reason: `scratchpad-total-exceeded: ${total} + ${content.length} > ${SCRATCHPAD_MAX_TOTAL_BYTES}. Clean up via "rm -rf .agentic-security/agent-scratchpad" or rotate sessions.`,
|
|
1696
1788
|
};
|
|
1697
1789
|
}
|
|
@@ -1699,7 +1791,7 @@ const append_scratchpad = {
|
|
|
1699
1791
|
try { if (external_node_fs_.existsSync(abs)) existing = external_node_fs_.statSync(abs).size; } catch {}
|
|
1700
1792
|
if (existing + content.length > SCRATCHPAD_MAX_FILE_BYTES) {
|
|
1701
1793
|
return {
|
|
1702
|
-
_meta:
|
|
1794
|
+
_meta: tools_META, ok: false,
|
|
1703
1795
|
reason: `scratchpad-file-exceeded: ${existing} + ${content.length} > ${SCRATCHPAD_MAX_FILE_BYTES}. Start a new file.`,
|
|
1704
1796
|
};
|
|
1705
1797
|
}
|
|
@@ -1707,12 +1799,12 @@ const append_scratchpad = {
|
|
|
1707
1799
|
external_node_fs_.mkdirSync(external_node_path_.dirname(abs), { recursive: true });
|
|
1708
1800
|
external_node_fs_.appendFileSync(abs, content);
|
|
1709
1801
|
return {
|
|
1710
|
-
_meta:
|
|
1802
|
+
_meta: tools_META, ok: true,
|
|
1711
1803
|
path: relPath, bytesWritten: content.length, fileSize: existing + content.length,
|
|
1712
1804
|
scratchpadTotal: total + content.length,
|
|
1713
1805
|
};
|
|
1714
1806
|
} catch (e) {
|
|
1715
|
-
return { _meta:
|
|
1807
|
+
return { _meta: tools_META, ok: false, reason: `write-failed: ${e.message}` };
|
|
1716
1808
|
}
|
|
1717
1809
|
},
|
|
1718
1810
|
};
|
|
@@ -1732,14 +1824,14 @@ const read_scratchpad = {
|
|
|
1732
1824
|
},
|
|
1733
1825
|
async handler({ path: relPath, offset, limit }, ctx) {
|
|
1734
1826
|
const v = _validateScratchpadPath(relPath);
|
|
1735
|
-
if (!v.ok) return { _meta:
|
|
1827
|
+
if (!v.ok) return { _meta: tools_META, ok: false, reason: v.reason };
|
|
1736
1828
|
let abs;
|
|
1737
1829
|
try { abs = _scratchpadAbs(ctx.sessionRoot, relPath); }
|
|
1738
|
-
catch (e) { return { _meta:
|
|
1739
|
-
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' };
|
|
1740
1832
|
let stat;
|
|
1741
|
-
try { stat = external_node_fs_.statSync(abs); } catch (e) { return { _meta:
|
|
1742
|
-
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' };
|
|
1743
1835
|
const off = Number.isInteger(offset) ? Math.max(0, offset) : 0;
|
|
1744
1836
|
const lim = Number.isInteger(limit) ? Math.min(64 * 1024, Math.max(1, limit)) : 4096;
|
|
1745
1837
|
let buf;
|
|
@@ -1749,10 +1841,10 @@ const read_scratchpad = {
|
|
|
1749
1841
|
const read = external_node_fs_.readSync(fd, tmp, 0, lim, off);
|
|
1750
1842
|
external_node_fs_.closeSync(fd);
|
|
1751
1843
|
buf = tmp.slice(0, read);
|
|
1752
|
-
} catch (e) { return { _meta:
|
|
1844
|
+
} catch (e) { return { _meta: tools_META, ok: false, reason: `read-failed: ${e.message}` }; }
|
|
1753
1845
|
const text = buf.toString('utf8');
|
|
1754
1846
|
return {
|
|
1755
|
-
_meta:
|
|
1847
|
+
_meta: tools_META, ok: true,
|
|
1756
1848
|
path: relPath,
|
|
1757
1849
|
offset: off, limit: lim, bytesRead: buf.length,
|
|
1758
1850
|
totalSize: stat.size,
|
|
@@ -1784,7 +1876,7 @@ const append_agents_memory = {
|
|
|
1784
1876
|
},
|
|
1785
1877
|
async handler({ agent, body }, ctx) {
|
|
1786
1878
|
const r = appendAgentsMemory(ctx.sessionRoot, { agent, body });
|
|
1787
|
-
return { _meta:
|
|
1879
|
+
return { _meta: tools_META, ...r };
|
|
1788
1880
|
},
|
|
1789
1881
|
};
|
|
1790
1882
|
|
|
@@ -1800,16 +1892,16 @@ const read_agents_memory = {
|
|
|
1800
1892
|
},
|
|
1801
1893
|
async handler({ full }, ctx) {
|
|
1802
1894
|
const body = readAgentsMemory(ctx.sessionRoot);
|
|
1803
|
-
if (!body) return { _meta:
|
|
1804
|
-
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 };
|
|
1805
1897
|
// Tail-only — same logic as summarizeForSession but inlined to avoid a
|
|
1806
1898
|
// second import surface.
|
|
1807
1899
|
const limit = 6 * 1024;
|
|
1808
|
-
if (body.length <= limit) return { _meta:
|
|
1900
|
+
if (body.length <= limit) return { _meta: tools_META, present: true, length: body.length, content: body };
|
|
1809
1901
|
const tail = body.slice(-limit);
|
|
1810
1902
|
const firstSection = tail.indexOf('\n## ');
|
|
1811
1903
|
const slice = firstSection >= 0 ? tail.slice(firstSection) : tail;
|
|
1812
|
-
return { _meta:
|
|
1904
|
+
return { _meta: tools_META, present: true, length: body.length, truncated: true, content: slice };
|
|
1813
1905
|
},
|
|
1814
1906
|
};
|
|
1815
1907
|
|
|
@@ -1843,10 +1935,10 @@ const query_triage_memory = {
|
|
|
1843
1935
|
// they landed in, rather than hardcoding a field allowlist that could
|
|
1844
1936
|
// miss one.
|
|
1845
1937
|
let results;
|
|
1846
|
-
try { results = JSON.parse(redactString(JSON.stringify(raw))); }
|
|
1938
|
+
try { results = JSON.parse((0,redact/* redactString */.rd)(JSON.stringify(raw))); }
|
|
1847
1939
|
catch { results = raw; }
|
|
1848
1940
|
return {
|
|
1849
|
-
_meta:
|
|
1941
|
+
_meta: tools_META,
|
|
1850
1942
|
count: results.length,
|
|
1851
1943
|
results,
|
|
1852
1944
|
};
|
|
@@ -1871,7 +1963,7 @@ const query_findings_memory = {
|
|
|
1871
1963
|
required: ['query'],
|
|
1872
1964
|
},
|
|
1873
1965
|
async handler({ query }, ctx) {
|
|
1874
|
-
const { queryFindingsMemory } = await __webpack_require__.e(/* import() */
|
|
1966
|
+
const { queryFindingsMemory } = await __webpack_require__.e(/* import() */ 3839).then(__webpack_require__.bind(__webpack_require__, 3839));
|
|
1875
1967
|
const raw = queryFindingsMemory(ctx.sessionRoot, query || '');
|
|
1876
1968
|
// Stage 6 correctness audit — same redaction gap and same fix as
|
|
1877
1969
|
// query_triage_memory just above: this mixes four differently-shaped
|
|
@@ -1879,9 +1971,9 @@ const query_findings_memory = {
|
|
|
1879
1971
|
// whole-structure redactString round-trip is applied rather than a
|
|
1880
1972
|
// per-field allowlist that could miss one of the four shapes.
|
|
1881
1973
|
let body;
|
|
1882
|
-
try { body = JSON.parse(redactString(JSON.stringify(raw))); }
|
|
1974
|
+
try { body = JSON.parse((0,redact/* redactString */.rd)(JSON.stringify(raw))); }
|
|
1883
1975
|
catch { body = raw; }
|
|
1884
|
-
return { _meta:
|
|
1976
|
+
return { _meta: tools_META, ...body };
|
|
1885
1977
|
},
|
|
1886
1978
|
};
|
|
1887
1979
|
|
|
@@ -1902,7 +1994,7 @@ const lookup_cve = {
|
|
|
1902
1994
|
},
|
|
1903
1995
|
async handler({ cve }, _ctx) {
|
|
1904
1996
|
const r = lookupCve(cve);
|
|
1905
|
-
return { _meta:
|
|
1997
|
+
return { _meta: tools_META, ...r };
|
|
1906
1998
|
},
|
|
1907
1999
|
};
|
|
1908
2000
|
|
|
@@ -1920,9 +2012,9 @@ const query_cache_telemetry = {
|
|
|
1920
2012
|
},
|
|
1921
2013
|
async handler({ transcript_path } = {}, ctx) {
|
|
1922
2014
|
const result = (0,cache_economics.analyzeTranscript)({ transcriptPath: transcript_path, projectDir: ctx?.sessionRoot || process.cwd() });
|
|
1923
|
-
if (!result.ok) return { _meta:
|
|
2015
|
+
if (!result.ok) return { _meta: tools_META, ok: false, reason: result.reason };
|
|
1924
2016
|
return {
|
|
1925
|
-
_meta:
|
|
2017
|
+
_meta: tools_META,
|
|
1926
2018
|
ok: true,
|
|
1927
2019
|
metrics: result.metrics,
|
|
1928
2020
|
leaks: result.leaks,
|
|
@@ -1938,7 +2030,7 @@ const query_cache_telemetry = {
|
|
|
1938
2030
|
// ecosystem's native --dry-run command. Safe to call any number of times.
|
|
1939
2031
|
let _scaUpgrade;
|
|
1940
2032
|
async function _getScaUpgrade() {
|
|
1941
|
-
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));
|
|
1942
2034
|
return _scaUpgrade;
|
|
1943
2035
|
}
|
|
1944
2036
|
const synthesize_sca_upgrade = {
|
|
@@ -1958,11 +2050,11 @@ const synthesize_sca_upgrade = {
|
|
|
1958
2050
|
const f = _findById(scan, finding_id);
|
|
1959
2051
|
if (!f) throw new Error(`Finding not found: ${finding_id}`);
|
|
1960
2052
|
if (f.type !== 'vulnerable_dep') {
|
|
1961
|
-
return { _meta:
|
|
2053
|
+
return { _meta: tools_META, ok: false, reason: 'finding is not an SCA vulnerable_dep — use synthesize_fix for SAST findings' };
|
|
1962
2054
|
}
|
|
1963
2055
|
const { planScaUpgrade } = await _getScaUpgrade();
|
|
1964
2056
|
const plan = await planScaUpgrade({ scanRoot: ctx.sessionRoot, finding: f });
|
|
1965
|
-
return { _meta:
|
|
2057
|
+
return { _meta: tools_META, ...plan };
|
|
1966
2058
|
},
|
|
1967
2059
|
};
|
|
1968
2060
|
|
|
@@ -1989,24 +2081,24 @@ const apply_sca_upgrade = {
|
|
|
1989
2081
|
},
|
|
1990
2082
|
async handler({ finding_id, confirm, run_tests = true }, ctx) {
|
|
1991
2083
|
if (confirm !== true) {
|
|
1992
|
-
return { _meta:
|
|
2084
|
+
return { _meta: tools_META, applied: false, reason: 'apply_sca_upgrade requires confirm: true.' };
|
|
1993
2085
|
}
|
|
1994
2086
|
const { scan, status } = _readLastScanVerified(ctx.sessionRoot, { allowUnsigned: false });
|
|
1995
2087
|
if (!scan) {
|
|
1996
|
-
return { _meta:
|
|
2088
|
+
return { _meta: tools_META, applied: false, reason: `last-scan.json failed integrity check: ${status}. Run a fresh scan.` };
|
|
1997
2089
|
}
|
|
1998
2090
|
const f = _findById(scan, finding_id);
|
|
1999
|
-
if (!f) return { _meta:
|
|
2091
|
+
if (!f) return { _meta: tools_META, applied: false, reason: `Finding not found: ${finding_id}` };
|
|
2000
2092
|
if (f.type !== 'vulnerable_dep') {
|
|
2001
|
-
return { _meta:
|
|
2093
|
+
return { _meta: tools_META, applied: false, reason: 'finding is not an SCA vulnerable_dep — use apply_fix for SAST findings' };
|
|
2002
2094
|
}
|
|
2003
2095
|
const { applyScaUpgrade } = await _getScaUpgrade();
|
|
2004
2096
|
const result = await applyScaUpgrade({ scanRoot: ctx.sessionRoot, finding: f, runTests: run_tests });
|
|
2005
|
-
return { _meta:
|
|
2097
|
+
return { _meta: tools_META, ...result };
|
|
2006
2098
|
},
|
|
2007
2099
|
};
|
|
2008
2100
|
|
|
2009
|
-
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];
|
|
2010
2102
|
|
|
2011
2103
|
;// CONCATENATED MODULE: ./src/mcp/validate.js
|
|
2012
2104
|
// Minimal JSON Schema validator — just the subset our tool schemas use.
|
|
@@ -2104,7 +2196,7 @@ const SESSION_ID = `${process.pid}-${Date.now().toString(36).slice(-6)}`;
|
|
|
2104
2196
|
function _summarize(args) {
|
|
2105
2197
|
let s;
|
|
2106
2198
|
try { s = JSON.stringify(args); } catch { s = '<unserializable>'; }
|
|
2107
|
-
s = redactArgsBlob(s);
|
|
2199
|
+
s = (0,redact/* redactArgsBlob */.MC)(s);
|
|
2108
2200
|
if (s.length > MAX_ARG_BYTES) s = s.slice(0, MAX_ARG_BYTES) + `…(+${s.length - MAX_ARG_BYTES})`;
|
|
2109
2201
|
return s;
|
|
2110
2202
|
}
|
|
@@ -2249,7 +2341,7 @@ const TOOLS_BY_NAME = Object.fromEntries(ALL_TOOLS.map(t => [t.name, t]));
|
|
|
2249
2341
|
function _codeFingerprint() {
|
|
2250
2342
|
try {
|
|
2251
2343
|
const here = external_node_path_.dirname((0,external_node_url_.fileURLToPath)(import.meta.url));
|
|
2252
|
-
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'];
|
|
2253
2345
|
const h = external_node_crypto_.createHash('sha256');
|
|
2254
2346
|
for (const f of files) {
|
|
2255
2347
|
try { h.update(f); h.update(external_node_fs_.readFileSync(external_node_path_.join(here, f))); } catch {}
|
|
@@ -2737,6 +2829,307 @@ const _internal = {
|
|
|
2737
2829
|
};
|
|
2738
2830
|
|
|
2739
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
|
+
|
|
2740
3133
|
/***/ })
|
|
2741
3134
|
|
|
2742
3135
|
};
|