bigtech-sucks/packages/core/db/render.mjs
CLU c994a785a4 feat: render the public site from the database
The site is now a view over bigtech.db, not a hand-maintained set of pages.

- packages/core/db/render.mjs emits report/<product>.html and method.html into
  web/public, so Vite copies them into the deployable bundle. Re-running
  db:build regenerates them from whatever the database currently says.
- The report page leads with the score, then the dimension table with the
  weakest rung highlighted, then findings, then the evidence table with its
  tiers, then what was not tested, then how to clone and re-run the audit.
- The method page publishes the eight weights, the evidence tiers, both caps
  (reproducible builds and the closed-source ladder), the D1-D4 dynamic test,
  and a plain section on what the method cannot do.

Deployable bundle is 372K: the game, the method, the oath, the catalog and the
report. No server-side code, so it can sit on a Caddy static root and be nobody
else's problem to host.

Rendered 1 report page + method.html. All instrument checks still pass.
2026-10-29 16:24:13 -05:00

252 lines
13 KiB
JavaScript

#!/usr/bin/env node
/**
* Render the public site from the living database.
*
* Nothing here is hand-written HTML. Every number on these pages comes out of
* catalog.json, which comes out of bigtech.db, which fails its build if a score
* does not recompute from its evidence. That chain is the product.
*
* Emits:
* packages/web/public/report/<product>.html
* packages/web/public/method.html
*/
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
const HERE = dirname(fileURLToPath(import.meta.url));
const CORE = join(HERE, '..');
const PUB = join(CORE, '..', 'web', 'public');
const cat = JSON.parse(readFileSync(join(CORE, 'src', 'data', 'generated', 'catalog.json'), 'utf8'));
const esc = (s) => String(s ?? '').replace(/[&<>"]/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[c]));
const CSS = `
:root{--ink:#0d0f0e;--bone:#e8e2d4;--dim:#8a8272;--amber:#d9a441;--good:#7fae7a;--bad:#c9563f;--rule:#2a2d2a}
*{box-sizing:border-box}
body{margin:0;padding:3rem 1.25rem 7rem;background:var(--ink);color:var(--bone);
font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;font-size:15px;line-height:1.7;
-webkit-font-smoothing:antialiased}
.wrap{max-width:58rem;margin:0 auto}
nav{font-size:.72rem;letter-spacing:.18em;text-transform:uppercase;color:var(--dim);margin-bottom:3rem;display:flex;gap:1.5rem;flex-wrap:wrap}
nav a{color:var(--dim);text-decoration:none;border-bottom:1px solid transparent}
nav a:hover{color:var(--amber);border-bottom-color:var(--amber)}
h1{font-size:clamp(1.3rem,3.6vw,2.1rem);font-weight:400;line-height:1.2;margin:0 0 .6rem;letter-spacing:-.01em}
h2{font-size:.78rem;letter-spacing:.22em;text-transform:uppercase;color:var(--dim);font-weight:400;
margin:3.5rem 0 1rem;padding-bottom:.5rem;border-bottom:1px solid var(--rule)}
.sub{color:var(--dim);font-size:.85rem;margin:0 0 2.5rem}
.score{display:flex;align-items:baseline;gap:1rem;flex-wrap:wrap;margin:0 0 .5rem}
.score .n{font-size:clamp(2.6rem,9vw,4.4rem);line-height:1;color:var(--amber);letter-spacing:-.03em}
.score .g{font-size:1rem;color:var(--dim);letter-spacing:.2em}
.lede{font-size:1.02rem;margin:1.5rem 0 0;padding-left:1rem;border-left:2px solid var(--amber);color:var(--bone)}
table{width:100%;border-collapse:collapse;font-size:.84rem;margin:0}
th{text-align:left;color:var(--dim);font-weight:400;font-size:.7rem;letter-spacing:.14em;text-transform:uppercase;
padding:.5rem .6rem;border-bottom:1px solid var(--rule)}
td{padding:.55rem .6rem;border-bottom:1px solid #1b1e1b;vertical-align:top}
td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
.bar{display:inline-block;height:7px;background:var(--amber);vertical-align:middle;border-radius:1px}
.bar.low{background:var(--bad)}
.weak td{background:rgba(201,86,63,.07)}
.sev{font-size:.66rem;letter-spacing:.12em;text-transform:uppercase;padding:.1rem .4rem;border:1px solid var(--rule)}
.sev.high{border-color:var(--bad);color:var(--bad)}
.sev.medium{border-color:var(--amber);color:var(--amber)}
.sev.low,.sev.info{color:var(--dim)}
.tier{color:var(--dim);font-size:.72rem}
a{color:var(--amber);text-decoration:none;border-bottom:1px solid rgba(217,164,65,.35)}
a:hover{border-bottom-color:var(--amber)}
.note{color:var(--dim);font-size:.8rem;line-height:1.65;margin:1rem 0 0}
pre{overflow-x:auto;background:#111413;border:1px solid var(--rule);padding:1rem;font-size:.76rem;line-height:1.5;color:var(--bone)}
code{font-size:.86em;color:var(--bone)}
footer{margin-top:5rem;padding-top:1.5rem;border-top:1px solid var(--rule);color:var(--dim);font-size:.75rem}
`;
const NAV = `<nav>
<a href="/method.html">Method</a>
<a href="/oath.html">The Oath</a>
<a href="https://forge.binaryranch.com/bunkhouse/bigtech-sucks">Source</a>
</nav>`;
function page(title, body) {
return `<!doctype html>
<html lang="en"><head><meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<title>${esc(title)}</title>
<style>${CSS}</style></head>
<body><div class="wrap">${NAV}${body}
<footer>Generated from <code>bigtech.db</code> on ${esc(cat.generated_at.slice(0, 10))}.
Method ${esc(cat.method_version)}. Every number recomputes from its evidence or the build fails.</footer>
</div></body></html>`;
}
// ------------------------------------------------------------------ report
function renderReport(a) {
const product = cat.products.find((p) => p.id === a.product_id) ?? {};
const scores = cat.scores.filter((s) => s.assessment_id === a.id);
const evidence = cat.evidence.filter((e) => e.assessment_id === a.id);
const findings = cat.findings.filter((f) => f.assessment_id === a.id);
const maxW = Math.max(...scores.map((s) => s.weight));
const rows = scores.map((s) => {
const cls = s.dimension_id === a.weakest_dimension ? 'weak' : '';
return `<tr class="${cls}">
<td>${esc(s.label)}</td>
<td class="num">${s.weight}</td>
<td class="num">${s.value}</td>
<td style="width:150px"><span class="bar${s.value <= 5 ? ' low' : ''}" style="width:${(s.value * 14).toFixed(0)}px"></span></td>
</tr>`;
}).join('');
const evRows = evidence.map((e) => `<tr>
<td>${esc(e.claim)}</td>
<td class="tier">T${e.tier}</td>
<td>${e.artifact && e.artifact.startsWith('http')
? `<a href="${esc(e.artifact)}">source</a>`
: `<code>${esc((e.artifact || '').split('/').slice(-1)[0])}</code>`}</td>
</tr>`).join('');
const fRows = findings.map((f) => `<tr>
<td><span class="sev ${esc(f.severity)}">${esc(f.severity)}</span></td>
<td>${esc(f.title)}${f.cve ? ` <span class="tier">${esc(f.cve)}</span>` : ''}</td>
<td class="tier">${esc(f.status)}</td>
</tr>`).join('');
const body = `
<h1>${esc(product.name)} <span style="color:var(--dim);font-size:.55em">${esc(a.version)} on ${esc(a.platform)}</span></h1>
<p class="sub">Assessed ${esc(a.assessed_on)} by CLU, Binary Ranch. Source commit
<code>${esc((a.source_commit || '').slice(0, 12))}</code>. Method ${esc(a.method_version)}.</p>
<div class="score"><span class="n">${a.weighted_total}</span><span class="g">/ 100 &nbsp; GRADE ${esc(a.grade)}</span></div>
<p class="lede">${esc(a.headline)}</p>
<h2>The score</h2>
<table>
<tr><th>Dimension</th><th class="num" style="text-align:right">Weight</th><th class="num" style="text-align:right">Score</th><th></th></tr>
${rows}
</table>
<p class="note">The weighted average is not the headline. <strong>The weakest dimension is</strong>, because a
high average with one bad rung is an average that hides the thing that will hurt you.</p>
<h2>Findings</h2>
<table>
<tr><th>Severity</th><th>Finding</th><th>Status</th></tr>
${fRows}
</table>
<h2>Evidence</h2>
<table>
<tr><th>Claim</th><th>Tier</th><th>Artifact</th></tr>
${evRows}
</table>
<p class="note">Tier 1 is something we observed and saved. Tier 2 is a primary source anyone can open.
Tier 3 is credible second-hand. Tier 4 is a claim with no artifact, and <strong>no tier 4 claim is
allowed to move a score</strong>.</p>
<h2>What was not tested</h2>
<p class="note">${esc(a.not_tested || 'Not recorded.')}</p>
<h2>Check it yourself</h2>
<p class="note">The audit script that produced the tier 1 rows is public and re-runnable.
The database that holds this score is public. The build refuses to pass if the total does not
recompute from these weights and these scores.</p>
<pre>git clone ssh://git@forge.binaryranch.com:2222/bunkhouse/bigtech-sucks.git
cd bigtech-sucks &amp;&amp; npm run db:build</pre>`;
return page(`${product.name} scored ${a.weighted_total} — BigTech.sucks`, body);
}
// ------------------------------------------------------------------ method
function renderMethod() {
const dims = cat.dimensions.map((d) => `<tr>
<td>${esc(d.label)}</td>
<td class="num">${d.weight}</td>
<td class="tier">${esc(d.anchor_10)}</td>
</tr>`).join('');
const body = `
<h1>The method</h1>
<p class="sub">How every score on this site is produced, and what we refuse to do to get one.</p>
<p class="lede">A privacy claim that cannot be checked is a slogan. We publish the method so a
stranger can catch us lying.</p>
<h2>Eight dimensions, weighted to 100</h2>
<table>
<tr><th>Dimension</th><th class="num" style="text-align:right">Weight</th><th>What a 10 requires</th></tr>
${dims}
</table>
<h2>Evidence tiers</h2>
<table>
<tr><th>Tier</th><th>Meaning</th></tr>
<tr><td>T1</td><td>We observed it, saved the artifact, and the procedure is re-runnable.</td></tr>
<tr><td>T2</td><td>A primary source anyone can open: a vendor repository, a court filing, a regulator advisory.</td></tr>
<tr><td>T3</td><td>Credible second-hand: reporting, an academic paper, an independent audit summary.</td></tr>
<tr><td>T4</td><td>A claim with no artifact. <strong>Never allowed to move a score.</strong> It appears in an appendix, labelled, and that is all.</td></tr>
</table>
<h2>The caps</h2>
<p class="note">These are the only two places where a score is limited by something other than evidence.
Both exist because the alternative is pretending to know something we cannot.</p>
<h3 style="font-size:.9rem;margin:1.8rem 0 .4rem">Reproducible builds</h3>
<pre>no reproducible build verifiability <= 3
experimental, unverified verifiability <= 6
verified, bit-identical verifiability <= 10</pre>
<p class="note">Reading the source proves what the code does. It does not prove the binary running on
your machine is that code. Reproducible builds are the only bridge, and nothing else is.</p>
<h3 style="font-size:.9rem;margin:1.8rem 0 .4rem">Closed source</h3>
<p class="note">Closed source is not penalised. It starts lower and climbs a public ladder. The ceiling
is the rung you are standing on.</p>
<pre>L0 unattested -> 3 nothing but your word
L1 declared -> 5 SBOM + signed hash + published destination allowlist
L2 observed -> 7 someone other than you watched this exact binary
L3 reproducible -> 9 anyone can rebuild it and get the same bytes
L4 contested -> 10 someone tried to break it and published the attempt</pre>
<p class="note">A court filing lifts the ceiling entirely, because it needs no vendor cooperation.</p>
<h3 style="font-size:.9rem;margin:1.8rem 0 .4rem">And the rule that makes it honest</h3>
<pre>a vendor running our harness produces a DECLARATION, not an OBSERVATION.</pre>
<p class="note">A self-run test is a claim about oneself, so it caps at L1. But L3 and L4 survive a
self-run, because a public rebuild recipe and a published adversarial review can be re-executed by
anyone. That is the whole escape hatch: reproducibility converts a claim into a check.</p>
<h2>The dynamic test</h2>
<table>
<tr><th>Run</th><th>What it does</th></tr>
<tr><td>D1</td><td>Cold idle. Fresh install, no account, one hour. Capture every packet.</td></tr>
<tr><td>D2</td><td>Normal use, 24 hours. Capture all egress.</td></tr>
<tr><td>D3</td><td>Device access trace: audio, camera, location, clipboard, contacts, screen.</td></tr>
<tr><td>D4</td><td>Update cycle. Capture the channel, verify the signature, check the pinning.</td></tr>
</table>
<p class="note">The vendor publishes a destination allowlist. Every destination observed in D1 and D2
must appear on it. <strong>Anything unlisted is a defect, regardless of what it turns out to be.</strong></p>
<h2>What this method cannot do</h2>
<p class="note">It cannot prove the absence of a backdoor. Rice's theorem makes non-trivial behavioural
properties of arbitrary programs undecidable, and a backdoor triggered by remote configuration or by
message content is invisible to static inspection.</p>
<p class="note">So the honest claim is not purity. It is this: <strong>we can prove what a specific
build did while we watched, and we can make a false statement expensive to sign.</strong> An attestation
raises the cost of a backdoor and shortens its shelf life. It is not a certificate of innocence, and
anyone selling you one is selling you nothing.</p>
<h2>Our own stack goes first</h2>
<p class="note">brainix will be scored by this instrument before it is advertised by it. "Zero telemetry"
will mean a captured packet log with nothing in it, published, dated, and re-runnable, or we do not get
to say it.</p>`;
return page('The method — BigTech.sucks', body);
}
// ------------------------------------------------------------------ write
mkdirSync(join(PUB, 'report'), { recursive: true });
let written = 0;
for (const a of cat.assessments) {
const product = cat.products.find((p) => p.id === a.product_id);
if (!product) continue;
writeFileSync(join(PUB, 'report', `${a.product_id}.html`), renderReport(a));
written++;
}
writeFileSync(join(PUB, 'method.html'), renderMethod());
console.log(`rendered ${written} report page(s) + method.html into packages/web/public/`);