@redocly/cli 2.37.0 → 2.39.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 +1 -1
- package/lib/chunks/{IVW4MILQ.js → 2LEVMD2A.js} +3 -3
- package/lib/chunks/{YCTAWG2U.js → 4FB5ZIPP.js} +1 -1
- package/lib/chunks/BA5AZJUM.js +681 -0
- package/lib/chunks/BNHQNMUG.js +525 -0
- package/lib/chunks/{LJUXYC4S.js → C3PXBQU5.js} +1 -1
- package/lib/chunks/C5HID4LT.js +571 -0
- package/lib/chunks/{CPNIO4J3.js → KYZEVOA2.js} +1679 -1400
- package/lib/chunks/{FRVYIHIR.js → OVY6O6WL.js} +5 -3
- package/lib/chunks/{4UQ4YHWW.js → OYZMAAZV.js} +5 -10
- package/lib/chunks/U4V3W7MN.js +17 -0
- package/lib/chunks/UUU33DK3.js +125 -0
- package/lib/chunks/{M7TYKZDD.js → V6CVT3XE.js} +6 -4
- package/lib/chunks/{QFYLVVG2.js → V74PIVJZ.js} +8 -5
- package/lib/chunks/{ZHITMP73.js → W5R2GYDZ.js} +3 -2
- package/lib/chunks/WST4KAJO.js +2788 -0
- package/lib/chunks/{U3LS66ZD.js → XB6C62FW.js} +100 -771
- package/lib/index.js +157 -14
- package/package.json +1 -1
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from 'node:module';
|
|
2
|
+
const require = __createRequire(import.meta.url);
|
|
3
|
+
import {
|
|
4
|
+
ValidationSession,
|
|
5
|
+
coerceNumber,
|
|
6
|
+
coerceString,
|
|
7
|
+
createNormalizedExchange,
|
|
8
|
+
decodeBody,
|
|
9
|
+
iterateJsonArray,
|
|
10
|
+
listFilesRecursively,
|
|
11
|
+
loadOpenApiIndex,
|
|
12
|
+
normalizeContentType,
|
|
13
|
+
normalizeFsPath,
|
|
14
|
+
parseCsv,
|
|
15
|
+
pickHeaderContentType,
|
|
16
|
+
readProbe,
|
|
17
|
+
renderReport,
|
|
18
|
+
streamNdjsonObjects
|
|
19
|
+
} from "./WST4KAJO.js";
|
|
20
|
+
import {
|
|
21
|
+
AbortFlowError,
|
|
22
|
+
exitWithError
|
|
23
|
+
} from "./U4V3W7MN.js";
|
|
24
|
+
import {
|
|
25
|
+
isPlainObject,
|
|
26
|
+
logger
|
|
27
|
+
} from "./KYZEVOA2.js";
|
|
28
|
+
import "./Z2I5YXYN.js";
|
|
29
|
+
import "./5ILQMFXK.js";
|
|
30
|
+
|
|
31
|
+
// src/commands/drift/index.ts
|
|
32
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
33
|
+
import path5 from "node:path";
|
|
34
|
+
|
|
35
|
+
// src/commands/drift/log-formats/har.ts
|
|
36
|
+
import path from "node:path";
|
|
37
|
+
function formParamsToBody(mimeType, params) {
|
|
38
|
+
if (normalizeContentType(mimeType) !== "application/x-www-form-urlencoded") {
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
if (!Array.isArray(params)) {
|
|
42
|
+
return void 0;
|
|
43
|
+
}
|
|
44
|
+
const searchParams = new URLSearchParams();
|
|
45
|
+
for (const param of params) {
|
|
46
|
+
if (!isPlainObject(param)) {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const name = coerceString(param.name);
|
|
50
|
+
if (name === void 0) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
searchParams.append(name, coerceString(param.value) ?? "");
|
|
54
|
+
}
|
|
55
|
+
return searchParams.size > 0 ? searchParams.toString() : void 0;
|
|
56
|
+
}
|
|
57
|
+
function normalizeHarEntry(entry, index, source) {
|
|
58
|
+
const request = entry?.request;
|
|
59
|
+
const response = entry?.response;
|
|
60
|
+
if (!request) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const postData = request?.postData;
|
|
64
|
+
const requestContentType = coerceString(postData?.mimeType);
|
|
65
|
+
const requestBody = postData?.text ?? formParamsToBody(requestContentType, postData?.params);
|
|
66
|
+
const requestEncoding = coerceString(postData?.encoding);
|
|
67
|
+
const responseBody = response?.content?.text;
|
|
68
|
+
const responseEncoding = coerceString(response?.content?.encoding);
|
|
69
|
+
const responseStatus = coerceNumber(response?.status);
|
|
70
|
+
return createNormalizedExchange(
|
|
71
|
+
{
|
|
72
|
+
method: coerceString(request?.method),
|
|
73
|
+
url: coerceString(request?.url),
|
|
74
|
+
requestHeaders: request?.headers,
|
|
75
|
+
requestBody: decodeBody(requestBody, requestEncoding),
|
|
76
|
+
requestContentType,
|
|
77
|
+
responseStatus: responseStatus === 0 ? void 0 : responseStatus,
|
|
78
|
+
responseStatusText: coerceString(response?.statusText),
|
|
79
|
+
responseHeaders: response?.headers,
|
|
80
|
+
responseBody: decodeBody(responseBody, responseEncoding),
|
|
81
|
+
responseContentType: coerceString(response?.content?.mimeType),
|
|
82
|
+
startedAt: coerceString(entry?.startedDateTime),
|
|
83
|
+
raw: entry
|
|
84
|
+
},
|
|
85
|
+
index,
|
|
86
|
+
source
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
var HarTrafficParser = class {
|
|
90
|
+
id = "har";
|
|
91
|
+
canParse(filePath, probe) {
|
|
92
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
93
|
+
if (ext === ".har") {
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const normalizedProbe = probe.toLowerCase();
|
|
97
|
+
return normalizedProbe.includes('"log"') && normalizedProbe.includes('"entries"');
|
|
98
|
+
}
|
|
99
|
+
async *parse(filePath) {
|
|
100
|
+
let index = 0;
|
|
101
|
+
for await (const entry of iterateJsonArray(filePath, "log.entries")) {
|
|
102
|
+
const exchange = normalizeHarEntry(entry, index, filePath);
|
|
103
|
+
if (exchange) {
|
|
104
|
+
yield exchange;
|
|
105
|
+
}
|
|
106
|
+
index += 1;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
// src/commands/drift/log-formats/kong.ts
|
|
112
|
+
import path2 from "node:path";
|
|
113
|
+
function buildKongUrl(request) {
|
|
114
|
+
const directUrl = coerceString(request.url ?? request.uri);
|
|
115
|
+
if (directUrl) {
|
|
116
|
+
return { url: directUrl };
|
|
117
|
+
}
|
|
118
|
+
const host = coerceString(request.host ?? request.headers?.host);
|
|
119
|
+
const requestPath = coerceString(request.path ?? request.request_uri);
|
|
120
|
+
const explicitScheme = coerceString(request.scheme ?? request.forwarded_proto);
|
|
121
|
+
if (host && requestPath) {
|
|
122
|
+
return {
|
|
123
|
+
url: `${explicitScheme ?? "http"}://${host}${requestPath}`,
|
|
124
|
+
schemeKnown: Boolean(explicitScheme)
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
return { url: requestPath, schemeKnown: Boolean(explicitScheme) };
|
|
128
|
+
}
|
|
129
|
+
function normalizeKongRecord(record, index, source) {
|
|
130
|
+
const request = record?.request ?? record?.req ?? record?.http?.request;
|
|
131
|
+
if (!request) {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
const response = record?.response ?? record?.res ?? record?.http?.response;
|
|
135
|
+
const responseBody = response?.body ?? response?.raw_body ?? response?.payload ?? response?.data;
|
|
136
|
+
const requestBody = request?.body ?? request?.raw_body ?? request?.payload ?? request?.data;
|
|
137
|
+
const { url, schemeKnown } = buildKongUrl(request);
|
|
138
|
+
return createNormalizedExchange(
|
|
139
|
+
{
|
|
140
|
+
method: coerceString(request?.method ?? request?.http_method),
|
|
141
|
+
url,
|
|
142
|
+
schemeKnown,
|
|
143
|
+
requestHeaders: request?.headers,
|
|
144
|
+
requestBody,
|
|
145
|
+
requestContentType: pickHeaderContentType(request?.headers),
|
|
146
|
+
responseStatus: coerceNumber(response?.status ?? response?.statusCode),
|
|
147
|
+
responseHeaders: response?.headers,
|
|
148
|
+
responseBody,
|
|
149
|
+
responseContentType: pickHeaderContentType(response?.headers),
|
|
150
|
+
startedAt: coerceString(record?.started_at ?? record?.startedAt ?? record?.timestamp),
|
|
151
|
+
raw: record
|
|
152
|
+
},
|
|
153
|
+
index,
|
|
154
|
+
source
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
var KongTrafficParser = class {
|
|
158
|
+
id = "kong";
|
|
159
|
+
canParse(filePath, probe) {
|
|
160
|
+
const ext = path2.extname(filePath).toLowerCase();
|
|
161
|
+
if (ext === ".kong") {
|
|
162
|
+
return true;
|
|
163
|
+
}
|
|
164
|
+
const lowerProbe = probe.toLowerCase();
|
|
165
|
+
return lowerProbe.includes('"latencies"') || lowerProbe.includes('"request"') && lowerProbe.includes('"response"') && lowerProbe.includes('"route"');
|
|
166
|
+
}
|
|
167
|
+
async *parse(filePath) {
|
|
168
|
+
const firstChar = (await readProbe(filePath, 16)).trim().slice(0, 1);
|
|
169
|
+
let index = 0;
|
|
170
|
+
if (firstChar === "[") {
|
|
171
|
+
for await (const record of iterateJsonArray(filePath)) {
|
|
172
|
+
const normalized = normalizeKongRecord(record, index, filePath);
|
|
173
|
+
if (normalized) {
|
|
174
|
+
yield normalized;
|
|
175
|
+
}
|
|
176
|
+
index += 1;
|
|
177
|
+
}
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
for await (const record of streamNdjsonObjects(filePath)) {
|
|
181
|
+
const normalized = normalizeKongRecord(record, index, filePath);
|
|
182
|
+
if (normalized) {
|
|
183
|
+
yield normalized;
|
|
184
|
+
}
|
|
185
|
+
index += 1;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// src/commands/drift/log-formats/ndjson.ts
|
|
191
|
+
import path3 from "node:path";
|
|
192
|
+
function getRequestCandidate(record) {
|
|
193
|
+
return record?.request ?? record?.req ?? record?.httpRequest ?? record?.http?.request ?? record;
|
|
194
|
+
}
|
|
195
|
+
function getResponseCandidate(record) {
|
|
196
|
+
return record?.response ?? record?.res ?? record?.httpResponse ?? record?.http?.response;
|
|
197
|
+
}
|
|
198
|
+
function buildUrl(record, request) {
|
|
199
|
+
const directUrl = coerceString(
|
|
200
|
+
request?.url ?? request?.uri ?? request?.requestUrl ?? record?.url
|
|
201
|
+
);
|
|
202
|
+
if (directUrl) {
|
|
203
|
+
return { url: directUrl };
|
|
204
|
+
}
|
|
205
|
+
const host = coerceString(request?.host ?? request?.headers?.host ?? record?.host);
|
|
206
|
+
const path6 = coerceString(request?.path ?? record?.path ?? request?.pathname);
|
|
207
|
+
const explicitScheme = coerceString(request?.scheme ?? record?.scheme);
|
|
208
|
+
if (host && path6) {
|
|
209
|
+
return {
|
|
210
|
+
url: `${explicitScheme ?? "http"}://${host}${path6}`,
|
|
211
|
+
schemeKnown: Boolean(explicitScheme)
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return { url: path6, schemeKnown: Boolean(explicitScheme) };
|
|
215
|
+
}
|
|
216
|
+
function normalizeGenericRecord(record, index, source) {
|
|
217
|
+
const request = getRequestCandidate(record);
|
|
218
|
+
const response = getResponseCandidate(record);
|
|
219
|
+
const requestBody = request?.body ?? request?.bodyText ?? request?.payload ?? request?.data ?? request?.rawBody;
|
|
220
|
+
const responseBody = response?.body ?? response?.bodyText ?? response?.payload ?? response?.data ?? response?.rawBody;
|
|
221
|
+
const { url, schemeKnown } = buildUrl(record, request);
|
|
222
|
+
return createNormalizedExchange(
|
|
223
|
+
{
|
|
224
|
+
method: coerceString(
|
|
225
|
+
request?.method ?? request?.httpMethod ?? record?.method ?? record?.httpMethod
|
|
226
|
+
),
|
|
227
|
+
url,
|
|
228
|
+
schemeKnown,
|
|
229
|
+
requestHeaders: request?.headers,
|
|
230
|
+
requestBody,
|
|
231
|
+
requestContentType: pickHeaderContentType(request?.headers),
|
|
232
|
+
responseStatus: coerceNumber(
|
|
233
|
+
response?.status ?? response?.statusCode ?? record?.status ?? record?.statusCode
|
|
234
|
+
),
|
|
235
|
+
responseHeaders: response?.headers,
|
|
236
|
+
responseBody,
|
|
237
|
+
responseContentType: pickHeaderContentType(response?.headers),
|
|
238
|
+
startedAt: coerceString(record?.startedAt ?? record?.timestamp ?? record?.time),
|
|
239
|
+
raw: record
|
|
240
|
+
},
|
|
241
|
+
index,
|
|
242
|
+
source
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
var NdjsonTrafficParser = class {
|
|
246
|
+
id = "ndjson";
|
|
247
|
+
canParse(filePath, probe) {
|
|
248
|
+
const ext = path3.extname(filePath).toLowerCase();
|
|
249
|
+
if (ext === ".ndjson" || ext === ".jsonl" || ext === ".jsonlines") {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
const lastNewlineIndex = probe.lastIndexOf("\n");
|
|
253
|
+
const completeLines = lastNewlineIndex === -1 ? probe : probe.slice(0, lastNewlineIndex);
|
|
254
|
+
const firstLine = completeLines.split("\n").map((line) => line.trim()).find(Boolean);
|
|
255
|
+
if (!firstLine?.startsWith("{") || !firstLine.endsWith("}")) {
|
|
256
|
+
return false;
|
|
257
|
+
}
|
|
258
|
+
try {
|
|
259
|
+
JSON.parse(firstLine);
|
|
260
|
+
return true;
|
|
261
|
+
} catch {
|
|
262
|
+
return false;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async *parse(filePath) {
|
|
266
|
+
let index = 0;
|
|
267
|
+
for await (const record of streamNdjsonObjects(filePath)) {
|
|
268
|
+
const normalized = normalizeGenericRecord(record, index, filePath);
|
|
269
|
+
if (normalized) {
|
|
270
|
+
yield normalized;
|
|
271
|
+
}
|
|
272
|
+
index += 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// src/commands/drift/log-formats/webserver-json.ts
|
|
278
|
+
import path4 from "node:path";
|
|
279
|
+
function parseRequestLine(requestLine) {
|
|
280
|
+
if (!requestLine) {
|
|
281
|
+
return {};
|
|
282
|
+
}
|
|
283
|
+
const parts = requestLine.trim().split(/\s+/);
|
|
284
|
+
if (parts.length < 2) {
|
|
285
|
+
return {};
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
method: parts[0],
|
|
289
|
+
path: parts[1],
|
|
290
|
+
protocol: parts[2]
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function buildUrl2(record, request) {
|
|
294
|
+
const directUrl = coerceString(
|
|
295
|
+
request?.url ?? request?.request_uri ?? request?.uri ?? record?.url ?? record?.request_url ?? record?.absolute_uri
|
|
296
|
+
);
|
|
297
|
+
const requestLine = parseRequestLine(coerceString(record?.request ?? request?.request_line));
|
|
298
|
+
const requestPath = coerceString(
|
|
299
|
+
request?.request_uri ?? request?.uri ?? request?.path ?? record?.request_uri ?? record?.uri
|
|
300
|
+
) ?? requestLine.path;
|
|
301
|
+
if (directUrl?.startsWith("http://") || directUrl?.startsWith("https://")) {
|
|
302
|
+
return { url: directUrl };
|
|
303
|
+
}
|
|
304
|
+
const host = coerceString(
|
|
305
|
+
request?.host ?? request?.headers?.host ?? record?.host ?? record?.http_host ?? record?.server_name ?? record?.vhost
|
|
306
|
+
);
|
|
307
|
+
const explicitScheme = coerceString(record?.scheme ?? request?.scheme ?? record?.request_scheme);
|
|
308
|
+
const schemeKnown = Boolean(explicitScheme);
|
|
309
|
+
if (host && (directUrl || requestPath)) {
|
|
310
|
+
const scheme = explicitScheme ?? "http";
|
|
311
|
+
const targetPath = directUrl ?? requestPath;
|
|
312
|
+
if (targetPath?.startsWith("/")) {
|
|
313
|
+
return { url: `${scheme}://${host}${targetPath}`, schemeKnown };
|
|
314
|
+
}
|
|
315
|
+
return { url: `${scheme}://${host}/${targetPath}`, schemeKnown };
|
|
316
|
+
}
|
|
317
|
+
return { url: directUrl ?? requestPath, schemeKnown };
|
|
318
|
+
}
|
|
319
|
+
function normalizeWebServerRecord(record, index, source) {
|
|
320
|
+
const request = isPlainObject(record?.request) ? record.request : record;
|
|
321
|
+
const response = isPlainObject(record?.response) ? record.response : void 0;
|
|
322
|
+
const requestLine = parseRequestLine(coerceString(record?.request));
|
|
323
|
+
const method = coerceString(
|
|
324
|
+
request?.method ?? request?.request_method ?? record?.request_method ?? record?.method
|
|
325
|
+
) ?? requestLine.method;
|
|
326
|
+
const { url, schemeKnown } = buildUrl2(record, request);
|
|
327
|
+
return createNormalizedExchange(
|
|
328
|
+
{
|
|
329
|
+
method,
|
|
330
|
+
url,
|
|
331
|
+
schemeKnown,
|
|
332
|
+
requestHeaders: request?.headers ?? request?.request_headers ?? record?.request_headers ?? record?.headers,
|
|
333
|
+
requestBody: request?.body ?? request?.request_body ?? record?.request_body ?? record?.body,
|
|
334
|
+
requestContentType: pickHeaderContentType(
|
|
335
|
+
request?.headers ?? request?.request_headers ?? record?.request_headers
|
|
336
|
+
),
|
|
337
|
+
responseStatus: coerceNumber(
|
|
338
|
+
response?.status ?? response?.status_code ?? record?.status ?? record?.status_code ?? record?.response_status
|
|
339
|
+
),
|
|
340
|
+
responseHeaders: response?.headers ?? response?.response_headers ?? record?.response_headers ?? record?.headers_out,
|
|
341
|
+
responseBody: response?.body ?? response?.response_body ?? record?.response_body,
|
|
342
|
+
responseContentType: pickHeaderContentType(
|
|
343
|
+
response?.headers ?? response?.response_headers ?? record?.response_headers
|
|
344
|
+
),
|
|
345
|
+
startedAt: coerceString(
|
|
346
|
+
record?.time_iso8601 ?? record?.timestamp ?? record?.time ?? record?.time_local ?? record?.["@timestamp"]
|
|
347
|
+
),
|
|
348
|
+
raw: record
|
|
349
|
+
},
|
|
350
|
+
index,
|
|
351
|
+
source
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
async function* parseJsonFile(filePath) {
|
|
355
|
+
const firstChar = (await readProbe(filePath, 16)).trim().slice(0, 1);
|
|
356
|
+
if (firstChar === "[") {
|
|
357
|
+
yield* iterateJsonArray(filePath);
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
for await (const record of streamNdjsonObjects(filePath)) {
|
|
361
|
+
yield record;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
var NginxJsonTrafficParser = class {
|
|
365
|
+
id = "nginx-json";
|
|
366
|
+
canParse(filePath, probe) {
|
|
367
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
368
|
+
const lowerFilePath = filePath.toLowerCase();
|
|
369
|
+
if (ext === ".ndjson" && lowerFilePath.includes("nginx")) {
|
|
370
|
+
return true;
|
|
371
|
+
}
|
|
372
|
+
const lowerProbe = probe.toLowerCase();
|
|
373
|
+
return lowerProbe.includes('"request_uri"') || lowerProbe.includes('"time_iso8601"') || lowerProbe.includes('"upstream_response_time"') || lowerProbe.includes('"remote_addr"');
|
|
374
|
+
}
|
|
375
|
+
async *parse(filePath) {
|
|
376
|
+
let index = 0;
|
|
377
|
+
for await (const record of parseJsonFile(filePath)) {
|
|
378
|
+
const normalized = normalizeWebServerRecord(record, index, filePath);
|
|
379
|
+
if (normalized) {
|
|
380
|
+
yield normalized;
|
|
381
|
+
}
|
|
382
|
+
index += 1;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
var ApacheJsonTrafficParser = class {
|
|
387
|
+
id = "apache-json";
|
|
388
|
+
canParse(filePath, probe) {
|
|
389
|
+
const ext = path4.extname(filePath).toLowerCase();
|
|
390
|
+
const lowerFilePath = filePath.toLowerCase();
|
|
391
|
+
if (ext === ".ndjson" && lowerFilePath.includes("apache")) {
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
const lowerProbe = probe.toLowerCase();
|
|
395
|
+
return lowerProbe.includes('"request_method"') || lowerProbe.includes('"request_uri"') || lowerProbe.includes('"vhost"') || lowerProbe.includes('"response_status"');
|
|
396
|
+
}
|
|
397
|
+
async *parse(filePath) {
|
|
398
|
+
let index = 0;
|
|
399
|
+
for await (const record of parseJsonFile(filePath)) {
|
|
400
|
+
const normalized = normalizeWebServerRecord(record, index, filePath);
|
|
401
|
+
if (normalized) {
|
|
402
|
+
yield normalized;
|
|
403
|
+
}
|
|
404
|
+
index += 1;
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
// src/commands/drift/log-formats/registry.ts
|
|
410
|
+
var PARSERS = [
|
|
411
|
+
new HarTrafficParser(),
|
|
412
|
+
new KongTrafficParser(),
|
|
413
|
+
new NginxJsonTrafficParser(),
|
|
414
|
+
new ApacheJsonTrafficParser(),
|
|
415
|
+
new NdjsonTrafficParser()
|
|
416
|
+
];
|
|
417
|
+
async function selectTrafficParser(filePath, format) {
|
|
418
|
+
if (format !== "auto") {
|
|
419
|
+
const parser = PARSERS.find((candidate) => candidate.id === format);
|
|
420
|
+
if (!parser) {
|
|
421
|
+
throw new Error(`Unsupported parser format: ${format}`);
|
|
422
|
+
}
|
|
423
|
+
return parser;
|
|
424
|
+
}
|
|
425
|
+
const probe = await readProbe(filePath);
|
|
426
|
+
return PARSERS.find((candidate) => candidate.canParse(filePath, probe));
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// src/commands/drift/engine/runner.ts
|
|
430
|
+
async function runTrafficValidation(options) {
|
|
431
|
+
const trafficFiles = await listFilesRecursively(options.trafficPath);
|
|
432
|
+
if (trafficFiles.length === 0) {
|
|
433
|
+
throw new Error("No traffic files found in the provided traffic path.");
|
|
434
|
+
}
|
|
435
|
+
const session = ValidationSession.create({
|
|
436
|
+
openApiIndex: options.openApiIndex,
|
|
437
|
+
matchMode: options.matchMode,
|
|
438
|
+
ignoreCookies: options.ignoreCookies,
|
|
439
|
+
previewFindingsLimit: options.previewFindingsLimit,
|
|
440
|
+
activeRules: options.activeRules,
|
|
441
|
+
server: options.server,
|
|
442
|
+
minSeverity: options.minSeverity
|
|
443
|
+
});
|
|
444
|
+
let supportedTrafficFileCount = 0;
|
|
445
|
+
let exchangeIndex = 0;
|
|
446
|
+
for (const trafficFile of trafficFiles) {
|
|
447
|
+
const parser = await selectTrafficParser(trafficFile, options.format);
|
|
448
|
+
if (!parser) {
|
|
449
|
+
logger.warn(`Skipping traffic file with unrecognized format: ${trafficFile}
|
|
450
|
+
`);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
supportedTrafficFileCount += 1;
|
|
454
|
+
for await (const exchange of parser.parse(trafficFile)) {
|
|
455
|
+
await session.process({ ...exchange, index: exchangeIndex });
|
|
456
|
+
exchangeIndex += 1;
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
if (supportedTrafficFileCount === 0) {
|
|
460
|
+
throw new Error(
|
|
461
|
+
"No supported traffic files found. In auto mode, files must match built-in traffic parser signatures."
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
if (exchangeIndex === 0) {
|
|
465
|
+
throw new Error("No HTTP exchanges were parsed from the provided traffic files.");
|
|
466
|
+
}
|
|
467
|
+
return session.finalize();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// src/commands/drift/index.ts
|
|
471
|
+
var USE_COLOR = Boolean(process.stdout.isTTY) && process.env.NO_COLOR === void 0;
|
|
472
|
+
function collectSpecServerUrls(openApiIndex) {
|
|
473
|
+
const urls = /* @__PURE__ */ new Set();
|
|
474
|
+
for (const operations of openApiIndex.operationsByMethod.values()) {
|
|
475
|
+
for (const operation of operations) {
|
|
476
|
+
for (const server of operation.servers) {
|
|
477
|
+
urls.add(server.rawUrl);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return Array.from(urls).sort();
|
|
482
|
+
}
|
|
483
|
+
function warnWhenNothingMatched(summary, openApiIndex, server) {
|
|
484
|
+
const validatedExchanges = summary.totalExchanges - summary.skippedExchanges;
|
|
485
|
+
if (validatedExchanges === 0 && summary.skippedExchanges > 0) {
|
|
486
|
+
logger.warn(
|
|
487
|
+
`All ${summary.skippedExchanges} exchange(s) were outside the --server "${server}" and were skipped. Check that the server matches the traffic URLs.
|
|
488
|
+
`
|
|
489
|
+
);
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
492
|
+
if (summary.documentedExchanges > 0 || validatedExchanges === 0) {
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
const serverUrls = collectSpecServerUrls(openApiIndex);
|
|
496
|
+
const hint = server ? `Check that the --server "${server}" matches the traffic URLs and that the description paths align with the remainder.` : summary.hostCompatibleExchanges === validatedExchanges ? `The traffic hosts are compatible with the description servers (${serverUrls.join(
|
|
497
|
+
", "
|
|
498
|
+
)}), so the endpoints are likely undocumented; if they should be documented, check that the description base paths and paths align with the traffic URLs, or use --server to declare the server the traffic was captured against.` : `Check that the traffic host and base path match the description servers (${serverUrls.join(
|
|
499
|
+
", "
|
|
500
|
+
)}), or use --server to declare the server the traffic was captured against.`;
|
|
501
|
+
logger.warn(
|
|
502
|
+
`None of the ${validatedExchanges} validated exchange(s) matched a documented operation. ${hint}
|
|
503
|
+
`
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
async function writeOutput(outputPath, content) {
|
|
507
|
+
const resolved = normalizeFsPath(outputPath);
|
|
508
|
+
await mkdir(path5.dirname(resolved), { recursive: true });
|
|
509
|
+
await writeFile(resolved, content, "utf8");
|
|
510
|
+
}
|
|
511
|
+
async function handleDrift({ argv, config }) {
|
|
512
|
+
const trafficPath = normalizeFsPath(argv.traffic);
|
|
513
|
+
const trafficFormat = argv["traffic-format"];
|
|
514
|
+
const activeRules = argv.rules ? parseCsv(argv.rules) : void 0;
|
|
515
|
+
const server = argv.server;
|
|
516
|
+
if (server && argv["match-mode"]) {
|
|
517
|
+
return exitWithError(
|
|
518
|
+
"The --server and --match-mode options are mutually exclusive: --match-mode controls how requests are located via the description servers, while --server replaces the description servers with the one the traffic was captured against."
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const matchMode = argv["match-mode"] ?? "strict-host";
|
|
522
|
+
const specPath = normalizeFsPath(argv.api);
|
|
523
|
+
const openApiIndex = await loadOpenApiIndex(specPath, config);
|
|
524
|
+
if (openApiIndex.loadedOperations === 0) {
|
|
525
|
+
return exitWithError(`No OpenAPI operations were loaded from: ${specPath}`);
|
|
526
|
+
}
|
|
527
|
+
const { runId, summary, findings } = await runTrafficValidation({
|
|
528
|
+
trafficPath,
|
|
529
|
+
format: trafficFormat,
|
|
530
|
+
matchMode,
|
|
531
|
+
ignoreCookies: argv["ignore-cookies"],
|
|
532
|
+
previewFindingsLimit: argv["max-findings"],
|
|
533
|
+
activeRules,
|
|
534
|
+
openApiIndex,
|
|
535
|
+
server,
|
|
536
|
+
minSeverity: argv["min-severity"]
|
|
537
|
+
});
|
|
538
|
+
warnWhenNothingMatched(summary, openApiIndex, server);
|
|
539
|
+
const report = renderReport(
|
|
540
|
+
{
|
|
541
|
+
runId,
|
|
542
|
+
summary,
|
|
543
|
+
findings,
|
|
544
|
+
meta: {
|
|
545
|
+
specSource: specPath,
|
|
546
|
+
trafficPath,
|
|
547
|
+
format: trafficFormat,
|
|
548
|
+
matchMode,
|
|
549
|
+
server
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
{
|
|
553
|
+
format: argv["report-format"],
|
|
554
|
+
color: USE_COLOR && argv["report-format"] === "pretty" && !argv.output,
|
|
555
|
+
maxFindings: argv["max-findings"]
|
|
556
|
+
}
|
|
557
|
+
);
|
|
558
|
+
if (argv.output) {
|
|
559
|
+
await writeOutput(argv.output, report);
|
|
560
|
+
logger.info(`Drift report written to: ${normalizeFsPath(argv.output)}
|
|
561
|
+
`);
|
|
562
|
+
} else {
|
|
563
|
+
logger.output(report);
|
|
564
|
+
}
|
|
565
|
+
if (summary.findingsBySeverity.error > 0) {
|
|
566
|
+
throw new AbortFlowError("Drift detected.");
|
|
567
|
+
}
|
|
568
|
+
}
|
|
569
|
+
export {
|
|
570
|
+
handleDrift
|
|
571
|
+
};
|