diff --git a/packages/core/db/build.mjs b/packages/core/db/build.mjs index d7cf423..15d8872 100644 --- a/packages/core/db/build.mjs +++ b/packages/core/db/build.mjs @@ -145,7 +145,7 @@ function migrate(db) { if (!cols('assessment').includes('source_available')) { db.exec('ALTER TABLE assessment ADD COLUMN source_available INTEGER NOT NULL DEFAULT 1'); } - for (const v of ['v_backlog', 'v_coverage', 'v_product_score']) { + for (const v of ['v_backlog', 'v_coverage', 'v_product_score', 'v_requested']) { db.exec(`DROP VIEW IF EXISTS ${v}`); } } diff --git a/packages/core/db/hooks/probe.mjs b/packages/core/db/hooks/probe.mjs new file mode 100644 index 0000000..5e52d2b --- /dev/null +++ b/packages/core/db/hooks/probe.mjs @@ -0,0 +1,244 @@ +#!/usr/bin/env node +/** + * The hook prober. + * + * Drop in a URL, learn where Big Tech has custody of it. + * + * Everything here is PASSIVE and PUBLIC: DNS records, TLS certificate metadata, + * HTTP response headers, and the third-party hosts named in the page's own HTML. + * Nothing is scanned, nothing is probed for weakness, nothing is authenticated. + * If a site tells the world it sends email through Google, this reads it. + * + * The claim this supports is custody, not betrayal: + * your data sits with a third party who can be compelled, and you will not be told. + * That is provable from DNS. Whether the third party is malicious is a different + * question and this tool does not answer it. + * + * Usage: + * node probe.mjs https://example.com + * node probe.mjs example.com --json + */ +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; + +const run = promisify(execFile); +const UA = 'bigtech-hooks/0.1 (+https://bigtech.sucks; passive infrastructure survey)'; + +// ------------------------------------------------------------------ the map +// Patterns are matched against hostnames, and ASNs against the resolved origin. +const VENDORS = [ + { vendor: 'Amazon', hook: 'hosting', asns: ['16509'], hosts: ['amazonaws.com', 'cloudfront.net', 'awsstatic.com'], note: 'Amazon Web Services' }, + { vendor: 'Google', hook: 'hosting', asns: ['15169', '396982'], hosts: ['googleusercontent.com', '1e100.net', 'googleapis.com', 'gstatic.com'], note: 'Google Cloud' }, + { vendor: 'Microsoft', hook: 'hosting', asns: ['8075'], hosts: ['azureedge.net', 'microsoftonline.com', 'windows.net', 'azure.com'], note: 'Microsoft Azure' }, + { vendor: 'Cloudflare', hook: 'dns/cdn', asns: ['13335'], hosts: ['cloudflare.com', 'cloudflare.net', 'cdnjs.cloudflare.com'], note: 'Cloudflare' }, + { vendor: 'Fastly', hook: 'cdn', asns: ['54113'], hosts: ['fastly.net', 'fastlylb.net'], note: 'Fastly' }, + { vendor: 'Akamai', hook: 'cdn', asns: ['20940', '16625'], hosts: ['akamai.net', 'akamaized.net'], note: 'Akamai' }, + { vendor: 'Vercel', hook: 'hosting', asns: ['13335'], hosts: ['vercel.app', 'vercel.com', 'now.sh'], note: 'Vercel' }, + { vendor: 'Netlify', hook: 'hosting', asns: [], hosts: ['netlify.app', 'netlify.com'], note: 'Netlify' }, + { vendor: 'Google', hook: 'analytics', asns: [], hosts: ['googletagmanager.com', 'google-analytics.com', 'doubleclick.net'], note: 'Google Analytics / Tag Manager' }, + { vendor: 'Google', hook: 'fonts', asns: [], hosts: ['fonts.googleapis.com', 'fonts.gstatic.com'], note: 'Google Fonts, leaks visitor IPs to Google' }, + { vendor: 'Meta', hook: 'analytics', asns: [], hosts: ['connect.facebook.net', 'facebook.com', 'fbcdn.net'], note: 'Meta pixel' }, + { vendor: 'Google', hook: 'email', asns: ['15169'], hosts: ['googlemail.com', 'google.com'], note: 'Gmail / Google Workspace' }, + { vendor: 'Microsoft', hook: 'email', asns: ['8075'], hosts: ['outlook.com', 'protection.outlook.com', 'microsoft.com'], note: 'Microsoft 365' }, + { vendor: 'Amazon', hook: 'email', asns: ['16509'], hosts: ['amazonses.com', 'sendgrid.net'], note: 'Amazon SES / SendGrid' }, + { vendor: 'Salesforce', hook: 'email', asns: [], hosts: ['salesforce.com', 'exacttarget.com'], note: 'Salesforce Marketing Cloud' }, + { vendor: 'Mailchimp', hook: 'email', asns: [], hosts: ['mailchimp.com', 'list-manage.com'], note: 'Mailchimp' }, + { vendor: 'Stripe', hook: 'payments', asns: [], hosts: ['stripe.com', 'stripe.network', 'js.stripe.com'], note: 'Stripe' }, + { vendor: 'PayPal', hook: 'payments', asns: [], hosts: ['paypal.com', 'paypalobjects.com'], note: 'PayPal' }, + { vendor: 'Auth0', hook: 'identity', asns: [], hosts: ['auth0.com'], note: 'Auth0' }, + { vendor: 'Okta', hook: 'identity', asns: [], hosts: ['okta.com', 'oktapreview.com'], note: 'Okta' }, + { vendor: 'Twilio', hook: 'messaging', asns: [], hosts: ['twilio.com', 'twiliocdn.com'], note: 'Twilio' }, + { vendor: 'Intercom', hook: 'support', asns: [], hosts: ['intercom.io', 'intercomcdn.com'], note: 'Intercom' }, + { vendor: 'HubSpot', hook: 'marketing', asns: [], hosts: ['hubspot.com', 'hubspot.net', 'hs-scripts.com'], note: 'HubSpot' }, + { vendor: 'Slack', hook: 'chat', asns: [], hosts: ['slack.com', 'slack-edge.com'], note: 'Slack' }, + { vendor: 'Discord', hook: 'chat', asns: [], hosts: ['discord.com', 'discordapp.com', 'discord.gg'], note: 'Discord' }, +]; + +const ASN_VENDOR = { + '16509': 'Amazon', '14618': 'Amazon', '8987': 'Amazon', + '15169': 'Google', '396982': 'Google', '19527': 'Google', + '8075': 'Microsoft', '8068': 'Microsoft', + '13335': 'Cloudflare', + '54113': 'Fastly', + '20940': 'Akamai', '16625': 'Akamai', + '32934': 'Meta', '63293': 'Meta', + '36459': 'Meta', '54115': 'Meta', +}; + +// ------------------------------------------------------------------ dns +async function dig(name, type) { + try { + const { stdout } = await run('dig', ['+short', '+time=4', '+tries=2', type, name], { timeout: 8000 }); + return stdout.split('\n').map((s) => s.trim()).filter(Boolean); + } catch { return []; } +} + +async function asnFor(ip) { + if (!/^\d+\.\d+\.\d+\.\d+$/.test(ip)) return null; + const rev = ip.split('.').reverse().join('.'); + const txt = await dig(`${rev}.origin.asn.cymru.com`, 'TXT'); + if (!txt.length) return null; + const asn = txt[0].replace(/"/g, '').split('|')[0].trim(); + if (!asn) return null; + const name = await dig(`AS${asn}.asn.cymru.com`, 'TXT'); + const org = name.length ? name[0].replace(/"/g, '').split('|').slice(-1)[0].trim() : ''; + return { asn, org }; +} + +// ------------------------------------------------------------------ http +async function fetchHead(url) { + try { + const res = await fetch(url, { + method: 'GET', redirect: 'follow', headers: { 'User-Agent': UA }, + signal: AbortSignal.timeout(15000), + }); + const text = await res.text(); + return { + status: res.status, + finalUrl: res.url, + headers: Object.fromEntries(res.headers.entries()), + html: text.slice(0, 400000), + }; + } catch (err) { + return { error: String(err.message || err) }; + } +} + +function hostsInHtml(html) { + const hosts = new Set(); + const re = /(?:https?:)?\/\/([a-z0-9.-]+\.[a-z]{2,})/gi; + let m; + while ((m = re.exec(html))) hosts.add(m[1].toLowerCase()); + return [...hosts]; +} + +function classify(hostlist) { + const hits = []; + for (const h of hostlist) { + for (const v of VENDORS) { + if (v.hosts.some((p) => h === p || h.endsWith('.' + p))) { + hits.push({ vendor: v.vendor, hook: v.hook, evidence: h, note: v.note }); + break; + } + } + } + // dedupe by vendor+hook, keeping the first evidence + const seen = new Set(); + return hits.filter((h) => { + const k = h.vendor + '|' + h.hook; + if (seen.has(k)) return false; + seen.add(k); + return true; + }); +} + +// ------------------------------------------------------------------ probe +export async function probe(input) { + const url = input.startsWith('http') ? input : `https://${input}`; + const host = new URL(url).hostname.replace(/^www\./, ''); + const apex = host.split('.').slice(-2).join('.'); + + const [a, aaaa, ns, mx, txt] = await Promise.all([ + dig(host, 'A'), dig(host, 'AAAA'), dig(apex, 'NS'), dig(apex, 'MX'), dig(apex, 'TXT'), + ]); + + const origins = []; + for (const ip of [...a, ...aaaa].slice(0, 4)) { + const asn = await asnFor(ip); + if (asn) origins.push({ ip, ...asn }); + } + + const page = await fetchHead(url); + const pageHosts = page.html ? hostsInHtml(page.html) : []; + const scriptHosts = page.html + ? [...page.html.matchAll(/]+src=["']([^"']+)/gi)].map((m) => m[1]) + : []; + + const hooks = []; + const add = (vendor, hook, evidence, note) => { + if (!vendor) return; + if (hooks.some((h) => h.vendor === vendor && h.hook === hook)) return; + hooks.push({ vendor, hook, evidence, note }); + }; + + // hosting, from the ASN of the edge that answered + for (const o of origins) { + const v = ASN_VENDOR[o.asn]; + if (v) add(v, 'hosting', `${o.ip} AS${o.asn} ${o.org}`.trim(), 'edge network'); + } + // dns + for (const n of ns) { + const c = classify([n])[0]; + if (c) add(c.vendor, 'dns', n, c.note); + } + // email + for (const m of mx) { + const h = m.split(' ').pop(); + const c = classify([h])[0]; + if (c) add(c.vendor, 'email', h, c.note); + } + // what the SPF record says about who sends their mail + for (const t of txt) { + if (!/v=spf1/i.test(t)) continue; + for (const c of VENDORS) { + if (c.hook !== 'email') continue; + if (c.hosts.some((p) => t.toLowerCase().includes(p.split('.')[0]))) { + add(c.vendor, 'email-sender', c.hosts[0], c.note); + } + } + } + // headers + const srv = (page.headers?.server || '').toLowerCase(); + if (srv.includes('cloudflare')) add('Cloudflare', 'cdn', 'server: cloudflare', 'Cloudflare'); + if (srv.includes('vercel')) add('Vercel', 'hosting', 'server: vercel', 'Vercel'); + if (srv.includes('netlify')) add('Netlify', 'hosting', 'server: netlify', 'Netlify'); + if (page.headers?.['x-powered-by']) add(null, null, null, null); + + // what the page itself loads + for (const c of classify(pageHosts)) add(c.vendor, c.hook, c.evidence, c.note); + + return { + input, + host, + apex, + fetched: page.finalUrl || url, + status: page.status ?? null, + error: page.error ?? null, + dns: { a, aaaa, ns, mx, txt: txt.filter((t) => /spf|verification|site|domain/i.test(t)) }, + origins, + thirdPartyHosts: pageHosts.slice(0, 60), + scriptSources: scriptHosts.slice(0, 20), + hooks: hooks.sort((x, y) => x.vendor.localeCompare(y.vendor) || x.hook.localeCompare(y.hook)), + distinctVendors: [...new Set(hooks.map((h) => h.vendor))], + }; +} + +// ------------------------------------------------------------------ cli +if (process.argv[1] && process.argv[1].endsWith('probe.mjs')) { + const target = process.argv[2]; + const asJson = process.argv.includes('--json'); + if (!target) { + console.error('usage: node probe.mjs [--json]'); + process.exit(2); + } + const r = await probe(target); + if (asJson) { console.log(JSON.stringify(r, null, 2)); process.exit(0); } + + console.log(`\n${r.host}${r.status ? ` (HTTP ${r.status})` : ''}`); + console.log('='.repeat(60)); + if (r.error) console.log(` note: ${r.error}`); + + console.log('\nEDGE'); + for (const o of r.origins) console.log(` ${o.ip.padEnd(40)} AS${o.asn} ${o.org}`); + + if (r.hooks.length) { + console.log('\nHOOKS'); + for (const h of r.hooks) { + console.log(` ${h.vendor.padEnd(14)} ${h.hook.padEnd(14)} ${h.evidence}`); + } + console.log(`\n distinct vendors with a hook: ${r.distinctVendors.length} -> ${r.distinctVendors.join(', ')}`); + } else { + console.log('\nHOOKS\n none detected from public records'); + } + console.log(''); +} diff --git a/packages/core/db/render.mjs b/packages/core/db/render.mjs index 7fd99ce..ceed840 100644 --- a/packages/core/db/render.mjs +++ b/packages/core/db/render.mjs @@ -47,6 +47,8 @@ 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)} @@ -61,8 +63,10 @@ footer{margin-top:5rem;padding-top:1.5rem;border-top:1px solid var(--rule);color `; const NAV = ``; @@ -237,6 +241,121 @@ 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); +} + // ------------------------------------------------------------------ write mkdirSync(join(PUB, 'report'), { recursive: true }); @@ -248,5 +367,6 @@ for (const a of cat.assessments) { written++; } writeFileSync(join(PUB, 'method.html'), renderMethod()); +writeFileSync(join(PUB, 'index.html'), renderList()); -console.log(`rendered ${written} report page(s) + method.html into packages/web/public/`); +console.log(`rendered ${written} report page(s) + the list (index.html) + method.html into packages/web/public/`); diff --git a/packages/core/db/schema.sql b/packages/core/db/schema.sql index fe57e26..caf2eb4 100644 --- a/packages/core/db/schema.sql +++ b/packages/core/db/schema.sql @@ -178,6 +178,35 @@ CREATE TABLE IF NOT EXISTS attestation ( CREATE INDEX IF NOT EXISTS idx_attestation_product ON attestation(product_id, version); +-- ---------------------------------------------------------------- suggestions +-- Anyone can ask for a product to be rated. The backlog is built from these. +-- No account, no email required. An optional contact so we can tell them it shipped. +CREATE TABLE IF NOT EXISTS suggestion ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + product_name TEXT NOT NULL, + category_hint TEXT, + note TEXT, + contact TEXT, + suggested_at TEXT NOT NULL, + ip_hash TEXT, -- salted, never the raw address + status TEXT NOT NULL DEFAULT 'pending' + CHECK (status IN ('pending','accepted','duplicate','declined')) +); + +CREATE INDEX IF NOT EXISTS idx_suggestion_name ON suggestion(LOWER(product_name)); + +-- What the public is asking for, most-requested first. +CREATE VIEW IF NOT EXISTS v_requested AS +SELECT + LOWER(TRIM(s.product_name)) AS key, + TRIM(MIN(s.product_name)) AS product_name, + COUNT(*) AS requests, + MAX(s.suggested_at) AS latest +FROM suggestion s +WHERE s.status = 'pending' +GROUP BY LOWER(TRIM(s.product_name)) +ORDER BY requests DESC, latest DESC; + -- ---------------------------------------------------------------- derived views -- The single number the game and the report both use. CREATE VIEW IF NOT EXISTS v_product_score AS diff --git a/packages/core/src/data/generated/catalog.json b/packages/core/src/data/generated/catalog.json index 62ca7c2..c40a67d 100644 --- a/packages/core/src/data/generated/catalog.json +++ b/packages/core/src/data/generated/catalog.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-10-29T22:18:53.099Z", + "generated_at": "2026-10-29T22:21:27.412Z", "method_version": "btr-0.1", "dimensions": [ { @@ -3159,7 +3159,7 @@ ], "evidence": [ { - "id": 55, + "id": 66, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "local_data_exposure", "claim": "Attachments are stored unencrypted on the filesystem; the SQLCipher key is held by the OS keyring.", @@ -3170,7 +3170,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 56, + "id": 67, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Electron media permission defaults to false and audio is denied unless the user has enabled mediaPermissions.", @@ -3181,7 +3181,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 57, + "id": 68, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Zero analytics, tracker or crash-upload dependencies across 224 packages.", @@ -3192,7 +3192,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 58, + "id": 69, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Only two audio capture call sites exist and both are reachable only from an explicit UI action.", @@ -3203,7 +3203,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 59, + "id": 70, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "verifiability", "claim": "Signal states reproducible builds for macOS and Windows are not available, and the Linux build is experimental.", @@ -3214,7 +3214,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 60, + "id": 71, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "server_retention", "claim": "Grand jury subpoena response returned two timestamps and nothing else.", @@ -3225,7 +3225,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 61, + "id": 72, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "ownership_jurisdiction", "claim": "Signal Technology Foundation is a 501(c)(3) with EIN 82-4506840.", @@ -3236,7 +3236,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 62, + "id": 73, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "incident_record", "claim": "FBI and CISA PSA260626: Russian intelligence compromised individual Signal accounts via phishing and Backup Recovery Key theft, but NOT the encryption or the application itself.", @@ -3247,7 +3247,7 @@ "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 63, + "id": 74, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "incident_record", "claim": "Signal statement, 28 April 2026: \"Signal was not hacked, in that our encryption, infrastructure, and the integrity of the app code was not compromised.\" Additional linkage confirmations shipped 13 May 2026.", @@ -3258,7 +3258,7 @@ "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 64, + "id": 75, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "ownership_jurisdiction", "claim": "Open Technology Fund funded Open Whisper Systems $2,955,000 across 2013 to 2016, itemised on OTF own project page. This is the factual core under the \"US government funded Signal\" claim, and it is already reflected in the two points docked on this dimension.", @@ -3269,7 +3269,7 @@ "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 65, + "id": 76, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "user_control_exit", "claim": "Signal publicly committed to leaving the UK (2023, Online Safety Act) and Sweden (Feb 2025) rather than weaken encryption. A vendor that threatens to lose a market is behaving incompatibly with a secret backdoor.", @@ -3282,7 +3282,7 @@ ], "findings": [ { - "id": 25, + "id": 29, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "high", "title": "Received attachments are stored unencrypted on disk", @@ -3291,7 +3291,7 @@ "cve": "CVE-2023-24069" }, { - "id": 26, + "id": 30, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "medium", "title": "No verified reproducible build for the Linux package", @@ -3300,7 +3300,7 @@ "cve": null }, { - "id": 27, + "id": 31, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "medium", "title": "Local database key is held in the OS keyring", @@ -3309,7 +3309,7 @@ "cve": null }, { - "id": 28, + "id": 32, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "low", "title": "Registration requires a phone number", diff --git a/packages/core/src/data/generated/catalog.ts b/packages/core/src/data/generated/catalog.ts index ea62744..dc78549 100644 --- a/packages/core/src/data/generated/catalog.ts +++ b/packages/core/src/data/generated/catalog.ts @@ -39,7 +39,7 @@ export interface CatalogDimension { } export const catalog = { - "generated_at": "2026-10-29T22:18:53.099Z", + "generated_at": "2026-10-29T22:21:27.412Z", "method_version": "btr-0.1", "dimensions": [ { @@ -3199,7 +3199,7 @@ export const catalog = { ], "evidence": [ { - "id": 55, + "id": 66, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "local_data_exposure", "claim": "Attachments are stored unencrypted on the filesystem; the SQLCipher key is held by the OS keyring.", @@ -3210,7 +3210,7 @@ export const catalog = { "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 56, + "id": 67, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Electron media permission defaults to false and audio is denied unless the user has enabled mediaPermissions.", @@ -3221,7 +3221,7 @@ export const catalog = { "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 57, + "id": 68, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Zero analytics, tracker or crash-upload dependencies across 224 packages.", @@ -3232,7 +3232,7 @@ export const catalog = { "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 58, + "id": 69, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Only two audio capture call sites exist and both are reachable only from an explicit UI action.", @@ -3243,7 +3243,7 @@ export const catalog = { "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 59, + "id": 70, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "verifiability", "claim": "Signal states reproducible builds for macOS and Windows are not available, and the Linux build is experimental.", @@ -3254,7 +3254,7 @@ export const catalog = { "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 60, + "id": 71, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "server_retention", "claim": "Grand jury subpoena response returned two timestamps and nothing else.", @@ -3265,7 +3265,7 @@ export const catalog = { "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 61, + "id": 72, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "ownership_jurisdiction", "claim": "Signal Technology Foundation is a 501(c)(3) with EIN 82-4506840.", @@ -3276,7 +3276,7 @@ export const catalog = { "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 62, + "id": 73, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "incident_record", "claim": "FBI and CISA PSA260626: Russian intelligence compromised individual Signal accounts via phishing and Backup Recovery Key theft, but NOT the encryption or the application itself.", @@ -3287,7 +3287,7 @@ export const catalog = { "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 63, + "id": 74, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "incident_record", "claim": "Signal statement, 28 April 2026: \"Signal was not hacked, in that our encryption, infrastructure, and the integrity of the app code was not compromised.\" Additional linkage confirmations shipped 13 May 2026.", @@ -3298,7 +3298,7 @@ export const catalog = { "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 64, + "id": 75, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "ownership_jurisdiction", "claim": "Open Technology Fund funded Open Whisper Systems $2,955,000 across 2013 to 2016, itemised on OTF own project page. This is the factual core under the \"US government funded Signal\" claim, and it is already reflected in the two points docked on this dimension.", @@ -3309,7 +3309,7 @@ export const catalog = { "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 65, + "id": 76, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "user_control_exit", "claim": "Signal publicly committed to leaving the UK (2023, Online Safety Act) and Sweden (Feb 2025) rather than weaken encryption. A vendor that threatens to lose a market is behaving incompatibly with a secret backdoor.", @@ -3322,7 +3322,7 @@ export const catalog = { ], "findings": [ { - "id": 25, + "id": 29, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "high", "title": "Received attachments are stored unencrypted on disk", @@ -3331,7 +3331,7 @@ export const catalog = { "cve": "CVE-2023-24069" }, { - "id": 26, + "id": 30, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "medium", "title": "No verified reproducible build for the Linux package", @@ -3340,7 +3340,7 @@ export const catalog = { "cve": null }, { - "id": 27, + "id": 31, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "medium", "title": "Local database key is held in the OS keyring", @@ -3349,7 +3349,7 @@ export const catalog = { "cve": null }, { - "id": 28, + "id": 32, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "low", "title": "Registration requires a phone number", diff --git a/packages/server/src/index.js b/packages/server/src/index.js index ff5accb..6e041b4 100644 --- a/packages/server/src/index.js +++ b/packages/server/src/index.js @@ -1,4 +1,5 @@ import { createServer } from 'node:http'; +import { createHash } from 'node:crypto'; import { appendFile, mkdir } from 'node:fs/promises'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; @@ -7,6 +8,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const PORT = process.env.PORT || 3001; const DATA_DIR = process.env.WAITLIST_DIR || join(__dirname, '..', 'data'); const WAITLIST_FILE = join(DATA_DIR, 'waitlist.jsonl'); +const SUGGEST_FILE = join(DATA_DIR, 'suggestions.jsonl'); /** §8.3 — server stores ONLY waitlist email + channel + settlerSince */ async function handleWaitlist(req, res, body) { @@ -32,6 +34,54 @@ async function handleWaitlist(req, res, body) { res.end(JSON.stringify({ ok: true, stored: ['email', 'channel', 'settlerSince'] })); } +/** + * §suggestions — anyone can ask for a product to be rated. + * + * Stores the product name, an optional category hint, an optional note, and an + * optional contact. The visitor's address is salted and hashed, never stored raw, + * and only to rate-limit abuse. No account, no tracking, no cookie. + */ +async function handleSuggest(req, res, body) { + let data; + try { + data = JSON.parse(body); + } catch { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'invalid JSON' })); + return; + } + + const productName = String(data.product_name ?? '').trim().slice(0, 120); + if (productName.length < 2) { + res.writeHead(400, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'product_name required' })); + return; + } + + const entry = { + product_name: productName, + category_hint: String(data.category_hint ?? '').trim().slice(0, 60) || null, + note: String(data.note ?? '').trim().slice(0, 280) || null, + contact: String(data.contact ?? '').trim().slice(0, 160) || null, + suggested_at: new Date().toISOString(), + ip_hash: hashIp(req), + }; + + await mkdir(DATA_DIR, { recursive: true }); + await appendFile(SUGGEST_FILE, JSON.stringify(entry) + '\n'); + + res.writeHead(201, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ ok: true, product_name: entry.product_name })); +} + +/** Salted hash of the client address. The salt is per-deployment, never shipped. */ +function hashIp(req) { + const ip = (req.headers['x-forwarded-for'] || '').split(',')[0].trim() + || req.socket?.remoteAddress || 'unknown'; + const salt = process.env.SUGGEST_SALT || 'bigtech-local-salt'; + return createHash('sha256').update(salt + '|' + ip).digest('hex').slice(0, 32); +} + const server = createServer(async (req, res) => { res.setHeader('Access-Control-Allow-Origin', '*'); res.setHeader('Access-Control-Allow-Methods', 'POST, OPTIONS'); @@ -56,6 +106,19 @@ const server = createServer(async (req, res) => { return; } + if (req.method === 'POST' && req.url === '/api/suggest') { + const chunks = []; + for await (const chunk of req) chunks.push(chunk); + try { + await handleSuggest(req, res, Buffer.concat(chunks).toString()); + } catch (err) { + res.writeHead(500, { 'Content-Type': 'application/json' }); + res.end(JSON.stringify({ error: 'internal error' })); + console.error(err); + } + return; + } + if (req.method === 'GET' && req.url === '/api/health') { res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ ok: true, honeypot: false })); diff --git a/packages/web/index.html b/packages/web/game.html similarity index 100% rename from packages/web/index.html rename to packages/web/game.html diff --git a/packages/web/public/catalog.json b/packages/web/public/catalog.json index 62ca7c2..c40a67d 100644 --- a/packages/web/public/catalog.json +++ b/packages/web/public/catalog.json @@ -1,5 +1,5 @@ { - "generated_at": "2026-10-29T22:18:53.099Z", + "generated_at": "2026-10-29T22:21:27.412Z", "method_version": "btr-0.1", "dimensions": [ { @@ -3159,7 +3159,7 @@ ], "evidence": [ { - "id": 55, + "id": 66, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "local_data_exposure", "claim": "Attachments are stored unencrypted on the filesystem; the SQLCipher key is held by the OS keyring.", @@ -3170,7 +3170,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 56, + "id": 67, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Electron media permission defaults to false and audio is denied unless the user has enabled mediaPermissions.", @@ -3181,7 +3181,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 57, + "id": 68, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Zero analytics, tracker or crash-upload dependencies across 224 packages.", @@ -3192,7 +3192,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 58, + "id": 69, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "telemetry_egress", "claim": "Only two audio capture call sites exist and both are reachable only from an explicit UI action.", @@ -3203,7 +3203,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 59, + "id": 70, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "verifiability", "claim": "Signal states reproducible builds for macOS and Windows are not available, and the Linux build is experimental.", @@ -3214,7 +3214,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 60, + "id": 71, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "server_retention", "claim": "Grand jury subpoena response returned two timestamps and nothing else.", @@ -3225,7 +3225,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 61, + "id": 72, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "ownership_jurisdiction", "claim": "Signal Technology Foundation is a 501(c)(3) with EIN 82-4506840.", @@ -3236,7 +3236,7 @@ "observed_at": "2026-10-29T13:13:56-05:00" }, { - "id": 62, + "id": 73, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "incident_record", "claim": "FBI and CISA PSA260626: Russian intelligence compromised individual Signal accounts via phishing and Backup Recovery Key theft, but NOT the encryption or the application itself.", @@ -3247,7 +3247,7 @@ "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 63, + "id": 74, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "incident_record", "claim": "Signal statement, 28 April 2026: \"Signal was not hacked, in that our encryption, infrastructure, and the integrity of the app code was not compromised.\" Additional linkage confirmations shipped 13 May 2026.", @@ -3258,7 +3258,7 @@ "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 64, + "id": 75, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "ownership_jurisdiction", "claim": "Open Technology Fund funded Open Whisper Systems $2,955,000 across 2013 to 2016, itemised on OTF own project page. This is the factual core under the \"US government funded Signal\" claim, and it is already reflected in the two points docked on this dimension.", @@ -3269,7 +3269,7 @@ "observed_at": "2026-10-29T15:26:00-05:00" }, { - "id": 65, + "id": 76, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "dimension_id": "user_control_exit", "claim": "Signal publicly committed to leaving the UK (2023, Online Safety Act) and Sweden (Feb 2025) rather than weaken encryption. A vendor that threatens to lose a market is behaving incompatibly with a secret backdoor.", @@ -3282,7 +3282,7 @@ ], "findings": [ { - "id": 25, + "id": 29, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "high", "title": "Received attachments are stored unencrypted on disk", @@ -3291,7 +3291,7 @@ "cve": "CVE-2023-24069" }, { - "id": 26, + "id": 30, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "medium", "title": "No verified reproducible build for the Linux package", @@ -3300,7 +3300,7 @@ "cve": null }, { - "id": 27, + "id": 31, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "medium", "title": "Local database key is held in the OS keyring", @@ -3309,7 +3309,7 @@ "cve": null }, { - "id": 28, + "id": 32, "assessment_id": "A-2026-10-29-SIGNAL-DESKTOP", "severity": "low", "title": "Registration requires a phone number", diff --git a/packages/web/public/index.html b/packages/web/public/index.html new file mode 100644 index 0000000..58e3c2a --- /dev/null +++ b/packages/web/public/index.html @@ -0,0 +1,1136 @@ + + + +The software rating list — BigTech.sucks + +
+

The software rating list

+

1 rated of 137 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

+ + + + + + + + + + +
ProductVendorCategoryScoreGradeWeakest
Signal DesktopSignalmessaging76.4Blocal_data_exposure
+ +

Not yet rated

+

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ProductVendorCategoryScoreStatus
ai 12
AlpacaUnknown vendorai—not rated
ChatGPTOpenAIai—not rated
ChatQTUnknown vendorai—not rated
GaiUnknown vendorai—not rated
Google GeminiGoogle LLCai—not rated
GPT4ALLUnknown vendorai—not rated
LM StudioUnknown vendorai—not rated
MindMateUnknown vendorai—not rated
MooseUnknown vendorai—not rated
Reins: Chat for OllamaUnknown vendorai—not rated
TarotCasterUnknown vendorai—not rated
Writing ToolsUnknown vendorai—not rated
browser 11
Chromium Web BrowserUnknown vendorbrowser—not rated
FireDragonUnknown vendorbrowser—not rated
FirefoxUnknown vendorbrowser—not rated
Google ChromeGoogle LLCbrowser—not rated
LibreWolfUnknown vendorbrowser—not rated
Microsoft EdgeUnknown vendorbrowser—not rated
Mullvad BrowserUnknown vendorbrowser—not rated
WaterfoxUnknown vendorbrowser—not rated
WebUnknown vendorbrowser—not rated
Web AppsUnknown vendorbrowser—not rated
Yandex BrowserUnknown vendorbrowser—not rated
calendar 3
Google CalendarGoogle LLCcalendar—not rated
Nextcloud CalendarNextcloud GmbHcalendar—not rated
Nextcloud ContactsNextcloud GmbHcalendar—not rated
crm 1
SalesforceSalesforce, Inc.crm—not rated
dev 2
GitHubMicrosoft Corporationdev—not rated
VercelVercel Inc.dev—not rated
email 16
AerionUnknown vendoremail—not rated
BetterbirdUnknown vendoremail—not rated
ConveyUnknown vendoremail—not rated
GearyUnknown vendoremail—not rated
GmailGoogle LLCemail—not rated
KanmailUnknown vendoremail—not rated
MailUnknown vendoremail—not rated
MailspringUnknown vendoremail—not rated
MeliUnknown vendoremail—not rated
MeronUnknown vendoremail—not rated
Nextcloud MailNextcloud GmbHemail—not rated
OutlookMicrosoft Corporationemail—not rated
Proton MailProton AGemail—not rated
ThunderbirdUnknown vendoremail—not rated
Thunderbird ESRUnknown vendoremail—not rated
Tuta (Experimental)Unknown vendoremail—not rated
files 13
DropboxDropbox, Inc.files—not rated
Google DriveGoogle LLCfiles—not rated
LazyrsyncUnknown vendorfiles—not rated
Nextcloud (self-hosted)Nextcloud GmbHfiles—not rated
Nextcloud Dropbox MigrationNextcloud GmbHfiles—not rated
Nextcloud FilesNextcloud GmbHfiles—not rated
Nextcloud Google Drive MigrationNextcloud GmbHfiles—not rated
Nextcloud ServerNextcloud GmbHfiles—not rated
OcisUnknown vendorfiles—not rated
OneDriveMicrosoft Corporationfiles—not rated
OxiCloudUnknown vendorfiles—not rated
SeafileUnknown vendorfiles—not rated
Syncthing MacosUnknown vendorfiles—not rated
health 2
23andMe23andMe Holding Co.health—not rated
FitbitGoogle LLChealth—not rated
messaging 17
BitchatUnknown vendormessaging—not rated
ChatUnknown vendormessaging—not rated
DiscordDiscord Inc.messaging—not rated
FlareUnknown vendormessaging—not rated
FluxerUnknown vendormessaging—not rated
GajimUnknown vendormessaging—not rated
GNUnet MessengerUnknown vendormessaging—not rated
KaidanUnknown vendormessaging—not rated
KarereUnknown vendormessaging—not rated
LightningUnknown vendormessaging—not rated
MumbleUnknown vendormessaging—not rated
Nextcloud Spreed Signaling (HPB)Nextcloud GmbHmessaging—not rated
Nextcloud TalkNextcloud GmbHmessaging—not rated
ParlaUnknown vendormessaging—not rated
Simplex ChatUnknown vendormessaging—not rated
SlackSalesforce, Inc.messaging—not rated
WhatsieUnknown vendormessaging—not rated
money 2
Mint (Intuit)Intuit Inc.money—not rated
PlaidPlaid Inc.money—not rated
notes 12
Beaver NotesUnknown vendornotes—not rated
EvernoteEvernotenotes—not rated
Nextcloud NotesNextcloud GmbHnotes—not rated
NotesUnknown vendornotes—not rated
NotesnookUnknown vendornotes—not rated
NotionNotion Labsnotes—not rated
Obsidian SyncDynalist Inc.notes—not rated
Scrivano for Handwritten NotesUnknown vendornotes—not rated
Standard NotesUnknown vendornotes—not rated
Sticky NotesUnknown vendornotes—not rated
Swifty NotesUnknown vendornotes—not rated
Trilium NotesUnknown vendornotes—not rated
os 2
Google AndroidGoogle LLCos—not rated
Windows 11Microsoft Corporationos—not rated
passwords 13
1PasswordAgileBits Inc.passwords—not rated
Key RackUnknown vendorpasswords—not rated
LastPassLastPasspasswords—not rated
Nextcloud Password ClientUnknown vendorpasswords—not rated
Nextcloud PasswordsNextcloud GmbHpasswords—not rated
Nitrokey App2Unknown vendorpasswords—not rated
OneKeePassUnknown vendorpasswords—not rated
PasswordUnknown vendorpasswords—not rated
Password GeneratorUnknown vendorpasswords—not rated
Password SafeUnknown vendorpasswords—not rated
Password SecureUnknown vendorpasswords—not rated
Passwords and KeysUnknown vendorpasswords—not rated
Proton PassUnknown vendorpasswords—not rated
photos 13
A Photo Tool (Libre)Unknown vendorphotos—not rated
CameraUnknown vendorphotos—not rated
Ente PhotosUnknown vendorphotos—not rated
GNU Image Manipulation ProgramUnknown vendorphotos—not rated
Google PhotosGoogle LLCphotos—not rated
iCloud PhotosApple Inc.photos—not rated
Nextcloud PhotosNextcloud GmbHphotos—not rated
PhotoboothUnknown vendorphotos—not rated
PhotoCollageUnknown vendorphotos—not rated
PhotoflareUnknown vendorphotos—not rated
PhotometryUnknown vendorphotos—not rated
PhotoQtUnknown vendorphotos—not rated
PhotosUnknown vendorphotos—not rated
productivity 3
Google WorkspaceGoogle LLCproductivity—not rated
Microsoft 365Microsoft Corporationproductivity—not rated
Nextcloud OfficeNextcloud GmbHproductivity—not rated
project 3
AsanaAsana, Inc.project—not rated
Nextcloud DeckNextcloud GmbHproject—not rated
TrelloAtlassian Corporationproject—not rated
smarthome 2
Google NestGoogle LLCsmarthome—not rated
Ring (Amazon)Amazon.com, Inc.smarthome—not rated
social 2
FacebookMeta Platformssocial—not rated
X (Twitter)X Corp.social—not rated
streaming 1
NetflixNetflix, Inc.streaming—not rated
video 4
KinoKino AIvideo—not rated
Microsoft TeamsMicrosoft Corporationvideo—not rated
Otter.aiOtter.aivideo—not rated
ZoomZoom Communicationsvideo—not rated
voice 1
OpenAI Whisper APIOpenAIvoice—not rated
vpn 1
NordVPNNord Securityvpn—not rated
+ +

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.

+ +
+ + + + + +

+
+ +
Generated from bigtech.db on 2026-10-29. +Method btr-0.1. Every number recomputes from its evidence or the build fails.
+
\ No newline at end of file diff --git a/packages/web/public/method.html b/packages/web/public/method.html index 7c76ddb..89ab1b7 100644 --- a/packages/web/public/method.html +++ b/packages/web/public/method.html @@ -28,6 +28,8 @@ 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)} @@ -41,8 +43,10 @@ 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}

The method

diff --git a/packages/web/public/oath.json b/packages/web/public/oath.json index 3263e77..1522179 100644 --- a/packages/web/public/oath.json +++ b/packages/web/public/oath.json @@ -1,9 +1,9 @@ { - "digest": "1993b8de020ef8807e3e7499d9d45b429df9963b90182823ddc6e02b0ccd031c", + "digest": "26267b38b11900741b6933e21ae8a604a6545f754198f80cc74ccf54a478148a", "algorithm": "sha256", "file": "catalog.json", "byte_length": 91034, - "rebuilt_at": "2026-10-29T22:18:53.103Z", + "rebuilt_at": "2026-10-29T22:21:27.417Z", "method_version": "btr-0.1", "products": 137, "assessed": 1 diff --git a/packages/web/public/report/signal-desktop.html b/packages/web/public/report/signal-desktop.html index 3db6f5d..1b86554 100644 --- a/packages/web/public/report/signal-desktop.html +++ b/packages/web/public/report/signal-desktop.html @@ -28,6 +28,8 @@ 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)} @@ -41,8 +43,10 @@ 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}

Signal Desktop 8.36.0-alpha.1 on

diff --git a/packages/web/vite.config.ts b/packages/web/vite.config.ts index 9077eb8..a17ff3b 100644 --- a/packages/web/vite.config.ts +++ b/packages/web/vite.config.ts @@ -9,6 +9,14 @@ export default defineConfig({ '@bigtech-sucks/core': path.resolve(__dirname, '../core/src/index.ts'), }, }, + build: { + rollupOptions: { + // The front door is the generated rating list in public/index.html. + // The game is a separate page at /game.html, and will move to its own + // subdomain when it becomes its own funnel. + input: { game: path.resolve(__dirname, 'game.html') }, + }, + }, server: { port: 5173, proxy: {