@redocly/cli 2.39.0 → 2.41.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/THIRD_PARTY_NOTICES +7 -6
- package/lib/chunks/{C3PXBQU5.js → 5YSNZVUA.js} +4 -4
- package/lib/chunks/{OVY6O6WL.js → 6ABF7ZRJ.js} +17 -1
- package/lib/chunks/{V74PIVJZ.js → 7H6CC6CT.js} +89 -23
- package/lib/chunks/{U4V3W7MN.js → DT4UM7U4.js} +1 -1
- package/lib/chunks/ER45DAHG.js +492 -0
- package/lib/chunks/{C5HID4LT.js → GBUMU6BX.js} +8 -165
- package/lib/chunks/{BNHQNMUG.js → GOFLD5MJ.js} +9 -6
- package/lib/chunks/HQDT5XFP.js +175 -0
- package/lib/chunks/{ICAVAH7N.js → ICPYP2LP.js} +96 -82
- package/lib/chunks/LCWGF7G6.js +79 -0
- package/lib/chunks/PFPGSTSG.js +1756 -0
- package/lib/chunks/{BA5AZJUM.js → Q3KTKRJH.js} +2 -2
- package/lib/chunks/{V6CVT3XE.js → SZLWDHZK.js} +3 -3
- package/lib/chunks/{W5R2GYDZ.js → VMHXZFTB.js} +2 -2
- package/lib/chunks/{OYZMAAZV.js → XDNYWH7V.js} +2 -2
- package/lib/chunks/{KYZEVOA2.js → YK6T7IHG.js} +3844 -3093
- package/lib/chunks/{2LEVMD2A.js → Z5H4OEIW.js} +4 -4
- package/lib/chunks/{WST4KAJO.js → ZBZADNJT.js} +134 -518
- package/lib/index.js +125 -117
- package/package.json +1 -1
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { createRequire as __createRequire } from 'node:module';
|
|
2
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 "./ER45DAHG.js";
|
|
3
17
|
import {
|
|
4
18
|
BaseResolver,
|
|
5
19
|
blue,
|
|
@@ -21,7 +35,7 @@ import {
|
|
|
21
35
|
require_dist,
|
|
22
36
|
walkDocument,
|
|
23
37
|
yellow
|
|
24
|
-
} from "./
|
|
38
|
+
} from "./YK6T7IHG.js";
|
|
25
39
|
import {
|
|
26
40
|
__toESM
|
|
27
41
|
} from "./5ILQMFXK.js";
|
|
@@ -422,278 +436,9 @@ function renderReport(result, options) {
|
|
|
422
436
|
}
|
|
423
437
|
}
|
|
424
438
|
|
|
425
|
-
// src/commands/drift/utils/files.ts
|
|
426
|
-
import { open, readdir, stat } from "node:fs/promises";
|
|
427
|
-
import path2 from "node:path";
|
|
428
|
-
var SPEC_FILE_EXTENSIONS = /* @__PURE__ */ new Set([".yaml", ".yml", ".json"]);
|
|
429
|
-
async function listOpenApiFiles(rootDir) {
|
|
430
|
-
const output = [];
|
|
431
|
-
async function walk(currentDir) {
|
|
432
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
433
|
-
for (const entry of entries) {
|
|
434
|
-
if (entry.name.startsWith(".")) {
|
|
435
|
-
continue;
|
|
436
|
-
}
|
|
437
|
-
const absolutePath = path2.join(currentDir, entry.name);
|
|
438
|
-
if (entry.isDirectory()) {
|
|
439
|
-
await walk(absolutePath);
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
if (entry.isFile() && SPEC_FILE_EXTENSIONS.has(path2.extname(entry.name).toLowerCase())) {
|
|
443
|
-
output.push(absolutePath);
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
await walk(rootDir);
|
|
448
|
-
output.sort();
|
|
449
|
-
return output;
|
|
450
|
-
}
|
|
451
|
-
async function readProbe(filePath, maxBytes = 4096) {
|
|
452
|
-
const fileHandle = await open(filePath, "r");
|
|
453
|
-
try {
|
|
454
|
-
const buffer = Buffer.allocUnsafe(maxBytes);
|
|
455
|
-
const { bytesRead } = await fileHandle.read(buffer, 0, maxBytes, 0);
|
|
456
|
-
return buffer.toString("utf8", 0, bytesRead);
|
|
457
|
-
} finally {
|
|
458
|
-
await fileHandle.close();
|
|
459
|
-
}
|
|
460
|
-
}
|
|
461
|
-
function normalizeFsPath(value) {
|
|
462
|
-
return path2.resolve(process.cwd(), value);
|
|
463
|
-
}
|
|
464
|
-
async function listFilesRecursively(rootPath) {
|
|
465
|
-
const stats = await stat(rootPath);
|
|
466
|
-
if (stats.isFile()) {
|
|
467
|
-
return [rootPath];
|
|
468
|
-
}
|
|
469
|
-
if (!stats.isDirectory()) {
|
|
470
|
-
return [];
|
|
471
|
-
}
|
|
472
|
-
const output = [];
|
|
473
|
-
async function walk(currentDir) {
|
|
474
|
-
const entries = await readdir(currentDir, { withFileTypes: true });
|
|
475
|
-
for (const entry of entries) {
|
|
476
|
-
if (entry.name.startsWith(".")) {
|
|
477
|
-
continue;
|
|
478
|
-
}
|
|
479
|
-
const absolutePath = path2.join(currentDir, entry.name);
|
|
480
|
-
if (entry.isDirectory()) {
|
|
481
|
-
await walk(absolutePath);
|
|
482
|
-
continue;
|
|
483
|
-
}
|
|
484
|
-
if (entry.isFile()) {
|
|
485
|
-
output.push(absolutePath);
|
|
486
|
-
}
|
|
487
|
-
}
|
|
488
|
-
}
|
|
489
|
-
await walk(rootPath);
|
|
490
|
-
output.sort();
|
|
491
|
-
return output;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
439
|
// src/commands/drift/engine/validation-session.ts
|
|
495
440
|
import { randomUUID } from "node:crypto";
|
|
496
441
|
|
|
497
|
-
// src/commands/drift/utils/http.ts
|
|
498
|
-
var DUMMY_HOST = "drift.local";
|
|
499
|
-
var DUMMY_BASE_URL = `http://${DUMMY_HOST}`;
|
|
500
|
-
function isSyntheticHost(host) {
|
|
501
|
-
return host === DUMMY_HOST;
|
|
502
|
-
}
|
|
503
|
-
var IGNORED_UNDOCUMENTED_HEADERS = /* @__PURE__ */ new Set([
|
|
504
|
-
"accept",
|
|
505
|
-
"accept-charset",
|
|
506
|
-
"accept-encoding",
|
|
507
|
-
"accept-language",
|
|
508
|
-
"authorization",
|
|
509
|
-
"baggage",
|
|
510
|
-
"cache-control",
|
|
511
|
-
"cdn-loop",
|
|
512
|
-
"cookie",
|
|
513
|
-
"connection",
|
|
514
|
-
"content-length",
|
|
515
|
-
"content-type",
|
|
516
|
-
"dpr",
|
|
517
|
-
"dnt",
|
|
518
|
-
"downlink",
|
|
519
|
-
"ect",
|
|
520
|
-
"forwarded",
|
|
521
|
-
"host",
|
|
522
|
-
"if-match",
|
|
523
|
-
"if-modified-since",
|
|
524
|
-
"if-none-match",
|
|
525
|
-
"if-range",
|
|
526
|
-
"if-unmodified-since",
|
|
527
|
-
"origin",
|
|
528
|
-
"pragma",
|
|
529
|
-
"priority",
|
|
530
|
-
"range",
|
|
531
|
-
"referer",
|
|
532
|
-
"sec-fetch-dest",
|
|
533
|
-
"sec-fetch-mode",
|
|
534
|
-
"sec-fetch-site",
|
|
535
|
-
"sec-fetch-user",
|
|
536
|
-
"sec-gpc",
|
|
537
|
-
"sentry-trace",
|
|
538
|
-
"te",
|
|
539
|
-
"traceparent",
|
|
540
|
-
"tracestate",
|
|
541
|
-
"upgrade",
|
|
542
|
-
"upgrade-insecure-requests",
|
|
543
|
-
"user-agent",
|
|
544
|
-
"via",
|
|
545
|
-
"x-amzn-trace-id",
|
|
546
|
-
"x-client-trace-id",
|
|
547
|
-
"x-cloud-trace-context",
|
|
548
|
-
"x-correlation-id",
|
|
549
|
-
"x-http-method-override",
|
|
550
|
-
"x-method-override",
|
|
551
|
-
"x-real-ip",
|
|
552
|
-
"x-request-id",
|
|
553
|
-
"x-forwarded-for",
|
|
554
|
-
"x-forwarded-host",
|
|
555
|
-
"x-forwarded-port",
|
|
556
|
-
"x-forwarded-proto"
|
|
557
|
-
]);
|
|
558
|
-
var IGNORED_UNDOCUMENTED_HEADER_PREFIXES = [
|
|
559
|
-
"cf-",
|
|
560
|
-
"sec-ch-",
|
|
561
|
-
"sec-fetch-",
|
|
562
|
-
"x-b3-",
|
|
563
|
-
"x-envoy-",
|
|
564
|
-
"x-forwarded-"
|
|
565
|
-
];
|
|
566
|
-
var SET_COOKIE_SEPARATOR = "\n";
|
|
567
|
-
function splitSetCookieHeader(value) {
|
|
568
|
-
return value.split(SET_COOKIE_SEPARATOR);
|
|
569
|
-
}
|
|
570
|
-
function appendHeaderValue(result, name, value) {
|
|
571
|
-
const key = name.toLowerCase();
|
|
572
|
-
const existing = result[key];
|
|
573
|
-
if (existing === void 0) {
|
|
574
|
-
result[key] = value;
|
|
575
|
-
return;
|
|
576
|
-
}
|
|
577
|
-
result[key] = key === "set-cookie" ? `${existing}${SET_COOKIE_SEPARATOR}${value}` : `${existing},${value}`;
|
|
578
|
-
}
|
|
579
|
-
function normalizeHeaders(input) {
|
|
580
|
-
if (!isPlainObject(input) && !Array.isArray(input)) {
|
|
581
|
-
return {};
|
|
582
|
-
}
|
|
583
|
-
const result = {};
|
|
584
|
-
if (Array.isArray(input)) {
|
|
585
|
-
for (const item of input) {
|
|
586
|
-
if (!isPlainObject(item)) {
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
const { name, value } = item;
|
|
590
|
-
if (typeof name === "string" && value !== void 0) {
|
|
591
|
-
appendHeaderValue(result, name, String(value));
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
|
-
return result;
|
|
595
|
-
}
|
|
596
|
-
for (const [key, value] of Object.entries(input)) {
|
|
597
|
-
if (value === void 0 || value === null) {
|
|
598
|
-
continue;
|
|
599
|
-
}
|
|
600
|
-
if (Array.isArray(value)) {
|
|
601
|
-
for (const item of value) {
|
|
602
|
-
appendHeaderValue(result, key, String(item));
|
|
603
|
-
}
|
|
604
|
-
continue;
|
|
605
|
-
}
|
|
606
|
-
appendHeaderValue(result, key, String(value));
|
|
607
|
-
}
|
|
608
|
-
return result;
|
|
609
|
-
}
|
|
610
|
-
function parseUrl(input) {
|
|
611
|
-
try {
|
|
612
|
-
return new URL(input);
|
|
613
|
-
} catch {
|
|
614
|
-
return new URL(input, DUMMY_BASE_URL);
|
|
615
|
-
}
|
|
616
|
-
}
|
|
617
|
-
function getPathWithoutTrailingSlash(pathname) {
|
|
618
|
-
if (pathname.length > 1 && pathname.endsWith("/")) {
|
|
619
|
-
return pathname.slice(0, -1);
|
|
620
|
-
}
|
|
621
|
-
return pathname;
|
|
622
|
-
}
|
|
623
|
-
function escapeRegex(value) {
|
|
624
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
625
|
-
}
|
|
626
|
-
function compileOpenApiPath(pathTemplate) {
|
|
627
|
-
const params = [];
|
|
628
|
-
let score = 0;
|
|
629
|
-
const absolutePath = pathTemplate.startsWith("/") ? pathTemplate : `/${pathTemplate}`;
|
|
630
|
-
const normalizedPath = getPathWithoutTrailingSlash(absolutePath);
|
|
631
|
-
const regexBody = normalizedPath.split("/").map((segment) => {
|
|
632
|
-
if (!segment) {
|
|
633
|
-
return "";
|
|
634
|
-
}
|
|
635
|
-
const paramMatch = segment.match(/^\{([^}]+)\}$/);
|
|
636
|
-
if (paramMatch) {
|
|
637
|
-
params.push(paramMatch[1]);
|
|
638
|
-
return "([^/]+)";
|
|
639
|
-
}
|
|
640
|
-
score += 2;
|
|
641
|
-
return escapeRegex(segment);
|
|
642
|
-
}).join("/");
|
|
643
|
-
return {
|
|
644
|
-
regex: new RegExp(`^${regexBody || "/"}$`),
|
|
645
|
-
params,
|
|
646
|
-
score
|
|
647
|
-
};
|
|
648
|
-
}
|
|
649
|
-
function parseJsonBodyIfPresent(contentType, bodyText) {
|
|
650
|
-
if (!bodyText) {
|
|
651
|
-
return void 0;
|
|
652
|
-
}
|
|
653
|
-
if (!isJsonMime(contentType)) {
|
|
654
|
-
return void 0;
|
|
655
|
-
}
|
|
656
|
-
try {
|
|
657
|
-
return JSON.parse(bodyText);
|
|
658
|
-
} catch {
|
|
659
|
-
return void 0;
|
|
660
|
-
}
|
|
661
|
-
}
|
|
662
|
-
function normalizeContentType(contentType) {
|
|
663
|
-
if (!contentType) {
|
|
664
|
-
return "";
|
|
665
|
-
}
|
|
666
|
-
return contentType.split(";")[0]?.trim().toLowerCase() ?? "";
|
|
667
|
-
}
|
|
668
|
-
function isJsonMime(contentType) {
|
|
669
|
-
const mime = normalizeContentType(contentType);
|
|
670
|
-
return mime === "application/json" || mime.endsWith("+json");
|
|
671
|
-
}
|
|
672
|
-
function pickSchemaByMime(contentMap, contentType) {
|
|
673
|
-
const requestedMime = normalizeContentType(contentType);
|
|
674
|
-
if (!requestedMime) {
|
|
675
|
-
return contentMap["application/json"] ?? contentMap["*/*"];
|
|
676
|
-
}
|
|
677
|
-
if (contentMap[requestedMime] !== void 0) {
|
|
678
|
-
return contentMap[requestedMime];
|
|
679
|
-
}
|
|
680
|
-
const [type, subtype] = requestedMime.split("/");
|
|
681
|
-
if (type && subtype) {
|
|
682
|
-
const wildcardSubtype = `${type}/*`;
|
|
683
|
-
if (contentMap[wildcardSubtype] !== void 0) {
|
|
684
|
-
return contentMap[wildcardSubtype];
|
|
685
|
-
}
|
|
686
|
-
}
|
|
687
|
-
return contentMap["*/*"];
|
|
688
|
-
}
|
|
689
|
-
function shouldIgnoreHeaderAsUndocumented(headerName) {
|
|
690
|
-
const normalizedHeaderName = headerName.toLowerCase();
|
|
691
|
-
if (normalizedHeaderName.startsWith(":")) {
|
|
692
|
-
return true;
|
|
693
|
-
}
|
|
694
|
-
return IGNORED_UNDOCUMENTED_HEADERS.has(normalizedHeaderName) || IGNORED_UNDOCUMENTED_HEADER_PREFIXES.some((prefix) => normalizedHeaderName.startsWith(prefix));
|
|
695
|
-
}
|
|
696
|
-
|
|
697
442
|
// src/commands/drift/openapi/matcher.ts
|
|
698
443
|
function toRelativePath(requestPath, server) {
|
|
699
444
|
const normalizedRequestPath = getPathWithoutTrailingSlash(requestPath || "/") || "/";
|
|
@@ -1278,11 +1023,30 @@ function validateParameter(parameter, actualValue, context, findings) {
|
|
|
1278
1023
|
});
|
|
1279
1024
|
}
|
|
1280
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
|
+
}
|
|
1281
1042
|
function getActualParameterValue(parameter, context, cookies) {
|
|
1282
1043
|
switch (parameter.in) {
|
|
1283
1044
|
case "path":
|
|
1284
1045
|
return context.matchedOperation?.pathParams[parameter.name];
|
|
1285
1046
|
case "query": {
|
|
1047
|
+
if (parameter.style === "deepObject") {
|
|
1048
|
+
return getDeepObjectParameterValue(parameter.name, context.exchange.request.query);
|
|
1049
|
+
}
|
|
1286
1050
|
const values = context.exchange.request.query.getAll(parameter.name);
|
|
1287
1051
|
if (values.length === 0) {
|
|
1288
1052
|
return void 0;
|
|
@@ -1308,29 +1072,38 @@ function createUndocumentedParameterFindings(context, matchedOperation) {
|
|
|
1308
1072
|
header: /* @__PURE__ */ new Set(),
|
|
1309
1073
|
cookie: /* @__PURE__ */ new Set()
|
|
1310
1074
|
};
|
|
1075
|
+
const deepObjectQueryParams = /* @__PURE__ */ new Set();
|
|
1311
1076
|
for (const parameter of matchedOperation.operation.requestParameters) {
|
|
1312
1077
|
if (parameter.in === "header") {
|
|
1313
1078
|
paramsByLocation.header.add(parameter.name.toLowerCase());
|
|
1314
1079
|
} else if (parameter.in === "query" || parameter.in === "cookie") {
|
|
1315
1080
|
paramsByLocation[parameter.in].add(parameter.name);
|
|
1081
|
+
if (parameter.in === "query" && parameter.style === "deepObject") {
|
|
1082
|
+
deepObjectQueryParams.add(parameter.name);
|
|
1083
|
+
}
|
|
1316
1084
|
}
|
|
1317
1085
|
}
|
|
1318
1086
|
for (const name of new Set(context.exchange.request.query.keys())) {
|
|
1319
|
-
if (
|
|
1320
|
-
|
|
1321
|
-
ruleId: "schema-consistency",
|
|
1322
|
-
severity: "warning",
|
|
1323
|
-
category: "documentation",
|
|
1324
|
-
message: `Undocumented query parameter in traffic: "${name}"`,
|
|
1325
|
-
exchangeIndex: context.exchange.index,
|
|
1326
|
-
operationId: matchedOperation.operation.operationId,
|
|
1327
|
-
specSource: matchedOperation.operation.specSource,
|
|
1328
|
-
target: "request"
|
|
1329
|
-
});
|
|
1087
|
+
if (paramsByLocation.query.has(name)) {
|
|
1088
|
+
continue;
|
|
1330
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
|
+
});
|
|
1331
1104
|
}
|
|
1332
1105
|
for (const headerName of Object.keys(context.exchange.request.headers)) {
|
|
1333
|
-
if (shouldIgnoreHeaderAsUndocumented(headerName)) {
|
|
1106
|
+
if (shouldIgnoreHeaderAsUndocumented(headerName, context.ignoreHeaders)) {
|
|
1334
1107
|
continue;
|
|
1335
1108
|
}
|
|
1336
1109
|
if (!paramsByLocation.header.has(headerName.toLowerCase())) {
|
|
@@ -1348,6 +1121,10 @@ function createUndocumentedParameterFindings(context, matchedOperation) {
|
|
|
1348
1121
|
}
|
|
1349
1122
|
return findings;
|
|
1350
1123
|
}
|
|
1124
|
+
function isRequestRejectedByServer(context) {
|
|
1125
|
+
const response = context.exchange.response;
|
|
1126
|
+
return response !== void 0 && response.status >= 400 && response.status < 500;
|
|
1127
|
+
}
|
|
1351
1128
|
function pickResponseSchema(context, matchedOperation) {
|
|
1352
1129
|
const response = context.exchange.response;
|
|
1353
1130
|
if (!response) {
|
|
@@ -1393,65 +1170,67 @@ var SchemaConsistencyRule = class {
|
|
|
1393
1170
|
const findings = [];
|
|
1394
1171
|
const cookies = parseCookies(context.exchange.request.headers.cookie);
|
|
1395
1172
|
findings.push(...createUndocumentedParameterFindings(context, matchedOperation));
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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);
|
|
1399
1193
|
}
|
|
1400
|
-
const
|
|
1401
|
-
|
|
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) {
|
|
1402
1201
|
findings.push({
|
|
1403
1202
|
ruleId: this.id,
|
|
1404
1203
|
severity: "error",
|
|
1405
1204
|
category: "documentation",
|
|
1406
|
-
message:
|
|
1205
|
+
message: "Missing required request body",
|
|
1407
1206
|
exchangeIndex: context.exchange.index,
|
|
1408
1207
|
operationId: matchedOperation.operation.operationId,
|
|
1409
1208
|
specSource: matchedOperation.operation.specSource,
|
|
1410
1209
|
target: "request"
|
|
1411
1210
|
});
|
|
1412
|
-
continue;
|
|
1413
1211
|
}
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
findings.push({
|
|
1437
|
-
ruleId: this.id,
|
|
1438
|
-
severity: "error",
|
|
1439
|
-
category: "schema",
|
|
1440
|
-
message: "Request body is not valid JSON for JSON content-type",
|
|
1441
|
-
exchangeIndex: context.exchange.index,
|
|
1442
|
-
operationId: matchedOperation.operation.operationId,
|
|
1443
|
-
specSource: matchedOperation.operation.specSource,
|
|
1444
|
-
target: "request"
|
|
1445
|
-
});
|
|
1446
|
-
} else {
|
|
1447
|
-
findings.push(
|
|
1448
|
-
...validateSchemaResult(
|
|
1449
|
-
requestSchema,
|
|
1450
|
-
context.exchange.request.bodyJson,
|
|
1451
|
-
context,
|
|
1452
|
-
"request"
|
|
1453
|
-
)
|
|
1454
|
-
);
|
|
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
|
+
}
|
|
1455
1234
|
}
|
|
1456
1235
|
}
|
|
1457
1236
|
const responseSchema = pickResponseSchema(context, matchedOperation);
|
|
@@ -1732,7 +1511,9 @@ function createSecuritySummary(issues) {
|
|
|
1732
1511
|
if (optionSummaries.length === 1) {
|
|
1733
1512
|
return `Authentication check failed. ${optionSummaries[0]}`;
|
|
1734
1513
|
}
|
|
1735
|
-
return `None of the documented authentication options matched. Any one of these options would satisfy the OpenAPI security requirements: ${optionSummaries.join(
|
|
1514
|
+
return `None of the documented authentication options matched. Any one of these options would satisfy the OpenAPI security requirements: ${optionSummaries.join(
|
|
1515
|
+
" | "
|
|
1516
|
+
)}`;
|
|
1736
1517
|
}
|
|
1737
1518
|
function getSensitiveQueryKeys(context) {
|
|
1738
1519
|
return Array.from(context.exchange.request.query.keys()).filter((key) => {
|
|
@@ -1740,8 +1521,21 @@ function getSensitiveQueryKeys(context) {
|
|
|
1740
1521
|
return normalizedKey.includes("token") || normalizedKey.includes("apikey") || normalizedKey.includes("api_key") || normalizedKey.includes("access_key");
|
|
1741
1522
|
});
|
|
1742
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
|
+
}
|
|
1743
1537
|
function shouldFlagInsecureTransport(context) {
|
|
1744
|
-
if (context.exchange.request.protocol !== "http:" || !context.exchange.request.protocolKnown) {
|
|
1538
|
+
if (context.exchange.request.protocol !== "http:" || !context.exchange.request.protocolKnown || isLoopbackRequest(context.exchange.request)) {
|
|
1745
1539
|
return {
|
|
1746
1540
|
flag: false,
|
|
1747
1541
|
hasAuthHeader: false,
|
|
@@ -1835,9 +1629,9 @@ var UndocumentedEndpointRule = class {
|
|
|
1835
1629
|
if (context.matchedOperation) {
|
|
1836
1630
|
return [];
|
|
1837
1631
|
}
|
|
1838
|
-
const { method, path:
|
|
1632
|
+
const { method, path: path2, host } = context.exchange.request;
|
|
1839
1633
|
const isHostMismatch = context.matchMode === "strict-host" && !context.hostCompatibleWithSpecServers;
|
|
1840
|
-
const message = isHostMismatch ? `Undocumented server: ${method} ${
|
|
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}`;
|
|
1841
1635
|
return [
|
|
1842
1636
|
{
|
|
1843
1637
|
ruleId: this.id,
|
|
@@ -1879,34 +1673,6 @@ function loadRules(activeRuleIds) {
|
|
|
1879
1673
|
});
|
|
1880
1674
|
}
|
|
1881
1675
|
|
|
1882
|
-
// src/commands/drift/utils/server.ts
|
|
1883
|
-
function normalizeServerPrefix(server) {
|
|
1884
|
-
const trimmed = server?.replace(/\/+$/, "");
|
|
1885
|
-
return trimmed || void 0;
|
|
1886
|
-
}
|
|
1887
|
-
function stripPrefixFromPath(pathname, prefixPath) {
|
|
1888
|
-
if (!prefixPath || prefixPath === "/") {
|
|
1889
|
-
return pathname || "/";
|
|
1890
|
-
}
|
|
1891
|
-
if (pathname === prefixPath) {
|
|
1892
|
-
return "/";
|
|
1893
|
-
}
|
|
1894
|
-
if (!pathname.startsWith(`${prefixPath}/`)) {
|
|
1895
|
-
return void 0;
|
|
1896
|
-
}
|
|
1897
|
-
return pathname.slice(prefixPath.length) || "/";
|
|
1898
|
-
}
|
|
1899
|
-
function resolvePathForServer(request, server) {
|
|
1900
|
-
if (server.startsWith("/")) {
|
|
1901
|
-
return stripPrefixFromPath(request.path, server);
|
|
1902
|
-
}
|
|
1903
|
-
const serverUrl = parseUrl(server.includes("://") ? server : `http://${server}`);
|
|
1904
|
-
if (isSyntheticHost(serverUrl.host) || request.host !== void 0 && request.host.toLowerCase() !== serverUrl.host) {
|
|
1905
|
-
return void 0;
|
|
1906
|
-
}
|
|
1907
|
-
return stripPrefixFromPath(request.path, getPathWithoutTrailingSlash(serverUrl.pathname));
|
|
1908
|
-
}
|
|
1909
|
-
|
|
1910
1676
|
// src/commands/drift/engine/schema-validator.ts
|
|
1911
1677
|
var import__ = __toESM(require__(), 1);
|
|
1912
1678
|
var import_ajv_formats = __toESM(require_dist(), 1);
|
|
@@ -2175,6 +1941,7 @@ var ValidationSession = class _ValidationSession {
|
|
|
2175
1941
|
problemKeyStats = /* @__PURE__ */ new Map();
|
|
2176
1942
|
totalProblemGroups = 0;
|
|
2177
1943
|
server;
|
|
1944
|
+
ignoreHeaders;
|
|
2178
1945
|
minSeverityRank;
|
|
2179
1946
|
specServerHosts;
|
|
2180
1947
|
hasHostlessSpecServer;
|
|
@@ -2182,6 +1949,7 @@ var ValidationSession = class _ValidationSession {
|
|
|
2182
1949
|
this.options = options;
|
|
2183
1950
|
this.rules = rules;
|
|
2184
1951
|
this.server = normalizeServerPrefix(options.server);
|
|
1952
|
+
this.ignoreHeaders = options.ignoreHeaders ? parseHeaderIgnoreList(options.ignoreHeaders) : void 0;
|
|
2185
1953
|
const { specServerHosts, hasHostlessSpecServer } = collectSpecServerHosts(options.openApiIndex);
|
|
2186
1954
|
this.specServerHosts = specServerHosts;
|
|
2187
1955
|
this.hasHostlessSpecServer = hasHostlessSpecServer;
|
|
@@ -2229,6 +1997,7 @@ var ValidationSession = class _ValidationSession {
|
|
|
2229
1997
|
matchMode: this.options.matchMode,
|
|
2230
1998
|
hostCompatibleWithSpecServers: relativePathOverride !== void 0 || hostCompatible,
|
|
2231
1999
|
ignoreCookies: this.options.ignoreCookies ?? false,
|
|
2000
|
+
ignoreHeaders: this.ignoreHeaders,
|
|
2232
2001
|
validateSchema: (schema, value, options) => options?.coerce ? this.coercingSchemaValidator.validate(schema, value, options?.target) : this.schemaValidator.validate(schema, value, options?.target)
|
|
2233
2002
|
});
|
|
2234
2003
|
const records = [];
|
|
@@ -2295,7 +2064,7 @@ var ValidationSession = class _ValidationSession {
|
|
|
2295
2064
|
};
|
|
2296
2065
|
|
|
2297
2066
|
// src/commands/drift/openapi/loader.ts
|
|
2298
|
-
import { stat
|
|
2067
|
+
import { stat } from "node:fs/promises";
|
|
2299
2068
|
|
|
2300
2069
|
// src/commands/drift/utils/openapi.ts
|
|
2301
2070
|
function resolveServerUrl(rawUrl, variables) {
|
|
@@ -2351,6 +2120,7 @@ function normalizeParameters(parameters) {
|
|
|
2351
2120
|
name: String(entry.name ?? ""),
|
|
2352
2121
|
in: location,
|
|
2353
2122
|
required: Boolean(entry.required) || location === "path",
|
|
2123
|
+
style: typeof entry.style === "string" ? entry.style : void 0,
|
|
2354
2124
|
schema: entry.schema
|
|
2355
2125
|
});
|
|
2356
2126
|
}
|
|
@@ -2570,7 +2340,7 @@ function finalizeIndex(operationsByMethod, loadedSpecs) {
|
|
|
2570
2340
|
return { operationsByMethod, loadedSpecs, loadedOperations };
|
|
2571
2341
|
}
|
|
2572
2342
|
async function resolveSpecFiles(specPath) {
|
|
2573
|
-
const stats = await
|
|
2343
|
+
const stats = await stat(specPath);
|
|
2574
2344
|
if (stats.isDirectory()) {
|
|
2575
2345
|
return { specFiles: await listOpenApiFiles(specPath), fromDirectory: true };
|
|
2576
2346
|
}
|
|
@@ -2626,162 +2396,8 @@ function parseCsv(input) {
|
|
|
2626
2396
|
return input.split(",").map((value) => value.trim()).filter(Boolean);
|
|
2627
2397
|
}
|
|
2628
2398
|
|
|
2629
|
-
// src/commands/drift/log-formats/helpers.ts
|
|
2630
|
-
import { createReadStream } from "node:fs";
|
|
2631
|
-
import { readFile } from "node:fs/promises";
|
|
2632
|
-
import { createInterface } from "node:readline";
|
|
2633
|
-
function coerceString(value) {
|
|
2634
|
-
if (value === void 0 || value === null) {
|
|
2635
|
-
return void 0;
|
|
2636
|
-
}
|
|
2637
|
-
if (typeof value === "string") {
|
|
2638
|
-
return value;
|
|
2639
|
-
}
|
|
2640
|
-
if (Buffer.isBuffer(value)) {
|
|
2641
|
-
return value.toString("utf8");
|
|
2642
|
-
}
|
|
2643
|
-
if (isPlainObject(value) || Array.isArray(value)) {
|
|
2644
|
-
try {
|
|
2645
|
-
return JSON.stringify(value);
|
|
2646
|
-
} catch {
|
|
2647
|
-
return String(value);
|
|
2648
|
-
}
|
|
2649
|
-
}
|
|
2650
|
-
return String(value);
|
|
2651
|
-
}
|
|
2652
|
-
function coerceNumber(value) {
|
|
2653
|
-
if (typeof value === "number" && Number.isFinite(value)) {
|
|
2654
|
-
return value;
|
|
2655
|
-
}
|
|
2656
|
-
if (typeof value === "string" && value.trim() !== "") {
|
|
2657
|
-
const parsed = Number(value);
|
|
2658
|
-
if (Number.isFinite(parsed)) {
|
|
2659
|
-
return parsed;
|
|
2660
|
-
}
|
|
2661
|
-
}
|
|
2662
|
-
return void 0;
|
|
2663
|
-
}
|
|
2664
|
-
function decodeBody(value, encoding) {
|
|
2665
|
-
if (value === void 0 || value === null) {
|
|
2666
|
-
return void 0;
|
|
2667
|
-
}
|
|
2668
|
-
if (encoding === "base64" && typeof value === "string") {
|
|
2669
|
-
try {
|
|
2670
|
-
return Buffer.from(value, "base64").toString("utf8");
|
|
2671
|
-
} catch {
|
|
2672
|
-
return value;
|
|
2673
|
-
}
|
|
2674
|
-
}
|
|
2675
|
-
return coerceString(value);
|
|
2676
|
-
}
|
|
2677
|
-
function createNormalizedExchange(seed, index, source) {
|
|
2678
|
-
const method = seed.method?.toUpperCase();
|
|
2679
|
-
const url = seed.url;
|
|
2680
|
-
if (!method || !url) {
|
|
2681
|
-
return null;
|
|
2682
|
-
}
|
|
2683
|
-
const requestHeaders = normalizeHeaders(seed.requestHeaders);
|
|
2684
|
-
let parsedUrl = parseUrl(url);
|
|
2685
|
-
if (isSyntheticHost(parsedUrl.host) && requestHeaders.host) {
|
|
2686
|
-
try {
|
|
2687
|
-
parsedUrl = new URL(
|
|
2688
|
-
`${parsedUrl.protocol}//${requestHeaders.host}${parsedUrl.pathname}${parsedUrl.search}`
|
|
2689
|
-
);
|
|
2690
|
-
} catch {
|
|
2691
|
-
parsedUrl = parseUrl(url);
|
|
2692
|
-
}
|
|
2693
|
-
}
|
|
2694
|
-
const requestContentType = seed.requestContentType ?? requestHeaders["content-type"];
|
|
2695
|
-
const requestBodyText = decodeBody(seed.requestBody);
|
|
2696
|
-
const request = {
|
|
2697
|
-
method,
|
|
2698
|
-
url: parsedUrl.toString(),
|
|
2699
|
-
path: parsedUrl.pathname,
|
|
2700
|
-
query: parsedUrl.searchParams,
|
|
2701
|
-
protocol: parsedUrl.protocol,
|
|
2702
|
-
protocolKnown: seed.schemeKnown ?? /^https?:\/\//i.test(url),
|
|
2703
|
-
host: isSyntheticHost(parsedUrl.host) ? void 0 : parsedUrl.host || void 0,
|
|
2704
|
-
headers: requestHeaders,
|
|
2705
|
-
contentType: requestContentType,
|
|
2706
|
-
bodyText: requestBodyText,
|
|
2707
|
-
bodyJson: parseJsonBodyIfPresent(requestContentType, requestBodyText)
|
|
2708
|
-
};
|
|
2709
|
-
let response;
|
|
2710
|
-
const responseStatus = seed.responseStatus;
|
|
2711
|
-
if (responseStatus !== void 0) {
|
|
2712
|
-
const responseHeaders = normalizeHeaders(seed.responseHeaders);
|
|
2713
|
-
const responseContentType = seed.responseContentType ?? responseHeaders["content-type"];
|
|
2714
|
-
const responseBodyText = decodeBody(seed.responseBody);
|
|
2715
|
-
response = {
|
|
2716
|
-
status: responseStatus,
|
|
2717
|
-
statusText: seed.responseStatusText,
|
|
2718
|
-
headers: responseHeaders,
|
|
2719
|
-
contentType: responseContentType,
|
|
2720
|
-
bodyText: responseBodyText,
|
|
2721
|
-
bodyJson: parseJsonBodyIfPresent(responseContentType, responseBodyText)
|
|
2722
|
-
};
|
|
2723
|
-
}
|
|
2724
|
-
return {
|
|
2725
|
-
index,
|
|
2726
|
-
source,
|
|
2727
|
-
startedAt: seed.startedAt,
|
|
2728
|
-
request,
|
|
2729
|
-
response,
|
|
2730
|
-
raw: seed.raw
|
|
2731
|
-
};
|
|
2732
|
-
}
|
|
2733
|
-
async function* streamNdjsonObjects(filePath) {
|
|
2734
|
-
const readStream = createReadStream(filePath, { encoding: "utf8" });
|
|
2735
|
-
const reader = createInterface({ input: readStream, crlfDelay: Infinity });
|
|
2736
|
-
for await (const line of reader) {
|
|
2737
|
-
const trimmed = line.trim();
|
|
2738
|
-
if (!trimmed) {
|
|
2739
|
-
continue;
|
|
2740
|
-
}
|
|
2741
|
-
try {
|
|
2742
|
-
const parsed = JSON.parse(trimmed);
|
|
2743
|
-
if (isPlainObject(parsed)) {
|
|
2744
|
-
yield parsed;
|
|
2745
|
-
}
|
|
2746
|
-
} catch {
|
|
2747
|
-
}
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
async function* iterateJsonArray(filePath, arrayPath) {
|
|
2751
|
-
const content = await readFile(filePath, "utf8");
|
|
2752
|
-
let value = JSON.parse(content);
|
|
2753
|
-
if (arrayPath) {
|
|
2754
|
-
for (const key of arrayPath.split(".")) {
|
|
2755
|
-
value = isPlainObject(value) ? value[key] : void 0;
|
|
2756
|
-
}
|
|
2757
|
-
}
|
|
2758
|
-
if (Array.isArray(value)) {
|
|
2759
|
-
for (const item of value) {
|
|
2760
|
-
if (isPlainObject(item)) {
|
|
2761
|
-
yield item;
|
|
2762
|
-
}
|
|
2763
|
-
}
|
|
2764
|
-
}
|
|
2765
|
-
}
|
|
2766
|
-
function pickHeaderContentType(headers) {
|
|
2767
|
-
const normalized = normalizeHeaders(headers);
|
|
2768
|
-
return normalized["content-type"];
|
|
2769
|
-
}
|
|
2770
|
-
|
|
2771
2399
|
export {
|
|
2772
2400
|
renderReport,
|
|
2773
|
-
readProbe,
|
|
2774
|
-
normalizeFsPath,
|
|
2775
|
-
listFilesRecursively,
|
|
2776
|
-
normalizeContentType,
|
|
2777
|
-
isJsonMime,
|
|
2778
|
-
coerceString,
|
|
2779
|
-
coerceNumber,
|
|
2780
|
-
decodeBody,
|
|
2781
|
-
createNormalizedExchange,
|
|
2782
|
-
streamNdjsonObjects,
|
|
2783
|
-
iterateJsonArray,
|
|
2784
|
-
pickHeaderContentType,
|
|
2785
2401
|
ValidationSession,
|
|
2786
2402
|
loadOpenApiIndex,
|
|
2787
2403
|
parseCsv
|