@redocly/cli 2.41.2 → 2.43.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.
Files changed (73) hide show
  1. package/README.md +15 -0
  2. package/THIRD_PARTY_NOTICES +3 -2
  3. package/lib/chunks/32XCOMOR.js +7 -0
  4. package/lib/chunks/3BCHPW6T.js +22 -0
  5. package/lib/chunks/3VZLNQK3.js +47 -0
  6. package/lib/chunks/4VF6MPJJ.js +11 -0
  7. package/lib/chunks/55GMSQDD.js +7 -0
  8. package/lib/chunks/5E3VKU5T.js +7 -0
  9. package/lib/chunks/AE6JGTY4.js +7 -0
  10. package/lib/chunks/C35TOMVY.js +8 -0
  11. package/lib/chunks/D5T6ETPZ.js +8 -0
  12. package/lib/chunks/DNOXLF53.js +8 -0
  13. package/lib/chunks/E36IFTLX.js +59 -0
  14. package/lib/chunks/ECZUPR2N.js +7 -0
  15. package/lib/chunks/EU6GURJZ.js +7 -0
  16. package/lib/chunks/HXYO6QS2.js +3132 -0
  17. package/lib/chunks/IW4HG6VD.js +19 -0
  18. package/lib/chunks/KIVPQZDC.js +31 -0
  19. package/lib/chunks/KLY5RV3W.js +7 -0
  20. package/lib/chunks/KM7RL6C4.js +2317 -0
  21. package/lib/chunks/L3YH7RCT.js +8 -0
  22. package/lib/chunks/NEWT5A6S.js +159 -0
  23. package/lib/chunks/NGPG6D6W.js +7 -0
  24. package/lib/chunks/O6MCQRCP.js +59 -0
  25. package/lib/chunks/O6PRCLSV.js +13 -0
  26. package/lib/chunks/OFAMJNQ6.js +7 -0
  27. package/lib/chunks/P26PQ2RU.js +7 -0
  28. package/lib/chunks/PFD7KGH7.js +93 -0
  29. package/lib/chunks/QFG7TTKG.js +22 -0
  30. package/lib/chunks/RFWIIJFG.js +7 -0
  31. package/lib/chunks/RS5PUOTT.js +18 -0
  32. package/lib/chunks/TMQJQANP.js +7 -0
  33. package/lib/chunks/UPPWQBGX.js +7 -0
  34. package/lib/chunks/UXH3ZSSV.js +9 -0
  35. package/lib/chunks/UYZXI3NO.js +7 -0
  36. package/lib/chunks/WNTLDT6L.js +184 -0
  37. package/lib/chunks/WZXA5XPB.js +7 -0
  38. package/lib/chunks/X6IKW6K4.js +7 -0
  39. package/lib/index.js +226 -13885
  40. package/package.json +1 -1
  41. package/lib/chunks/3CX2Z7JI.js +0 -496
  42. package/lib/chunks/3UUTQVCJ.js +0 -3614
  43. package/lib/chunks/4FB5ZIPP.js +0 -1133
  44. package/lib/chunks/5ILQMFXK.js +0 -59
  45. package/lib/chunks/7F5O43EU.js +0 -1756
  46. package/lib/chunks/7IX44JLO.js +0 -16
  47. package/lib/chunks/7MOL2QVN.js +0 -92635
  48. package/lib/chunks/A3VFVVHD.js +0 -25
  49. package/lib/chunks/A6JSOHCP.js +0 -41
  50. package/lib/chunks/AIT3U3JT.js +0 -22
  51. package/lib/chunks/ASYQPOQY.js +0 -681
  52. package/lib/chunks/BVP23YP7.js +0 -13374
  53. package/lib/chunks/FO6BDVGE.js +0 -25
  54. package/lib/chunks/GFU5KGSW.js +0 -92
  55. package/lib/chunks/ICPYP2LP.js +0 -401
  56. package/lib/chunks/KPMZLKQG.js +0 -79
  57. package/lib/chunks/KRMBH6JF.js +0 -17902
  58. package/lib/chunks/NLIKC7UP.js +0 -528
  59. package/lib/chunks/NW3XJZXQ.js +0 -175
  60. package/lib/chunks/P6UGG7F6.js +0 -414
  61. package/lib/chunks/RRC5IDC3.js +0 -2416
  62. package/lib/chunks/T6UZU3XM.js +0 -17
  63. package/lib/chunks/TS33ZDKX.js +0 -35
  64. package/lib/chunks/UUU33DK3.js +0 -125
  65. package/lib/chunks/VUDTNFRL.js +0 -42
  66. package/lib/chunks/VXIQEZPR.js +0 -2564
  67. package/lib/chunks/W7G5WVPB.js +0 -44
  68. package/lib/chunks/XB6C62FW.js +0 -18497
  69. package/lib/chunks/XHPY6UJF.js +0 -1638
  70. package/lib/chunks/Y6DTFCLS.js +0 -15
  71. package/lib/chunks/Z2664VV5.js +0 -39331
  72. package/lib/chunks/Z2I5YXYN.js +0 -20272
  73. package/lib/chunks/ZK3QRKL5.js +0 -129
@@ -1,2416 +0,0 @@
1
- import { createRequire as __createRequire } from 'node:module';
2
- const require = __createRequire(import.meta.url);
3
- import {
4
- compileOpenApiPath,
5
- getPathWithoutTrailingSlash,
6
- isJsonMime,
7
- isSyntheticHost,
8
- listOpenApiFiles,
9
- normalizeServerPrefix,
10
- parseHeaderIgnoreList,
11
- parseUrl,
12
- pickSchemaByMime,
13
- resolvePathForServer,
14
- shouldIgnoreHeaderAsUndocumented,
15
- splitSetCookieHeader
16
- } from "./3CX2Z7JI.js";
17
- import {
18
- BaseResolver,
19
- blue,
20
- bold,
21
- bundle,
22
- cyan,
23
- detectSpec,
24
- dim,
25
- getMajorSpecVersion,
26
- getTypes,
27
- gray,
28
- green,
29
- isPlainObject,
30
- logger,
31
- normalizeTypes,
32
- normalizeVisitors,
33
- red,
34
- require__,
35
- require_dist,
36
- walkDocument,
37
- yellow
38
- } from "./Z2664VV5.js";
39
- import {
40
- __toESM
41
- } from "./5ILQMFXK.js";
42
-
43
- // src/commands/drift/engine/reporter.ts
44
- import path from "node:path";
45
-
46
- // src/commands/drift/utils/finding-groups.ts
47
- var KEY_SEPARATOR = "";
48
- function normalizePart(value) {
49
- if (!value) {
50
- return "";
51
- }
52
- return value.trim();
53
- }
54
- function createProblemKey(finding) {
55
- const operationOrPath = normalizePart(finding.operationId) || normalizePart(finding.path);
56
- return [
57
- normalizePart(finding.ruleId),
58
- normalizePart(finding.severity),
59
- normalizePart(finding.message),
60
- operationOrPath,
61
- normalizePart(finding.target),
62
- normalizePart(finding.schemaPath)
63
- ].join(KEY_SEPARATOR);
64
- }
65
-
66
- // src/commands/drift/engine/reporter.ts
67
- var cyanBold = (text) => bold(cyan(text));
68
- function mapSeverityToSarifLevel(severity) {
69
- if (severity === "error") return "error";
70
- if (severity === "warning") return "warning";
71
- return "note";
72
- }
73
- function escapeCsvCell(value) {
74
- if (value === void 0 || value === null) {
75
- return "";
76
- }
77
- const stringValue = String(value);
78
- if (!stringValue.includes(",") && !stringValue.includes('"') && !stringValue.includes("\n")) {
79
- return stringValue;
80
- }
81
- return `"${stringValue.replace(/"/g, '""')}"`;
82
- }
83
- function toRelativeSpecPath(specSource) {
84
- if (!path.isAbsolute(specSource)) {
85
- return specSource;
86
- }
87
- const relativePath = path.relative(process.cwd(), specSource);
88
- return relativePath || ".";
89
- }
90
- function getOperationTemplatePath(details) {
91
- if (!details) {
92
- return null;
93
- }
94
- const maybePath = details.operationPathTemplate;
95
- return typeof maybePath === "string" && maybePath.length > 0 ? maybePath : null;
96
- }
97
- function sortRuleCounts(findingsByRule) {
98
- return Object.entries(findingsByRule).sort((a, b) => {
99
- const countDiff = b[1] - a[1];
100
- if (countDiff !== 0) {
101
- return countDiff;
102
- }
103
- return a[0].localeCompare(b[0]);
104
- });
105
- }
106
- function groupFindingsIntoProblems(findings) {
107
- const problems = [];
108
- const keyToIndex = /* @__PURE__ */ new Map();
109
- for (const finding of findings) {
110
- const key = createProblemKey({
111
- ruleId: finding.ruleId,
112
- severity: finding.severity,
113
- message: finding.message,
114
- operationId: finding.operationId,
115
- path: finding.path,
116
- target: finding.target,
117
- schemaPath: finding.schemaPath
118
- });
119
- const existingIndex = keyToIndex.get(key);
120
- if (existingIndex !== void 0) {
121
- problems[existingIndex].occurrences += 1;
122
- continue;
123
- }
124
- keyToIndex.set(key, problems.length);
125
- problems.push({ finding, occurrences: 1 });
126
- }
127
- return problems;
128
- }
129
- function formatJson(result) {
130
- const problems = groupFindingsIntoProblems(result.findings).map((problem) => ({
131
- occurrences: problem.occurrences,
132
- exchangeIndex: problem.finding.exchangeIndex,
133
- ruleId: problem.finding.ruleId,
134
- severity: problem.finding.severity,
135
- category: problem.finding.category,
136
- message: problem.finding.message,
137
- operationId: problem.finding.operationId,
138
- specSource: problem.finding.specSource,
139
- target: problem.finding.target,
140
- schemaPath: problem.finding.schemaPath,
141
- dataPath: problem.finding.dataPath,
142
- details: problem.finding.details,
143
- method: problem.finding.method,
144
- url: problem.finding.url,
145
- path: problem.finding.path,
146
- status: problem.finding.status
147
- }));
148
- const payload = {
149
- run: {
150
- id: result.runId,
151
- specSource: result.meta.specSource,
152
- trafficPath: result.meta.trafficPath,
153
- format: result.meta.format,
154
- matchMode: result.meta.matchMode,
155
- server: result.meta.server,
156
- totalExchanges: result.summary.totalExchanges,
157
- documentedExchanges: result.summary.documentedExchanges,
158
- undocumentedExchanges: result.summary.undocumentedExchanges,
159
- skippedExchanges: result.summary.skippedExchanges,
160
- findingsBySeverity: result.summary.findingsBySeverity,
161
- findingsByRule: result.summary.findingsByRule,
162
- totalProblems: result.summary.totalProblemGroups,
163
- durationMs: result.summary.durationMs
164
- },
165
- problems
166
- };
167
- return `${JSON.stringify(payload, null, 2)}
168
- `;
169
- }
170
- function formatCsv(result) {
171
- const header = [
172
- "run_id",
173
- "finding_id",
174
- "exchange_index",
175
- "severity",
176
- "category",
177
- "rule_id",
178
- "message",
179
- "method",
180
- "path",
181
- "url",
182
- "status",
183
- "operation_id",
184
- "spec_source",
185
- "target",
186
- "schema_path",
187
- "data_path"
188
- ];
189
- const rows = result.findings.map((finding) => [
190
- result.runId,
191
- finding.id,
192
- finding.exchangeIndex,
193
- finding.severity,
194
- finding.category,
195
- finding.ruleId,
196
- finding.message,
197
- finding.method,
198
- finding.path,
199
- finding.url,
200
- finding.status,
201
- finding.operationId,
202
- finding.specSource,
203
- finding.target,
204
- finding.schemaPath,
205
- finding.dataPath
206
- ]);
207
- const csvRows = [header, ...rows].map((row) => row.map(escapeCsvCell).join(","));
208
- return `${csvRows.join("\n")}
209
- `;
210
- }
211
- function formatSarif(result) {
212
- const ruleSet = /* @__PURE__ */ new Map();
213
- for (const finding of result.findings) {
214
- if (!ruleSet.has(finding.ruleId)) {
215
- ruleSet.set(finding.ruleId, {
216
- id: finding.ruleId,
217
- name: finding.ruleId,
218
- shortDescription: { text: `${finding.category} detection` }
219
- });
220
- }
221
- }
222
- const results = result.findings.map((finding) => {
223
- const locationUri = finding.specSource ?? finding.url ?? void 0;
224
- return {
225
- ruleId: finding.ruleId,
226
- level: mapSeverityToSarifLevel(finding.severity),
227
- message: { text: finding.message },
228
- locations: locationUri ? [{ physicalLocation: { artifactLocation: { uri: locationUri } } }] : void 0,
229
- properties: {
230
- runId: result.runId,
231
- exchangeIndex: finding.exchangeIndex,
232
- method: finding.method,
233
- path: finding.path,
234
- status: finding.status,
235
- operationId: finding.operationId,
236
- target: finding.target,
237
- schemaPath: finding.schemaPath,
238
- dataPath: finding.dataPath
239
- }
240
- };
241
- });
242
- const sarifPayload = {
243
- $schema: "https://json.schemastore.org/sarif-2.1.0.json",
244
- version: "2.1.0",
245
- runs: [{ tool: { driver: { name: "drift", rules: Array.from(ruleSet.values()) } }, results }]
246
- };
247
- return `${JSON.stringify(sarifPayload, null, 2)}
248
- `;
249
- }
250
- function totalFindings(summary) {
251
- return summary.findingsBySeverity.error + summary.findingsBySeverity.warning + summary.findingsBySeverity.info;
252
- }
253
- function statusColor(status) {
254
- if (status >= 500) return red;
255
- if (status >= 400) return yellow;
256
- if (status >= 300) return blue;
257
- if (status >= 200) return green;
258
- return gray;
259
- }
260
- function formatPretty(result, color, maxFindings) {
261
- const colorize = (text, paint) => color ? paint(text) : text;
262
- const severityIcon = (severity) => severity === "error" ? colorize("\u2716", red) : severity === "warning" ? colorize("\u25B2", yellow) : colorize("\u25CF", blue);
263
- const severityLabel = (severity) => severity === "error" ? colorize("ERROR", red) : severity === "warning" ? colorize("WARN", yellow) : colorize("INFO", blue);
264
- const { summary, meta } = result;
265
- const findingsCount = totalFindings(summary);
266
- const findingsToRender = summary.previewFindings.slice(0, maxFindings);
267
- const lines = [];
268
- lines.push(colorize("\u250F\u2501 Drift Report", cyanBold));
269
- lines.push(`\u2503 Run: ${result.runId}`);
270
- lines.push(`\u2503 Spec: ${meta.specSource}`);
271
- lines.push(`\u2503 Traffic: ${meta.trafficPath}`);
272
- lines.push(
273
- meta.server ? `\u2503 Server: ${meta.server} (overrides description servers)` : `\u2503 Match mode: ${meta.matchMode}`
274
- );
275
- lines.push(`\u2503 Traffic format: ${meta.format}${meta.format === "auto" ? " (auto-detect)" : ""}`);
276
- lines.push(
277
- `\u2503 Exchanges: total=${summary.totalExchanges} documented=${summary.documentedExchanges} undocumented=${summary.undocumentedExchanges}${summary.skippedExchanges > 0 ? ` skipped=${summary.skippedExchanges}` : ""}`
278
- );
279
- lines.push(
280
- `\u2503 Findings: total=${findingsCount} ${colorize(`error=${summary.findingsBySeverity.error}`, red)} ${colorize(
281
- `warning=${summary.findingsBySeverity.warning}`,
282
- yellow
283
- )} ${colorize(`info=${summary.findingsBySeverity.info}`, blue)}`
284
- );
285
- lines.push(`\u2503 Problems: total=${summary.totalProblemGroups}`);
286
- lines.push(`\u2517 Duration: ${summary.durationMs}ms`);
287
- if (findingsCount === 0) {
288
- lines.push("");
289
- lines.push(colorize("\u2714 No findings.", cyan));
290
- return `${lines.join("\n")}
291
- `;
292
- }
293
- lines.push("");
294
- lines.push(colorize(`Types (${Object.keys(summary.problemGroupsByRule).length})`, cyanBold));
295
- for (const [ruleId, problemsCount] of sortRuleCounts(summary.problemGroupsByRule)) {
296
- const findingsForRule = summary.findingsByRule[ruleId] ?? problemsCount;
297
- lines.push(
298
- ` ${colorize("\u2022", gray)} ${ruleId}: ${problemsCount} problems / ${findingsForRule} findings`
299
- );
300
- }
301
- const groupedFindings = /* @__PURE__ */ new Map();
302
- for (const finding of findingsToRender) {
303
- if (!groupedFindings.has(finding.ruleId)) {
304
- groupedFindings.set(finding.ruleId, []);
305
- }
306
- groupedFindings.get(finding.ruleId).push(finding);
307
- }
308
- const orderedRuleIds = Array.from(groupedFindings.keys()).sort((a, b) => {
309
- const countDiff = (summary.problemGroupsByRule[b] ?? 0) - (summary.problemGroupsByRule[a] ?? 0);
310
- if (countDiff !== 0) {
311
- return countDiff;
312
- }
313
- return a.localeCompare(b);
314
- });
315
- lines.push("");
316
- lines.push(
317
- colorize(
318
- `Problems by type (showing first ${findingsToRender.length} of ${summary.totalProblemGroups})`,
319
- cyanBold
320
- )
321
- );
322
- let renderedIndex = 0;
323
- for (const ruleId of orderedRuleIds) {
324
- const group = groupedFindings.get(ruleId);
325
- if (!group || group.length === 0) {
326
- continue;
327
- }
328
- const ruleProblemTotal = summary.problemGroupsByRule[ruleId] ?? group.length;
329
- const ruleFindingTotal = summary.findingsByRule[ruleId] ?? ruleProblemTotal;
330
- lines.push("");
331
- lines.push(
332
- `${colorize("\u25C9", cyan)} ${colorize(ruleId, cyanBold)} ${colorize(
333
- `(${group.length} problems shown / ${ruleProblemTotal} total, ${ruleFindingTotal} findings)`,
334
- gray
335
- )}`
336
- );
337
- for (const finding of group) {
338
- renderedIndex += 1;
339
- const occurrences = finding.occurrences;
340
- const statusText = finding.status !== void 0 ? colorize(String(finding.status), statusColor(finding.status)) : colorize("-", gray);
341
- const displayPath = getOperationTemplatePath(finding.details) ?? finding.path;
342
- const operationLabel = finding.operationId ? ` ${colorize(finding.operationId, dim)}` : "";
343
- lines.push(
344
- `${severityIcon(finding.severity)} ${severityLabel(finding.severity)} #${renderedIndex}${occurrences > 1 ? ` ${colorize(`\xD7${occurrences}`, gray)}` : ""} ${colorize("\u2192", gray)} ${finding.message}`
345
- );
346
- lines.push(
347
- ` \u21B3 sample exchange=${finding.exchangeIndex} ${colorize(finding.method, cyan)} ${displayPath} (${statusText})${operationLabel}`
348
- );
349
- lines.push("");
350
- lines.push(` ${colorize(finding.url, gray)}`);
351
- if (finding.specSource || finding.schemaPath || finding.dataPath) {
352
- lines.push("");
353
- if (finding.specSource) {
354
- lines.push(
355
- ` ${colorize("spec:", dim)} ${colorize(toRelativeSpecPath(finding.specSource), cyan)}`
356
- );
357
- }
358
- if (finding.schemaPath) {
359
- lines.push(` ${colorize("schemaPath=", dim)} ${colorize(finding.schemaPath, dim)}`);
360
- }
361
- if (finding.dataPath) {
362
- lines.push(` ${colorize("dataPath=", dim)} ${colorize(finding.dataPath, cyan)}`);
363
- }
364
- }
365
- if (finding.details) {
366
- lines.push("");
367
- if (finding.ruleId === "security-baseline" && typeof finding.details.summary === "string") {
368
- lines.push(` ${colorize("security:", dim)} ${finding.details.summary}`);
369
- } else if (finding.ruleId === "owasp-api-top10" && typeof finding.details.summary === "string") {
370
- const issueId = typeof finding.details.issueId === "string" ? finding.details.issueId : null;
371
- const issueTitle = typeof finding.details.issueTitle === "string" ? finding.details.issueTitle : null;
372
- lines.push(` ${colorize("owasp:", dim)} ${finding.details.summary}`);
373
- if (issueId || issueTitle) {
374
- lines.push(
375
- ` ${colorize("issue:", dim)} ${[issueId, issueTitle].filter(Boolean).join(" - ")}`
376
- );
377
- }
378
- } else if (finding.ruleId === "schema-consistency" && typeof finding.details.summary === "string") {
379
- const detailPath = typeof finding.details.path === "string" ? finding.details.path : null;
380
- const expected = typeof finding.details.expected === "string" ? finding.details.expected : null;
381
- const actual = typeof finding.details.actual === "string" ? finding.details.actual : null;
382
- const suggestion = typeof finding.details.suggestion === "string" ? finding.details.suggestion : null;
383
- lines.push(` ${colorize("schema:", dim)} ${finding.details.summary}`);
384
- if (detailPath) {
385
- lines.push(` ${colorize("path:", dim)} ${colorize(detailPath, cyan)}`);
386
- }
387
- if (expected) {
388
- lines.push(` ${colorize("expected:", dim)} ${expected}`);
389
- }
390
- if (actual) {
391
- lines.push(` ${colorize("actual:", dim)} ${actual}`);
392
- }
393
- if (suggestion) {
394
- lines.push(` ${colorize("hint:", dim)}`);
395
- for (const suggestionLine of suggestion.split("\n")) {
396
- lines.push(` ${suggestionLine}`);
397
- }
398
- }
399
- } else {
400
- lines.push(` ${colorize("details:", dim)} ${JSON.stringify(finding.details)}`);
401
- }
402
- }
403
- lines.push("");
404
- }
405
- }
406
- const omittedProblemsCount = summary.totalProblemGroups - findingsToRender.length;
407
- if (omittedProblemsCount > 0) {
408
- if (lines[lines.length - 1] !== "") {
409
- lines.push("");
410
- }
411
- lines.push(
412
- colorize(
413
- `\u2026 ${omittedProblemsCount} problems omitted from terminal output. Use --format json/csv/sarif for the complete export.`,
414
- gray
415
- )
416
- );
417
- } else if (lines[lines.length - 1] === "") {
418
- lines.pop();
419
- }
420
- return `${lines.join("\n")}
421
- `;
422
- }
423
- function renderReport(result, options) {
424
- const maxFindings = options.maxFindings ?? 10;
425
- switch (options.format) {
426
- case "json":
427
- return formatJson(result);
428
- case "csv":
429
- return formatCsv(result);
430
- case "sarif":
431
- return formatSarif(result);
432
- case "pretty":
433
- return formatPretty(result, options.color ?? false, maxFindings);
434
- default:
435
- throw new Error(`Unsupported report format: ${options.format}`);
436
- }
437
- }
438
-
439
- // src/commands/drift/engine/validation-session.ts
440
- import { randomUUID } from "node:crypto";
441
-
442
- // src/commands/drift/openapi/matcher.ts
443
- function toRelativePath(requestPath, server) {
444
- const normalizedRequestPath = getPathWithoutTrailingSlash(requestPath || "/") || "/";
445
- const normalizedBasePath = getPathWithoutTrailingSlash(server.basePath || "/") || "/";
446
- if (normalizedBasePath === "/") {
447
- return normalizedRequestPath;
448
- }
449
- if (normalizedRequestPath === normalizedBasePath) {
450
- return "/";
451
- }
452
- if (!normalizedRequestPath.startsWith(`${normalizedBasePath}/`)) {
453
- return null;
454
- }
455
- return normalizedRequestPath.slice(normalizedBasePath.length) || "/";
456
- }
457
- function hostMatches(operationServer, requestHost) {
458
- if (!operationServer.host || !requestHost) {
459
- return true;
460
- }
461
- return operationServer.host === requestHost.toLowerCase();
462
- }
463
- function decodePathParam(value) {
464
- try {
465
- return decodeURIComponent(value);
466
- } catch {
467
- return value;
468
- }
469
- }
470
- function scoreCandidate(operation, server, mode) {
471
- const hostScore = mode === "strict-host" ? server.host ? 20 : 5 : 0;
472
- return operation.pathScore * 10 + hostScore;
473
- }
474
- function extractPathParams(operation, pathMatch) {
475
- const params = {};
476
- for (let index = 0; index < operation.pathParams.length; index += 1) {
477
- const paramName = operation.pathParams[index];
478
- const paramValue = pathMatch[index + 1];
479
- if (paramName && paramValue !== void 0) {
480
- params[paramName] = decodePathParam(paramValue);
481
- }
482
- }
483
- return params;
484
- }
485
- function matchOperationByRelativePath(operationCandidates, relativePath) {
486
- const normalizedPath = getPathWithoutTrailingSlash(relativePath) || "/";
487
- for (const operation of operationCandidates) {
488
- const pathMatch = operation.pathRegex.exec(normalizedPath);
489
- if (pathMatch) {
490
- return { operation, pathParams: extractPathParams(operation, pathMatch) };
491
- }
492
- }
493
- return null;
494
- }
495
- function matchOperation(index, exchange, mode, relativePathOverride) {
496
- const method = exchange.request.method.toLowerCase();
497
- const operationCandidates = index.operationsByMethod.get(method);
498
- if (!operationCandidates || operationCandidates.length === 0) {
499
- return null;
500
- }
501
- if (relativePathOverride !== void 0) {
502
- return matchOperationByRelativePath(operationCandidates, relativePathOverride);
503
- }
504
- let bestCandidate = null;
505
- for (const operation of operationCandidates) {
506
- for (const server of operation.servers) {
507
- if (mode === "strict-host" && !hostMatches(server, exchange.request.host)) {
508
- continue;
509
- }
510
- const relativePath = toRelativePath(exchange.request.path, server);
511
- if (!relativePath) {
512
- continue;
513
- }
514
- const pathMatch = operation.pathRegex.exec(relativePath);
515
- if (!pathMatch) {
516
- continue;
517
- }
518
- const candidate = {
519
- score: scoreCandidate(operation, server, mode),
520
- matched: {
521
- operation,
522
- pathParams: extractPathParams(operation, pathMatch)
523
- }
524
- };
525
- if (!bestCandidate || candidate.score > bestCandidate.score) {
526
- bestCandidate = candidate;
527
- }
528
- }
529
- }
530
- return bestCandidate?.matched ?? null;
531
- }
532
-
533
- // src/commands/drift/rules/builtins/owasp.ts
534
- var MAX_EXPOSED_PATHS = 6;
535
- var MAX_SCAN_NODES = 2e3;
536
- var MAX_SCAN_ARRAY_ITEMS = 50;
537
- var MAX_SENSITIVE_QUERY_PARAMS = 5;
538
- var LARGE_RESPONSE_THRESHOLD_BYTES = 1e6;
539
- var SENSITIVE_QUERY_KEY_PATTERN = /(?:token|api[_-]?key|apikey|password|passwd|secret|authorization|jwt)/i;
540
- var SENSITIVE_RESPONSE_KEY_PATTERN = /(?:password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|private[_-]?key|ssn|credit[_-]?card|card[_-]?number|cvv)/i;
541
- var PAGINATION_QUERY_KEYS = /* @__PURE__ */ new Set([
542
- "page",
543
- "limit",
544
- "offset",
545
- "cursor",
546
- "per_page",
547
- "perpage",
548
- "size",
549
- "start",
550
- "after",
551
- "before"
552
- ]);
553
- var MASK_REVEALED_PREFIX_LENGTH = 4;
554
- var MASK_FULL_THRESHOLD = MASK_REVEALED_PREFIX_LENGTH * 2;
555
- function maskValue(value) {
556
- if (value.length <= MASK_FULL_THRESHOLD) {
557
- return "***";
558
- }
559
- return `${value.slice(0, MASK_REVEALED_PREFIX_LENGTH)}\u2026`;
560
- }
561
- function normalizeJsonPointerSegment(segment) {
562
- return segment.replace(/~/g, "~0").replace(/\//g, "~1");
563
- }
564
- function looksLikePrimitive(value) {
565
- return typeof value === "string" || typeof value === "number";
566
- }
567
- function isFlagLikeSensitiveField(key) {
568
- const lower = key.toLowerCase();
569
- if (!(lower.includes("secret") || lower.includes("token") || lower.includes("key"))) {
570
- return false;
571
- }
572
- return lower.startsWith("is") || lower.startsWith("has") || lower.startsWith("can") || lower.startsWith("should") || lower.startsWith("enable") || lower.startsWith("allow");
573
- }
574
- function isBooleanLikeValue(value) {
575
- if (typeof value === "number") {
576
- return value === 0 || value === 1;
577
- }
578
- const normalized = value.trim().toLowerCase();
579
- return normalized === "true" || normalized === "false" || normalized === "0" || normalized === "1";
580
- }
581
- function findSensitiveResponsePaths(payload) {
582
- if (!isPlainObject(payload) && !Array.isArray(payload)) {
583
- return [];
584
- }
585
- const exposedPaths = [];
586
- const queue = [{ value: payload, path: "" }];
587
- let visitedNodes = 0;
588
- while (queue.length > 0 && visitedNodes < MAX_SCAN_NODES && exposedPaths.length < MAX_EXPOSED_PATHS) {
589
- const current = queue.pop();
590
- if (!current) {
591
- break;
592
- }
593
- visitedNodes += 1;
594
- if (Array.isArray(current.value)) {
595
- const limit = Math.min(current.value.length, MAX_SCAN_ARRAY_ITEMS);
596
- for (let index = 0; index < limit; index += 1) {
597
- const item = current.value[index];
598
- if (isPlainObject(item) || Array.isArray(item)) {
599
- queue.push({ value: item, path: `${current.path}/${index}` });
600
- }
601
- }
602
- continue;
603
- }
604
- if (!isPlainObject(current.value)) {
605
- continue;
606
- }
607
- for (const [key, value] of Object.entries(current.value)) {
608
- const pointerPath = `${current.path}/${normalizeJsonPointerSegment(key)}`;
609
- if (SENSITIVE_RESPONSE_KEY_PATTERN.test(key) && looksLikePrimitive(value)) {
610
- const stringified = String(value);
611
- if (isFlagLikeSensitiveField(key) && isBooleanLikeValue(value)) {
612
- continue;
613
- }
614
- if (stringified.length > 0) {
615
- exposedPaths.push(pointerPath || "/");
616
- if (exposedPaths.length >= MAX_EXPOSED_PATHS) {
617
- break;
618
- }
619
- }
620
- }
621
- if (isPlainObject(value) || Array.isArray(value)) {
622
- queue.push({ value, path: pointerPath });
623
- }
624
- }
625
- }
626
- return exposedPaths;
627
- }
628
- function hasPaginationHint(query) {
629
- for (const key of query.keys()) {
630
- if (PAGINATION_QUERY_KEYS.has(key.toLowerCase())) {
631
- return true;
632
- }
633
- }
634
- return false;
635
- }
636
- function buildSecurityFinding(context, message, target, severity, details) {
637
- return {
638
- ruleId: "owasp-api-top10",
639
- severity,
640
- category: "security",
641
- message,
642
- exchangeIndex: context.exchange.index,
643
- operationId: context.matchedOperation?.operation.operationId,
644
- specSource: context.matchedOperation?.operation.specSource,
645
- target,
646
- details
647
- };
648
- }
649
- function detectSensitiveQueryParams(context) {
650
- const matches = [];
651
- for (const [key, value] of context.exchange.request.query.entries()) {
652
- if (!SENSITIVE_QUERY_KEY_PATTERN.test(key)) {
653
- continue;
654
- }
655
- matches.push({
656
- key,
657
- valuePreview: maskValue(value)
658
- });
659
- if (matches.length >= MAX_SENSITIVE_QUERY_PARAMS) {
660
- break;
661
- }
662
- }
663
- if (matches.length === 0) {
664
- return [];
665
- }
666
- return [
667
- buildSecurityFinding(
668
- context,
669
- "Sensitive credential-like query parameters detected",
670
- "request",
671
- "warning",
672
- {
673
- issueId: "API2:2023",
674
- issueTitle: "Broken Authentication",
675
- summary: "Authentication data should not be passed via query parameters because it can leak via logs, browser history, and referrers.",
676
- sensitiveQueryParams: matches
677
- }
678
- )
679
- ];
680
- }
681
- function detectInsecureCors(context) {
682
- const response = context.exchange.response;
683
- if (!response) {
684
- return [];
685
- }
686
- const allowOrigin = response.headers["access-control-allow-origin"]?.trim();
687
- const allowCredentials = response.headers["access-control-allow-credentials"]?.trim().toLowerCase();
688
- if (allowOrigin !== "*" || allowCredentials !== "true") {
689
- return [];
690
- }
691
- return [
692
- buildSecurityFinding(
693
- context,
694
- "Insecure CORS policy: wildcard origin with credentials enabled",
695
- "response",
696
- "error",
697
- {
698
- issueId: "API8:2023",
699
- issueTitle: "Security Misconfiguration",
700
- summary: 'Access-Control-Allow-Origin is "*" while Access-Control-Allow-Credentials is true. Browsers may expose authenticated API responses to untrusted origins.',
701
- headers: {
702
- accessControlAllowOrigin: allowOrigin,
703
- accessControlAllowCredentials: allowCredentials
704
- }
705
- }
706
- )
707
- ];
708
- }
709
- function detectWeakCookieAttributes(context) {
710
- const response = context.exchange.response;
711
- if (!response) {
712
- return [];
713
- }
714
- const setCookie = response.headers["set-cookie"];
715
- if (!setCookie) {
716
- return [];
717
- }
718
- const findings = [];
719
- for (const cookie of splitSetCookieHeader(setCookie)) {
720
- const attributeNames = new Set(
721
- cookie.split(";").slice(1).map((attribute) => attribute.split("=")[0].trim().toLowerCase())
722
- );
723
- if (context.exchange.request.protocol === "https:" && !attributeNames.has("secure")) {
724
- findings.push(
725
- buildSecurityFinding(
726
- context,
727
- 'Set-Cookie header is missing "Secure" attribute',
728
- "response",
729
- "warning",
730
- {
731
- issueId: "API2:2023",
732
- issueTitle: "Broken Authentication",
733
- summary: 'Session cookies over HTTPS should include "Secure" to prevent transmission over plain HTTP.',
734
- setCookiePreview: cookie.slice(0, 200)
735
- }
736
- )
737
- );
738
- }
739
- if (!attributeNames.has("httponly")) {
740
- findings.push(
741
- buildSecurityFinding(
742
- context,
743
- 'Set-Cookie header is missing "HttpOnly" attribute',
744
- "response",
745
- "warning",
746
- {
747
- issueId: "API2:2023",
748
- issueTitle: "Broken Authentication",
749
- summary: 'Session cookies should include "HttpOnly" to reduce risk of token theft via client-side script access.',
750
- setCookiePreview: cookie.slice(0, 200)
751
- }
752
- )
753
- );
754
- }
755
- if (!attributeNames.has("samesite")) {
756
- findings.push(
757
- buildSecurityFinding(
758
- context,
759
- 'Set-Cookie header is missing "SameSite" attribute',
760
- "response",
761
- "info",
762
- {
763
- issueId: "API8:2023",
764
- issueTitle: "Security Misconfiguration",
765
- summary: 'Set "SameSite" on session cookies to reduce CSRF risk for state-changing requests.',
766
- setCookiePreview: cookie.slice(0, 200)
767
- }
768
- )
769
- );
770
- }
771
- }
772
- return findings;
773
- }
774
- function detectSensitiveResponseData(context) {
775
- const response = context.exchange.response;
776
- if (!response || response.bodyJson === void 0) {
777
- return [];
778
- }
779
- const exposedPaths = findSensitiveResponsePaths(response.bodyJson);
780
- if (exposedPaths.length === 0) {
781
- return [];
782
- }
783
- return [
784
- buildSecurityFinding(
785
- context,
786
- "Potential sensitive data exposure in response payload",
787
- "response",
788
- "warning",
789
- {
790
- issueId: "API3:2019",
791
- issueTitle: "Excessive Data Exposure",
792
- summary: `Sensitive-looking fields detected in response payload: ${exposedPaths.join(", ")}`,
793
- exposedPaths
794
- }
795
- )
796
- ];
797
- }
798
- function detectLargeUnpaginatedResponses(context) {
799
- const response = context.exchange.response;
800
- if (!response || response.status >= 400 || context.exchange.request.method !== "GET") {
801
- return [];
802
- }
803
- if (!response.bodyText || response.bodyText.length < LARGE_RESPONSE_THRESHOLD_BYTES) {
804
- return [];
805
- }
806
- if (hasPaginationHint(context.exchange.request.query)) {
807
- return [];
808
- }
809
- return [
810
- buildSecurityFinding(
811
- context,
812
- "Large response without pagination parameters (possible resource-consumption risk)",
813
- "response",
814
- "info",
815
- {
816
- issueId: "API4:2023",
817
- issueTitle: "Unrestricted Resource Consumption",
818
- summary: "A large response was returned without common pagination query parameters. Consider explicit page/limit controls for large collections.",
819
- responseBytes: response.bodyText.length
820
- }
821
- )
822
- ];
823
- }
824
- var OwaspApiTop10Rule = class {
825
- id = "owasp-api-top10";
826
- analyze(context) {
827
- if (!context.matchedOperation) {
828
- return [];
829
- }
830
- return [
831
- ...detectSensitiveQueryParams(context),
832
- ...detectInsecureCors(context),
833
- ...detectWeakCookieAttributes(context),
834
- ...detectSensitiveResponseData(context),
835
- ...detectLargeUnpaginatedResponses(context)
836
- ];
837
- }
838
- };
839
-
840
- // src/commands/drift/rules/builtins/schema.ts
841
- var MAX_ACTUAL_VALUE_LENGTH = 200;
842
- function hasBodyContent(bodyText) {
843
- return bodyText !== void 0 && bodyText !== "";
844
- }
845
- function parseCookies(headerValue) {
846
- if (!headerValue) {
847
- return {};
848
- }
849
- const cookies = {};
850
- for (const pair of headerValue.split(";")) {
851
- const [rawName, ...rawValueParts] = pair.trim().split("=");
852
- if (!rawName) {
853
- continue;
854
- }
855
- const value = rawValueParts.join("=").trim();
856
- cookies[rawName] = value;
857
- }
858
- return cookies;
859
- }
860
- function decodeJsonPointerSegment(segment) {
861
- return segment.replace(/~1/g, "/").replace(/~0/g, "~");
862
- }
863
- function encodeJsonPointerSegment(segment) {
864
- return segment.replace(/~/g, "~0").replace(/\//g, "~1");
865
- }
866
- function normalizeJsonPointer(pointer) {
867
- if (!pointer || pointer === "#") {
868
- return "";
869
- }
870
- if (pointer.startsWith("#")) {
871
- return pointer.slice(1);
872
- }
873
- return pointer;
874
- }
875
- function jsonPointerSegments(pointer) {
876
- const normalized = normalizeJsonPointer(pointer);
877
- if (!normalized || normalized === "/") {
878
- return [];
879
- }
880
- return normalized.split("/").slice(1).map((segment) => decodeJsonPointerSegment(segment));
881
- }
882
- function joinJsonPointer(basePath, segment) {
883
- const baseSegments = jsonPointerSegments(basePath);
884
- return `/${[...baseSegments, encodeJsonPointerSegment(segment)].join("/")}`;
885
- }
886
- function getValueAtJsonPointer(root, pointer) {
887
- const segments = jsonPointerSegments(pointer);
888
- let current = root;
889
- for (const segment of segments) {
890
- if (Array.isArray(current)) {
891
- const index = Number.parseInt(segment, 10);
892
- if (!Number.isInteger(index) || index < 0 || index >= current.length) {
893
- return void 0;
894
- }
895
- current = current[index];
896
- continue;
897
- }
898
- if (!isPlainObject(current)) {
899
- return void 0;
900
- }
901
- current = current[segment];
902
- }
903
- return current;
904
- }
905
- function normalizeDataPathForError(error) {
906
- const basePath = error.instancePath ?? "";
907
- const params = error.params ?? {};
908
- if (error.keyword === "required" && typeof params.missingProperty === "string") {
909
- return joinJsonPointer(basePath, params.missingProperty);
910
- }
911
- if (error.keyword === "additionalProperties" && typeof params.additionalProperty === "string") {
912
- return joinJsonPointer(basePath, params.additionalProperty);
913
- }
914
- return basePath;
915
- }
916
- function renderReadablePath(pointer) {
917
- const segments = jsonPointerSegments(pointer);
918
- if (segments.length === 0) {
919
- return "root";
920
- }
921
- const pathParts = [];
922
- for (const segment of segments) {
923
- if (/^\d+$/.test(segment)) {
924
- pathParts.push(`[${segment}]`);
925
- } else if (pathParts.length === 0) {
926
- pathParts.push(segment);
927
- } else {
928
- pathParts.push(`.${segment}`);
929
- }
930
- }
931
- return pathParts.join("");
932
- }
933
- function compactActualValue(value) {
934
- if (value === void 0) {
935
- return "[missing]";
936
- }
937
- const serialized = JSON.stringify(value);
938
- if (!serialized) {
939
- return String(value);
940
- }
941
- return serialized.length > MAX_ACTUAL_VALUE_LENGTH ? `${serialized.slice(0, MAX_ACTUAL_VALUE_LENGTH)}\u2026` : serialized;
942
- }
943
- function createExpectedHint(error) {
944
- const params = error.params ?? {};
945
- if (error.keyword === "required" && typeof params.missingProperty === "string") {
946
- return `required field "${params.missingProperty}"`;
947
- }
948
- if (error.keyword === "type" && typeof params.type === "string") {
949
- return `type "${params.type}"`;
950
- }
951
- if (error.keyword === "additionalProperties") {
952
- return "no additional undocumented properties";
953
- }
954
- if (error.keyword === "format" && typeof params.format === "string") {
955
- return `format "${params.format}"`;
956
- }
957
- return void 0;
958
- }
959
- function createFallbackSchemaErrorSummary(target, error, highlightedDataPath) {
960
- const targetLabel = target === "request" ? "Request" : "Response";
961
- const params = error.params ?? {};
962
- const location = renderReadablePath(highlightedDataPath);
963
- if (error.keyword === "required" && typeof params.missingProperty === "string") {
964
- return `${targetLabel} is missing required field "${location}".`;
965
- }
966
- if (error.keyword === "additionalProperties" && typeof params.additionalProperty === "string") {
967
- return `${targetLabel} has undocumented field "${location}".`;
968
- }
969
- if (error.keyword === "type" && typeof params.type === "string") {
970
- return `${targetLabel} field "${location}" must be ${params.type}.`;
971
- }
972
- if (error.keyword === "enum") {
973
- return `${targetLabel} field "${location}" has a value outside allowed enum values.`;
974
- }
975
- if (error.keyword === "format" && typeof params.format === "string") {
976
- return `${targetLabel} field "${location}" does not match format "${params.format}".`;
977
- }
978
- return `${targetLabel} schema mismatch at "${location}": ${error.message ?? "validation error"}`;
979
- }
980
- function createSchemaErrorDetails(value, error, target) {
981
- const highlightedDataPath = normalizeDataPathForError(error);
982
- const actualValue = getValueAtJsonPointer(value, highlightedDataPath);
983
- const summary = createFallbackSchemaErrorSummary(target, error, highlightedDataPath);
984
- return {
985
- summary,
986
- keyword: error.keyword ?? null,
987
- path: highlightedDataPath || "/",
988
- expected: createExpectedHint(error) ?? null,
989
- actual: compactActualValue(actualValue),
990
- suggestion: null,
991
- params: error.params ?? {},
992
- ajvMessage: error.message ?? "validation error"
993
- };
994
- }
995
- function validateParameter(parameter, actualValue, context, findings) {
996
- if (actualValue === void 0 || actualValue === null || !parameter.schema) {
997
- return;
998
- }
999
- const result = context.validateSchema(parameter.schema, actualValue, { coerce: true });
1000
- if (result.valid) {
1001
- return;
1002
- }
1003
- for (const error of result.errors) {
1004
- const details = createSchemaErrorDetails(actualValue, error, "request");
1005
- findings.push({
1006
- ruleId: "schema-consistency",
1007
- severity: "error",
1008
- category: "schema",
1009
- message: `Invalid ${parameter.in} parameter "${parameter.name}": ${typeof details.summary === "string" ? details.summary : error.message ?? "schema mismatch"}`,
1010
- exchangeIndex: context.exchange.index,
1011
- operationId: context.matchedOperation?.operation.operationId,
1012
- specSource: context.matchedOperation?.operation.specSource,
1013
- target: "request",
1014
- schemaPath: error.schemaPath,
1015
- dataPath: normalizeDataPathForError(error),
1016
- details: {
1017
- ...details,
1018
- parameter: {
1019
- name: parameter.name,
1020
- in: parameter.in
1021
- }
1022
- }
1023
- });
1024
- }
1025
- }
1026
- var DEEP_OBJECT_QUERY_KEY_REGEX = /^([^[\]]+)\[([^[\]]+)\]$/;
1027
- function parseDeepObjectQueryKey(key) {
1028
- const keyMatch = key.match(DEEP_OBJECT_QUERY_KEY_REGEX);
1029
- return keyMatch ? { parameterName: keyMatch[1], property: keyMatch[2] } : void 0;
1030
- }
1031
- function getDeepObjectParameterValue(parameterName, query) {
1032
- let objectValue;
1033
- for (const [key, value] of query) {
1034
- const deepObjectKey = parseDeepObjectQueryKey(key);
1035
- if (deepObjectKey?.parameterName === parameterName) {
1036
- objectValue ??= {};
1037
- objectValue[deepObjectKey.property] = value;
1038
- }
1039
- }
1040
- return objectValue;
1041
- }
1042
- function getActualParameterValue(parameter, context, cookies) {
1043
- switch (parameter.in) {
1044
- case "path":
1045
- return context.matchedOperation?.pathParams[parameter.name];
1046
- case "query": {
1047
- if (parameter.style === "deepObject") {
1048
- return getDeepObjectParameterValue(parameter.name, context.exchange.request.query);
1049
- }
1050
- const values = context.exchange.request.query.getAll(parameter.name);
1051
- if (values.length === 0) {
1052
- return void 0;
1053
- }
1054
- const schemaType = isPlainObject(parameter.schema) ? parameter.schema.type : void 0;
1055
- if (schemaType === "array" || values.length > 1) {
1056
- return values;
1057
- }
1058
- return values[0];
1059
- }
1060
- case "header":
1061
- return context.exchange.request.headers[parameter.name.toLowerCase()];
1062
- case "cookie":
1063
- return cookies[parameter.name];
1064
- default:
1065
- return void 0;
1066
- }
1067
- }
1068
- function createUndocumentedParameterFindings(context, matchedOperation) {
1069
- const findings = [];
1070
- const paramsByLocation = {
1071
- query: /* @__PURE__ */ new Set(),
1072
- header: /* @__PURE__ */ new Set(),
1073
- cookie: /* @__PURE__ */ new Set()
1074
- };
1075
- const deepObjectQueryParams = /* @__PURE__ */ new Set();
1076
- for (const parameter of matchedOperation.operation.requestParameters) {
1077
- if (parameter.in === "header") {
1078
- paramsByLocation.header.add(parameter.name.toLowerCase());
1079
- } else if (parameter.in === "query" || parameter.in === "cookie") {
1080
- paramsByLocation[parameter.in].add(parameter.name);
1081
- if (parameter.in === "query" && parameter.style === "deepObject") {
1082
- deepObjectQueryParams.add(parameter.name);
1083
- }
1084
- }
1085
- }
1086
- for (const name of new Set(context.exchange.request.query.keys())) {
1087
- if (paramsByLocation.query.has(name)) {
1088
- continue;
1089
- }
1090
- const deepObjectKey = parseDeepObjectQueryKey(name);
1091
- if (deepObjectKey && deepObjectQueryParams.has(deepObjectKey.parameterName)) {
1092
- continue;
1093
- }
1094
- findings.push({
1095
- ruleId: "schema-consistency",
1096
- severity: "warning",
1097
- category: "documentation",
1098
- message: `Undocumented query parameter in traffic: "${name}"`,
1099
- exchangeIndex: context.exchange.index,
1100
- operationId: matchedOperation.operation.operationId,
1101
- specSource: matchedOperation.operation.specSource,
1102
- target: "request"
1103
- });
1104
- }
1105
- for (const headerName of Object.keys(context.exchange.request.headers)) {
1106
- if (shouldIgnoreHeaderAsUndocumented(headerName, context.ignoreHeaders)) {
1107
- continue;
1108
- }
1109
- if (!paramsByLocation.header.has(headerName.toLowerCase())) {
1110
- findings.push({
1111
- ruleId: "schema-consistency",
1112
- severity: "info",
1113
- category: "documentation",
1114
- message: `Undocumented header in traffic: "${headerName}"`,
1115
- exchangeIndex: context.exchange.index,
1116
- operationId: matchedOperation.operation.operationId,
1117
- specSource: matchedOperation.operation.specSource,
1118
- target: "request"
1119
- });
1120
- }
1121
- }
1122
- return findings;
1123
- }
1124
- function isRequestRejectedByServer(context) {
1125
- const response = context.exchange.response;
1126
- return response !== void 0 && response.status >= 400 && response.status < 500;
1127
- }
1128
- function pickResponseSchema(context, matchedOperation) {
1129
- const response = context.exchange.response;
1130
- if (!response) {
1131
- return void 0;
1132
- }
1133
- const statusCode = String(response.status);
1134
- const statusClass = `${Math.floor(response.status / 100)}XX`;
1135
- const responseContentMap = matchedOperation.operation.responseBodyContent[statusCode] ?? matchedOperation.operation.responseBodyContent[statusClass] ?? matchedOperation.operation.responseBodyContent[statusClass.toLowerCase()] ?? matchedOperation.operation.responseBodyContent.default;
1136
- if (!responseContentMap) {
1137
- return void 0;
1138
- }
1139
- return pickSchemaByMime(responseContentMap, response.contentType);
1140
- }
1141
- function validateSchemaResult(schema, value, context, target) {
1142
- const result = context.validateSchema(schema, value, { target });
1143
- if (result.valid) {
1144
- return [];
1145
- }
1146
- return result.errors.map((error) => {
1147
- const details = createSchemaErrorDetails(value, error, target);
1148
- return {
1149
- ruleId: "schema-consistency",
1150
- severity: "error",
1151
- category: "schema",
1152
- message: typeof details.summary === "string" ? details.summary : `Schema mismatch (${target}): ${error.message ?? "validation error"}`,
1153
- exchangeIndex: context.exchange.index,
1154
- operationId: context.matchedOperation?.operation.operationId,
1155
- specSource: context.matchedOperation?.operation.specSource,
1156
- target,
1157
- schemaPath: error.schemaPath,
1158
- dataPath: normalizeDataPathForError(error),
1159
- details
1160
- };
1161
- });
1162
- }
1163
- var SchemaConsistencyRule = class {
1164
- id = "schema-consistency";
1165
- analyze(context) {
1166
- const matchedOperation = context.matchedOperation;
1167
- if (!matchedOperation) {
1168
- return [];
1169
- }
1170
- const findings = [];
1171
- const cookies = parseCookies(context.exchange.request.headers.cookie);
1172
- findings.push(...createUndocumentedParameterFindings(context, matchedOperation));
1173
- if (!isRequestRejectedByServer(context)) {
1174
- for (const parameter of matchedOperation.operation.requestParameters) {
1175
- if (context.ignoreCookies && parameter.in === "cookie") {
1176
- continue;
1177
- }
1178
- const actualValue = getActualParameterValue(parameter, context, cookies);
1179
- if (parameter.required && (actualValue === void 0 || actualValue === null)) {
1180
- findings.push({
1181
- ruleId: this.id,
1182
- severity: "error",
1183
- category: "documentation",
1184
- message: `Missing required ${parameter.in} parameter: "${parameter.name}"`,
1185
- exchangeIndex: context.exchange.index,
1186
- operationId: matchedOperation.operation.operationId,
1187
- specSource: matchedOperation.operation.specSource,
1188
- target: "request"
1189
- });
1190
- continue;
1191
- }
1192
- validateParameter(parameter, actualValue, context, findings);
1193
- }
1194
- const requestContentType = context.exchange.request.contentType;
1195
- const requestSchema = pickSchemaByMime(
1196
- matchedOperation.operation.requestBodyContent,
1197
- requestContentType
1198
- );
1199
- const hasRequestBody = hasBodyContent(context.exchange.request.bodyText);
1200
- if (matchedOperation.operation.requestBodyRequired && !hasRequestBody) {
1201
- findings.push({
1202
- ruleId: this.id,
1203
- severity: "error",
1204
- category: "documentation",
1205
- message: "Missing required request body",
1206
- exchangeIndex: context.exchange.index,
1207
- operationId: matchedOperation.operation.operationId,
1208
- specSource: matchedOperation.operation.specSource,
1209
- target: "request"
1210
- });
1211
- }
1212
- if (requestSchema && hasRequestBody && isJsonMime(requestContentType)) {
1213
- if (context.exchange.request.bodyJson === void 0) {
1214
- findings.push({
1215
- ruleId: this.id,
1216
- severity: "error",
1217
- category: "schema",
1218
- message: "Request body is not valid JSON for JSON content-type",
1219
- exchangeIndex: context.exchange.index,
1220
- operationId: matchedOperation.operation.operationId,
1221
- specSource: matchedOperation.operation.specSource,
1222
- target: "request"
1223
- });
1224
- } else {
1225
- findings.push(
1226
- ...validateSchemaResult(
1227
- requestSchema,
1228
- context.exchange.request.bodyJson,
1229
- context,
1230
- "request"
1231
- )
1232
- );
1233
- }
1234
- }
1235
- }
1236
- const responseSchema = pickResponseSchema(context, matchedOperation);
1237
- if (responseSchema && context.exchange.response && hasBodyContent(context.exchange.response.bodyText)) {
1238
- const responseContentType = context.exchange.response.contentType;
1239
- if (isJsonMime(responseContentType)) {
1240
- if (context.exchange.response.bodyJson === void 0) {
1241
- findings.push({
1242
- ruleId: this.id,
1243
- severity: "error",
1244
- category: "schema",
1245
- message: "Response body is not valid JSON for JSON content-type",
1246
- exchangeIndex: context.exchange.index,
1247
- operationId: matchedOperation.operation.operationId,
1248
- specSource: matchedOperation.operation.specSource,
1249
- target: "response"
1250
- });
1251
- } else {
1252
- findings.push(
1253
- ...validateSchemaResult(
1254
- responseSchema,
1255
- context.exchange.response.bodyJson,
1256
- context,
1257
- "response"
1258
- )
1259
- );
1260
- }
1261
- }
1262
- }
1263
- return findings;
1264
- }
1265
- };
1266
-
1267
- // src/commands/drift/rules/builtins/security.ts
1268
- function extractAuthorizationScheme(authorizationHeader) {
1269
- if (!authorizationHeader) {
1270
- return null;
1271
- }
1272
- const [scheme] = authorizationHeader.trim().split(/\s+/, 1);
1273
- return scheme ? scheme.toLowerCase() : null;
1274
- }
1275
- function hasAuthorizationHeader(context) {
1276
- return Boolean(context.exchange.request.headers.authorization);
1277
- }
1278
- function hasCookieValue(cookieHeader, cookieName) {
1279
- if (!cookieHeader) {
1280
- return false;
1281
- }
1282
- return cookieHeader.split(";").map((part) => part.trim()).some((part) => part.startsWith(`${cookieName}=`));
1283
- }
1284
- function evaluateScheme(context, schemeName) {
1285
- const operation = context.matchedOperation?.operation;
1286
- if (!operation) {
1287
- return {
1288
- schemeName,
1289
- schemeType: "unknown",
1290
- satisfied: true,
1291
- reason: "No matched OpenAPI operation."
1292
- };
1293
- }
1294
- const scheme = operation.securitySchemes[schemeName];
1295
- if (!scheme) {
1296
- return {
1297
- schemeName,
1298
- schemeType: "missing",
1299
- satisfied: false,
1300
- reason: "Security scheme is referenced but not defined in components.securitySchemes."
1301
- };
1302
- }
1303
- const schemeType = String(scheme.type ?? "unknown");
1304
- if (scheme.type === "apiKey") {
1305
- const parameterName = String(scheme.name ?? "");
1306
- const parameterLocation = String(scheme.in ?? "header");
1307
- if (!parameterName) {
1308
- return {
1309
- schemeName,
1310
- schemeType,
1311
- satisfied: false,
1312
- reason: 'apiKey scheme is missing the "name" field in the OpenAPI spec.'
1313
- };
1314
- }
1315
- if (parameterLocation === "header") {
1316
- const present = Boolean(context.exchange.request.headers[parameterName.toLowerCase()]);
1317
- return {
1318
- schemeName,
1319
- schemeType,
1320
- satisfied: present,
1321
- reason: present ? `Found required apiKey header "${parameterName}".` : `Missing required apiKey header "${parameterName}".`
1322
- };
1323
- }
1324
- if (parameterLocation === "query") {
1325
- const present = context.exchange.request.query.has(parameterName);
1326
- return {
1327
- schemeName,
1328
- schemeType,
1329
- satisfied: present,
1330
- reason: present ? `Found required apiKey query parameter "${parameterName}".` : `Missing required apiKey query parameter "${parameterName}".`
1331
- };
1332
- }
1333
- if (parameterLocation === "cookie") {
1334
- if (context.ignoreCookies) {
1335
- return {
1336
- schemeName,
1337
- schemeType,
1338
- satisfied: true,
1339
- skipped: true,
1340
- reason: `Skipped apiKey cookie "${parameterName}" validation because --ignore-cookies is enabled.`
1341
- };
1342
- }
1343
- const present = hasCookieValue(context.exchange.request.headers.cookie, parameterName);
1344
- return {
1345
- schemeName,
1346
- schemeType,
1347
- satisfied: present,
1348
- reason: present ? `Found required apiKey cookie "${parameterName}".` : `Missing required apiKey cookie "${parameterName}".`
1349
- };
1350
- }
1351
- return {
1352
- schemeName,
1353
- schemeType,
1354
- satisfied: false,
1355
- reason: `Unsupported apiKey location "${parameterLocation}" in OpenAPI security scheme.`
1356
- };
1357
- }
1358
- if (scheme.type === "http") {
1359
- const auth = context.exchange.request.headers.authorization;
1360
- if (!auth) {
1361
- return {
1362
- schemeName,
1363
- schemeType,
1364
- satisfied: false,
1365
- reason: "Missing Authorization header."
1366
- };
1367
- }
1368
- const httpScheme = String(scheme.scheme ?? "").toLowerCase();
1369
- const actualAuthScheme = extractAuthorizationScheme(auth);
1370
- if (!httpScheme) {
1371
- return {
1372
- schemeName,
1373
- schemeType,
1374
- satisfied: true,
1375
- reason: "Authorization header is present and no specific HTTP auth scheme is documented."
1376
- };
1377
- }
1378
- if (!actualAuthScheme) {
1379
- return {
1380
- schemeName,
1381
- schemeType,
1382
- satisfied: false,
1383
- reason: "Authorization header is malformed and does not include an auth scheme."
1384
- };
1385
- }
1386
- const satisfied = actualAuthScheme === httpScheme;
1387
- return {
1388
- schemeName,
1389
- schemeType,
1390
- satisfied,
1391
- reason: satisfied ? `Authorization uses expected "${httpScheme}" scheme.` : `Authorization uses "${actualAuthScheme}" but "${httpScheme}" is required.`
1392
- };
1393
- }
1394
- if (scheme.type === "oauth2" || scheme.type === "openIdConnect") {
1395
- const auth = context.exchange.request.headers.authorization;
1396
- if (!auth) {
1397
- return {
1398
- schemeName,
1399
- schemeType,
1400
- satisfied: false,
1401
- reason: "Missing Authorization header for OAuth2/OpenID Connect scheme."
1402
- };
1403
- }
1404
- const actualAuthScheme = extractAuthorizationScheme(auth);
1405
- const satisfied = actualAuthScheme === "bearer";
1406
- return {
1407
- schemeName,
1408
- schemeType,
1409
- satisfied,
1410
- reason: satisfied ? "Authorization uses bearer token as expected for OAuth2/OpenID Connect." : `Authorization scheme "${actualAuthScheme ?? "unknown"}" does not satisfy OAuth2/OpenID Connect (expected bearer).`
1411
- };
1412
- }
1413
- if (scheme.type === "mutualTLS") {
1414
- return {
1415
- schemeName,
1416
- schemeType,
1417
- satisfied: true,
1418
- reason: "mutualTLS cannot be validated from HTTP traffic logs alone."
1419
- };
1420
- }
1421
- return {
1422
- schemeName,
1423
- schemeType,
1424
- satisfied: false,
1425
- reason: `Unsupported security scheme type "${schemeType}".`
1426
- };
1427
- }
1428
- function evaluateSecurityRequirements(context) {
1429
- const security = context.matchedOperation?.operation.security;
1430
- if (!security) {
1431
- return {
1432
- satisfied: true,
1433
- requirements: []
1434
- };
1435
- }
1436
- if (security.length === 0) {
1437
- return {
1438
- satisfied: true,
1439
- requirements: []
1440
- };
1441
- }
1442
- const requirements = security.map((requirement, requirementIndex) => {
1443
- const schemeNames = Object.keys(requirement);
1444
- if (schemeNames.length === 0) {
1445
- return {
1446
- requirementIndex,
1447
- satisfied: true,
1448
- schemeResults: []
1449
- };
1450
- }
1451
- const schemeResults = schemeNames.map((schemeName) => evaluateScheme(context, schemeName));
1452
- return {
1453
- requirementIndex,
1454
- satisfied: schemeResults.every((result) => result.satisfied),
1455
- schemeResults
1456
- };
1457
- });
1458
- return {
1459
- satisfied: requirements.some((requirement) => requirement.satisfied),
1460
- requirements
1461
- };
1462
- }
1463
- function collectSecurityIssues(evaluation) {
1464
- const issues = [];
1465
- for (const requirement of evaluation.requirements) {
1466
- for (const schemeResult of requirement.schemeResults) {
1467
- if (!schemeResult.satisfied) {
1468
- issues.push({
1469
- requirement: requirement.requirementIndex + 1,
1470
- schemeName: schemeResult.schemeName,
1471
- reason: schemeResult.reason
1472
- });
1473
- }
1474
- }
1475
- }
1476
- return issues;
1477
- }
1478
- function collectSkippedSecurityChecks(evaluation) {
1479
- const skippedChecks = [];
1480
- for (const requirement of evaluation.requirements) {
1481
- for (const schemeResult of requirement.schemeResults) {
1482
- if (schemeResult.skipped) {
1483
- skippedChecks.push({
1484
- requirement: requirement.requirementIndex + 1,
1485
- schemeName: schemeResult.schemeName,
1486
- reason: schemeResult.reason
1487
- });
1488
- }
1489
- }
1490
- }
1491
- return skippedChecks;
1492
- }
1493
- function createSecuritySummary(issues) {
1494
- if (issues.length === 0) {
1495
- return "All documented security requirements are satisfied.";
1496
- }
1497
- const groupedByRequirement = /* @__PURE__ */ new Map();
1498
- for (const issue of issues) {
1499
- const grouped = groupedByRequirement.get(issue.requirement);
1500
- if (grouped) {
1501
- grouped.push(issue);
1502
- } else {
1503
- groupedByRequirement.set(issue.requirement, [issue]);
1504
- }
1505
- }
1506
- const optionSummaries = Array.from(groupedByRequirement.entries()).sort((a, b) => a[0] - b[0]).map(([requirement, optionIssues]) => {
1507
- const schemeNames = optionIssues.map((issue) => issue.schemeName).join(" + ");
1508
- const reasons = optionIssues.map((issue) => issue.reason).join(" ");
1509
- return `Option ${requirement} (${schemeNames}): ${reasons}`;
1510
- });
1511
- if (optionSummaries.length === 1) {
1512
- return `Authentication check failed. ${optionSummaries[0]}`;
1513
- }
1514
- return `None of the documented authentication options matched. Any one of these options would satisfy the OpenAPI security requirements: ${optionSummaries.join(
1515
- " | "
1516
- )}`;
1517
- }
1518
- function getSensitiveQueryKeys(context) {
1519
- return Array.from(context.exchange.request.query.keys()).filter((key) => {
1520
- const normalizedKey = key.toLowerCase();
1521
- return normalizedKey.includes("token") || normalizedKey.includes("apikey") || normalizedKey.includes("api_key") || normalizedKey.includes("access_key");
1522
- });
1523
- }
1524
- var LOOPBACK_HOSTNAME_PATTERN = /^(?:(?:.+\.)?localhost|127(?:\.\d{1,3}){3}|::1)$/i;
1525
- function isLoopbackRequest(request) {
1526
- let hostname;
1527
- try {
1528
- hostname = new URL(request.url).hostname;
1529
- } catch {
1530
- hostname = request.host?.replace(/:\d+$/, "");
1531
- }
1532
- if (!hostname) {
1533
- return false;
1534
- }
1535
- return LOOPBACK_HOSTNAME_PATTERN.test(hostname.replace(/^\[|\]$/g, ""));
1536
- }
1537
- function shouldFlagInsecureTransport(context) {
1538
- if (context.exchange.request.protocol !== "http:" || !context.exchange.request.protocolKnown || isLoopbackRequest(context.exchange.request)) {
1539
- return {
1540
- flag: false,
1541
- hasAuthHeader: false,
1542
- sensitiveQueryKeys: []
1543
- };
1544
- }
1545
- const hasAuthHeader = hasAuthorizationHeader(context);
1546
- const sensitiveQueryKeys = getSensitiveQueryKeys(context);
1547
- return {
1548
- flag: hasAuthHeader || sensitiveQueryKeys.length > 0,
1549
- hasAuthHeader,
1550
- sensitiveQueryKeys
1551
- };
1552
- }
1553
- var SecurityRule = class {
1554
- id = "security-baseline";
1555
- analyze(context) {
1556
- if (!context.matchedOperation) {
1557
- return [];
1558
- }
1559
- const findings = [];
1560
- const securityEvaluation = evaluateSecurityRequirements(context);
1561
- if (!securityEvaluation.satisfied) {
1562
- const issues = collectSecurityIssues(securityEvaluation);
1563
- findings.push({
1564
- ruleId: this.id,
1565
- severity: "error",
1566
- category: "security",
1567
- message: "Request does not satisfy documented OpenAPI security requirements",
1568
- exchangeIndex: context.exchange.index,
1569
- operationId: context.matchedOperation.operation.operationId,
1570
- specSource: context.matchedOperation.operation.specSource,
1571
- target: "request",
1572
- details: {
1573
- summary: createSecuritySummary(issues),
1574
- failedChecks: issues,
1575
- skippedChecks: collectSkippedSecurityChecks(securityEvaluation),
1576
- authorizationScheme: extractAuthorizationScheme(
1577
- context.exchange.request.headers.authorization
1578
- ),
1579
- hasAuthorizationHeader: hasAuthorizationHeader(context)
1580
- }
1581
- });
1582
- } else {
1583
- const skippedChecks = collectSkippedSecurityChecks(securityEvaluation);
1584
- if (skippedChecks.length > 0) {
1585
- findings.push({
1586
- ruleId: this.id,
1587
- severity: "info",
1588
- category: "security",
1589
- message: "Some documented cookie-based security checks were skipped",
1590
- exchangeIndex: context.exchange.index,
1591
- operationId: context.matchedOperation.operation.operationId,
1592
- specSource: context.matchedOperation.operation.specSource,
1593
- target: "request",
1594
- details: {
1595
- summary: skippedChecks.map(
1596
- (check) => `Skipped option ${check.requirement} (${check.schemeName}): ${check.reason}`
1597
- ).join(" "),
1598
- skippedChecks
1599
- }
1600
- });
1601
- }
1602
- }
1603
- const insecureTransport = shouldFlagInsecureTransport(context);
1604
- if (insecureTransport.flag) {
1605
- findings.push({
1606
- ruleId: this.id,
1607
- severity: "warning",
1608
- category: "security",
1609
- message: "Potential credential exposure over insecure HTTP transport",
1610
- exchangeIndex: context.exchange.index,
1611
- operationId: context.matchedOperation.operation.operationId,
1612
- specSource: context.matchedOperation.operation.specSource,
1613
- target: "request",
1614
- details: {
1615
- protocol: context.exchange.request.protocol,
1616
- hasAuthorizationHeader: insecureTransport.hasAuthHeader,
1617
- sensitiveQueryKeys: insecureTransport.sensitiveQueryKeys
1618
- }
1619
- });
1620
- }
1621
- return findings;
1622
- }
1623
- };
1624
-
1625
- // src/commands/drift/rules/builtins/undocumented-endpoint.ts
1626
- var UndocumentedEndpointRule = class {
1627
- id = "undocumented-endpoint";
1628
- analyze(context) {
1629
- if (context.matchedOperation) {
1630
- return [];
1631
- }
1632
- const { method, path: path2, host } = context.exchange.request;
1633
- const isHostMismatch = context.matchMode === "strict-host" && !context.hostCompatibleWithSpecServers;
1634
- const message = isHostMismatch ? `Undocumented server: ${method} ${path2} was sent to "${host}", which does not match any server in the description` : `Undocumented endpoint: ${method} ${path2}`;
1635
- return [
1636
- {
1637
- ruleId: this.id,
1638
- severity: "error",
1639
- category: "documentation",
1640
- message,
1641
- exchangeIndex: context.exchange.index,
1642
- target: "request"
1643
- }
1644
- ];
1645
- }
1646
- };
1647
-
1648
- // src/commands/drift/rules/registry.ts
1649
- var BUILTIN_RULE_FACTORIES = {
1650
- "undocumented-endpoint": () => new UndocumentedEndpointRule(),
1651
- "schema-consistency": () => new SchemaConsistencyRule(),
1652
- "security-baseline": () => new SecurityRule(),
1653
- "owasp-api-top10": () => new OwaspApiTop10Rule()
1654
- };
1655
- var BUILTIN_RULE_IDS = Object.keys(BUILTIN_RULE_FACTORIES);
1656
- var DEFAULT_BUILTIN_RULE_IDS = [
1657
- "undocumented-endpoint",
1658
- "schema-consistency",
1659
- "security-baseline"
1660
- ];
1661
- function loadRules(activeRuleIds) {
1662
- if (!activeRuleIds || activeRuleIds.length === 0) {
1663
- return DEFAULT_BUILTIN_RULE_IDS.map((ruleId) => BUILTIN_RULE_FACTORIES[ruleId]());
1664
- }
1665
- return [...new Set(activeRuleIds)].map((ruleId) => {
1666
- const factory = BUILTIN_RULE_FACTORIES[ruleId];
1667
- if (!factory) {
1668
- throw new Error(
1669
- `Unknown rule id: "${ruleId}". Available rules: ${BUILTIN_RULE_IDS.join(", ")}`
1670
- );
1671
- }
1672
- return factory();
1673
- });
1674
- }
1675
-
1676
- // src/commands/drift/engine/schema-validator.ts
1677
- var import__ = __toESM(require__(), 1);
1678
- var import_ajv_formats = __toESM(require_dist(), 1);
1679
- var AjvConstructor = import__.default;
1680
- var applyFormats = import_ajv_formats.default;
1681
- function isPropertyExcludedFromTarget(propertySchema, target) {
1682
- if (!isPlainObject(propertySchema)) {
1683
- return false;
1684
- }
1685
- return target === "request" ? propertySchema.readOnly === true : propertySchema.writeOnly === true;
1686
- }
1687
- function isRequiredNameExcludedFromTarget(schema, name, target, seen = /* @__PURE__ */ new WeakSet()) {
1688
- if (!isPlainObject(schema) || seen.has(schema)) {
1689
- return false;
1690
- }
1691
- seen.add(schema);
1692
- const properties = schema.properties;
1693
- if (isPlainObject(properties) && isPropertyExcludedFromTarget(properties[name], target)) {
1694
- return true;
1695
- }
1696
- return [schema.allOf, schema.oneOf, schema.anyOf].some(
1697
- (branches) => Array.isArray(branches) && branches.some((branch) => isRequiredNameExcludedFromTarget(branch, name, target, seen))
1698
- );
1699
- }
1700
- var SINGLE_SCHEMA_KEYWORDS = /* @__PURE__ */ new Set([
1701
- "items",
1702
- "additionalItems",
1703
- "additionalProperties",
1704
- "unevaluatedItems",
1705
- "unevaluatedProperties",
1706
- "contains",
1707
- "propertyNames",
1708
- "not",
1709
- "if",
1710
- "then",
1711
- "else"
1712
- ]);
1713
- var SCHEMA_LIST_KEYWORDS = /* @__PURE__ */ new Set(["allOf", "anyOf", "oneOf", "prefixItems"]);
1714
- var SCHEMA_MAP_KEYWORDS = /* @__PURE__ */ new Set([
1715
- "properties",
1716
- "patternProperties",
1717
- "dependentSchemas",
1718
- "$defs",
1719
- "definitions"
1720
- ]);
1721
- function relaxRequiredForTarget(schema, target, ancestors = /* @__PURE__ */ new WeakSet()) {
1722
- if (!isPlainObject(schema)) {
1723
- return schema;
1724
- }
1725
- if (ancestors.has(schema)) {
1726
- return true;
1727
- }
1728
- ancestors.add(schema);
1729
- const relaxValue = (value) => relaxRequiredForTarget(value, target, ancestors);
1730
- const output = {};
1731
- for (const [key, value] of Object.entries(schema)) {
1732
- if (SINGLE_SCHEMA_KEYWORDS.has(key)) {
1733
- output[key] = Array.isArray(value) ? value.map(relaxValue) : relaxValue(value);
1734
- } else if (SCHEMA_LIST_KEYWORDS.has(key) && Array.isArray(value)) {
1735
- output[key] = value.map(relaxValue);
1736
- } else if (SCHEMA_MAP_KEYWORDS.has(key) && isPlainObject(value)) {
1737
- output[key] = Object.fromEntries(
1738
- Object.entries(value).map(([mapKey, mapValue]) => [mapKey, relaxValue(mapValue)])
1739
- );
1740
- } else {
1741
- output[key] = value;
1742
- }
1743
- }
1744
- if (Array.isArray(schema.required)) {
1745
- output.required = schema.required.filter(
1746
- (name) => typeof name !== "string" || !isRequiredNameExcludedFromTarget(schema, name, target)
1747
- );
1748
- }
1749
- ancestors.delete(schema);
1750
- return output;
1751
- }
1752
- var SchemaValidator = class {
1753
- ajv;
1754
- fallbackAjv;
1755
- objectSchemaCaches = {
1756
- none: /* @__PURE__ */ new WeakMap(),
1757
- request: /* @__PURE__ */ new WeakMap(),
1758
- response: /* @__PURE__ */ new WeakMap()
1759
- };
1760
- scalarSchemaCache = /* @__PURE__ */ new Map();
1761
- constructor(options) {
1762
- const ajvOptions = {
1763
- strict: false,
1764
- allErrors: true,
1765
- allowUnionTypes: true,
1766
- discriminator: true,
1767
- coerceTypes: options?.coerceTypes ? "array" : false,
1768
- validateFormats: true,
1769
- verbose: true,
1770
- formats: {
1771
- // Some specs misuse `format: "enum"` instead of the `enum` keyword.
1772
- // Treat it as a no-op format to avoid noisy unknown-format warnings.
1773
- enum: true
1774
- }
1775
- };
1776
- this.ajv = new AjvConstructor(ajvOptions);
1777
- this.fallbackAjv = new AjvConstructor({ ...ajvOptions, discriminator: false });
1778
- applyFormats(this.ajv);
1779
- applyFormats(this.fallbackAjv);
1780
- }
1781
- validate(schema, value, target) {
1782
- if (schema === void 0) {
1783
- return { valid: true, errors: [] };
1784
- }
1785
- let validate;
1786
- try {
1787
- validate = this.getOrCompileValidator(schema, target);
1788
- } catch (error) {
1789
- return {
1790
- valid: false,
1791
- errors: [
1792
- {
1793
- message: `Schema compilation failed: ${error.message}`
1794
- }
1795
- ]
1796
- };
1797
- }
1798
- const valid = Boolean(validate(value));
1799
- const errors = validate.errors ?? [];
1800
- return { valid, errors };
1801
- }
1802
- getOrCompileValidator(schema, target) {
1803
- if (isPlainObject(schema)) {
1804
- const cache = this.objectSchemaCaches[target ?? "none"];
1805
- const cached2 = cache.get(schema);
1806
- if (cached2) {
1807
- return cached2;
1808
- }
1809
- const effectiveSchema = target ? relaxRequiredForTarget(schema, target) : schema;
1810
- const compiled2 = this.compileSchema(effectiveSchema);
1811
- cache.set(schema, compiled2);
1812
- return compiled2;
1813
- }
1814
- const cacheKey = `${target ?? "none"}:${JSON.stringify(schema)}`;
1815
- const cached = this.scalarSchemaCache.get(cacheKey);
1816
- if (cached) {
1817
- return cached;
1818
- }
1819
- const compiled = this.compileSchema(schema);
1820
- this.scalarSchemaCache.set(cacheKey, compiled);
1821
- return compiled;
1822
- }
1823
- compileSchema(schema) {
1824
- try {
1825
- return this.ajv.compile(schema);
1826
- } catch {
1827
- return this.fallbackAjv.compile(schema);
1828
- }
1829
- }
1830
- };
1831
-
1832
- // src/commands/drift/engine/validation-session.ts
1833
- var DEFAULT_FINDINGS_PREVIEW_LIMIT = 10;
1834
- var SEVERITY_RANK = {
1835
- info: 0,
1836
- warning: 1,
1837
- error: 2
1838
- };
1839
- function createInitialCounters() {
1840
- return {
1841
- totalExchanges: 0,
1842
- documentedExchanges: 0,
1843
- undocumentedExchanges: 0,
1844
- skippedExchanges: 0,
1845
- hostCompatibleExchanges: 0,
1846
- findingsBySeverity: {
1847
- info: 0,
1848
- warning: 0,
1849
- error: 0
1850
- },
1851
- findingsByRule: {}
1852
- };
1853
- }
1854
- function accumulateFinding(counters, finding) {
1855
- counters.findingsBySeverity[finding.severity] += 1;
1856
- counters.findingsByRule[finding.ruleId] = (counters.findingsByRule[finding.ruleId] ?? 0) + 1;
1857
- }
1858
- function toFindingRecord(finding, exchange) {
1859
- return {
1860
- ...finding,
1861
- id: randomUUID(),
1862
- method: exchange.request.method,
1863
- url: exchange.request.url,
1864
- path: exchange.request.path,
1865
- status: exchange.response?.status
1866
- };
1867
- }
1868
- function mapFindingToPreview(finding, exchange) {
1869
- return {
1870
- exchangeIndex: finding.exchangeIndex,
1871
- ruleId: finding.ruleId,
1872
- severity: finding.severity,
1873
- category: finding.category,
1874
- message: finding.message,
1875
- occurrences: 1,
1876
- operationId: finding.operationId,
1877
- specSource: finding.specSource,
1878
- target: finding.target,
1879
- schemaPath: finding.schemaPath,
1880
- dataPath: finding.dataPath,
1881
- details: finding.details,
1882
- method: exchange.request.method,
1883
- url: exchange.request.url,
1884
- path: exchange.request.path,
1885
- status: exchange.response?.status
1886
- };
1887
- }
1888
- function ensureOperationContextInFinding(finding, matchedOperation) {
1889
- if (!matchedOperation) {
1890
- return;
1891
- }
1892
- if (!finding.operationId) {
1893
- finding.operationId = matchedOperation.operation.operationId;
1894
- }
1895
- if (!finding.specSource) {
1896
- finding.specSource = matchedOperation.operation.specSource;
1897
- }
1898
- const existingDetails = finding.details ?? {};
1899
- if (!isPlainObject(existingDetails)) {
1900
- finding.details = {
1901
- operationPathTemplate: matchedOperation.operation.pathTemplate
1902
- };
1903
- return;
1904
- }
1905
- if (typeof existingDetails.operationPathTemplate !== "string") {
1906
- existingDetails.operationPathTemplate = matchedOperation.operation.pathTemplate;
1907
- }
1908
- finding.details = existingDetails;
1909
- }
1910
- function collectSpecServerHosts(openApiIndex) {
1911
- const specServerHosts = /* @__PURE__ */ new Set();
1912
- let hasHostlessSpecServer = false;
1913
- for (const operations of openApiIndex.operationsByMethod.values()) {
1914
- for (const operation of operations) {
1915
- for (const server of operation.servers) {
1916
- if (server.host) {
1917
- specServerHosts.add(server.host);
1918
- } else {
1919
- hasHostlessSpecServer = true;
1920
- }
1921
- }
1922
- }
1923
- }
1924
- return { specServerHosts, hasHostlessSpecServer };
1925
- }
1926
- async function executeRules(rules, context) {
1927
- const findings = [];
1928
- for (const rule of rules) {
1929
- try {
1930
- const ruleFindings = await rule.analyze(context);
1931
- if (ruleFindings.length > 0) {
1932
- findings.push(...ruleFindings);
1933
- }
1934
- } catch (error) {
1935
- logger.warn(`Rule "${rule.id}" failed to execute: ${error.message}
1936
- `);
1937
- }
1938
- }
1939
- return findings;
1940
- }
1941
- var ValidationSession = class _ValidationSession {
1942
- startedAt = Date.now();
1943
- runId = randomUUID();
1944
- previewLimit;
1945
- rules;
1946
- schemaValidator = new SchemaValidator();
1947
- coercingSchemaValidator = new SchemaValidator({ coerceTypes: true });
1948
- options;
1949
- counters = createInitialCounters();
1950
- findings = [];
1951
- previewFindings = [];
1952
- problemGroupsByRule = {};
1953
- problemKeyStats = /* @__PURE__ */ new Map();
1954
- totalProblemGroups = 0;
1955
- server;
1956
- ignoreHeaders;
1957
- minSeverityRank;
1958
- specServerHosts;
1959
- hasHostlessSpecServer;
1960
- constructor(options, rules) {
1961
- this.options = options;
1962
- this.rules = rules;
1963
- this.server = normalizeServerPrefix(options.server);
1964
- this.ignoreHeaders = options.ignoreHeaders ? parseHeaderIgnoreList(options.ignoreHeaders) : void 0;
1965
- const { specServerHosts, hasHostlessSpecServer } = collectSpecServerHosts(options.openApiIndex);
1966
- this.specServerHosts = specServerHosts;
1967
- this.hasHostlessSpecServer = hasHostlessSpecServer;
1968
- this.minSeverityRank = SEVERITY_RANK[options.minSeverity ?? "info"];
1969
- this.previewLimit = options.previewFindingsLimit && options.previewFindingsLimit > 0 ? options.previewFindingsLimit : DEFAULT_FINDINGS_PREVIEW_LIMIT;
1970
- }
1971
- static create(options) {
1972
- if (options.openApiIndex.loadedOperations === 0) {
1973
- throw new Error("No OpenAPI operations available to validate traffic against.");
1974
- }
1975
- return new _ValidationSession(options, loadRules(options.activeRules));
1976
- }
1977
- /**
1978
- * Validate a single exchange and return the finding records produced for it,
1979
- * so callers (e.g. the proxy) can surface findings live as they arrive.
1980
- */
1981
- async process(exchange) {
1982
- this.counters.totalExchanges += 1;
1983
- let relativePathOverride;
1984
- if (this.server !== void 0) {
1985
- relativePathOverride = resolvePathForServer(exchange.request, this.server);
1986
- if (relativePathOverride === void 0) {
1987
- this.counters.skippedExchanges += 1;
1988
- return [];
1989
- }
1990
- }
1991
- const hostCompatible = this.isHostCompatible(exchange.request.host);
1992
- if (hostCompatible) {
1993
- this.counters.hostCompatibleExchanges += 1;
1994
- }
1995
- const matchedOperation = matchOperation(
1996
- this.options.openApiIndex,
1997
- exchange,
1998
- this.options.matchMode,
1999
- relativePathOverride
2000
- );
2001
- if (matchedOperation) {
2002
- this.counters.documentedExchanges += 1;
2003
- } else {
2004
- this.counters.undocumentedExchanges += 1;
2005
- }
2006
- const exchangeFindings = await executeRules(this.rules, {
2007
- exchange,
2008
- matchedOperation,
2009
- matchMode: this.options.matchMode,
2010
- hostCompatibleWithSpecServers: relativePathOverride !== void 0 || hostCompatible,
2011
- ignoreCookies: this.options.ignoreCookies ?? false,
2012
- ignoreHeaders: this.ignoreHeaders,
2013
- validateSchema: (schema, value, options) => options?.coerce ? this.coercingSchemaValidator.validate(schema, value, options?.target) : this.schemaValidator.validate(schema, value, options?.target)
2014
- });
2015
- const records = [];
2016
- for (const finding of exchangeFindings) {
2017
- if (SEVERITY_RANK[finding.severity] < this.minSeverityRank) {
2018
- continue;
2019
- }
2020
- ensureOperationContextInFinding(finding, matchedOperation);
2021
- const record = toFindingRecord(finding, exchange);
2022
- this.findings.push(record);
2023
- records.push(record);
2024
- accumulateFinding(this.counters, finding);
2025
- const problemKey = createProblemKey({
2026
- ruleId: finding.ruleId,
2027
- severity: finding.severity,
2028
- message: finding.message,
2029
- operationId: finding.operationId,
2030
- path: exchange.request.path,
2031
- target: finding.target,
2032
- schemaPath: finding.schemaPath
2033
- });
2034
- const existingProblem = this.problemKeyStats.get(problemKey);
2035
- if (existingProblem) {
2036
- existingProblem.occurrences += 1;
2037
- if (existingProblem.previewIndex !== null) {
2038
- this.previewFindings[existingProblem.previewIndex].occurrences = existingProblem.occurrences;
2039
- }
2040
- continue;
2041
- }
2042
- this.totalProblemGroups += 1;
2043
- this.problemGroupsByRule[finding.ruleId] = (this.problemGroupsByRule[finding.ruleId] ?? 0) + 1;
2044
- if (this.previewFindings.length < this.previewLimit) {
2045
- const previewIndex = this.previewFindings.length;
2046
- this.previewFindings.push(mapFindingToPreview(finding, exchange));
2047
- this.problemKeyStats.set(problemKey, { occurrences: 1, previewIndex });
2048
- continue;
2049
- }
2050
- this.problemKeyStats.set(problemKey, { occurrences: 1, previewIndex: null });
2051
- }
2052
- return records;
2053
- }
2054
- isHostCompatible(requestHost) {
2055
- return !requestHost || this.hasHostlessSpecServer || this.specServerHosts.has(requestHost.toLowerCase());
2056
- }
2057
- finalize() {
2058
- const summary = {
2059
- runId: this.runId,
2060
- totalExchanges: this.counters.totalExchanges,
2061
- documentedExchanges: this.counters.documentedExchanges,
2062
- undocumentedExchanges: this.counters.undocumentedExchanges,
2063
- skippedExchanges: this.counters.skippedExchanges,
2064
- hostCompatibleExchanges: this.counters.hostCompatibleExchanges,
2065
- findingsBySeverity: this.counters.findingsBySeverity,
2066
- findingsByRule: this.counters.findingsByRule,
2067
- problemGroupsByRule: this.problemGroupsByRule,
2068
- totalProblemGroups: this.totalProblemGroups,
2069
- durationMs: Date.now() - this.startedAt,
2070
- previewFindings: this.previewFindings,
2071
- previewLimit: this.previewLimit,
2072
- previewTruncated: this.totalProblemGroups > this.previewFindings.length
2073
- };
2074
- return { runId: this.runId, summary, findings: this.findings };
2075
- }
2076
- };
2077
-
2078
- // src/commands/drift/openapi/loader.ts
2079
- import { stat } from "node:fs/promises";
2080
-
2081
- // src/commands/drift/utils/openapi.ts
2082
- function resolveServerUrl(rawUrl, variables) {
2083
- const resolvedUrl = rawUrl.replace(/\{([^}]+)\}/g, (_, variableName) => {
2084
- const variable = variables?.[variableName];
2085
- if (variable?.default !== void 0) {
2086
- return String(variable.default);
2087
- }
2088
- if (Array.isArray(variable?.enum) && variable.enum.length > 0) {
2089
- return String(variable.enum[0]);
2090
- }
2091
- return "";
2092
- });
2093
- const parsedUrl = parseUrl(resolvedUrl);
2094
- const hasExplicitHost = Boolean(parsedUrl.host && !isSyntheticHost(parsedUrl.host));
2095
- return {
2096
- rawUrl,
2097
- protocol: hasExplicitHost ? parsedUrl.protocol : void 0,
2098
- host: hasExplicitHost ? parsedUrl.host.toLowerCase() : void 0,
2099
- basePath: getPathWithoutTrailingSlash(parsedUrl.pathname || "/")
2100
- };
2101
- }
2102
- function ensureLeadingSlash(value) {
2103
- if (!value) {
2104
- return "/";
2105
- }
2106
- return value.startsWith("/") ? value : `/${value}`;
2107
- }
2108
-
2109
- // src/commands/drift/openapi/loader.ts
2110
- var HTTP_METHODS = ["get", "put", "post", "delete", "patch", "head", "options", "trace"];
2111
- var PARAMETER_LOCATIONS = /* @__PURE__ */ new Set(["query", "header", "path", "cookie"]);
2112
- function isHttpMethod(value) {
2113
- return HTTP_METHODS.includes(value);
2114
- }
2115
- function isParameterLocation(value) {
2116
- return PARAMETER_LOCATIONS.has(value);
2117
- }
2118
- function normalizeParameters(parameters) {
2119
- if (!Array.isArray(parameters)) {
2120
- return [];
2121
- }
2122
- const normalized = [];
2123
- for (const entry of parameters) {
2124
- if (!isPlainObject(entry)) {
2125
- continue;
2126
- }
2127
- const location = entry.in;
2128
- if (typeof location !== "string" || !isParameterLocation(location)) {
2129
- continue;
2130
- }
2131
- normalized.push({
2132
- name: String(entry.name ?? ""),
2133
- in: location,
2134
- required: Boolean(entry.required) || location === "path",
2135
- style: typeof entry.style === "string" ? entry.style : void 0,
2136
- schema: entry.schema
2137
- });
2138
- }
2139
- return normalized;
2140
- }
2141
- function parameterKey(parameter) {
2142
- const caseInsensitive = parameter.in === "header";
2143
- return `${parameter.in}:${caseInsensitive ? parameter.name.toLowerCase() : parameter.name}`;
2144
- }
2145
- function mergeParameters(baseParameters, operationParameters) {
2146
- const map = /* @__PURE__ */ new Map();
2147
- for (const parameter of baseParameters) {
2148
- map.set(parameterKey(parameter), parameter);
2149
- }
2150
- for (const parameter of operationParameters) {
2151
- map.set(parameterKey(parameter), parameter);
2152
- }
2153
- return Array.from(map.values());
2154
- }
2155
- function extractMediaSchemas(content) {
2156
- const output = {};
2157
- if (!isPlainObject(content)) {
2158
- return output;
2159
- }
2160
- for (const [mime, mediaTypeObject] of Object.entries(content)) {
2161
- if (isPlainObject(mediaTypeObject) && "schema" in mediaTypeObject) {
2162
- output[mime.toLowerCase()] = mediaTypeObject.schema;
2163
- }
2164
- }
2165
- return output;
2166
- }
2167
- function extractRequestBodyContent(requestBody) {
2168
- if (!isPlainObject(requestBody)) {
2169
- return {};
2170
- }
2171
- return extractMediaSchemas(requestBody.content);
2172
- }
2173
- function extractResponseBodyContent(responses) {
2174
- if (!isPlainObject(responses)) {
2175
- return {};
2176
- }
2177
- const output = {};
2178
- for (const [statusCode, responseObject] of Object.entries(responses)) {
2179
- if (!isPlainObject(responseObject) || !("content" in responseObject)) {
2180
- continue;
2181
- }
2182
- output[statusCode] = extractMediaSchemas(responseObject.content);
2183
- }
2184
- return output;
2185
- }
2186
- function resolveOperationServers(operationServers, pathServers, rootServers) {
2187
- const sourceServers = operationServers ?? pathServers ?? rootServers;
2188
- if (!Array.isArray(sourceServers) || sourceServers.length === 0) {
2189
- return [resolveServerUrl("/")];
2190
- }
2191
- return sourceServers.map((server) => {
2192
- if (!isPlainObject(server)) {
2193
- return resolveServerUrl("/");
2194
- }
2195
- const url = typeof server.url === "string" ? server.url : "/";
2196
- const variables = isPlainObject(server.variables) ? server.variables : void 0;
2197
- return resolveServerUrl(url, variables);
2198
- });
2199
- }
2200
- function toOperationId(method, pathTemplate, declaredOperationId) {
2201
- if (declaredOperationId) {
2202
- return declaredOperationId;
2203
- }
2204
- return `${method.toUpperCase()} ${pathTemplate}`;
2205
- }
2206
- function detectOpenApi3Spec(document) {
2207
- try {
2208
- const specVersion = detectSpec(document.parsed);
2209
- return getMajorSpecVersion(specVersion) === "oas3" ? specVersion : null;
2210
- } catch {
2211
- return null;
2212
- }
2213
- }
2214
- function normalizeSecurity(value) {
2215
- return Array.isArray(value) ? value : void 0;
2216
- }
2217
- function createIndexVisitor(specSource, operationsByMethod) {
2218
- let rootServers;
2219
- let rootSecurity;
2220
- let securitySchemes = {};
2221
- let currentPathTemplate = "/";
2222
- let currentPathParameters = [];
2223
- let currentPathServers;
2224
- return {
2225
- Root: {
2226
- enter(root) {
2227
- rootServers = root.servers;
2228
- rootSecurity = root.security;
2229
- const componentsSecuritySchemes = isPlainObject(root.components) ? root.components.securitySchemes : void 0;
2230
- securitySchemes = isPlainObject(componentsSecuritySchemes) ? componentsSecuritySchemes : {};
2231
- }
2232
- },
2233
- Paths: {
2234
- PathItem: {
2235
- enter(pathItem, ctx) {
2236
- currentPathTemplate = ensureLeadingSlash(String(ctx.key));
2237
- currentPathParameters = normalizeParameters(pathItem.parameters);
2238
- currentPathServers = pathItem.servers;
2239
- },
2240
- Operation: {
2241
- enter(operation, ctx) {
2242
- const method = String(ctx.key);
2243
- if (!isHttpMethod(method)) {
2244
- return;
2245
- }
2246
- const mergedParameters = mergeParameters(
2247
- currentPathParameters,
2248
- normalizeParameters(operation.parameters)
2249
- );
2250
- const compiledPath = compileOpenApiPath(currentPathTemplate);
2251
- const servers = resolveOperationServers(
2252
- operation.servers,
2253
- currentPathServers,
2254
- rootServers
2255
- );
2256
- const requestBody = operation.requestBody;
2257
- const item = {
2258
- operationId: toOperationId(
2259
- method,
2260
- currentPathTemplate,
2261
- typeof operation.operationId === "string" ? operation.operationId : void 0
2262
- ),
2263
- method,
2264
- pathTemplate: currentPathTemplate,
2265
- pathRegex: compiledPath.regex,
2266
- pathParams: compiledPath.params,
2267
- pathScore: compiledPath.score,
2268
- servers,
2269
- requestParameters: mergedParameters,
2270
- requestBodyContent: extractRequestBodyContent(requestBody),
2271
- requestBodyRequired: isPlainObject(requestBody) && Boolean(requestBody.required),
2272
- responseBodyContent: extractResponseBodyContent(operation.responses),
2273
- security: normalizeSecurity(operation.security) ?? normalizeSecurity(rootSecurity),
2274
- securitySchemes,
2275
- specSource
2276
- };
2277
- const methodOperations = operationsByMethod.get(method) ?? [];
2278
- methodOperations.push(item);
2279
- operationsByMethod.set(method, methodOperations);
2280
- }
2281
- }
2282
- }
2283
- }
2284
- };
2285
- }
2286
- function indexDocument(document, specVersion, config, specSource, operationsByMethod) {
2287
- const types = normalizeTypes(config.extendTypes(getTypes(specVersion), specVersion), config);
2288
- const resolvedRefMap = /* @__PURE__ */ new Map();
2289
- const ctx = { problems: [], specVersion, config, visitorsData: {} };
2290
- const normalizedVisitors = normalizeVisitors(
2291
- [
2292
- {
2293
- severity: "warn",
2294
- ruleId: "drift-index",
2295
- visitor: createIndexVisitor(specSource, operationsByMethod)
2296
- }
2297
- ],
2298
- types
2299
- );
2300
- walkDocument({
2301
- document,
2302
- rootType: types.Root,
2303
- normalizedVisitors,
2304
- resolvedRefMap,
2305
- ctx
2306
- });
2307
- }
2308
- function serverIdentity(server) {
2309
- return `${server.host ?? ""}|${server.basePath}`;
2310
- }
2311
- function warnAboutCollidingOperations(operationsByMethod) {
2312
- for (const operations of operationsByMethod.values()) {
2313
- const byPathPattern = /* @__PURE__ */ new Map();
2314
- for (const operation of operations) {
2315
- const group = byPathPattern.get(operation.pathRegex.source) ?? [];
2316
- group.push(operation);
2317
- byPathPattern.set(operation.pathRegex.source, group);
2318
- }
2319
- for (const group of byPathPattern.values()) {
2320
- const sourcesByServer = /* @__PURE__ */ new Map();
2321
- for (const operation of group) {
2322
- for (const server of operation.servers) {
2323
- const key = serverIdentity(server);
2324
- const sources = sourcesByServer.get(key) ?? /* @__PURE__ */ new Set();
2325
- sources.add(operation.specSource);
2326
- sourcesByServer.set(key, sources);
2327
- }
2328
- }
2329
- for (const sources of sourcesByServer.values()) {
2330
- if (sources.size > 1) {
2331
- const [first] = group;
2332
- logger.warn(
2333
- `"${first.method.toUpperCase()} ${first.pathTemplate}" is documented for the same server in multiple descriptions (${Array.from(
2334
- sources
2335
- ).join(", ")}). Matching traffic is validated against only one of them.
2336
- `
2337
- );
2338
- break;
2339
- }
2340
- }
2341
- }
2342
- }
2343
- }
2344
- function finalizeIndex(operationsByMethod, loadedSpecs) {
2345
- for (const operations of operationsByMethod.values()) {
2346
- operations.sort((left, right) => right.pathScore - left.pathScore);
2347
- }
2348
- const loadedOperations = Array.from(operationsByMethod.values()).reduce(
2349
- (acc, current) => acc + current.length,
2350
- 0
2351
- );
2352
- return { operationsByMethod, loadedSpecs, loadedOperations };
2353
- }
2354
- async function resolveSpecFiles(specPath) {
2355
- const stats = await stat(specPath);
2356
- if (stats.isDirectory()) {
2357
- return { specFiles: await listOpenApiFiles(specPath), fromDirectory: true };
2358
- }
2359
- return { specFiles: [specPath], fromDirectory: false };
2360
- }
2361
- async function loadOpenApiIndex(specPath, config) {
2362
- const { specFiles, fromDirectory } = await resolveSpecFiles(specPath);
2363
- const operationsByMethod = /* @__PURE__ */ new Map();
2364
- const externalRefResolver = new BaseResolver(config.resolve);
2365
- let loadedSpecs = 0;
2366
- for (const specFile of specFiles) {
2367
- const resolvedDocument = await externalRefResolver.resolveDocument(null, specFile, true);
2368
- if (resolvedDocument instanceof Error) {
2369
- logger.warn(`Failed to load OpenAPI description ${specFile}: ${resolvedDocument.message}
2370
- `);
2371
- continue;
2372
- }
2373
- const specVersion = detectOpenApi3Spec(resolvedDocument);
2374
- if (!specVersion) {
2375
- if (!fromDirectory) {
2376
- logger.warn(`Skipping ${specFile}: not an OpenAPI 3.x description.
2377
- `);
2378
- }
2379
- continue;
2380
- }
2381
- let document;
2382
- try {
2383
- const { bundle: bundled } = await bundle({
2384
- config,
2385
- doc: resolvedDocument,
2386
- externalRefResolver,
2387
- dereference: true
2388
- });
2389
- document = bundled;
2390
- } catch (error) {
2391
- logger.warn(
2392
- `Failed to bundle OpenAPI description ${specFile}: ${error.message}
2393
- `
2394
- );
2395
- continue;
2396
- }
2397
- loadedSpecs += 1;
2398
- indexDocument(document, specVersion, config, specFile, operationsByMethod);
2399
- }
2400
- if (loadedSpecs > 1) {
2401
- warnAboutCollidingOperations(operationsByMethod);
2402
- }
2403
- return finalizeIndex(operationsByMethod, loadedSpecs);
2404
- }
2405
-
2406
- // src/commands/drift/utils/args.ts
2407
- function parseCsv(input) {
2408
- return input.split(",").map((value) => value.trim()).filter(Boolean);
2409
- }
2410
-
2411
- export {
2412
- renderReport,
2413
- ValidationSession,
2414
- loadOpenApiIndex,
2415
- parseCsv
2416
- };