@expressots/studio-agent 4.0.0-preview.1
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/README.md +143 -0
- package/dist/agent.d.ts +127 -0
- package/dist/agent.d.ts.map +1 -0
- package/dist/agent.js +1031 -0
- package/dist/agent.js.map +1 -0
- package/dist/discovery/index.d.ts +2 -0
- package/dist/discovery/index.d.ts.map +1 -0
- package/dist/discovery/index.js +2 -0
- package/dist/discovery/index.js.map +1 -0
- package/dist/discovery/route-scanner.d.ts +35 -0
- package/dist/discovery/route-scanner.d.ts.map +1 -0
- package/dist/discovery/route-scanner.js +385 -0
- package/dist/discovery/route-scanner.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation/index.d.ts +2 -0
- package/dist/instrumentation/index.d.ts.map +1 -0
- package/dist/instrumentation/index.js +2 -0
- package/dist/instrumentation/index.js.map +1 -0
- package/dist/instrumentation/tracer.d.ts +40 -0
- package/dist/instrumentation/tracer.d.ts.map +1 -0
- package/dist/instrumentation/tracer.js +190 -0
- package/dist/instrumentation/tracer.js.map +1 -0
- package/dist/introspection/container-introspector.d.ts +81 -0
- package/dist/introspection/container-introspector.d.ts.map +1 -0
- package/dist/introspection/container-introspector.js +251 -0
- package/dist/introspection/container-introspector.js.map +1 -0
- package/dist/logging/log-capture.d.ts +58 -0
- package/dist/logging/log-capture.d.ts.map +1 -0
- package/dist/logging/log-capture.js +184 -0
- package/dist/logging/log-capture.js.map +1 -0
- package/dist/recording/index.d.ts +2 -0
- package/dist/recording/index.d.ts.map +1 -0
- package/dist/recording/index.js +2 -0
- package/dist/recording/index.js.map +1 -0
- package/dist/recording/request-recorder.d.ts +43 -0
- package/dist/recording/request-recorder.d.ts.map +1 -0
- package/dist/recording/request-recorder.js +373 -0
- package/dist/recording/request-recorder.js.map +1 -0
- package/dist/security/fix-resolver.d.ts +40 -0
- package/dist/security/fix-resolver.d.ts.map +1 -0
- package/dist/security/fix-resolver.js +283 -0
- package/dist/security/fix-resolver.js.map +1 -0
- package/dist/security/fix-runner.d.ts +60 -0
- package/dist/security/fix-runner.d.ts.map +1 -0
- package/dist/security/fix-runner.js +188 -0
- package/dist/security/fix-runner.js.map +1 -0
- package/dist/security/index.d.ts +140 -0
- package/dist/security/index.d.ts.map +1 -0
- package/dist/security/index.js +460 -0
- package/dist/security/index.js.map +1 -0
- package/dist/security/lockfile-graph.d.ts +69 -0
- package/dist/security/lockfile-graph.d.ts.map +1 -0
- package/dist/security/lockfile-graph.js +245 -0
- package/dist/security/lockfile-graph.js.map +1 -0
- package/dist/security/npm-audit.d.ts +67 -0
- package/dist/security/npm-audit.d.ts.map +1 -0
- package/dist/security/npm-audit.js +320 -0
- package/dist/security/npm-audit.js.map +1 -0
- package/dist/security/osv-cache.d.ts +51 -0
- package/dist/security/osv-cache.d.ts.map +1 -0
- package/dist/security/osv-cache.js +99 -0
- package/dist/security/osv-cache.js.map +1 -0
- package/dist/security/osv-client.d.ts +47 -0
- package/dist/security/osv-client.d.ts.map +1 -0
- package/dist/security/osv-client.js +247 -0
- package/dist/security/osv-client.js.map +1 -0
- package/dist/security/posture-analyzer.d.ts +44 -0
- package/dist/security/posture-analyzer.d.ts.map +1 -0
- package/dist/security/posture-analyzer.js +397 -0
- package/dist/security/posture-analyzer.js.map +1 -0
- package/dist/security/reachability.d.ts +59 -0
- package/dist/security/reachability.d.ts.map +1 -0
- package/dist/security/reachability.js +302 -0
- package/dist/security/reachability.js.map +1 -0
- package/dist/security/score.d.ts +36 -0
- package/dist/security/score.d.ts.map +1 -0
- package/dist/security/score.js +94 -0
- package/dist/security/score.js.map +1 -0
- package/dist/types/index.d.ts +587 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +14 -0
- package/dist/types/index.js.map +1 -0
- package/package.json +75 -0
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime security posture analyzer.
|
|
3
|
+
*
|
|
4
|
+
* This is Studio's unique contribution: Snyk-style scanners only see
|
|
5
|
+
* `package.json`, but Studio has the recorded traffic, the routes, the
|
|
6
|
+
* DI graph and live logs. The analyzer turns all of that into a set
|
|
7
|
+
* of OWASP-mapped findings the user can act on directly from the UI.
|
|
8
|
+
*
|
|
9
|
+
* Rules implemented here are intentionally conservative — false
|
|
10
|
+
* positives erode trust quickly in a security tool. Every rule has
|
|
11
|
+
* either (a) a high-confidence runtime signal (e.g. literal
|
|
12
|
+
* `Access-Control-Allow-Origin: *`) or (b) an explicit "advisory"
|
|
13
|
+
* severity so users know the finding is heuristic.
|
|
14
|
+
*
|
|
15
|
+
* All passes are pure functions over a snapshot of in-memory state.
|
|
16
|
+
* The engine is responsible for debouncing and change detection.
|
|
17
|
+
*/
|
|
18
|
+
import { createHash } from 'node:crypto';
|
|
19
|
+
/**
|
|
20
|
+
* Run every posture check over the given snapshot. Returns an
|
|
21
|
+
* unsorted list — the engine aggregates and dedupes by `id`.
|
|
22
|
+
*/
|
|
23
|
+
export function analyzePosture(input) {
|
|
24
|
+
const findings = [];
|
|
25
|
+
findings.push(...checkSecurityHeaders(input));
|
|
26
|
+
findings.push(...checkPermissiveCors(input));
|
|
27
|
+
findings.push(...checkAuthGaps(input));
|
|
28
|
+
findings.push(...checkVerboseErrors(input));
|
|
29
|
+
findings.push(...checkSecretLeakage(input));
|
|
30
|
+
findings.push(...checkInputValidation(input));
|
|
31
|
+
return findings;
|
|
32
|
+
}
|
|
33
|
+
// ────────────────────────────────────────────────────────────────────
|
|
34
|
+
// Rule: missing standard security response headers
|
|
35
|
+
// ────────────────────────────────────────────────────────────────────
|
|
36
|
+
const REQUIRED_HEADERS = [
|
|
37
|
+
{ name: 'content-security-policy', severity: 'MEDIUM', owasp: 'API8:2023' },
|
|
38
|
+
{ name: 'strict-transport-security', severity: 'MEDIUM', owasp: 'API8:2023' },
|
|
39
|
+
{ name: 'x-content-type-options', severity: 'LOW', owasp: 'API8:2023' },
|
|
40
|
+
{ name: 'x-frame-options', severity: 'LOW', owasp: 'API8:2023' },
|
|
41
|
+
{ name: 'referrer-policy', severity: 'LOW', owasp: 'API8:2023' },
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* For each route observed in the recorded exchanges, check whether the
|
|
45
|
+
* most recent successful response carried the standard security
|
|
46
|
+
* headers. We pick the *last* 2xx response per route so transient
|
|
47
|
+
* misses on error paths don't flag every route.
|
|
48
|
+
*/
|
|
49
|
+
function checkSecurityHeaders(input) {
|
|
50
|
+
const out = [];
|
|
51
|
+
const lastSuccessByRoute = mostRecentSuccessByRoute(input.exchanges);
|
|
52
|
+
for (const [, exchange] of lastSuccessByRoute) {
|
|
53
|
+
const headers = lowercaseHeaderKeys(exchange.response.headers);
|
|
54
|
+
for (const required of REQUIRED_HEADERS) {
|
|
55
|
+
if (headers[required.name])
|
|
56
|
+
continue;
|
|
57
|
+
out.push({
|
|
58
|
+
id: stableId('missing-header', required.name, exchange.request.path),
|
|
59
|
+
rule: `missing-${required.name}`,
|
|
60
|
+
owasp: required.owasp,
|
|
61
|
+
severity: required.severity,
|
|
62
|
+
title: `Missing ${prettyHeader(required.name)} header`,
|
|
63
|
+
description: `Responses to \`${exchange.request.method} ${exchange.request.path}\` ` +
|
|
64
|
+
`are not sending the \`${prettyHeader(required.name)}\` header. ` +
|
|
65
|
+
'This relaxes browser-side defences against a range of common attacks.',
|
|
66
|
+
evidence: { kind: 'exchange', exchangeId: exchange.id },
|
|
67
|
+
fixHint: 'Register `helmet()` (or set the header explicitly in a response interceptor).',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
// ────────────────────────────────────────────────────────────────────
|
|
74
|
+
// Rule: permissive CORS
|
|
75
|
+
// ────────────────────────────────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Flags responses that pair `Access-Control-Allow-Origin: *` with
|
|
78
|
+
* authenticated requests — that combination effectively turns CORS
|
|
79
|
+
* off for cross-origin attackers who can persuade a victim to issue
|
|
80
|
+
* a request bearing their own credentials.
|
|
81
|
+
*/
|
|
82
|
+
function checkPermissiveCors(input) {
|
|
83
|
+
const out = [];
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
for (const ex of input.exchanges) {
|
|
86
|
+
const headers = lowercaseHeaderKeys(ex.response.headers);
|
|
87
|
+
if (headers['access-control-allow-origin'] !== '*')
|
|
88
|
+
continue;
|
|
89
|
+
const reqHeaders = lowercaseHeaderKeys(ex.request.headers);
|
|
90
|
+
const hasAuth = Boolean(reqHeaders['authorization'] || reqHeaders['cookie']);
|
|
91
|
+
const key = `${ex.request.method}:${ex.request.path}:${hasAuth}`;
|
|
92
|
+
if (seen.has(key))
|
|
93
|
+
continue;
|
|
94
|
+
seen.add(key);
|
|
95
|
+
out.push({
|
|
96
|
+
id: stableId('permissive-cors', ex.request.method, ex.request.path, String(hasAuth)),
|
|
97
|
+
rule: 'permissive-cors',
|
|
98
|
+
owasp: 'API8:2023',
|
|
99
|
+
severity: hasAuth ? 'HIGH' : 'LOW',
|
|
100
|
+
title: hasAuth
|
|
101
|
+
? 'Wildcard CORS on authenticated route'
|
|
102
|
+
: 'Wildcard CORS origin',
|
|
103
|
+
description: hasAuth
|
|
104
|
+
? `\`${ex.request.method} ${ex.request.path}\` returns \`Access-Control-Allow-Origin: *\` ` +
|
|
105
|
+
'while the client sends credentials. Browsers will refuse the response, but the ' +
|
|
106
|
+
'configuration is a footgun — pin the origin to your front-end host instead.'
|
|
107
|
+
: `\`${ex.request.method} ${ex.request.path}\` returns \`Access-Control-Allow-Origin: *\`. ` +
|
|
108
|
+
'Acceptable for fully public endpoints; replace with an explicit allow-list otherwise.',
|
|
109
|
+
evidence: { kind: 'exchange', exchangeId: ex.id },
|
|
110
|
+
fixHint: 'Configure CORS with a concrete `origin` list rather than `*`.',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
// ────────────────────────────────────────────────────────────────────
|
|
116
|
+
// Rule: routes returning 2xx with no auth evidence
|
|
117
|
+
// ────────────────────────────────────────────────────────────────────
|
|
118
|
+
const PUBLIC_PATH_PATTERNS = [
|
|
119
|
+
/^\/?$/,
|
|
120
|
+
/^\/health/i,
|
|
121
|
+
/^\/healthz/i,
|
|
122
|
+
/^\/status/i,
|
|
123
|
+
/^\/metrics/i,
|
|
124
|
+
/^\/ready/i,
|
|
125
|
+
/^\/favicon/i,
|
|
126
|
+
];
|
|
127
|
+
/**
|
|
128
|
+
* For each route, look at the most recent 2xx response: if no request
|
|
129
|
+
* we've seen against it carried an `Authorization` header or a session
|
|
130
|
+
* cookie, *and* the static / runtime middleware list shows no obvious
|
|
131
|
+
* auth interceptor, flag it as potentially unauthenticated.
|
|
132
|
+
*
|
|
133
|
+
* Intentionally LOW severity (not HIGH) because plenty of routes are
|
|
134
|
+
* legitimately public. Users skim the list and mute false positives.
|
|
135
|
+
*/
|
|
136
|
+
function checkAuthGaps(input) {
|
|
137
|
+
const out = [];
|
|
138
|
+
const lastByRoute = mostRecentSuccessByRoute(input.exchanges);
|
|
139
|
+
const allByRoute = groupByRoute(input.exchanges);
|
|
140
|
+
const middlewareNames = (input.structure?.middleware ?? []).map((m) => m.toLowerCase());
|
|
141
|
+
const hasGlobalAuth = middlewareNames.some((n) => /auth|jwt|session|guard/.test(n));
|
|
142
|
+
for (const [routeKey, exchange] of lastByRoute) {
|
|
143
|
+
if (looksPublic(exchange.request.path))
|
|
144
|
+
continue;
|
|
145
|
+
if (hasGlobalAuth)
|
|
146
|
+
continue;
|
|
147
|
+
const everyExchange = allByRoute.get(routeKey) ?? [];
|
|
148
|
+
const sawCredentials = everyExchange.some((ex) => {
|
|
149
|
+
const h = lowercaseHeaderKeys(ex.request.headers);
|
|
150
|
+
return Boolean(h['authorization'] || h['cookie']);
|
|
151
|
+
});
|
|
152
|
+
if (sawCredentials)
|
|
153
|
+
continue;
|
|
154
|
+
out.push({
|
|
155
|
+
id: stableId('unauthenticated-route', exchange.request.method, exchange.request.path),
|
|
156
|
+
rule: 'unauthenticated-route',
|
|
157
|
+
owasp: 'API2:2023',
|
|
158
|
+
severity: 'LOW',
|
|
159
|
+
title: 'Route may be unauthenticated',
|
|
160
|
+
description: `\`${exchange.request.method} ${exchange.request.path}\` returned 2xx for every ` +
|
|
161
|
+
'recorded request, and Studio never observed an `Authorization` header or session ' +
|
|
162
|
+
'cookie. If this endpoint is meant to be private, add an auth interceptor.',
|
|
163
|
+
evidence: {
|
|
164
|
+
kind: 'route',
|
|
165
|
+
method: exchange.request.method,
|
|
166
|
+
path: exchange.request.path,
|
|
167
|
+
},
|
|
168
|
+
fixHint: 'Apply an auth interceptor on the controller or the route, or register one globally.',
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
// ────────────────────────────────────────────────────────────────────
|
|
174
|
+
// Rule: verbose error responses (stack trace leakage)
|
|
175
|
+
// ────────────────────────────────────────────────────────────────────
|
|
176
|
+
const STACK_REGEX = /at\s+[\w.<>$]+\s+\(.+:\d+:\d+\)/;
|
|
177
|
+
function checkVerboseErrors(input) {
|
|
178
|
+
const out = [];
|
|
179
|
+
const seen = new Set();
|
|
180
|
+
for (const ex of input.exchanges) {
|
|
181
|
+
if (ex.response.statusCode < 500)
|
|
182
|
+
continue;
|
|
183
|
+
const body = renderBody(ex.response.body);
|
|
184
|
+
if (!body)
|
|
185
|
+
continue;
|
|
186
|
+
if (!STACK_REGEX.test(body))
|
|
187
|
+
continue;
|
|
188
|
+
const key = `${ex.request.method}:${ex.request.path}`;
|
|
189
|
+
if (seen.has(key))
|
|
190
|
+
continue;
|
|
191
|
+
seen.add(key);
|
|
192
|
+
out.push({
|
|
193
|
+
id: stableId('verbose-error', ex.request.method, ex.request.path),
|
|
194
|
+
rule: 'verbose-error',
|
|
195
|
+
owasp: 'API8:2023',
|
|
196
|
+
severity: 'MEDIUM',
|
|
197
|
+
title: '5xx response includes a stack trace',
|
|
198
|
+
description: `\`${ex.request.method} ${ex.request.path}\` returned ${ex.response.statusCode} ` +
|
|
199
|
+
'with a response body containing what looks like a stack trace. Production clients ' +
|
|
200
|
+
'should never see implementation details from server-side errors.',
|
|
201
|
+
evidence: { kind: 'exchange', exchangeId: ex.id },
|
|
202
|
+
fixHint: 'Install a global error filter that strips stack frames from outbound responses.',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
return out;
|
|
206
|
+
}
|
|
207
|
+
// ────────────────────────────────────────────────────────────────────
|
|
208
|
+
// Rule: likely secret leakage in responses or logs
|
|
209
|
+
// ────────────────────────────────────────────────────────────────────
|
|
210
|
+
/**
|
|
211
|
+
* High-precision patterns only. A single false positive on a secret
|
|
212
|
+
* scanner is much worse than a missed one — users learn to ignore the
|
|
213
|
+
* tool. We deliberately don't try to catch generic "long random string
|
|
214
|
+
* that might be a token". Adopt the [trufflehog / git-leaks] vocab.
|
|
215
|
+
*/
|
|
216
|
+
const SECRET_PATTERNS = [
|
|
217
|
+
{ name: 'AWS Access Key', regex: /AKIA[0-9A-Z]{16}/ },
|
|
218
|
+
{ name: 'GitHub PAT', regex: /\bghp_[A-Za-z0-9]{36}\b/ },
|
|
219
|
+
{ name: 'GitHub Fine-grained PAT', regex: /\bgithub_pat_[A-Za-z0-9_]{82}\b/ },
|
|
220
|
+
{ name: 'Slack token', regex: /\bxox[abprs]-[A-Za-z0-9-]{10,48}\b/ },
|
|
221
|
+
{ name: 'Stripe live key', regex: /\bsk_live_[A-Za-z0-9]{24,}\b/ },
|
|
222
|
+
{ name: 'JWT', regex: /\beyJ[A-Za-z0-9_-]+\.eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/ },
|
|
223
|
+
];
|
|
224
|
+
function checkSecretLeakage(input) {
|
|
225
|
+
const out = [];
|
|
226
|
+
for (const ex of input.exchanges) {
|
|
227
|
+
const body = renderBody(ex.response.body);
|
|
228
|
+
if (!body)
|
|
229
|
+
continue;
|
|
230
|
+
for (const pat of SECRET_PATTERNS) {
|
|
231
|
+
if (!pat.regex.test(body))
|
|
232
|
+
continue;
|
|
233
|
+
out.push({
|
|
234
|
+
id: stableId('response-secret', pat.name, ex.request.method, ex.request.path),
|
|
235
|
+
rule: 'response-secret',
|
|
236
|
+
owasp: 'API3:2023',
|
|
237
|
+
severity: 'HIGH',
|
|
238
|
+
title: `Response body matches a ${pat.name} pattern`,
|
|
239
|
+
description: `\`${ex.request.method} ${ex.request.path}\` returned a body containing what ` +
|
|
240
|
+
`looks like a ${pat.name}. Strip credentials from response payloads — they should ` +
|
|
241
|
+
'never reach the client.',
|
|
242
|
+
evidence: { kind: 'exchange', exchangeId: ex.id },
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
for (let i = 0; i < input.logs.length; i++) {
|
|
247
|
+
const entry = input.logs[i];
|
|
248
|
+
for (const pat of SECRET_PATTERNS) {
|
|
249
|
+
if (!pat.regex.test(entry.message))
|
|
250
|
+
continue;
|
|
251
|
+
out.push({
|
|
252
|
+
id: stableId('log-secret', pat.name, entry.message.slice(0, 64)),
|
|
253
|
+
rule: 'log-secret',
|
|
254
|
+
owasp: 'API3:2023',
|
|
255
|
+
severity: 'MEDIUM',
|
|
256
|
+
title: `Log line matches a ${pat.name} pattern`,
|
|
257
|
+
description: `A captured log entry contains what looks like a ${pat.name}. Logs propagate to ` +
|
|
258
|
+
'log aggregators and rotated files; treat them as untrusted destinations for secrets.',
|
|
259
|
+
evidence: { kind: 'log', logIndex: i },
|
|
260
|
+
});
|
|
261
|
+
// Don't double-report on the same line for multiple patterns.
|
|
262
|
+
break;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return out;
|
|
266
|
+
}
|
|
267
|
+
// ────────────────────────────────────────────────────────────────────
|
|
268
|
+
// Rule: controllers accepting bodies without validation
|
|
269
|
+
// ────────────────────────────────────────────────────────────────────
|
|
270
|
+
/**
|
|
271
|
+
* Heuristic check: any controller whose recorded requests carry a
|
|
272
|
+
* non-empty body, but whose source file doesn't import any of the
|
|
273
|
+
* canonical validation libraries we recognise. Marked INFO because
|
|
274
|
+
* it's the most heuristic of the rules — frameworks differ, and the
|
|
275
|
+
* controller might validate at a layer below our scanner.
|
|
276
|
+
*/
|
|
277
|
+
function checkInputValidation(input) {
|
|
278
|
+
const out = [];
|
|
279
|
+
const seen = new Set();
|
|
280
|
+
const controllerByName = new Map();
|
|
281
|
+
for (const c of input.structure?.controllers ?? []) {
|
|
282
|
+
controllerByName.set(c.name, { filePath: c.filePath });
|
|
283
|
+
}
|
|
284
|
+
for (const ex of input.exchanges) {
|
|
285
|
+
if (!hasMeaningfulBody(ex.request.body))
|
|
286
|
+
continue;
|
|
287
|
+
const route = input.routes.find((r) => r.path === ex.request.path && r.method === ex.request.method);
|
|
288
|
+
if (!route)
|
|
289
|
+
continue;
|
|
290
|
+
const key = `${route.controller}.${route.controllerMethod}`;
|
|
291
|
+
if (seen.has(key))
|
|
292
|
+
continue;
|
|
293
|
+
seen.add(key);
|
|
294
|
+
const ctrl = controllerByName.get(route.controller);
|
|
295
|
+
if (!ctrl?.filePath)
|
|
296
|
+
continue;
|
|
297
|
+
// We can't reliably read the file here without async I/O; the engine
|
|
298
|
+
// does the file read once per scan and passes the import list via
|
|
299
|
+
// `srcRoot` (future enhancement). For now, surface the route and let
|
|
300
|
+
// the user verify — INFO severity reflects the uncertainty.
|
|
301
|
+
out.push({
|
|
302
|
+
id: stableId('unvalidated-body', route.controller, route.controllerMethod),
|
|
303
|
+
rule: 'unvalidated-body',
|
|
304
|
+
owasp: 'API4:2023',
|
|
305
|
+
severity: 'INFO',
|
|
306
|
+
title: `Verify request validation for ${route.controller}.${route.controllerMethod}`,
|
|
307
|
+
description: `\`${ex.request.method} ${ex.request.path}\` (handled by ` +
|
|
308
|
+
`\`${route.controller}.${route.controllerMethod}\`) accepts a body. Confirm that the ` +
|
|
309
|
+
'handler validates it (zod, class-validator, or a project DTO) before use.',
|
|
310
|
+
evidence: { kind: 'file', filePath: ctrl.filePath, lineNumber: route.lineNumber },
|
|
311
|
+
fixHint: 'Wrap the body parameter in a typed DTO + validation pipe or use a runtime schema check.',
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
// ────────────────────────────────────────────────────────────────────
|
|
317
|
+
// Helpers
|
|
318
|
+
// ────────────────────────────────────────────────────────────────────
|
|
319
|
+
function lowercaseHeaderKeys(headers) {
|
|
320
|
+
const out = {};
|
|
321
|
+
if (!headers)
|
|
322
|
+
return out;
|
|
323
|
+
for (const [k, v] of Object.entries(headers)) {
|
|
324
|
+
out[k.toLowerCase()] = v;
|
|
325
|
+
}
|
|
326
|
+
return out;
|
|
327
|
+
}
|
|
328
|
+
function looksPublic(path) {
|
|
329
|
+
return PUBLIC_PATH_PATTERNS.some((p) => p.test(path));
|
|
330
|
+
}
|
|
331
|
+
function mostRecentSuccessByRoute(exchanges) {
|
|
332
|
+
const out = new Map();
|
|
333
|
+
for (const ex of exchanges) {
|
|
334
|
+
if (ex.response.statusCode < 200 || ex.response.statusCode >= 300)
|
|
335
|
+
continue;
|
|
336
|
+
const key = routeKey(ex.request.method, ex.request.path);
|
|
337
|
+
const existing = out.get(key);
|
|
338
|
+
if (!existing || ex.request.timestamp > existing.request.timestamp) {
|
|
339
|
+
out.set(key, ex);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
344
|
+
function groupByRoute(exchanges) {
|
|
345
|
+
const out = new Map();
|
|
346
|
+
for (const ex of exchanges) {
|
|
347
|
+
const key = routeKey(ex.request.method, ex.request.path);
|
|
348
|
+
const list = out.get(key) ?? [];
|
|
349
|
+
list.push(ex);
|
|
350
|
+
out.set(key, list);
|
|
351
|
+
}
|
|
352
|
+
return out;
|
|
353
|
+
}
|
|
354
|
+
function routeKey(method, path) {
|
|
355
|
+
return `${method}:${path}`;
|
|
356
|
+
}
|
|
357
|
+
function renderBody(body) {
|
|
358
|
+
if (body == null)
|
|
359
|
+
return '';
|
|
360
|
+
if (typeof body === 'string')
|
|
361
|
+
return body;
|
|
362
|
+
try {
|
|
363
|
+
return JSON.stringify(body);
|
|
364
|
+
}
|
|
365
|
+
catch {
|
|
366
|
+
return '';
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function hasMeaningfulBody(body) {
|
|
370
|
+
if (body == null)
|
|
371
|
+
return false;
|
|
372
|
+
if (typeof body === 'string')
|
|
373
|
+
return body.trim().length > 0;
|
|
374
|
+
if (typeof body === 'object') {
|
|
375
|
+
return Object.keys(body).length > 0;
|
|
376
|
+
}
|
|
377
|
+
return true;
|
|
378
|
+
}
|
|
379
|
+
function prettyHeader(headerName) {
|
|
380
|
+
return headerName
|
|
381
|
+
.split('-')
|
|
382
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
383
|
+
.join('-');
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Build a deterministic id so re-runs of the analyzer produce the same
|
|
387
|
+
* id for the same finding — the engine relies on this for change
|
|
388
|
+
* detection (hash the id set; only broadcast on transition).
|
|
389
|
+
*/
|
|
390
|
+
function stableId(...parts) {
|
|
391
|
+
const h = createHash('sha1');
|
|
392
|
+
for (const p of parts)
|
|
393
|
+
h.update(p);
|
|
394
|
+
h.update('\0');
|
|
395
|
+
return h.digest('hex').slice(0, 16);
|
|
396
|
+
}
|
|
397
|
+
//# sourceMappingURL=posture-analyzer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"posture-analyzer.js","sourceRoot":"","sources":["../../src/security/posture-analyzer.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime reachability analysis for supply-chain findings.
|
|
3
|
+
*
|
|
4
|
+
* This is the single feature that justifies "why Studio over Snyk?". A
|
|
5
|
+
* static scanner can tell you that `lodash@4.17.10` has CVE-X; only a
|
|
6
|
+
* tool sitting inside the running app can tell you that:
|
|
7
|
+
*
|
|
8
|
+
* - `src/users/users.service.ts` actually imports lodash
|
|
9
|
+
* - that service is wired into `UsersController`, which owns `GET /users`
|
|
10
|
+
* - and `GET /users` has been hit 47 times in the last hour
|
|
11
|
+
*
|
|
12
|
+
* We combine three signals to land on one of four labels per finding:
|
|
13
|
+
*
|
|
14
|
+
* - **confirmed**: a route that imports (transitively) the vulnerable
|
|
15
|
+
* package has been exercised in the current session.
|
|
16
|
+
* - **likely**: the package is imported from `src/` but we haven't
|
|
17
|
+
* seen a request hit a route that reaches it yet.
|
|
18
|
+
* - **unreachable**: we have a complete import graph for `src/` and
|
|
19
|
+
* the package doesn't appear anywhere. Usually means it's only
|
|
20
|
+
* pulled in by a dev tool or another transitive dep.
|
|
21
|
+
* - **unknown**: we can't compute the graph (no src dir, scanning
|
|
22
|
+
* disabled). Default for transitive packages with no direct
|
|
23
|
+
* imports — we don't follow node_modules → node_modules edges.
|
|
24
|
+
*
|
|
25
|
+
* The analyzer is intentionally cheap: regex-based import extraction
|
|
26
|
+
* over the same source tree the route scanner already walked. We
|
|
27
|
+
* don't reach for a TypeScript AST — it'd be 100× slower for almost
|
|
28
|
+
* no precision gain at this granularity (we only need package names,
|
|
29
|
+
* not symbols).
|
|
30
|
+
*/
|
|
31
|
+
import type { AppStructure, DependencyFinding, RecordedExchange, RouteInfo } from '../types/index.js';
|
|
32
|
+
/**
|
|
33
|
+
* Reachability snapshot built once per security scan. We compute it on
|
|
34
|
+
* the agent thread (synchronous file I/O, but only ~200 small files for
|
|
35
|
+
* a typical service) and pass it to `enrichWithReachability`, which is
|
|
36
|
+
* then a pure map.
|
|
37
|
+
*/
|
|
38
|
+
export interface ReachabilitySnapshot {
|
|
39
|
+
/** package name → files in `src/` that import it. */
|
|
40
|
+
importedByPkg: Map<string, Set<string>>;
|
|
41
|
+
/** file path → routes whose handlers live in (or transitively reach) that file. */
|
|
42
|
+
routesByFile: Map<string, RouteInfo[]>;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Build a reachability snapshot for the host project. Returns an empty
|
|
46
|
+
* snapshot if `srcPath` doesn't exist — callers should treat that as
|
|
47
|
+
* "always emit `unknown`" rather than failing.
|
|
48
|
+
*
|
|
49
|
+
* Safe to call on every full scan; for a 1000-file project it usually
|
|
50
|
+
* completes in <100 ms.
|
|
51
|
+
*/
|
|
52
|
+
export declare function buildReachabilitySnapshot(cwd: string, structure: AppStructure | null): Promise<ReachabilitySnapshot>;
|
|
53
|
+
/**
|
|
54
|
+
* Attach a `ReachabilityInfo` block to every finding in `findings`.
|
|
55
|
+
* Pure given the snapshot + exchanges; same call with the same inputs
|
|
56
|
+
* always produces the same output, which keeps hashing stable.
|
|
57
|
+
*/
|
|
58
|
+
export declare function enrichWithReachability(findings: DependencyFinding[], snapshot: ReachabilitySnapshot, exchanges: RecordedExchange[]): DependencyFinding[];
|
|
59
|
+
//# sourceMappingURL=reachability.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reachability.d.ts","sourceRoot":"","sources":["../../src/security/reachability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AAKH,OAAO,KAAK,EACV,YAAY,EAEZ,iBAAiB,EAGjB,gBAAgB,EAChB,SAAS,EAEV,MAAM,mBAAmB,CAAC;AAE3B;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,qDAAqD;IACrD,aAAa,EAAE,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;IACxC,mFAAmF;IACnF,YAAY,EAAE,GAAG,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;CACxC;AAaD;;;;;;;GAOG;AACH,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,YAAY,GAAG,IAAI,GAC7B,OAAO,CAAC,oBAAoB,CAAC,CAkC/B;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,iBAAiB,EAAE,EAC7B,QAAQ,EAAE,oBAAoB,EAC9B,SAAS,EAAE,gBAAgB,EAAE,GAC5B,iBAAiB,EAAE,CA0BrB"}
|