@alwaysmeticulous/debug-workspace 2.302.0 → 2.304.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/dist/__tests__/redact-cookies-and-headers.spec.d.ts +2 -0
- package/dist/__tests__/redact-cookies-and-headers.spec.d.ts.map +1 -0
- package/dist/__tests__/redact-cookies-and-headers.spec.js +398 -0
- package/dist/__tests__/redact-cookies-and-headers.spec.js.map +1 -0
- package/dist/__tests__/redact-log-credentials.spec.d.ts +2 -0
- package/dist/__tests__/redact-log-credentials.spec.d.ts.map +1 -0
- package/dist/__tests__/redact-log-credentials.spec.js +125 -0
- package/dist/__tests__/redact-log-credentials.spec.js.map +1 -0
- package/dist/generate-debug-workspace.d.ts +4 -0
- package/dist/generate-debug-workspace.d.ts.map +1 -1
- package/dist/generate-debug-workspace.js +382 -11
- package/dist/generate-debug-workspace.js.map +1 -1
- package/dist/templates/CLAUDE.md +6 -1
- package/package.json +3 -3
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="
|
|
2
|
+
!function(){try{var e="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof globalThis?globalThis:"undefined"!=typeof self?self:{},n=(new e.Error).stack;n&&(e._sentryDebugIds=e._sentryDebugIds||{},e._sentryDebugIds[n]="b0e8824a-2c75-553c-abd1-f66bad1029e6")}catch(e){}}();
|
|
3
3
|
|
|
4
4
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
5
5
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
6
6
|
};
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
-
exports.detectSnapshotAssets = exports.generateDebugWorkspace = void 0;
|
|
8
|
+
exports.redactLogCredentials = exports.generateFilteredLogs = exports.redactCookiesAndHeaders = exports.redactCookiesAndHeadersInJson = exports.detectSnapshotAssets = exports.generateDebugWorkspace = void 0;
|
|
9
9
|
const child_process_1 = require("child_process");
|
|
10
10
|
const crypto_1 = require("crypto");
|
|
11
11
|
const fs_1 = require("fs");
|
|
@@ -23,7 +23,8 @@ const generateDebugWorkspace = async (options) => {
|
|
|
23
23
|
const { client, debugContext, workspaceDir } = options;
|
|
24
24
|
const claudeDir = (0, path_1.join)(workspaceDir, ".claude");
|
|
25
25
|
(0, fs_1.mkdirSync)(claudeDir, { recursive: true });
|
|
26
|
-
|
|
26
|
+
(0, exports.redactCookiesAndHeaders)(debugContext, workspaceDir);
|
|
27
|
+
(0, exports.generateFilteredLogs)(workspaceDir);
|
|
27
28
|
generateLogDiffs(debugContext, workspaceDir);
|
|
28
29
|
generateDiffSummaries(workspaceDir);
|
|
29
30
|
generatePrDiff(debugContext, workspaceDir);
|
|
@@ -236,29 +237,399 @@ const detectSnapshotAssets = (workspaceDir) => {
|
|
|
236
237
|
};
|
|
237
238
|
exports.detectSnapshotAssets = detectSnapshotAssets;
|
|
238
239
|
// ---------------------------------------------------------------------------
|
|
240
|
+
// Cookie, header & storage redaction
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
const REDACTED_VALUE_MARKER = "<redacted>";
|
|
243
|
+
// Array-valued keys whose entries are `{ name, value }` or `{ key, value }`
|
|
244
|
+
// pairs (or, in the case of `cookies.json`, richer cookie objects) where
|
|
245
|
+
// only `value` holds sensitive data, regardless of what object contains
|
|
246
|
+
// them.
|
|
247
|
+
const NAME_VALUE_ARRAY_KEYS = new Set([
|
|
248
|
+
"headers",
|
|
249
|
+
"cookies",
|
|
250
|
+
// Flat shape used by `applicationStorageDataOverride` inside
|
|
251
|
+
// `launchBrowserAndReplayParams.json` (see `ApplicationStorage`).
|
|
252
|
+
"localStorageEntries",
|
|
253
|
+
"sessionStorageEntries",
|
|
254
|
+
// Fixed/override authentication values baked into
|
|
255
|
+
// `replayExecutionOptions` inside `launchBrowserAndReplayParams.json` --
|
|
256
|
+
// these carry the project's configured authentication credentials (e.g.
|
|
257
|
+
// fixed auth cookies/storage entries) in plaintext, same shape as
|
|
258
|
+
// `cookies` / `*StorageEntries` above (see `ReplayExecutionOptions`).
|
|
259
|
+
"extraCookies",
|
|
260
|
+
"extraLocalStorageEntries",
|
|
261
|
+
"extraSessionStorageEntries",
|
|
262
|
+
// Flat HAR-style header arrays used by `NetworkRequestSnapshotData` (custom
|
|
263
|
+
// check network snapshots) instead of nesting under a `request`/`response`
|
|
264
|
+
// object with a `headers` key.
|
|
265
|
+
"requestHeaders",
|
|
266
|
+
"responseHeaders",
|
|
267
|
+
]);
|
|
268
|
+
// Scalar (non-array) fields that hold a secret token directly, rather than a
|
|
269
|
+
// name/value pair. `session.recordingToken` (camelCase, on the `session:
|
|
270
|
+
// RecordedSession` embedded in `launchBrowserAndReplayParams.json`) and
|
|
271
|
+
// `recording_token` (snake_case, on session `data.json`) both hold the
|
|
272
|
+
// project's recording token in plaintext.
|
|
273
|
+
const SENSITIVE_SCALAR_KEYS = new Set([
|
|
274
|
+
"recordingToken",
|
|
275
|
+
"recording_token",
|
|
276
|
+
]);
|
|
277
|
+
// `randomEvents.localStorage.state` / `randomEvents.sessionStorage.state` on
|
|
278
|
+
// session `data.json` nest their `{ key, value }` entries one level deeper
|
|
279
|
+
// than `localStorageEntries` / `sessionStorageEntries` above, so a `state`
|
|
280
|
+
// array is only redacted when its containing object was reached via one of
|
|
281
|
+
// these keys.
|
|
282
|
+
const STORAGE_STATE_CONTAINER_KEYS = new Set([
|
|
283
|
+
"localStorage",
|
|
284
|
+
"sessionStorage",
|
|
285
|
+
]);
|
|
286
|
+
// Backend span attributes (see `HttpSpanAttributes`) store raw header values
|
|
287
|
+
// as `http.request.header.<name>` / `http.response.header.<name>` arrays of
|
|
288
|
+
// strings, rather than `{ name, value }` pairs.
|
|
289
|
+
const HTTP_HEADER_ATTRIBUTE_KEY_RE = /^http\.(?:request|response)\.header\./;
|
|
290
|
+
// `replayExecutionOptions.customRequestHeaders` (see `InjectableRequestHeader`)
|
|
291
|
+
// carries the project's configured custom request headers, which can embed a
|
|
292
|
+
// plaintext secret value one level deeper than the other name/value shapes:
|
|
293
|
+
// `{ name, value: { type: "static", value: "<secret>" }, requestTargets }`.
|
|
294
|
+
const NESTED_STATIC_VALUE_ARRAY_KEYS = new Set([
|
|
295
|
+
"customRequestHeaders",
|
|
296
|
+
]);
|
|
297
|
+
const isRedactableArrayKey = (key, containerKey) => {
|
|
298
|
+
if (!key) {
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
if (NAME_VALUE_ARRAY_KEYS.has(key)) {
|
|
302
|
+
return true;
|
|
303
|
+
}
|
|
304
|
+
return (key === "state" &&
|
|
305
|
+
containerKey != null &&
|
|
306
|
+
STORAGE_STATE_CONTAINER_KEYS.has(containerKey));
|
|
307
|
+
};
|
|
308
|
+
// Recursively redacts cookie, header, storage, and secret-token *values*
|
|
309
|
+
// anywhere in a parsed JSON value, while preserving their names/keys (and
|
|
310
|
+
// array lengths) so an agent can still see which cookies/headers/storage
|
|
311
|
+
// entries were present and how many values a given header had. `key` is the
|
|
312
|
+
// property name `value` was reached through (if any) and `containerKey` is
|
|
313
|
+
// the key of the object that held that property one level up -- both are
|
|
314
|
+
// only used to decide whether an array should be treated as redactable.
|
|
315
|
+
// Handles:
|
|
316
|
+
// - HAR-style `headers: [{ name, value }, ...]` arrays, wherever they occur
|
|
317
|
+
// (e.g. inside `pollyHAR` entries' `request`/`response` objects).
|
|
318
|
+
// - `cookies: [{ name, value, domain, ... }, ...]` arrays, e.g. on session
|
|
319
|
+
// `data.json`, and `replayExecutionOptions.extraCookies`.
|
|
320
|
+
// - `randomEvents.localStorage.state` / `randomEvents.sessionStorage.state`,
|
|
321
|
+
// the flat `localStorageEntries` / `sessionStorageEntries` arrays, and
|
|
322
|
+
// `replayExecutionOptions.extraLocalStorageEntries` /
|
|
323
|
+
// `extraSessionStorageEntries` -- all arrays of `{ key, value }` entries.
|
|
324
|
+
// - `replayExecutionOptions.customRequestHeaders`, whose entries nest their
|
|
325
|
+
// secret value one level deeper (`{ name, value: { type, value } }`).
|
|
326
|
+
// - Backend span `http.request.header.*` / `http.response.header.*`
|
|
327
|
+
// attributes.
|
|
328
|
+
// - Scalar secret tokens such as `session.recordingToken` / the session
|
|
329
|
+
// `data.json` `recording_token` field.
|
|
330
|
+
// Exported for testing.
|
|
331
|
+
const redactCookiesAndHeadersInJson = (value, key, containerKey) => {
|
|
332
|
+
if (Array.isArray(value)) {
|
|
333
|
+
if (key && NESTED_STATIC_VALUE_ARRAY_KEYS.has(key)) {
|
|
334
|
+
return value.map(redactNestedStaticValueEntry);
|
|
335
|
+
}
|
|
336
|
+
if (isRedactableArrayKey(key, containerKey)) {
|
|
337
|
+
return value.map(redactNameValueEntryValue);
|
|
338
|
+
}
|
|
339
|
+
return value.map((item) => (0, exports.redactCookiesAndHeadersInJson)(item));
|
|
340
|
+
}
|
|
341
|
+
if (value !== null && typeof value === "object") {
|
|
342
|
+
const result = {};
|
|
343
|
+
for (const [childKey, childValue] of Object.entries(value)) {
|
|
344
|
+
if (SENSITIVE_SCALAR_KEYS.has(childKey) &&
|
|
345
|
+
typeof childValue === "string") {
|
|
346
|
+
result[childKey] = REDACTED_VALUE_MARKER;
|
|
347
|
+
}
|
|
348
|
+
else if (HTTP_HEADER_ATTRIBUTE_KEY_RE.test(childKey) &&
|
|
349
|
+
Array.isArray(childValue)) {
|
|
350
|
+
result[childKey] = childValue.map(() => REDACTED_VALUE_MARKER);
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
result[childKey] = (0, exports.redactCookiesAndHeadersInJson)(childValue, childKey, key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
return value;
|
|
359
|
+
};
|
|
360
|
+
exports.redactCookiesAndHeadersInJson = redactCookiesAndHeadersInJson;
|
|
361
|
+
const redactNameValueEntryValue = (item) => {
|
|
362
|
+
if (item !== null && typeof item === "object" && "value" in item) {
|
|
363
|
+
return {
|
|
364
|
+
...item,
|
|
365
|
+
value: REDACTED_VALUE_MARKER,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
return (0, exports.redactCookiesAndHeadersInJson)(item);
|
|
369
|
+
};
|
|
370
|
+
const redactNestedStaticValueEntry = (item) => {
|
|
371
|
+
if (item !== null &&
|
|
372
|
+
typeof item === "object" &&
|
|
373
|
+
"value" in item &&
|
|
374
|
+
item.value !== null &&
|
|
375
|
+
typeof item.value === "object" &&
|
|
376
|
+
"value" in item.value) {
|
|
377
|
+
const itemRecord = item;
|
|
378
|
+
return {
|
|
379
|
+
...itemRecord,
|
|
380
|
+
value: { ...itemRecord.value, value: REDACTED_VALUE_MARKER },
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
return (0, exports.redactCookiesAndHeadersInJson)(item);
|
|
384
|
+
};
|
|
385
|
+
// Redacts a JSON file in place. `topLevelKey` lets callers tell the redactor
|
|
386
|
+
// that the *root* of the file (not just a nested property) should be treated
|
|
387
|
+
// as a redactable array -- needed for `cookies.json`, which is uploaded as a
|
|
388
|
+
// bare `Cookie[]` rather than `{ cookies: [...] }`.
|
|
389
|
+
const redactCookiesAndHeadersInFile = (filePath, topLevelKey) => {
|
|
390
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
391
|
+
return false;
|
|
392
|
+
}
|
|
393
|
+
try {
|
|
394
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(filePath, "utf8"));
|
|
395
|
+
const redacted = (0, exports.redactCookiesAndHeadersInJson)(parsed, topLevelKey);
|
|
396
|
+
(0, fs_1.writeFileSync)(filePath, JSON.stringify(redacted, null, 2));
|
|
397
|
+
return true;
|
|
398
|
+
}
|
|
399
|
+
catch (error) {
|
|
400
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
401
|
+
console.warn(` Warning: Could not redact cookies/headers/storage in ${filePath}: ${message}`);
|
|
402
|
+
return false;
|
|
403
|
+
}
|
|
404
|
+
};
|
|
405
|
+
// Redacts an NDJSON file (one JSON value per line) in place, running
|
|
406
|
+
// `redactCookiesAndHeadersInJson` over each line independently. Used for
|
|
407
|
+
// `timeline.ndjson`, which -- unlike `timeline.json` -- is sometimes already
|
|
408
|
+
// present in the downloaded replay archive rather than generated fresh from
|
|
409
|
+
// the (by-then-redacted) `timeline.json`.
|
|
410
|
+
const redactCookiesAndHeadersInNdjsonFile = (filePath) => {
|
|
411
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
try {
|
|
415
|
+
const raw = (0, fs_1.readFileSync)(filePath, "utf8");
|
|
416
|
+
const redactedLines = raw.split("\n").map((line) => {
|
|
417
|
+
if (line.trim() === "") {
|
|
418
|
+
return line;
|
|
419
|
+
}
|
|
420
|
+
return JSON.stringify((0, exports.redactCookiesAndHeadersInJson)(JSON.parse(line)));
|
|
421
|
+
});
|
|
422
|
+
(0, fs_1.writeFileSync)(filePath, redactedLines.join("\n"));
|
|
423
|
+
return true;
|
|
424
|
+
}
|
|
425
|
+
catch (error) {
|
|
426
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
427
|
+
console.warn(` Warning: Could not redact cookies/headers/storage in ${filePath}: ${message}`);
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
// Redacts cookie, header, and local/session storage values from the raw
|
|
432
|
+
// JSON files placed into the workspace, before any other step reads or
|
|
433
|
+
// diffs them. This runs first so that downstream steps (e.g.
|
|
434
|
+
// `generateParamsDiffs`, `generateSessionSummaries`, `generateDebugDerivedFiles`)
|
|
435
|
+
// never see or propagate the real values.
|
|
436
|
+
// Exported for testing.
|
|
437
|
+
const redactCookiesAndHeaders = (debugContext, workspaceDir) => {
|
|
438
|
+
let count = 0;
|
|
439
|
+
for (const subDir of ["head", "base", "other"]) {
|
|
440
|
+
const subDirPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", subDir);
|
|
441
|
+
if (!(0, fs_1.existsSync)(subDirPath)) {
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
for (const replayId of (0, fs_1.readdirSync)(subDirPath)) {
|
|
445
|
+
const replayDir = (0, path_1.join)(subDirPath, replayId);
|
|
446
|
+
if (redactCookiesAndHeadersInFile((0, path_1.join)(replayDir, "cookies.json"), "cookies")) {
|
|
447
|
+
count++;
|
|
448
|
+
}
|
|
449
|
+
if (redactCookiesAndHeadersInFile((0, path_1.join)(replayDir, "launchBrowserAndReplayParams.json"))) {
|
|
450
|
+
count++;
|
|
451
|
+
}
|
|
452
|
+
// `timeline.json`'s network-request entries (`pollyReplay`,
|
|
453
|
+
// `passedThroughNetworkRequest`, etc.) embed full HAR `headers` arrays,
|
|
454
|
+
// including `Cookie`/`Authorization` values, for every request/response
|
|
455
|
+
// in the replay. Redact it before `generateDebugDerivedFiles` derives
|
|
456
|
+
// `timeline.ndjson` / `events-index` / `network-log` from it.
|
|
457
|
+
if (redactCookiesAndHeadersInFile((0, path_1.join)(replayDir, "timeline.json"))) {
|
|
458
|
+
count++;
|
|
459
|
+
}
|
|
460
|
+
// Usually generated fresh from (already-redacted) `timeline.json`, but
|
|
461
|
+
// redact in place too in case it was already present in the downloaded
|
|
462
|
+
// archive.
|
|
463
|
+
if (redactCookiesAndHeadersInNdjsonFile((0, path_1.join)(replayDir, "timeline.ndjson"))) {
|
|
464
|
+
count++;
|
|
465
|
+
}
|
|
466
|
+
// Feature-flagged custom-check network snapshot data, carrying the same
|
|
467
|
+
// HAR-style `requestHeaders`/`responseHeaders` in plaintext.
|
|
468
|
+
if (redactCookiesAndHeadersInFile((0, path_1.join)(replayDir, "custom-checks-snapshots", "network-requests.json"))) {
|
|
469
|
+
count++;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
for (const sessionId of debugContext.sessionIds) {
|
|
474
|
+
const dataPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "sessions", sessionId, "data.json");
|
|
475
|
+
if (redactCookiesAndHeadersInFile(dataPath)) {
|
|
476
|
+
count++;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (count > 0) {
|
|
480
|
+
console.log(chalk_1.default.green(` Redacted cookie, header, and storage values in ${count} file(s)`));
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
exports.redactCookiesAndHeaders = redactCookiesAndHeaders;
|
|
484
|
+
// ---------------------------------------------------------------------------
|
|
239
485
|
// Log filtering and diffs
|
|
240
486
|
// ---------------------------------------------------------------------------
|
|
487
|
+
// Exported for testing.
|
|
241
488
|
const generateFilteredLogs = (workspaceDir) => {
|
|
242
489
|
const replaySubDirs = ["head", "base", "other"];
|
|
243
|
-
let
|
|
490
|
+
let filteredCount = 0;
|
|
491
|
+
let redactedCount = 0;
|
|
244
492
|
for (const subDir of replaySubDirs) {
|
|
245
493
|
const subDirPath = (0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "replays", subDir);
|
|
246
494
|
if (!(0, fs_1.existsSync)(subDirPath)) {
|
|
247
495
|
continue;
|
|
248
496
|
}
|
|
249
497
|
for (const replayId of (0, fs_1.readdirSync)(subDirPath)) {
|
|
250
|
-
const
|
|
498
|
+
const replayDir = (0, path_1.join)(subDirPath, replayId);
|
|
499
|
+
// `logs.concise.txt` has no filtered counterpart, so redact credential
|
|
500
|
+
// values directly on it (in place) rather than leaving them in plaintext.
|
|
501
|
+
if (redactLogCredentialsInFile((0, path_1.join)(replayDir, "logs.concise.txt"))) {
|
|
502
|
+
redactedCount++;
|
|
503
|
+
}
|
|
504
|
+
// `logs.ndjson` is the raw source `logs.concise.txt` / `logs.deterministic.txt`
|
|
505
|
+
// are rendered from, and is also read directly by `generateDebugDerivedFiles`
|
|
506
|
+
// (for `logs-index` / `vt-progression`) -- redact it too so none of those
|
|
507
|
+
// downstream artifacts leak credentials that only this raw file still held.
|
|
508
|
+
if (redactLogCredentialsInNdjsonFile((0, path_1.join)(replayDir, "logs.ndjson"))) {
|
|
509
|
+
redactedCount++;
|
|
510
|
+
}
|
|
511
|
+
const logPath = (0, path_1.join)(replayDir, "logs.deterministic.txt");
|
|
251
512
|
if (!(0, fs_1.existsSync)(logPath)) {
|
|
252
513
|
continue;
|
|
253
514
|
}
|
|
254
515
|
const raw = (0, fs_1.readFileSync)(logPath, "utf8");
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
|
|
516
|
+
const credentialsRedacted = (0, exports.redactLogCredentials)(raw);
|
|
517
|
+
if (credentialsRedacted !== raw) {
|
|
518
|
+
(0, fs_1.writeFileSync)(logPath, credentialsRedacted);
|
|
519
|
+
redactedCount++;
|
|
520
|
+
}
|
|
521
|
+
const filtered = filterLogLines(credentialsRedacted);
|
|
522
|
+
(0, fs_1.writeFileSync)((0, path_1.join)(replayDir, "logs.deterministic.filtered.txt"), filtered);
|
|
523
|
+
filteredCount++;
|
|
258
524
|
}
|
|
259
525
|
}
|
|
260
|
-
if (
|
|
261
|
-
console.log(chalk_1.default.green(` Generated ${
|
|
526
|
+
if (filteredCount > 0) {
|
|
527
|
+
console.log(chalk_1.default.green(` Generated ${filteredCount} filtered log file(s)`));
|
|
528
|
+
}
|
|
529
|
+
if (redactedCount > 0) {
|
|
530
|
+
console.log(chalk_1.default.green(` Redacted credential values in ${redactedCount} log file(s)`));
|
|
531
|
+
}
|
|
532
|
+
};
|
|
533
|
+
exports.generateFilteredLogs = generateFilteredLogs;
|
|
534
|
+
// Header/cookie names whose values are treated as credentials in replay logs:
|
|
535
|
+
// unlike `LOG_NOISE_PATTERNS` below (general noise, only stripped from the
|
|
536
|
+
// `.filtered.txt` copy), matches here are redacted on the raw log file too,
|
|
537
|
+
// since a leaked session cookie or bearer token is a real credential rather
|
|
538
|
+
// than just diff noise.
|
|
539
|
+
const CREDENTIAL_HEADER_NAMES = "Authorization|Proxy-Authorization|Cookie|Set-Cookie|X-Api-Key|X-Auth-Token|X-Csrf-Token";
|
|
540
|
+
// Best-effort patterns for credential-bearing header/cookie values dumped
|
|
541
|
+
// verbatim into replay logs (e.g. from network-stubbing or header-injection
|
|
542
|
+
// debug output). Run against the *entire* log file content (not line-by-line)
|
|
543
|
+
// so JSON/object dumps pretty-printed across multiple lines are still caught.
|
|
544
|
+
// Since logs are free-text (not structured JSON like the files handled by
|
|
545
|
+
// `redactCookiesAndHeadersInJson`), this cannot be exhaustive.
|
|
546
|
+
const LOG_CREDENTIAL_REPLACEMENTS = [
|
|
547
|
+
{
|
|
548
|
+
// Plain `Header-Name: <value>` dumps -- redact the rest of the line
|
|
549
|
+
// after the colon, preserving the header name.
|
|
550
|
+
pattern: new RegExp(`\\b(${CREDENTIAL_HEADER_NAMES})(\\s*:\\s*)\\S.*`, "gi"),
|
|
551
|
+
replacement: `$1$2${REDACTED_VALUE_MARKER}`,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
// `{ name: 'Cookie', value: 'secret' }` / `{"name":"Cookie","value":"secret"}`
|
|
555
|
+
// (HAR-style header/cookie entries), whether logged via `util.inspect`
|
|
556
|
+
// (unquoted keys, single-quoted strings) or `JSON.stringify`.
|
|
557
|
+
pattern: new RegExp(`(['"]?name['"]?\\s*:\\s*['"]?(?:${CREDENTIAL_HEADER_NAMES})['"]?[\\s\\S]{0,80}?['"]?value['"]?\\s*:\\s*(['"]))[^'"\n]*\\2`, "gi"),
|
|
558
|
+
replacement: `$1${REDACTED_VALUE_MARKER}$2`,
|
|
559
|
+
},
|
|
560
|
+
{
|
|
561
|
+
// Raw bearer/basic auth tokens, wherever they appear (e.g. in error
|
|
562
|
+
// messages or stack traces, not just header dumps).
|
|
563
|
+
pattern: /\bBearer\s+[A-Za-z0-9\-._~+/]+=*/g,
|
|
564
|
+
replacement: `Bearer ${REDACTED_VALUE_MARKER}`,
|
|
565
|
+
},
|
|
566
|
+
{
|
|
567
|
+
pattern: /\bBasic\s+[A-Za-z0-9+/]+=*/g,
|
|
568
|
+
replacement: `Basic ${REDACTED_VALUE_MARKER}`,
|
|
569
|
+
},
|
|
570
|
+
];
|
|
571
|
+
// Exported for testing.
|
|
572
|
+
const redactLogCredentials = (raw) => {
|
|
573
|
+
let result = raw;
|
|
574
|
+
for (const { pattern, replacement } of LOG_CREDENTIAL_REPLACEMENTS) {
|
|
575
|
+
result = result.replace(pattern, replacement);
|
|
576
|
+
}
|
|
577
|
+
return result;
|
|
578
|
+
};
|
|
579
|
+
exports.redactLogCredentials = redactLogCredentials;
|
|
580
|
+
const redactLogCredentialsInFile = (filePath) => {
|
|
581
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
582
|
+
return false;
|
|
583
|
+
}
|
|
584
|
+
const raw = (0, fs_1.readFileSync)(filePath, "utf8");
|
|
585
|
+
const redacted = (0, exports.redactLogCredentials)(raw);
|
|
586
|
+
if (redacted === raw) {
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
(0, fs_1.writeFileSync)(filePath, redacted);
|
|
590
|
+
return true;
|
|
591
|
+
};
|
|
592
|
+
// Redacts credential values from the `message` field of each entry in a
|
|
593
|
+
// `logs.ndjson`-style file (one JSON object per line), leaving unaffected
|
|
594
|
+
// lines byte-for-byte identical.
|
|
595
|
+
const redactLogCredentialsInNdjsonFile = (filePath) => {
|
|
596
|
+
if (!(0, fs_1.existsSync)(filePath)) {
|
|
597
|
+
return false;
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const raw = (0, fs_1.readFileSync)(filePath, "utf8");
|
|
601
|
+
let changed = false;
|
|
602
|
+
const redactedLines = raw.split("\n").map((line) => {
|
|
603
|
+
if (line.trim() === "") {
|
|
604
|
+
return line;
|
|
605
|
+
}
|
|
606
|
+
let entry;
|
|
607
|
+
try {
|
|
608
|
+
entry = JSON.parse(line);
|
|
609
|
+
}
|
|
610
|
+
catch {
|
|
611
|
+
return line;
|
|
612
|
+
}
|
|
613
|
+
if (typeof entry.message !== "string") {
|
|
614
|
+
return line;
|
|
615
|
+
}
|
|
616
|
+
const redactedMessage = (0, exports.redactLogCredentials)(entry.message);
|
|
617
|
+
if (redactedMessage === entry.message) {
|
|
618
|
+
return line;
|
|
619
|
+
}
|
|
620
|
+
changed = true;
|
|
621
|
+
return JSON.stringify({ ...entry, message: redactedMessage });
|
|
622
|
+
});
|
|
623
|
+
if (!changed) {
|
|
624
|
+
return false;
|
|
625
|
+
}
|
|
626
|
+
(0, fs_1.writeFileSync)(filePath, redactedLines.join("\n"));
|
|
627
|
+
return true;
|
|
628
|
+
}
|
|
629
|
+
catch (error) {
|
|
630
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
631
|
+
console.warn(` Warning: Could not redact credentials in ${filePath}: ${message}`);
|
|
632
|
+
return false;
|
|
262
633
|
}
|
|
263
634
|
};
|
|
264
635
|
const LOG_NOISE_PATTERNS = [
|
|
@@ -1404,4 +1775,4 @@ const defaultWriteContextJson = (debugContext, workspaceDir, fileMetadata, proje
|
|
|
1404
1775
|
(0, fs_1.writeFileSync)((0, path_1.join)(workspaceDir, debug_constants_1.DEBUG_DATA_DIRECTORY, "context.json"), JSON.stringify(context, null, 2));
|
|
1405
1776
|
};
|
|
1406
1777
|
//# sourceMappingURL=generate-debug-workspace.js.map
|
|
1407
|
-
//# debugId=
|
|
1778
|
+
//# debugId=b0e8824a-2c75-553c-abd1-f66bad1029e6
|