create-claude-cabinet 0.29.6 → 0.29.8

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.
Files changed (24) hide show
  1. package/lib/site-audit-setup.js +15 -0
  2. package/package.json +1 -1
  3. package/templates/site-audit-runtime/package.json +2 -1
  4. package/templates/site-audit-runtime/src/checks/axe-core.mjs +1 -0
  5. package/templates/site-audit-runtime/src/checks/dns.mjs +1 -0
  6. package/templates/site-audit-runtime/src/checks/lighthouse.mjs +1 -0
  7. package/templates/site-audit-runtime/src/checks/linkinator.mjs +1 -0
  8. package/templates/site-audit-runtime/src/checks/meta-og.mjs +1 -0
  9. package/templates/site-audit-runtime/src/checks/nuclei.mjs +1 -0
  10. package/templates/site-audit-runtime/src/checks/observatory.mjs +3 -2
  11. package/templates/site-audit-runtime/src/checks/pa11y.mjs +1 -0
  12. package/templates/site-audit-runtime/src/checks/security-headers.mjs +8 -7
  13. package/templates/site-audit-runtime/src/checks/ssl-cert.mjs +1 -0
  14. package/templates/site-audit-runtime/src/checks/structured-data.mjs +4 -0
  15. package/templates/site-audit-runtime/src/checks/testssl.mjs +47 -42
  16. package/templates/site-audit-runtime/src/checks/unlighthouse.mjs +2 -1
  17. package/templates/site-audit-runtime/src/checks/website-carbon.mjs +1 -0
  18. package/templates/site-audit-runtime/src/orchestrator.mjs +2 -3
  19. package/templates/site-audit-runtime/src/report.mjs +26 -3
  20. package/templates/site-audit-runtime/tests/checks-tier1.test.mjs +4 -3
  21. package/templates/site-audit-runtime/tests/checks-tier3.test.mjs +0 -22
  22. package/templates/site-audit-runtime/tests/fixtures/testssl.json +1 -1
  23. package/templates/site-audit-runtime/src/checks/blacklight.mjs +0 -86
  24. package/templates/site-audit-runtime/tests/fixtures/blacklight.json +0 -1
@@ -91,6 +91,21 @@ function extractAndInstall(versionDir, tarballPath, results) {
91
91
  execSync(`tar xf "${tarballPath}" -C "${versionDir}"`, { encoding: 'utf8' });
92
92
  execSync('npm install --omit=dev --ignore-scripts --silent', { cwd: pkgDir, encoding: 'utf8' });
93
93
 
94
+ // Pa11y needs Puppeteer's Chrome; axe-core needs chromedriver.
95
+ // Both were skipped by --ignore-scripts. Install them now.
96
+ try {
97
+ execSync('npx puppeteer browsers install chrome', { cwd: pkgDir, encoding: 'utf8', timeout: 120_000 });
98
+ results.push(' Installed Puppeteer Chrome for Pa11y');
99
+ } catch {
100
+ results.push(' ⚠ Puppeteer Chrome install failed — Pa11y will be unavailable');
101
+ }
102
+ try {
103
+ execSync('node node_modules/chromedriver/install.js', { cwd: pkgDir, encoding: 'utf8', timeout: 120_000 });
104
+ results.push(' Installed chromedriver for axe-core');
105
+ } catch {
106
+ results.push(' ⚠ chromedriver install failed — axe-core will be unavailable');
107
+ }
108
+
94
109
  const binDir = path.join(versionDir, 'bin');
95
110
  fs.mkdirSync(binDir, { recursive: true });
96
111
  const binSrc = path.join(pkgDir, 'bin', 'cc-site-audit');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.29.6",
3
+ "version": "0.29.8",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-cabinet/site-audit",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Comprehensive deployed-site quality audit engine for Claude Cabinet. Runs checks across performance, accessibility, security, SEO, content, DNS, and privacy against a deployed URL; single-site and comparison modes; standalone HTML report.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -22,6 +22,7 @@
22
22
  "lighthouse": "^12.0.0",
23
23
  "linkinator": "^7.0.0",
24
24
  "pa11y": "^9.0.0",
25
+ "ssl-checker": "^3.0.0",
25
26
  "unlighthouse": "^0.16.0"
26
27
  }
27
28
  }
@@ -1,5 +1,6 @@
1
1
  export const checkId = 'axe-core';
2
2
  export const tool = 'axe-core (WCAG AA)';
3
+ export const whyItMatters = "Checks whether people with disabilities can use your site — screen readers, keyboard navigation, color contrast. Legal liability if not met.";
3
4
 
4
5
  export async function detect(executor) {
5
6
  const r = await executor.spawn('axe', ['--version'], { timeoutMs: 15_000 });
@@ -1,5 +1,6 @@
1
1
  export const checkId = 'dns';
2
2
  export const tool = 'DNS & Protocol';
3
+ export const whyItMatters = "DNS security prevents attackers from redirecting your visitors to fake versions of your site. HTTP/2 makes pages load faster.";
3
4
 
4
5
  export async function detect(executor) {
5
6
  const r = await executor.spawn('dig', ['-v'], { timeoutMs: 5_000 });
@@ -1,5 +1,6 @@
1
1
  export const checkId = 'lighthouse';
2
2
  export const tool = 'Lighthouse';
3
+ export const whyItMatters = "Google's own audit tool — scores four dimensions separately: Performance (page speed), Accessibility (usability for all), Best Practices (security and code quality), and SEO (search visibility). The overall score averages all four.";
3
4
  export const defaultTimeoutMs = 120_000;
4
5
 
5
6
  export async function detect(executor) {
@@ -1,5 +1,6 @@
1
1
  export const checkId = 'linkinator';
2
2
  export const tool = 'Linkinator (broken links)';
3
+ export const whyItMatters = "Broken links frustrate visitors and tell search engines your site isn't maintained — hurts both trust and rankings.";
3
4
 
4
5
  export async function detect(executor) {
5
6
  const r = await executor.spawn('linkinator', ['--version'], { timeoutMs: 15_000 });
@@ -1,5 +1,6 @@
1
1
  export const checkId = 'meta-og';
2
2
  export const tool = 'Meta & Open Graph';
3
+ export const whyItMatters = "Controls how your site appears when shared on social media and in search results — missing tags mean ugly or blank previews.";
3
4
 
4
5
  export async function detect() { return true; }
5
6
 
@@ -6,6 +6,7 @@
6
6
 
7
7
  export const checkId = 'nuclei';
8
8
  export const tool = 'Nuclei (CVE scan)';
9
+ export const whyItMatters = "Scans for known security vulnerabilities (CVEs) that attackers actively exploit — the kind that make the news.";
9
10
  export const defaultTimeoutMs = 300_000;
10
11
 
11
12
  export async function detect(executor) {
@@ -1,14 +1,15 @@
1
1
  export const checkId = 'observatory';
2
2
  export const tool = 'MDN HTTP Observatory';
3
+ export const whyItMatters = "Mozilla's security scorecard — grades your site's defenses against common web attacks like clickjacking and data injection.";
3
4
 
4
5
  export async function detect(executor) {
5
- const r = await executor.spawn('mdn-http-observatory', ['--version'], { timeoutMs: 15_000 });
6
+ const r = await executor.spawn('mdn-http-observatory-scan', ['--help'], { timeoutMs: 15_000 });
6
7
  return r.code === 0;
7
8
  }
8
9
 
9
10
  export async function run(url, executor) {
10
11
  const hostname = new URL(url).hostname;
11
- return executor.spawn('mdn-http-observatory', [hostname], { timeoutMs: 60_000 });
12
+ return executor.spawn('mdn-http-observatory-scan', [hostname], { timeoutMs: 60_000 });
12
13
  }
13
14
 
14
15
  const GRADE_SCORES = { 'A+': 100, 'A': 95, 'A-': 90, 'B+': 85, 'B': 80, 'B-': 75, 'C+': 70, 'C': 65, 'C-': 60, 'D+': 55, 'D': 50, 'D-': 45, 'F': 20 };
@@ -1,5 +1,6 @@
1
1
  export const checkId = 'pa11y';
2
2
  export const tool = 'Pa11y (WCAG AAA)';
3
+ export const whyItMatters = "Stricter accessibility testing (WCAG AAA) — catches issues that basic checks miss, like low contrast text and missing form labels.";
3
4
 
4
5
  export async function detect(executor) {
5
6
  const r = await executor.spawn('pa11y', ['--version'], { timeoutMs: 15_000 });
@@ -1,13 +1,14 @@
1
1
  export const checkId = 'security-headers';
2
2
  export const tool = 'Security Headers';
3
+ export const whyItMatters = "Missing headers let attackers inject malicious scripts, steal user data, or impersonate your site in browsers.";
3
4
 
4
5
  const CHECKS = [
5
- { header: 'content-security-policy', label: 'Content-Security-Policy', weight: 2 },
6
- { header: 'strict-transport-security', label: 'Strict-Transport-Security', weight: 2 },
7
- { header: 'x-frame-options', label: 'X-Frame-Options', weight: 1 },
8
- { header: 'x-content-type-options', label: 'X-Content-Type-Options', weight: 1 },
9
- { header: 'referrer-policy', label: 'Referrer-Policy', weight: 1 },
10
- { header: 'permissions-policy', label: 'Permissions-Policy', weight: 1 },
6
+ { header: 'content-security-policy', label: 'Content-Security-Policy', weight: 2, why: 'Controls which scripts/resources can load — primary defense against XSS attacks' },
7
+ { header: 'strict-transport-security', label: 'Strict-Transport-Security', weight: 2, why: 'Forces HTTPS — prevents downgrade attacks that intercept unencrypted traffic' },
8
+ { header: 'x-frame-options', label: 'X-Frame-Options', weight: 1, why: 'Prevents your site from being embedded in iframes — blocks clickjacking attacks' },
9
+ { header: 'x-content-type-options', label: 'X-Content-Type-Options', weight: 1, why: 'Stops browsers from guessing file types — prevents script injection via disguised files' },
10
+ { header: 'referrer-policy', label: 'Referrer-Policy', weight: 1, why: 'Controls what URL info leaks when users click links to other sites' },
11
+ { header: 'permissions-policy', label: 'Permissions-Policy', weight: 1, why: 'Restricts which browser features (camera, mic, geolocation) the page can use' },
11
12
  ];
12
13
 
13
14
  export async function detect() { return true; }
@@ -29,7 +30,7 @@ export function normalize(headers, durationMs) {
29
30
  if (headers[c.header]) {
30
31
  earned += c.weight;
31
32
  } else {
32
- findings.push({ severity: c.weight >= 2 ? 'serious' : 'moderate', message: `Missing ${c.label} header` });
33
+ findings.push({ severity: c.weight >= 2 ? 'serious' : 'moderate', message: `Missing ${c.label} header`, context: c.why });
33
34
  }
34
35
  }
35
36
 
@@ -1,5 +1,6 @@
1
1
  export const checkId = 'ssl-cert';
2
2
  export const tool = 'SSL Certificate';
3
+ export const whyItMatters = "An expired or misconfigured certificate shows scary browser warnings that immediately drive visitors away.";
3
4
 
4
5
  export async function detect(executor) {
5
6
  const r = await executor.spawn('openssl', ['version'], { timeoutMs: 5_000 });
@@ -1,5 +1,6 @@
1
1
  export const checkId = 'structured-data';
2
2
  export const tool = 'Structured Data (JSON-LD)';
3
+ export const whyItMatters = "Tells Google exactly what your business does — enables rich search results like star ratings, FAQ dropdowns, and business info panels.";
3
4
 
4
5
  export async function detect() { return true; }
5
6
 
@@ -70,9 +71,12 @@ export function normalize(html, durationMs) {
70
71
  ? `${scripts.length} JSON-LD block${scripts.length !== 1 ? 's' : ''} found with ${types.length} type${types.length !== 1 ? 's' : ''}: ${types.join(', ') || 'none'}`
71
72
  : undefined;
72
73
 
74
+ const details = types.length ? { types, blockCount: scripts.length } : undefined;
75
+
73
76
  return {
74
77
  checkId, tool, status: isPass ? 'pass' : 'fail',
75
78
  score, grade: null, severity: worstSev, findings, durationMs,
76
79
  ...(passSummary && { passSummary }),
80
+ ...(details && { details }),
77
81
  };
78
82
  }
@@ -1,70 +1,75 @@
1
1
  export const checkId = 'testssl';
2
- export const tool = 'testssl.sh (TLS depth)';
3
- export const defaultTimeoutMs = 180_000;
2
+ export const tool = 'TLS/SSL Check';
3
+ export const whyItMatters = "Weak encryption lets attackers intercept data between your users and your server — passwords, form submissions, everything.";
4
4
 
5
5
  export async function detect(executor) {
6
- const r = await executor.spawn('testssl.sh', ['--version'], { timeoutMs: 5_000 });
7
- return r.code === 0 || r.stdout.includes('testssl');
6
+ try {
7
+ await import('ssl-checker');
8
+ return true;
9
+ } catch {
10
+ return false;
11
+ }
8
12
  }
9
13
 
10
14
  export async function run(url, executor) {
11
15
  const hostname = new URL(url).hostname;
12
- return executor.spawn('testssl.sh', ['--jsonfile-pretty', '/dev/stdout', '--quiet', hostname], { timeoutMs: 180_000 });
16
+ const sslChecker = (await import('ssl-checker')).default;
17
+ const result = await sslChecker(hostname);
18
+ return { code: 0, stdout: JSON.stringify(result), stderr: '', timedOut: false };
13
19
  }
14
20
 
15
21
  export function normalize(raw, durationMs) {
16
22
  if (raw.code !== 0 && !raw.stdout) {
17
- return { checkId, tool, status: 'error', score: null, grade: null, severity: null, findings: [], durationMs, reason: raw.stderr || 'testssl.sh failed' };
23
+ return { checkId, tool, status: 'error', score: null, grade: null, severity: null, findings: [], durationMs, reason: raw.stderr || 'ssl-checker failed' };
18
24
  }
19
25
 
20
- let entries;
21
- try { entries = JSON.parse(raw.stdout); } catch {
22
- const lines = raw.stdout.split('\n').filter(l => l.trim());
23
- entries = lines.map(l => { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
26
+ let data;
27
+ try { data = JSON.parse(raw.stdout); } catch {
28
+ return { checkId, tool, status: 'error', score: null, grade: null, severity: null, findings: [], durationMs, reason: 'failed to parse ssl-checker output' };
24
29
  }
25
- if (!Array.isArray(entries)) entries = [entries];
26
30
 
27
31
  const findings = [];
28
- for (const e of entries.flat()) {
29
- if (!e || !e.severity) continue;
30
- const sev = mapSeverity(e.severity);
31
- if (sev && e.severity !== 'OK' && e.severity !== 'INFO') {
32
- findings.push({
33
- severity: sev,
34
- message: e.finding || e.id || 'unknown',
35
- context: e.cve || undefined,
36
- });
37
- }
32
+
33
+ if (!data.valid) {
34
+ findings.push({ severity: 'critical', message: 'Certificate is not valid' });
35
+ }
36
+
37
+ if (data.daysRemaining != null && data.daysRemaining < 30) {
38
+ findings.push({
39
+ severity: data.daysRemaining < 7 ? 'critical' : 'serious',
40
+ message: `Certificate expires in ${data.daysRemaining} days`,
41
+ });
42
+ }
43
+
44
+ const protocol = data.protocol || data.tlsVersion || '';
45
+ if (protocol && !protocol.includes('TLSv1.2') && !protocol.includes('TLSv1.3')) {
46
+ findings.push({ severity: 'serious', message: `Weak TLS protocol: ${protocol}` });
38
47
  }
39
48
 
40
- const worstSev = findings.length ? findings.reduce((w, f) => {
41
- const o = { critical: 0, serious: 1, moderate: 2, info: 3 };
42
- return (o[f.severity] ?? 3) < (o[w] ?? 3) ? f.severity : w;
43
- }, 'info') : null;
49
+ if (data.cipher) {
50
+ const weak = /RC4|DES|MD5|NULL|EXPORT|anon/i;
51
+ if (weak.test(data.cipher)) {
52
+ findings.push({ severity: 'serious', message: `Weak cipher: ${data.cipher}` });
53
+ }
54
+ }
44
55
 
45
56
  const hasCritical = findings.some(f => f.severity === 'critical');
46
57
  const hasSerious = findings.some(f => f.severity === 'serious');
47
-
48
58
  const isPass = !hasCritical && !hasSerious;
49
- const totalChecked = entries.flat().filter(e => e && e.severity).length;
50
- const passSummary = isPass
51
- ? (findings.length === 0
52
- ? `TLS configuration clean — ${totalChecked} check${totalChecked !== 1 ? 's' : ''} passed`
53
- : `No critical/serious TLS issues (${findings.length} low-severity item${findings.length !== 1 ? 's' : ''})`)
54
- : undefined;
59
+
60
+ const details = [];
61
+ if (data.valid) details.push('valid certificate');
62
+ if (data.daysRemaining != null) details.push(`${data.daysRemaining} days until expiry`);
63
+ if (protocol) details.push(protocol);
64
+ if (data.cipher) details.push(data.cipher);
65
+
66
+ const passSummary = isPass ? details.join(', ') : undefined;
55
67
 
56
68
  return {
57
69
  checkId, tool, status: isPass ? 'pass' : 'fail',
58
- score: null, grade: null, severity: worstSev, findings, durationMs,
70
+ score: null, grade: null,
71
+ severity: hasCritical ? 'critical' : hasSerious ? 'serious' : findings.length ? 'moderate' : null,
72
+ findings, durationMs,
59
73
  ...(passSummary && { passSummary }),
60
74
  };
61
75
  }
62
-
63
- function mapSeverity(s) {
64
- const l = String(s).toUpperCase();
65
- if (l === 'CRITICAL' || l === 'HIGH') return 'critical';
66
- if (l === 'MEDIUM' || l === 'WARN' || l === 'WARNING') return 'serious';
67
- if (l === 'LOW' || l === 'NOT OK') return 'moderate';
68
- if (l === 'INFO' || l === 'OK') return 'info';
69
- return 'info';
70
- }
@@ -4,7 +4,8 @@
4
4
 
5
5
  export const checkId = 'unlighthouse';
6
6
  export const tool = 'Unlighthouse (full-site crawl)';
7
- export const defaultTimeoutMs = 300_000;
7
+ export const whyItMatters = "Runs Lighthouse on every page of your site, not just the homepage — catches performance and accessibility problems hiding on inner pages.";
8
+ export const defaultTimeoutMs = 600_000;
8
9
 
9
10
  export async function detect(executor) {
10
11
  const r = await executor.spawn('unlighthouse', ['--version'], { timeoutMs: 15_000 });
@@ -4,6 +4,7 @@
4
4
 
5
5
  export const checkId = 'website-carbon';
6
6
  export const tool = 'Website Carbon';
7
+ export const whyItMatters = "Estimates your site's carbon footprint per visit — smaller pages are faster, cheaper to host, and better for the environment.";
7
8
 
8
9
  const KWH_PER_GB = 0.81;
9
10
  const CARBON_FACTOR_GRAMS_PER_KWH = 442;
@@ -54,8 +54,7 @@ const DEFAULT_TIMEOUTS = {
54
54
  lighthouse: 120_000,
55
55
  testssl: 180_000,
56
56
  nuclei: 300_000,
57
- unlighthouse: 300_000,
58
- blacklight: 120_000,
57
+ unlighthouse: 600_000,
59
58
  };
60
59
  const FALLBACK_TIMEOUT = 60_000;
61
60
  const MAX_CONCURRENCY = 4;
@@ -114,7 +113,6 @@ function guessCheckId(cmd, args) {
114
113
  if (allTokens.includes('nuclei')) return 'nuclei';
115
114
  if (allTokens.includes('unlighthouse')) return 'unlighthouse';
116
115
  if (allTokens.includes('observatory')) return 'observatory';
117
- if (allTokens.includes('blacklight')) return 'blacklight';
118
116
  if (allTokens.includes('dig')) return 'dns';
119
117
  if (allTokens.includes('openssl')) return 'ssl-cert';
120
118
  return basename(cmd).replace(/\.\w+$/, '');
@@ -242,6 +240,7 @@ export async function auditSite(url, checks, opts = {}) {
242
240
  reason: `normalize produced invalid CheckResult: ${errors.join('; ')}`,
243
241
  };
244
242
  }
243
+ if (check.whyItMatters) result.whyItMatters = check.whyItMatters;
245
244
  return result;
246
245
  } catch (err) {
247
246
  return {
@@ -120,6 +120,25 @@ function renderFindings(findings, result) {
120
120
  return html;
121
121
  }
122
122
 
123
+ function renderDetails(result) {
124
+ if (!result?.details) return '';
125
+ let html = '';
126
+ const cats = result.details.categories;
127
+ if (cats && typeof cats === 'object' && !Array.isArray(cats)) {
128
+ const items = Object.entries(cats).map(([k, v]) => {
129
+ const label = k.replace(/-/g, ' ');
130
+ const cls = v >= 90 ? 'score-pass' : v >= 50 ? '' : 'score-fail';
131
+ return `<span class="${cls}" style="display:inline-block;margin-right:1rem"><strong>${esc(label)}</strong> ${v}/100</span>`;
132
+ });
133
+ html += `<div style="margin-bottom:.75rem;font-size:.9rem">${items.join('')}</div>`;
134
+ }
135
+ const types = result.details.types;
136
+ if (Array.isArray(types) && types.length) {
137
+ html += `<div style="margin-bottom:.75rem;font-size:.85rem;color:#555">Schema types: ${types.map(t => `<strong>${esc(String(t))}</strong>`).join(', ')}</div>`;
138
+ }
139
+ return html;
140
+ }
141
+
123
142
  /**
124
143
  * Generate a 2-3 sentence executive summary for a comparison report.
125
144
  * @param {import('./diff.mjs').DeltaReport} delta
@@ -171,6 +190,8 @@ function checkSection(result) {
171
190
  </div>
172
191
  </div>
173
192
  <div class="check-body">
193
+ ${result.whyItMatters ? `<p style="color:#666;font-size:.85rem;font-style:italic;margin-bottom:.5rem">${esc(result.whyItMatters)}</p>` : ''}
194
+ ${renderDetails(result)}
174
195
  ${renderFindings(result.findings, result)}
175
196
  </div>
176
197
  </div>`;
@@ -306,18 +327,20 @@ export function renderComparison(delta) {
306
327
  ? `<span class="${d.deltaScore > 0 ? 'delta-pos' : 'delta-neg'}">(${d.deltaScore > 0 ? '+' : ''}${d.deltaScore})</span>`
307
328
  : '';
308
329
 
330
+ const why = d.a?.whyItMatters || d.b?.whyItMatters || '';
309
331
  html += `<div class="compare-card" id="compare-${esc(d.checkId)}">
310
332
  <div class="compare-card-header">
311
333
  <span class="check-title">${STATUS_ICON[d.a?.status || d.b?.status || 'skip'] || ''} ${esc(d.tool)} ${deltaLabel}</span>
312
334
  <div class="check-meta">${aBadge} ${bBadge}</div>
313
335
  </div>
314
- <div class="compare-card-body">`;
336
+ <div class="compare-card-body">
337
+ ${why ? `<p style="color:#666;font-size:.85rem;font-style:italic;margin-bottom:.75rem">${esc(why)}</p>` : ''}`;
315
338
 
316
339
  if (d.availability === 'both') {
317
340
  const { shared, aOnly: aOnlyF, bOnly: bOnlyF } = classifyFindings(d.a, d.b);
318
341
  html += '<div class="side-by-side">';
319
- html += `<div class="site-column"><h4>${esc(labelA)}</h4>${compareCardFindings(esc(labelA) + ' only', aOnlyF, d.a)}</div>`;
320
- html += `<div class="site-column"><h4>${esc(labelB)}</h4>${compareCardFindings(esc(labelB) + ' only', bOnlyF, d.b)}</div>`;
342
+ html += `<div class="site-column"><h4>${esc(labelA)}</h4>${renderDetails(d.a)}${compareCardFindings(esc(labelA) + ' only', aOnlyF, d.a)}</div>`;
343
+ html += `<div class="site-column"><h4>${esc(labelB)}</h4>${renderDetails(d.b)}${compareCardFindings(esc(labelB) + ' only', bOnlyF, d.b)}</div>`;
321
344
  html += '</div>';
322
345
  if (shared.length) {
323
346
  html += `<div class="finding-group-label" style="margin-top:1rem">Shared issues (both sites)</div>${renderFindings(shared)}`;
@@ -91,13 +91,14 @@ test('observatory: normalize fixture produces valid CheckResult with grade', ()
91
91
  // --- testssl ---
92
92
  import * as testssl from '../src/checks/testssl.mjs';
93
93
 
94
- test('testssl: normalize fixture produces valid CheckResult with TLS findings', () => {
94
+ test('testssl: normalize ssl-checker fixture produces valid CheckResult', () => {
95
95
  const r = testssl.normalize(ok('testssl'), 30000);
96
96
  const { valid, errors } = validateCheckResult(r);
97
97
  assert.ok(valid, errors.join('; '));
98
98
  assert.equal(r.checkId, 'testssl');
99
- assert.ok(r.findings.length >= 2, 'TLS 1.1 + SWEET32');
100
- assert.ok(r.findings.some(f => f.context?.includes('CVE')), 'SWEET32 has a CVE');
99
+ assert.equal(r.status, 'pass');
100
+ assert.equal(r.findings.length, 0);
101
+ assert.ok(r.passSummary?.includes('TLSv1.3'));
101
102
  });
102
103
 
103
104
  // --- meta-og ---
@@ -10,28 +10,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
10
10
  const fix = (id) => readFileSync(join(__dirname, 'fixtures', `${id}.json`), 'utf8');
11
11
  const ok = (id) => ({ code: 0, stdout: fix(id), stderr: '', timedOut: false });
12
12
 
13
- // --- blacklight ---
14
- import * as blacklight from '../src/checks/blacklight.mjs';
15
-
16
- test('blacklight: normalize fixture produces valid CheckResult with tracker findings', () => {
17
- const r = blacklight.normalize(ok('blacklight'), 20000);
18
- const { valid, errors } = validateCheckResult(r);
19
- assert.ok(valid, errors.join('; '));
20
- assert.equal(r.checkId, 'blacklight');
21
- assert.equal(r.score, null);
22
- assert.ok(r.findings.some(f => f.message.includes('Session replay')));
23
- assert.ok(r.findings.some(f => f.message.includes('Canvas fingerprinting')));
24
- assert.ok(r.findings.some(f => f.message.includes('Facebook Pixel')));
25
- assert.ok(r.findings.some(f => f.message.includes('third-party cookies')));
26
- });
27
-
28
- test('blacklight: clean site → no findings', () => {
29
- const r = blacklight.normalize({ code: 0, stdout: '{}', stderr: '', timedOut: false }, 5000);
30
- const { valid, errors } = validateCheckResult(r);
31
- assert.ok(valid, errors.join('; '));
32
- assert.equal(r.findings.length, 0);
33
- });
34
-
35
13
  // --- unlighthouse ---
36
14
  import * as unlighthouse from '../src/checks/unlighthouse.mjs';
37
15
 
@@ -1 +1 @@
1
- [{"id":"TLS1","severity":"OK","finding":"TLS 1.3 offered"},{"id":"TLS1_1","severity":"LOW","finding":"TLS 1.1 still offered"},{"id":"heartbleed","severity":"OK","finding":"not vulnerable"},{"id":"POODLE_SSL","severity":"OK","finding":"not vulnerable"},{"id":"SWEET32","severity":"MEDIUM","finding":"potentially vulnerable, uses 64 bit block ciphers","cve":"CVE-2016-2183"}]
1
+ {"valid":true,"validFrom":"2026-01-01T00:00:00.000Z","validTo":"2027-01-01T00:00:00.000Z","daysRemaining":215,"protocol":"TLSv1.3","cipher":"TLS_AES_256_GCM_SHA384"}
@@ -1,86 +0,0 @@
1
- // Blacklight detects runtime tracker behavior: session replay scripts,
2
- // canvas fingerprinting, keyloggers, ad trackers, third-party cookies.
3
- // It loads the page in headless Chromium and observes what the page does.
4
-
5
- export const checkId = 'blacklight';
6
- export const tool = 'Blacklight (tracker detection)';
7
- export const defaultTimeoutMs = 120_000;
8
-
9
- export async function detect(executor) {
10
- const r = await executor.spawn('blacklight-collector', ['--help'], { timeoutMs: 15_000 });
11
- return r.code === 0 || r.stdout.includes('blacklight');
12
- }
13
-
14
- export async function run(url, executor) {
15
- return executor.spawn('blacklight-collector', [url, '--json'], { timeoutMs: 120_000 });
16
- }
17
-
18
- const TRACKER_SEVERITY = {
19
- session_recorders: 'moderate',
20
- canvas_fingerprinters: 'moderate',
21
- key_logging: 'serious',
22
- fb_pixel: 'info',
23
- google_analytics: 'info',
24
- third_party_trackers: 'info',
25
- };
26
-
27
- export function normalize(raw, durationMs) {
28
- if (raw.code !== 0 && !raw.stdout) {
29
- return { checkId, tool, status: 'error', score: null, grade: null, severity: null, findings: [], durationMs, reason: raw.stderr || 'blacklight failed' };
30
- }
31
-
32
- let data;
33
- try { data = JSON.parse(raw.stdout); } catch {
34
- return { checkId, tool, status: 'error', score: null, grade: null, severity: null, findings: [], durationMs, reason: 'failed to parse blacklight JSON' };
35
- }
36
-
37
- const findings = [];
38
-
39
- if (data.session_recorders?.length) {
40
- for (const r of data.session_recorders) {
41
- findings.push({ severity: 'moderate', message: `Session replay: ${r.name || r.url || 'unknown'}`, url: r.url });
42
- }
43
- }
44
- if (data.canvas_fingerprinters?.length) {
45
- for (const f of data.canvas_fingerprinters) {
46
- findings.push({ severity: 'moderate', message: `Canvas fingerprinting: ${f.name || f.url || 'unknown'}`, url: f.url });
47
- }
48
- }
49
- if (data.key_logging?.length) {
50
- for (const k of data.key_logging) {
51
- findings.push({ severity: 'serious', message: `Key logging detected: ${k.name || k.url || 'unknown'}`, url: k.url });
52
- }
53
- }
54
- if (data.fb_pixel_events?.length || data.fb_pixel) {
55
- findings.push({ severity: 'info', message: `Facebook Pixel active (${(data.fb_pixel_events || []).length} events)` });
56
- }
57
- if (data.third_party_trackers?.length) {
58
- for (const t of data.third_party_trackers.slice(0, 10)) {
59
- findings.push({ severity: 'info', message: `Third-party tracker: ${t.name || t.url || 'unknown'}`, url: t.url });
60
- }
61
- if (data.third_party_trackers.length > 10) {
62
- findings.push({ severity: 'info', message: `... and ${data.third_party_trackers.length - 10} more trackers` });
63
- }
64
- }
65
- if (data.third_party_cookies?.length) {
66
- findings.push({ severity: 'info', message: `${data.third_party_cookies.length} third-party cookies set` });
67
- }
68
-
69
- const worstSev = findings.length ? findings.reduce((w, f) => {
70
- const o = { critical: 0, serious: 1, moderate: 2, info: 3 };
71
- return (o[f.severity] ?? 3) < (o[w] ?? 3) ? f.severity : w;
72
- }, 'info') : null;
73
-
74
- const hasSerious = findings.some(f => f.severity === 'serious' || f.severity === 'critical');
75
- const passSummary = !hasSerious
76
- ? (findings.length === 0
77
- ? 'No trackers, fingerprinters, or session recorders detected'
78
- : `No serious trackers detected (${findings.length} low-severity item${findings.length !== 1 ? 's' : ''} only)`)
79
- : undefined;
80
-
81
- return {
82
- checkId, tool, status: hasSerious ? 'fail' : 'pass',
83
- score: null, grade: null, severity: worstSev, findings, durationMs,
84
- ...(passSummary && { passSummary }),
85
- };
86
- }
@@ -1 +0,0 @@
1
- {"session_recorders":[{"name":"Hotjar","url":"https://static.hotjar.com/c/hotjar-123.js"}],"canvas_fingerprinters":[{"name":"FingerprintJS","url":"https://cdn.fingerprint.com/v3"}],"key_logging":[],"fb_pixel_events":[{"type":"PageView"}],"third_party_trackers":[{"name":"Google Analytics","url":"https://www.google-analytics.com/analytics.js"},{"name":"Segment","url":"https://cdn.segment.com/analytics.js"}],"third_party_cookies":[{"name":"_ga","domain":".google.com"},{"name":"_fbp","domain":".facebook.com"}]}