reporting-labs 0.6.2 → 0.6.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/mask.d.ts +1 -1
- package/dist/mask.js +48 -8
- package/dist/reporter.js +6 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -134,7 +134,7 @@ await testData(rowsFromExcelOrJson, 'Coupons'); // arr
|
|
|
134
134
|
await testData(fs.readFileSync('data/users.csv', 'utf8'), 'users.csv'); // CSV text → table
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
**Secrets are masked automatically
|
|
137
|
+
**Secrets are masked automatically**, wherever they show up: `console.log` output, `log()` lines, `testData()` blocks, API headers and bodies, step titles and assertion messages. Covered forms: `password=x`, `Password: x`, `{ password: 'x' }`, `"password":"x"`, `X-Api-Key: x`, `access_token=x`, "password is x", "with password S3cret@1", `Bearer ...` / `Basic ...`, JWTs, and well-known token formats (GitHub, AWS, Slack, Stripe, Google, GitLab, npm, SendGrid). A failing `expect(token)` shows `Received: "****"`. Add your own keys with `maskKeys: ['otp', 'pan']` in the config; they apply to text too.
|
|
138
138
|
|
|
139
139
|
## API calls: recorded on their own
|
|
140
140
|
|
package/dist/mask.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export declare function makeMasker(extraKeys?: string[]): {
|
|
2
2
|
mask: (v: unknown, key?: string) => unknown;
|
|
3
|
-
maskStr: (
|
|
3
|
+
maskStr: (input: string) => string;
|
|
4
4
|
isSensitive: (k: string) => boolean;
|
|
5
5
|
};
|
|
6
6
|
export declare function parseCsv(text: string): {
|
package/dist/mask.js
CHANGED
|
@@ -3,20 +3,60 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.makeMasker = makeMasker;
|
|
4
4
|
exports.parseCsv = parseCsv;
|
|
5
5
|
const DEFAULT_KEYS = ['password', 'passwd', 'pwd', 'secret', 'token', 'apikey', 'api_key', 'api-key', 'authorization', 'auth', 'cookie', 'set-cookie', 'session', 'credential', 'private', 'ssn', 'cvv', 'card'];
|
|
6
|
+
/** Values that are secrets on their own, whatever the surrounding text. */
|
|
6
7
|
const VALUE_PATTERNS = [
|
|
7
|
-
/\b(Bearer|Basic|Token)\s+[A-Za-z0-9._~+\/=-]{8,}/gi,
|
|
8
|
+
/\b(Bearer|Basic|Digest|Token)\s+[A-Za-z0-9._~+\/=-]{8,}/gi,
|
|
8
9
|
/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{5,}/g, // JWT
|
|
9
|
-
/\b(sk|pk|
|
|
10
|
-
|
|
10
|
+
/\b(?:sk|pk|rk)[-_](?:live|test)?[-_]?[A-Za-z0-9]{12,}\b/g, // Stripe-style keys
|
|
11
|
+
/\b(?:ghp|gho|ghu|ghs|ghr)_[A-Za-z0-9]{20,}\b/g, // GitHub tokens
|
|
12
|
+
/\bgithub_pat_[A-Za-z0-9_]{20,}\b/g,
|
|
13
|
+
/\bxox[abpr]-[A-Za-z0-9-]{10,}\b/g, // Slack
|
|
14
|
+
/\bAKIA[0-9A-Z]{16}\b/g, // AWS access key id
|
|
15
|
+
/\bAIza[0-9A-Za-z_-]{30,}\b/g, // Google API key
|
|
16
|
+
/\bya29\.[0-9A-Za-z_-]{20,}\b/g, // Google OAuth token
|
|
17
|
+
/\bglpat-[A-Za-z0-9_-]{20,}\b/g, // GitLab
|
|
18
|
+
/\bnpm_[A-Za-z0-9]{30,}\b/g, // npm
|
|
19
|
+
/\bSG\.[A-Za-z0-9_-]{16,}\.[A-Za-z0-9_-]{16,}\b/g, // SendGrid
|
|
11
20
|
];
|
|
21
|
+
/** Words after "password is ..." that are plainly not a secret. */
|
|
22
|
+
const NOT_A_SECRET = new Set(['valid', 'invalid', 'visible', 'hidden', 'required', 'optional', 'missing', 'empty', 'null', 'undefined', 'none',
|
|
23
|
+
'correct', 'incorrect', 'wrong', 'right', 'expired', 'set', 'unset', 'not', 'present', 'absent', 'ok', 'true', 'false', 'masked', 'blank',
|
|
24
|
+
'too', 'the', 'a', 'an', 'same', 'different', 'changed', 'unchanged', 'shown', 'displayed', 'accepted', 'rejected', 'weak', 'strong']);
|
|
25
|
+
const esc = (s) => s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
12
26
|
function makeMasker(extraKeys = []) {
|
|
13
27
|
const keys = [...DEFAULT_KEYS, ...extraKeys.map(k => k.toLowerCase())];
|
|
14
28
|
const isSensitive = (k) => { const l = k.toLowerCase(); return keys.some(s => l === s || l.includes(s)); };
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
})
|
|
29
|
+
// Key names as they appear in free text: password, X-Api-Key, access_token, userPassword, "password", db.password...
|
|
30
|
+
// `auth`, `otp`, `pin`... are matched as whole words only, so "author" or "pinned" stay untouched.
|
|
31
|
+
const compound = ['password', 'passwd', 'pwd', 'passcode', 'secret', 'token', 'api[_-]?key', 'cookie', 'credentials?', 'private[_-]?key', 'access[_-]?key', 'session[_-]?id', ...extraKeys.map(esc)];
|
|
32
|
+
const exact = ['authorization', 'auth', 'otp', 'pin', 'cvv', 'ssn'];
|
|
33
|
+
const KEY = `(?:[\\w.-]*?(?:${compound.join('|')})[\\w-]*|${exact.join('|')})`;
|
|
34
|
+
// key = value | key: value | "key": "value" | key => value | key -> value (value runs to a delimiter; a closing quote stays)
|
|
35
|
+
const KV = new RegExp(`(["']?)\\b(${KEY})\\b(["']?)(\\s*(?:=>|->|[=:])\\s*)(["']?)([^"'\\s&;,}\\]\\)]+)`, 'gi');
|
|
36
|
+
// password is S3cret | token was abc | password 'S3cret' | with password S3cret@123 (only when the value looks like a secret)
|
|
37
|
+
const SPOKEN_KEYS = ['password', 'passwd', 'pwd', 'passcode', 'secret', 'token', 'api[ _-]?key', 'access[ _-]?key', 'otp', 'pin', 'cvv', ...extraKeys.map(esc)];
|
|
38
|
+
const SPOKEN = new RegExp(`\\b(${SPOKEN_KEYS.join('|')})(s?\\b\\s+(?:is|was|of|as)?\\s*['"]?)([^\\s'",;]+)`, 'gi');
|
|
39
|
+
const looksSecret = (v) => !/\s/.test(v) && (/\d/.test(v) && v.length >= 4 || /[^A-Za-z0-9]/.test(v) && v.length >= 6 || /[a-z][A-Z]|[A-Z][a-z].*[A-Z]/.test(v) && v.length >= 8);
|
|
40
|
+
// Assertion output: when the text talks about a password/token/secret, the compared values are that secret.
|
|
41
|
+
// expect(token, 'token should match').toBe('x') -> Expected: "x" / Received: "tok_9f8e..."
|
|
42
|
+
const MENTIONS_SECRET = new RegExp(`\\b(?:${SPOKEN_KEYS.join('|')})s?\\b`, 'i');
|
|
43
|
+
const COMPARED = /\b(Expected|Received|Actual|expected|received|actual|got)(\s*[:=]\s*)(["'\u201c]?)([^"'\u201d\s]+)/g;
|
|
44
|
+
const maskStr = (input) => {
|
|
45
|
+
let s = input;
|
|
46
|
+
for (const re of VALUE_PATTERNS)
|
|
47
|
+
s = s.replace(re, m => /^(Bearer|Basic|Digest|Token)\s/i.test(m) ? m.split(/\s+/)[0] + ' ****' : '****');
|
|
48
|
+
s = s.replace(KV, (_m, q1, key, q2, sep, q3, _value) => `${q1}${key}${q2}${sep}${q3}****`);
|
|
49
|
+
s = s.replace(SPOKEN, (m, key, between, value) => {
|
|
50
|
+
const word = value.toLowerCase().replace(/[^a-z0-9]/g, '');
|
|
51
|
+
if (NOT_A_SECRET.has(word) || word === '****' || value === '****')
|
|
52
|
+
return m;
|
|
53
|
+
const explicit = /\b(is|was|of|as)\b|['"]$/.test(between);
|
|
54
|
+
return explicit || looksSecret(value) ? `${key}${between}****` : m;
|
|
55
|
+
});
|
|
56
|
+
if (MENTIONS_SECRET.test(s))
|
|
57
|
+
s = s.replace(COMPARED, (m, label, sep, q, value) => looksSecret(value) ? `${label}${sep}${q}****` : m);
|
|
58
|
+
return s;
|
|
59
|
+
};
|
|
20
60
|
const mask = (v, key = '') => {
|
|
21
61
|
if (key && isSensitive(key))
|
|
22
62
|
return '****';
|
package/dist/reporter.js
CHANGED
|
@@ -437,14 +437,15 @@ class ReportingLabsReporter {
|
|
|
437
437
|
return path.relative(base, file).split(path.sep).join('/');
|
|
438
438
|
}
|
|
439
439
|
serializeError(e) {
|
|
440
|
-
|
|
440
|
+
// Assertions print received/expected values, so a failing `expect(token)` would leak the token without this.
|
|
441
|
+
const out = { message: this.masker.maskStr(stripAnsi(e.message ?? e.value ?? '')) };
|
|
441
442
|
const why = (0, explain_1.explainError)(out.message);
|
|
442
443
|
if (why)
|
|
443
444
|
out.explain = why;
|
|
444
445
|
if (e.stack)
|
|
445
|
-
out.stack = stripAnsi(e.stack);
|
|
446
|
+
out.stack = this.masker.maskStr(stripAnsi(e.stack));
|
|
446
447
|
if (e.snippet)
|
|
447
|
-
out.snippet = stripAnsi(e.snippet);
|
|
448
|
+
out.snippet = this.masker.maskStr(stripAnsi(e.snippet));
|
|
448
449
|
if (e.location)
|
|
449
450
|
out.location = { file: this.rel(e.location.file), line: e.location.line, column: e.location.column };
|
|
450
451
|
return out;
|
|
@@ -538,10 +539,10 @@ class ReportingLabsReporter {
|
|
|
538
539
|
}
|
|
539
540
|
serializeStep(s) {
|
|
540
541
|
return {
|
|
541
|
-
title: s.title,
|
|
542
|
+
title: this.masker.maskStr(s.title),
|
|
542
543
|
category: s.category,
|
|
543
544
|
duration: s.duration,
|
|
544
|
-
error: s.error?.message ? stripAnsi(s.error.message) : undefined,
|
|
545
|
+
error: s.error?.message ? this.masker.maskStr(stripAnsi(s.error.message)) : undefined,
|
|
545
546
|
steps: s.steps.filter(c => !isInternalAttach(c)).map(c => this.serializeStep(c)),
|
|
546
547
|
};
|
|
547
548
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reporting-labs",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "reportingLabs – beautiful, customizable single-file HTML test reports. One line of config. Playwright adapter included, more runners on the way.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"test-report",
|