#!/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/.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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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)} tr.cathead td{background:#151815;color:var(--dim);font-size:.7rem;letter-spacing:.18em; text-transform:uppercase;padding:.9rem .6rem .45rem;border-bottom:1px solid var(--rule)} .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 = ``; function page(title, body) { return ` ${esc(title)}
${NAV}${body}
Generated from bigtech.db on ${esc(cat.generated_at.slice(0, 10))}. Method ${esc(cat.method_version)}. Every number recomputes from its evidence or the build fails.
`; } // ------------------------------------------------------------------ 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 ` ${esc(s.label)} ${s.weight} ${s.value} `; }).join(''); const evRows = evidence.map((e) => ` ${esc(e.claim)} T${e.tier} ${e.artifact && e.artifact.startsWith('http') ? `source` : `${esc((e.artifact || '').split('/').slice(-1)[0])}`} `).join(''); const fRows = findings.map((f) => ` ${esc(f.severity)} ${esc(f.title)}${f.cve ? ` ${esc(f.cve)}` : ''} ${esc(f.status)} `).join(''); const body = `

${esc(product.name)} ${esc(a.version)} on ${esc(a.platform)}

Assessed ${esc(a.assessed_on)} by CLU, Binary Ranch. Source commit ${esc((a.source_commit || '').slice(0, 12))}. Method ${esc(a.method_version)}.

${a.weighted_total}/ 100   GRADE ${esc(a.grade)}

${esc(a.headline)}

The score

${rows}
DimensionWeightScore

The weighted average is not the headline. The weakest dimension is, because a high average with one bad rung is an average that hides the thing that will hurt you.

Findings

${fRows}
SeverityFindingStatus

Evidence

${evRows}
ClaimTierArtifact

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 no tier 4 claim is allowed to move a score.

What was not tested

${esc(a.not_tested || 'Not recorded.')}

Check it yourself

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.

git clone ssh://git@forge.binaryranch.com:2222/bunkhouse/bigtech-sucks.git
cd bigtech-sucks && npm run db:build
`; return page(`${product.name} scored ${a.weighted_total} — BigTech.sucks`, body); } // ------------------------------------------------------------------ method function renderMethod() { const dims = cat.dimensions.map((d) => ` ${esc(d.label)} ${d.weight} ${esc(d.anchor_10)} `).join(''); const body = `

The method

How every score on this site is produced, and what we refuse to do to get one.

A privacy claim that cannot be checked is a slogan. We publish the method so a stranger can catch us lying.

Eight dimensions, weighted to 100

${dims}
DimensionWeightWhat a 10 requires

Evidence tiers

TierMeaning
T1We observed it, saved the artifact, and the procedure is re-runnable.
T2A primary source anyone can open: a vendor repository, a court filing, a regulator advisory.
T3Credible second-hand: reporting, an academic paper, an independent audit summary.
T4A claim with no artifact. Never allowed to move a score. It appears in an appendix, labelled, and that is all.

The caps

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.

Reproducible builds

no reproducible build        verifiability <= 3
experimental, unverified     verifiability <= 6
verified, bit-identical      verifiability <= 10

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.

Closed source

Closed source is not penalised. It starts lower and climbs a public ladder. The ceiling is the rung you are standing on.

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

A court filing lifts the ceiling entirely, because it needs no vendor cooperation.

And the rule that makes it honest

a vendor running our harness produces a DECLARATION, not an OBSERVATION.

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.

The dynamic test

RunWhat it does
D1Cold idle. Fresh install, no account, one hour. Capture every packet.
D2Normal use, 24 hours. Capture all egress.
D3Device access trace: audio, camera, location, clipboard, contacts, screen.
D4Update cycle. Capture the channel, verify the signature, check the pinning.

The vendor publishes a destination allowlist. Every destination observed in D1 and D2 must appear on it. Anything unlisted is a defect, regardless of what it turns out to be.

What this method cannot do

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.

So the honest claim is not purity. It is this: we can prove what a specific build did while we watched, and we can make a false statement expensive to sign. 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.

Our own stack goes first

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.

`; return page('The method — BigTech.sucks', body); } // ------------------------------------------------------------------ list // The front door. Not the game. The list. function renderList() { const rated = cat.products .filter((p) => p.weighted_total !== null) .sort((a, b) => b.weighted_total - a.weighted_total); const unrated = cat.products.filter((p) => p.weighted_total === null); const byCat = {}; for (const p of unrated) (byCat[p.category_id ?? 'other'] ||= []).push(p); const catOrder = Object.keys(byCat).sort(); const ratedRows = rated.map((p) => ` ${esc(p.name)} ${esc(p.vendor)} ${esc(p.category_id)} ${p.weighted_total} ${esc(p.grade)} ${esc(p.weakest_dimension || '')} `).join(''); const unratedBlocks = catOrder.map((c) => ` ${esc(c)} ${byCat[c].length} ${byCat[c].sort((a, b) => a.name.localeCompare(b.name)).map((p) => ` ${esc(p.name)} ${esc(p.vendor)} ${esc(p.category_id)} — not rated `).join('')}`).join(''); const body = `

The software rating list

${rated.length} rated of ${cat.products.length} listed. Every score is a measurement with an evidence trail, not an opinion with a number attached.

A rating is published only when a product has been audited, its evidence saved, and its total recomputed by the build. Anything without that is listed as not rated, which is a fact about our work, not about the software.

Rated

${ratedRows || ''}
ProductVendorCategoryScoreGradeWeakest
Nothing rated yet.

Not yet rated

Ordered by category. Ask for any of these at the bottom and it moves up the queue.

${unratedBlocks}
ProductVendorCategoryScoreStatus

Ask for something to be rated

No account. No email required. If enough people ask for the same thing, it gets rated next. If you leave a contact we will tell you when it ships.

`; return page('The software rating list — BigTech.sucks', body); } // ------------------------------------------------------------------ badges // A badge is not a sticker. It is a live reference to our record. // It is served from our domain, so a self-issued copy is not a badge and the // link will not resolve. It is regenerated on every build, so a vendor who // improves earns a better badge without asking, and a vendor who regresses // loses it without warning. function renderBadgeSvg(p, a) { const score = String(a.weighted_total); const grade = String(a.grade); const date = String(a.assessed_on); return ` BIGTECH.SUCKS RATED ${esc(date)} ${esc(score)} GRADE ${esc(grade)} `; } function renderBadges() { const rated = cat.products .filter((p) => p.weighted_total !== null) .sort((a, b) => b.weighted_total - a.weighted_total); const examples = rated.map((p) => { const a = cat.assessments.find((x) => x.product_id === p.id); return `
${esc(p.name)} rating
${esc(p.name)} · ${p.weighted_total} · grade ${esc(p.grade)}
assessed ${esc(a?.assessed_on ?? '')} · weakest: ${esc(p.weakest_dimension ?? '')}
<a href="https://bigtech.sucks/report/${esc(p.id)}.html">
  <img src="https://bigtech.sucks/badge/${esc(p.id)}.svg"
       width="260" height="56"
       alt="${esc(p.name)}: bigtech.sucks rating ${p.weighted_total}/100">
</a>
`; }).join(''); const body = `

The rating badge

Put it in your footer. It is not a sticker, and that is the point.

If you have been rated by us, you may display the badge. It is served from our domain and regenerated from the database, so it is not a picture you copy. It is a live reference to a record you do not control.

How it works

The badge isWhich means
Served from bigtech.sucksA copy pasted onto your own server is not a badge and its link will not resolve.
Generated from the recordThe score and date are not typed by anyone. They come out of the database.
Regenerated every buildImprove and your badge improves without asking. Regress and it regresses without warning.
Linked to the reportAnyone who clicks it lands on your evidence trail, not a marketing page.

The rules, published plainly

Only a rated product may display a badge. There is no badge for an unrated product, and we will not issue one as a favour. If a badge is used on a product that is not rated, or is altered to show a score we did not publish, we will say so publicly.

A badge is free. It always will be. It is marketing for you and it is receipts for your customer, and charging for it would make it worthless to both.

Available badges

${examples || '

Nothing rated yet, so no badges exist. That is the correct state.

'}

Why you would want this

Because every competitor who cannot show one is leaving a question on the table. A privacy claim in your own copy is a claim. A badge that links to a re-runnable audit is evidence, and it costs you nothing but the honesty of being measured.

`; return page('The rating badge — BigTech.sucks', body); } // ------------------------------------------------------------------ write mkdirSync(join(PUB, 'report'), { recursive: true }); mkdirSync(join(PUB, 'badge'), { 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)); writeFileSync(join(PUB, 'badge', `${a.product_id}.svg`), renderBadgeSvg(product, a)); written++; } writeFileSync(join(PUB, 'badges.html'), renderBadges()); writeFileSync(join(PUB, 'method.html'), renderMethod()); writeFileSync(join(PUB, 'index.html'), renderList()); console.log(`rendered ${written} report(s) + ${written} badge(s) + list + method + badges page into packages/web/public/`);