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
|
|
|
#!/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) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[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)}
|
feat: the hook prober, and the list becomes the front door
Christian: 'drop any URL and say tell me where Big Tech has its hooks... we're
not just rating software, we're rating software as a service.'
db/hooks/probe.mjs answers that from public records only: DNS, MX, SPF, ASN via
Team Cymru, HTTP headers, and the third-party hosts named in the page's own HTML.
Nothing is scanned. If a site tells the world it sends mail through Google, this
reads it.
Verified live, 2026-10-29:
parler.com -> AS16509 AMAZON, plus Microsoft email, plus Vercel
netflix.com -> AS40027 NETFLIX (their own Open Connect edge), NOT AWS
signal.org -> Cloudflare CDN, Google touchpoints
bigtech.sucks-> AS16509 AMAZON (GoDaddy parking sits on AWS)
That netflix result is the important one. The front door is Netflix's own network;
the catalog and user data are in AWS. Edge ASN is not backend custody, and the tool
says so in its own header comment rather than pretending otherwise.
Also:
- The list is now the front door (public/index.html, generated from the DB).
The game moved to /game.html and keeps working, ready for its own subdomain.
- suggestion table + v_requested view: anyone can ask for a product to be rated,
no account, no email. Optional contact. Addresses are salted-hashed, never stored.
- POST /api/suggest on the existing server package, with a graceful fallback on the
static page to a Forgejo issue so requests are never lost when the server is down.
2026-10-29 17:23:11 -05:00
|
|
|
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)}
|
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
|
|
|
.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>
|
feat: the hook prober, and the list becomes the front door
Christian: 'drop any URL and say tell me where Big Tech has its hooks... we're
not just rating software, we're rating software as a service.'
db/hooks/probe.mjs answers that from public records only: DNS, MX, SPF, ASN via
Team Cymru, HTTP headers, and the third-party hosts named in the page's own HTML.
Nothing is scanned. If a site tells the world it sends mail through Google, this
reads it.
Verified live, 2026-10-29:
parler.com -> AS16509 AMAZON, plus Microsoft email, plus Vercel
netflix.com -> AS40027 NETFLIX (their own Open Connect edge), NOT AWS
signal.org -> Cloudflare CDN, Google touchpoints
bigtech.sucks-> AS16509 AMAZON (GoDaddy parking sits on AWS)
That netflix result is the important one. The front door is Netflix's own network;
the catalog and user data are in AWS. Edge ASN is not backend custody, and the tool
says so in its own header comment rather than pretending otherwise.
Also:
- The list is now the front door (public/index.html, generated from the DB).
The game moved to /game.html and keeps working, ready for its own subdomain.
- suggestion table + v_requested view: anyone can ask for a product to be rated,
no account, no email. Optional contact. Addresses are salted-hashed, never stored.
- POST /api/suggest on the existing server package, with a graceful fallback on the
static page to a Forgejo issue so requests are never lost when the server is down.
2026-10-29 17:23:11 -05:00
|
|
|
<a href="/">The list</a>
|
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
|
|
|
<a href="/method.html">Method</a>
|
feat: the rating badge, served from our domain and regenerated from the record
Christian: 'I want them to put the BigTech.Sucks rating badge on their site.
It should be marketing for them.'
A sticker anyone can copy is worthless, so it is not a sticker.
- badge/<product>.svg is generated from the assessment row on every build.
Score, grade and date are never typed by a human.
- Served from bigtech.sucks. A copy pasted onto a vendor's own server is not a
badge and its link will not resolve. The domain is the signature.
- Regenerated every build, so improving earns a better badge without asking and
regressing loses it without warning.
- The SVG carries the record id, version and method in a comment, so a real
badge can be traced back to one assessment.
badges.html publishes the rules: free forever, only rated products, no badge as
a favour, and misuse called out publicly. Free matters, because charging for it
would make it worthless to both sides: marketing for the vendor, receipts for
their customer.
Also rewrote the write pass to emit report + badge per assessment in one loop, so
they can never drift apart.
2026-10-29 17:24:33 -05:00
|
|
|
<a href="/badges.html">Badge</a>
|
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
|
|
|
<a href="/oath.html">The Oath</a>
|
feat: the hook prober, and the list becomes the front door
Christian: 'drop any URL and say tell me where Big Tech has its hooks... we're
not just rating software, we're rating software as a service.'
db/hooks/probe.mjs answers that from public records only: DNS, MX, SPF, ASN via
Team Cymru, HTTP headers, and the third-party hosts named in the page's own HTML.
Nothing is scanned. If a site tells the world it sends mail through Google, this
reads it.
Verified live, 2026-10-29:
parler.com -> AS16509 AMAZON, plus Microsoft email, plus Vercel
netflix.com -> AS40027 NETFLIX (their own Open Connect edge), NOT AWS
signal.org -> Cloudflare CDN, Google touchpoints
bigtech.sucks-> AS16509 AMAZON (GoDaddy parking sits on AWS)
That netflix result is the important one. The front door is Netflix's own network;
the catalog and user data are in AWS. Edge ASN is not backend custody, and the tool
says so in its own header comment rather than pretending otherwise.
Also:
- The list is now the front door (public/index.html, generated from the DB).
The game moved to /game.html and keeps working, ready for its own subdomain.
- suggestion table + v_requested view: anyone can ask for a product to be rated,
no account, no email. Optional contact. Addresses are salted-hashed, never stored.
- POST /api/suggest on the existing server package, with a graceful fallback on the
static page to a Forgejo issue so requests are never lost when the server is down.
2026-10-29 17:23:11 -05:00
|
|
|
<a href="/game.html">Game</a>
|
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
|
|
|
<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 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 && 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);
|
|
|
|
|
}
|
|
|
|
|
|
feat: the hook prober, and the list becomes the front door
Christian: 'drop any URL and say tell me where Big Tech has its hooks... we're
not just rating software, we're rating software as a service.'
db/hooks/probe.mjs answers that from public records only: DNS, MX, SPF, ASN via
Team Cymru, HTTP headers, and the third-party hosts named in the page's own HTML.
Nothing is scanned. If a site tells the world it sends mail through Google, this
reads it.
Verified live, 2026-10-29:
parler.com -> AS16509 AMAZON, plus Microsoft email, plus Vercel
netflix.com -> AS40027 NETFLIX (their own Open Connect edge), NOT AWS
signal.org -> Cloudflare CDN, Google touchpoints
bigtech.sucks-> AS16509 AMAZON (GoDaddy parking sits on AWS)
That netflix result is the important one. The front door is Netflix's own network;
the catalog and user data are in AWS. Edge ASN is not backend custody, and the tool
says so in its own header comment rather than pretending otherwise.
Also:
- The list is now the front door (public/index.html, generated from the DB).
The game moved to /game.html and keeps working, ready for its own subdomain.
- suggestion table + v_requested view: anyone can ask for a product to be rated,
no account, no email. Optional contact. Addresses are salted-hashed, never stored.
- POST /api/suggest on the existing server package, with a graceful fallback on the
static page to a Forgejo issue so requests are never lost when the server is down.
2026-10-29 17:23:11 -05:00
|
|
|
// ------------------------------------------------------------------ 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) => `<tr>
|
|
|
|
|
<td><a href="/report/${esc(p.id)}.html">${esc(p.name)}</a></td>
|
|
|
|
|
<td class="tier">${esc(p.vendor)}</td>
|
|
|
|
|
<td class="tier">${esc(p.category_id)}</td>
|
|
|
|
|
<td class="num"><strong>${p.weighted_total}</strong></td>
|
|
|
|
|
<td class="num">${esc(p.grade)}</td>
|
|
|
|
|
<td class="tier">${esc(p.weakest_dimension || '')}</td>
|
|
|
|
|
</tr>`).join('');
|
|
|
|
|
|
|
|
|
|
const unratedBlocks = catOrder.map((c) => `
|
|
|
|
|
<tr class="cathead"><td colspan="6">${esc(c)} <span class="tier">${byCat[c].length}</span></td></tr>
|
|
|
|
|
${byCat[c].sort((a, b) => a.name.localeCompare(b.name)).map((p) => `<tr>
|
|
|
|
|
<td>${esc(p.name)}</td>
|
|
|
|
|
<td class="tier">${esc(p.vendor)}</td>
|
|
|
|
|
<td class="tier">${esc(p.category_id)}</td>
|
|
|
|
|
<td class="num tier">—</td>
|
|
|
|
|
<td class="num tier">not rated</td>
|
|
|
|
|
<td></td>
|
|
|
|
|
</tr>`).join('')}`).join('');
|
|
|
|
|
|
|
|
|
|
const body = `
|
|
|
|
|
<h1>The software rating list</h1>
|
|
|
|
|
<p class="sub">${rated.length} rated of ${cat.products.length} listed.
|
|
|
|
|
Every score is a measurement with an evidence trail, not an opinion with a number attached.</p>
|
|
|
|
|
|
|
|
|
|
<p class="lede">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 <em>not rated</em>,
|
|
|
|
|
which is a fact about our work, not about the software.</p>
|
|
|
|
|
|
|
|
|
|
<h2>Rated</h2>
|
|
|
|
|
<table>
|
|
|
|
|
<tr><th>Product</th><th>Vendor</th><th>Category</th><th class="num" style="text-align:right">Score</th><th class="num" style="text-align:right">Grade</th><th>Weakest</th></tr>
|
|
|
|
|
${ratedRows || '<tr><td colspan="6" class="tier">Nothing rated yet.</td></tr>'}
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<h2>Not yet rated</h2>
|
|
|
|
|
<p class="note">Ordered by category. Ask for any of these at the bottom and it moves up the queue.</p>
|
|
|
|
|
<table>
|
|
|
|
|
<tr><th>Product</th><th>Vendor</th><th>Category</th><th class="num" style="text-align:right">Score</th><th class="num" style="text-align:right">Status</th><th></th></tr>
|
|
|
|
|
${unratedBlocks}
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<h2>Ask for something to be rated</h2>
|
|
|
|
|
<p class="note">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.</p>
|
|
|
|
|
|
|
|
|
|
<form id="suggest" style="margin-top:1.25rem;max-width:34rem">
|
|
|
|
|
<label style="display:block;margin-bottom:.6rem">
|
|
|
|
|
<span class="tier">Product name</span>
|
|
|
|
|
<input name="product_name" required maxlength="120"
|
|
|
|
|
style="display:block;width:100%;margin-top:.3rem;padding:.55rem .6rem;background:#111413;
|
|
|
|
|
border:1px solid var(--rule);color:var(--bone);font:inherit;font-size:.9rem">
|
|
|
|
|
</label>
|
|
|
|
|
<label style="display:block;margin-bottom:.6rem">
|
|
|
|
|
<span class="tier">Category, if you know it (optional)</span>
|
|
|
|
|
<input name="category_hint" maxlength="60"
|
|
|
|
|
style="display:block;width:100%;margin-top:.3rem;padding:.55rem .6rem;background:#111413;
|
|
|
|
|
border:1px solid var(--rule);color:var(--bone);font:inherit;font-size:.9rem">
|
|
|
|
|
</label>
|
|
|
|
|
<label style="display:block;margin-bottom:.6rem">
|
|
|
|
|
<span class="tier">Why it matters (optional)</span>
|
|
|
|
|
<input name="note" maxlength="280"
|
|
|
|
|
style="display:block;width:100%;margin-top:.3rem;padding:.55rem .6rem;background:#111413;
|
|
|
|
|
border:1px solid var(--rule);color:var(--bone);font:inherit;font-size:.9rem">
|
|
|
|
|
</label>
|
|
|
|
|
<label style="display:block;margin-bottom:1rem">
|
|
|
|
|
<span class="tier">Contact, if you want a reply (optional)</span>
|
|
|
|
|
<input name="contact" maxlength="160"
|
|
|
|
|
style="display:block;width:100%;margin-top:.3rem;padding:.55rem .6rem;background:#111413;
|
|
|
|
|
border:1px solid var(--rule);color:var(--bone);font:inherit;font-size:.9rem">
|
|
|
|
|
</label>
|
|
|
|
|
<button type="submit" style="padding:.6rem 1.2rem;background:var(--amber);color:#14110a;
|
|
|
|
|
border:0;font:inherit;font-weight:600;cursor:pointer">Send it</button>
|
|
|
|
|
<p id="msg" class="note" style="min-height:1.4rem"></p>
|
|
|
|
|
</form>
|
|
|
|
|
<script>
|
|
|
|
|
(() => {
|
|
|
|
|
const form = document.getElementById('suggest');
|
|
|
|
|
const msg = document.getElementById('msg');
|
|
|
|
|
form.addEventListener('submit', async (e) => {
|
|
|
|
|
e.preventDefault();
|
|
|
|
|
const data = Object.fromEntries(new FormData(form).entries());
|
|
|
|
|
msg.textContent = 'Sending…';
|
|
|
|
|
try {
|
|
|
|
|
const r = await fetch('/api/suggest', {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(data),
|
|
|
|
|
});
|
|
|
|
|
if (!r.ok) throw new Error('HTTP ' + r.status);
|
|
|
|
|
msg.textContent = 'Got it. It is on the list now, and it goes up the queue as more people ask.';
|
|
|
|
|
form.reset();
|
|
|
|
|
} catch (err) {
|
|
|
|
|
msg.innerHTML = 'The suggestion endpoint is not live yet. ' +
|
|
|
|
|
'<a href="https://forge.binaryranch.com/bunkhouse/bigtech-sucks/issues/new">Open it as an issue instead</a> ' +
|
|
|
|
|
'and it will not get lost.';
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
})();
|
|
|
|
|
</script>`;
|
|
|
|
|
return page('The software rating list — BigTech.sucks', body);
|
|
|
|
|
}
|
|
|
|
|
|
feat: the rating badge, served from our domain and regenerated from the record
Christian: 'I want them to put the BigTech.Sucks rating badge on their site.
It should be marketing for them.'
A sticker anyone can copy is worthless, so it is not a sticker.
- badge/<product>.svg is generated from the assessment row on every build.
Score, grade and date are never typed by a human.
- Served from bigtech.sucks. A copy pasted onto a vendor's own server is not a
badge and its link will not resolve. The domain is the signature.
- Regenerated every build, so improving earns a better badge without asking and
regressing loses it without warning.
- The SVG carries the record id, version and method in a comment, so a real
badge can be traced back to one assessment.
badges.html publishes the rules: free forever, only rated products, no badge as
a favour, and misuse called out publicly. Free matters, because charging for it
would make it worthless to both sides: marketing for the vendor, receipts for
their customer.
Also rewrote the write pass to emit report + badge per assessment in one loop, so
they can never drift apart.
2026-10-29 17:24:33 -05:00
|
|
|
// ------------------------------------------------------------------ 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 `<svg xmlns="http://www.w3.org/2000/svg" width="260" height="56" viewBox="0 0 260 56" role="img" aria-label="bigtech.sucks rating: ${esc(score)} out of 100, grade ${esc(grade)}, assessed ${esc(date)}">
|
|
|
|
|
<!-- record: ${esc(a.id)} | ${esc(a.version)} | method ${esc(a.method_version)} -->
|
|
|
|
|
<rect x="0.5" y="0.5" width="259" height="55" rx="5" fill="#0d0f0e" stroke="#2a2d2a"/>
|
|
|
|
|
<text x="14" y="23" font-family="ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" font-size="10" letter-spacing="1.6" fill="#8a8272">BIGTECH.SUCKS</text>
|
|
|
|
|
<text x="14" y="39" font-family="ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" font-size="8.5" letter-spacing="0.6" fill="#5f5a50">RATED ${esc(date)}</text>
|
|
|
|
|
<line x1="150" y1="12" x2="150" y2="44" stroke="#2a2d2a"/>
|
|
|
|
|
<text x="246" y="33" text-anchor="end" font-family="ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" font-size="23" fill="#d9a441">${esc(score)}</text>
|
|
|
|
|
<text x="246" y="45" text-anchor="end" font-family="ui-monospace,SFMono-Regular,Menlo,Consolas,monospace" font-size="8.5" letter-spacing="1.2" fill="#8a8272">GRADE ${esc(grade)}</text>
|
|
|
|
|
</svg>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 `
|
|
|
|
|
<div style="margin:1.5rem 0;padding:1rem;border:1px solid var(--rule);background:#111413">
|
|
|
|
|
<div style="display:flex;align-items:center;gap:1.25rem;flex-wrap:wrap">
|
|
|
|
|
<img src="/badge/${esc(p.id)}.svg" width="260" height="56" alt="${esc(p.name)} rating">
|
|
|
|
|
<div>
|
|
|
|
|
<div style="font-size:.9rem">${esc(p.name)} · <strong>${p.weighted_total}</strong> · grade ${esc(p.grade)}</div>
|
|
|
|
|
<div class="tier">assessed ${esc(a?.assessed_on ?? '')} · weakest: ${esc(p.weakest_dimension ?? '')}</div>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
<pre style="margin:1rem 0 0"><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></pre>
|
|
|
|
|
</div>`;
|
|
|
|
|
}).join('');
|
|
|
|
|
|
|
|
|
|
const body = `
|
|
|
|
|
<h1>The rating badge</h1>
|
|
|
|
|
<p class="sub">Put it in your footer. It is not a sticker, and that is the point.</p>
|
|
|
|
|
|
|
|
|
|
<p class="lede">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.</p>
|
|
|
|
|
|
|
|
|
|
<h2>How it works</h2>
|
|
|
|
|
<table>
|
|
|
|
|
<tr><th>The badge is</th><th>Which means</th></tr>
|
|
|
|
|
<tr><td>Served from <code>bigtech.sucks</code></td><td>A copy pasted onto your own server is not a badge and its link will not resolve.</td></tr>
|
|
|
|
|
<tr><td>Generated from the record</td><td>The score and date are not typed by anyone. They come out of the database.</td></tr>
|
|
|
|
|
<tr><td>Regenerated every build</td><td>Improve and your badge improves without asking. Regress and it regresses without warning.</td></tr>
|
|
|
|
|
<tr><td>Linked to the report</td><td>Anyone who clicks it lands on your evidence trail, not a marketing page.</td></tr>
|
|
|
|
|
</table>
|
|
|
|
|
|
|
|
|
|
<h2>The rules, published plainly</h2>
|
|
|
|
|
<p class="note">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.</p>
|
|
|
|
|
<p class="note">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.</p>
|
|
|
|
|
|
|
|
|
|
<h2>Available badges</h2>
|
|
|
|
|
${examples || '<p class="note">Nothing rated yet, so no badges exist. That is the correct state.</p>'}
|
|
|
|
|
|
|
|
|
|
<h2>Why you would want this</h2>
|
|
|
|
|
<p class="note">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.</p>`;
|
|
|
|
|
|
|
|
|
|
return page('The rating badge — BigTech.sucks', body);
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
// ------------------------------------------------------------------ write
|
|
|
|
|
mkdirSync(join(PUB, 'report'), { recursive: true });
|
feat: the rating badge, served from our domain and regenerated from the record
Christian: 'I want them to put the BigTech.Sucks rating badge on their site.
It should be marketing for them.'
A sticker anyone can copy is worthless, so it is not a sticker.
- badge/<product>.svg is generated from the assessment row on every build.
Score, grade and date are never typed by a human.
- Served from bigtech.sucks. A copy pasted onto a vendor's own server is not a
badge and its link will not resolve. The domain is the signature.
- Regenerated every build, so improving earns a better badge without asking and
regressing loses it without warning.
- The SVG carries the record id, version and method in a comment, so a real
badge can be traced back to one assessment.
badges.html publishes the rules: free forever, only rated products, no badge as
a favour, and misuse called out publicly. Free matters, because charging for it
would make it worthless to both sides: marketing for the vendor, receipts for
their customer.
Also rewrote the write pass to emit report + badge per assessment in one loop, so
they can never drift apart.
2026-10-29 17:24:33 -05:00
|
|
|
mkdirSync(join(PUB, 'badge'), { recursive: true });
|
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
|
|
|
|
|
|
|
|
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));
|
feat: the rating badge, served from our domain and regenerated from the record
Christian: 'I want them to put the BigTech.Sucks rating badge on their site.
It should be marketing for them.'
A sticker anyone can copy is worthless, so it is not a sticker.
- badge/<product>.svg is generated from the assessment row on every build.
Score, grade and date are never typed by a human.
- Served from bigtech.sucks. A copy pasted onto a vendor's own server is not a
badge and its link will not resolve. The domain is the signature.
- Regenerated every build, so improving earns a better badge without asking and
regressing loses it without warning.
- The SVG carries the record id, version and method in a comment, so a real
badge can be traced back to one assessment.
badges.html publishes the rules: free forever, only rated products, no badge as
a favour, and misuse called out publicly. Free matters, because charging for it
would make it worthless to both sides: marketing for the vendor, receipts for
their customer.
Also rewrote the write pass to emit report + badge per assessment in one loop, so
they can never drift apart.
2026-10-29 17:24:33 -05:00
|
|
|
writeFileSync(join(PUB, 'badge', `${a.product_id}.svg`), renderBadgeSvg(product, a));
|
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
|
|
|
written++;
|
|
|
|
|
}
|
feat: the rating badge, served from our domain and regenerated from the record
Christian: 'I want them to put the BigTech.Sucks rating badge on their site.
It should be marketing for them.'
A sticker anyone can copy is worthless, so it is not a sticker.
- badge/<product>.svg is generated from the assessment row on every build.
Score, grade and date are never typed by a human.
- Served from bigtech.sucks. A copy pasted onto a vendor's own server is not a
badge and its link will not resolve. The domain is the signature.
- Regenerated every build, so improving earns a better badge without asking and
regressing loses it without warning.
- The SVG carries the record id, version and method in a comment, so a real
badge can be traced back to one assessment.
badges.html publishes the rules: free forever, only rated products, no badge as
a favour, and misuse called out publicly. Free matters, because charging for it
would make it worthless to both sides: marketing for the vendor, receipts for
their customer.
Also rewrote the write pass to emit report + badge per assessment in one loop, so
they can never drift apart.
2026-10-29 17:24:33 -05:00
|
|
|
writeFileSync(join(PUB, 'badges.html'), renderBadges());
|
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
|
|
|
writeFileSync(join(PUB, 'method.html'), renderMethod());
|
feat: the hook prober, and the list becomes the front door
Christian: 'drop any URL and say tell me where Big Tech has its hooks... we're
not just rating software, we're rating software as a service.'
db/hooks/probe.mjs answers that from public records only: DNS, MX, SPF, ASN via
Team Cymru, HTTP headers, and the third-party hosts named in the page's own HTML.
Nothing is scanned. If a site tells the world it sends mail through Google, this
reads it.
Verified live, 2026-10-29:
parler.com -> AS16509 AMAZON, plus Microsoft email, plus Vercel
netflix.com -> AS40027 NETFLIX (their own Open Connect edge), NOT AWS
signal.org -> Cloudflare CDN, Google touchpoints
bigtech.sucks-> AS16509 AMAZON (GoDaddy parking sits on AWS)
That netflix result is the important one. The front door is Netflix's own network;
the catalog and user data are in AWS. Edge ASN is not backend custody, and the tool
says so in its own header comment rather than pretending otherwise.
Also:
- The list is now the front door (public/index.html, generated from the DB).
The game moved to /game.html and keeps working, ready for its own subdomain.
- suggestion table + v_requested view: anyone can ask for a product to be rated,
no account, no email. Optional contact. Addresses are salted-hashed, never stored.
- POST /api/suggest on the existing server package, with a graceful fallback on the
static page to a Forgejo issue so requests are never lost when the server is down.
2026-10-29 17:23:11 -05:00
|
|
|
writeFileSync(join(PUB, 'index.html'), renderList());
|
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
|
|
|
|
feat: the rating badge, served from our domain and regenerated from the record
Christian: 'I want them to put the BigTech.Sucks rating badge on their site.
It should be marketing for them.'
A sticker anyone can copy is worthless, so it is not a sticker.
- badge/<product>.svg is generated from the assessment row on every build.
Score, grade and date are never typed by a human.
- Served from bigtech.sucks. A copy pasted onto a vendor's own server is not a
badge and its link will not resolve. The domain is the signature.
- Regenerated every build, so improving earns a better badge without asking and
regressing loses it without warning.
- The SVG carries the record id, version and method in a comment, so a real
badge can be traced back to one assessment.
badges.html publishes the rules: free forever, only rated products, no badge as
a favour, and misuse called out publicly. Free matters, because charging for it
would make it worthless to both sides: marketing for the vendor, receipts for
their customer.
Also rewrote the write pass to emit report + badge per assessment in one loop, so
they can never drift apart.
2026-10-29 17:24:33 -05:00
|
|
|
console.log(`rendered ${written} report(s) + ${written} badge(s) + list + method + badges page into packages/web/public/`);
|