reporting-labs 0.6.2 → 0.6.4
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 +5 -1
- package/dist/mask.d.ts +1 -1
- package/dist/mask.js +48 -8
- package/dist/reporter.js +6 -5
- package/dist/template.html +2 -2
- package/dist/template.js +2 -2
- 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
|
|
|
@@ -604,6 +604,10 @@ Your Slack step can then read `results.json` for pass / fail counts and top fail
|
|
|
604
604
|
- **Print.** A print stylesheet is included, so "Save as PDF" works.
|
|
605
605
|
- **Themes.** Light and dark follow the OS. The toggle in the header remembers your choice.
|
|
606
606
|
|
|
607
|
+
## Java teams
|
|
608
|
+
|
|
609
|
+
The same report, from Java: `dev.reportinglabs` on Maven Central, for TestNG and JUnit 5, with zero-code add-ons for Selenium and REST Assured and a one-line add-on for Playwright for Java. Source: [reporting-labs-java](https://github.com/naveenautomationlabs/reporting-labs-java). Guides: [reportinglabs.dev/get-started/java](https://reportinglabs.dev/get-started/java).
|
|
610
|
+
|
|
607
611
|
## Roadmap
|
|
608
612
|
|
|
609
613
|
- WebdriverIO, Cypress, Jest/Vitest and JUnit XML adapters
|
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/dist/template.html
CHANGED
|
@@ -1155,7 +1155,7 @@ function errorWhere(e){
|
|
|
1155
1155
|
function errorBody(e){
|
|
1156
1156
|
const msg=e.message||'';
|
|
1157
1157
|
const box=h('div',{class:'err'}, msg, e.stack&&e.stack!==msg? h('details',{}, h('summary',{},'Stack trace'), h('div',{class:'stack'},e.stack)) : null);
|
|
1158
|
-
const ex=msg.match(
|
|
1158
|
+
const ex=msg.match(/^\s*(Expected[^:\n]{0,40}):[ \t]*(.+)$/m), rc=msg.match(/^\s*((?:Received|Actual|But was)[^:\n]{0,40}):[ \t]*(.+)$/m);
|
|
1159
1159
|
if(ex&&rc&&ex[2].trim()&&rc[2].trim()&&ex[2].length<2000&&rc[2].length<2000){
|
|
1160
1160
|
const d=tokenDiff(ex[2].trim(), rc[2].trim());
|
|
1161
1161
|
if(d){ return h('div',{}, h('div',{class:'ediff'},
|
|
@@ -1591,7 +1591,7 @@ function compare(set){
|
|
|
1591
1591
|
/* ---- bug report: a ready-to-paste ticket built from the failure ---- */
|
|
1592
1592
|
function bugModel(t){
|
|
1593
1593
|
const r=t.results[t.results.length-1], e=r&&r.errors[0], x=e&&e.explain, msg=e?e.message:'';
|
|
1594
|
-
const ex=msg.match(
|
|
1594
|
+
const ex=msg.match(/^\s*(Expected[^:\n]{0,40}):[ \t]*(.+)$/m), rc=msg.match(/^\s*((?:Received|Actual|But was)[^:\n]{0,40}):[ \t]*(.+)$/m);
|
|
1595
1595
|
const si=sinceInfo(t);
|
|
1596
1596
|
const steps=[]; const walk=(list,depth)=>{ for(const st of list){ if(st.category==='hook'||/^Attach "/.test(st.title)||/^Worker Cleanup/.test(st.title)) continue; const user=st.category==='test.step'; if(user||depth===0) steps.push({title:st.title.replace(/^(Given|When|Then|And|But)\s+/,m=>m), failed:!!st.error, user}); if(user&&st.steps.length&&!st.steps.some(c=>c.category==='test.step')) continue; if(st.steps.length) walk(st.steps,depth+1); } };
|
|
1597
1597
|
if(r) walk(r.steps,0);
|
package/dist/template.js
CHANGED
|
@@ -1221,7 +1221,7 @@ function errorWhere(e){
|
|
|
1221
1221
|
function errorBody(e){
|
|
1222
1222
|
const msg=e.message||'';
|
|
1223
1223
|
const box=h('div',{class:'err'}, msg, e.stack&&e.stack!==msg? h('details',{}, h('summary',{},'Stack trace'), h('div',{class:'stack'},e.stack)) : null);
|
|
1224
|
-
const ex=msg.match(
|
|
1224
|
+
const ex=msg.match(/^\s*(Expected[^:\n]{0,40}):[ \t]*(.+)$/m), rc=msg.match(/^\s*((?:Received|Actual|But was)[^:\n]{0,40}):[ \t]*(.+)$/m);
|
|
1225
1225
|
if(ex&&rc&&ex[2].trim()&&rc[2].trim()&&ex[2].length<2000&&rc[2].length<2000){
|
|
1226
1226
|
const d=tokenDiff(ex[2].trim(), rc[2].trim());
|
|
1227
1227
|
if(d){ return h('div',{}, h('div',{class:'ediff'},
|
|
@@ -1657,7 +1657,7 @@ function compare(set){
|
|
|
1657
1657
|
/* ---- bug report: a ready-to-paste ticket built from the failure ---- */
|
|
1658
1658
|
function bugModel(t){
|
|
1659
1659
|
const r=t.results[t.results.length-1], e=r&&r.errors[0], x=e&&e.explain, msg=e?e.message:'';
|
|
1660
|
-
const ex=msg.match(
|
|
1660
|
+
const ex=msg.match(/^\s*(Expected[^:\n]{0,40}):[ \t]*(.+)$/m), rc=msg.match(/^\s*((?:Received|Actual|But was)[^:\n]{0,40}):[ \t]*(.+)$/m);
|
|
1661
1661
|
const si=sinceInfo(t);
|
|
1662
1662
|
const steps=[]; const walk=(list,depth)=>{ for(const st of list){ if(st.category==='hook'||/^Attach "/.test(st.title)||/^Worker Cleanup/.test(st.title)) continue; const user=st.category==='test.step'; if(user||depth===0) steps.push({title:st.title.replace(/^(Given|When|Then|And|But)\s+/,m=>m), failed:!!st.error, user}); if(user&&st.steps.length&&!st.steps.some(c=>c.category==='test.step')) continue; if(st.steps.length) walk(st.steps,depth+1); } };
|
|
1663
1663
|
if(r) walk(r.steps,0);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reporting-labs",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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",
|