bigtech-sucks/packages/core/db/build.mjs
CLU de53ac41a5 bigtech.sucks: living database + first assessment
The database is the source of truth; the site is a view over it.

- packages/core/db/schema.sql: vendors, products, categories, the 8-dimension
  instrument, assessments, scores, evidence, findings, off-ramp, plus the
  derived v_product_score view the game reads.
- packages/core/db/build.mjs: rebuilds bigtech.db, absorbs the legacy 43-product
  roster as unassessed, applies seeds, then verifies the instrument's own rules
  (weights sum to 100, totals recompute, grades match bands, every score has a
  rationale, no tier-4 evidence on a scored dimension, reproducible-build caps
  actually enforced).
- packages/core/db/seed/0001-signal.sql: Signal Desktop 8.36.0-alpha.1 on linux,
  75.9/100 grade B, weakest dimension local_data_exposure. Evidence points at
  the re-runnable audit artifacts in the Servetus vault.
- packages/core/src/data/generated/catalog.{ts,json}: generated export.

Verified: 44 products, 1 assessed, all checks pass.
2026-10-29 13:16:29 -05:00

320 lines
15 KiB
JavaScript

#!/usr/bin/env node
/**
* bigtech.sucks — database build
*
* The database is the source of truth. This script:
* 1. rebuilds bigtech.db from schema + seed
* 2. absorbs the legacy hand-authored outlaw roster as UNASSESSED products
* 3. applies every db/seed/*.sql in name order
* 4. verifies the result against the instrument's own rules
* 5. exports a generated module the website imports
*
* Run: npm run db:build
* Verify only: npm run db:verify
*/
import { DatabaseSync } from 'node:sqlite';
import { readFileSync, writeFileSync, readdirSync, mkdirSync, existsSync, unlinkSync } 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 DB_PATH = join(HERE, 'bigtech.db');
const SEED_DIR = join(HERE, 'seed');
const GEN_DIR = join(CORE, 'src', 'data', 'generated');
const GRADES = (n) => (n >= 90 ? 'A' : n >= 75 ? 'B' : n >= 60 ? 'C' : n >= 40 ? 'D' : 'F');
// ---------------------------------------------------------------- vendor map
// Outlaw id -> vendor. Everything here is checkable public record.
const VENDORS = {
google: ['Google LLC', 'private', 'US', 'advertising'],
microsoft: ['Microsoft Corporation', 'public', 'US', 'mixed'],
apple: ['Apple Inc.', 'public', 'US', 'hardware'],
meta: ['Meta Platforms', 'public', 'US', 'advertising'],
amazon: ['Amazon.com, Inc.', 'public', 'US', 'mixed'],
salesforce: ['Salesforce, Inc.', 'public', 'US', 'subscription'],
atlassian: ['Atlassian Corporation', 'public', 'AU', 'subscription'],
zoom: ['Zoom Communications', 'public', 'US', 'subscription'],
discord: ['Discord Inc.', 'private', 'US', 'subscription'],
otter: ['Otter.ai', 'private', 'US', 'subscription'],
notion: ['Notion Labs', 'private', 'US', 'subscription'],
evernote: ['Evernote (Bending Spoons)', 'private', 'IT', 'subscription'],
obsidian: ['Dynalist Inc. (Obsidian)', 'private', 'CA', 'license'],
openai: ['OpenAI', 'private', 'US', 'mixed'],
lastpass: ['LastPass (GoTo)', 'private', 'US', 'subscription'],
'1password': ['AgileBits Inc. (1Password)', 'private', 'CA', 'subscription'],
proton: ['Proton AG', 'private', 'CH', 'subscription'],
dropbox: ['Dropbox, Inc.', 'public', 'US', 'subscription'],
'x-corp': ['X Corp.', 'private', 'US', 'advertising'],
intuit: ['Intuit Inc.', 'public', 'US', 'subscription'],
plaid: ['Plaid Inc.', 'private', 'US', 'mixed'],
asana: ['Asana, Inc.', 'public', 'US', 'subscription'],
'23andme': ['23andMe Holding Co.', 'public', 'US', 'mixed'],
vercel: ['Vercel Inc.', 'private', 'US', 'subscription'],
'nord-security': ['Nord Security', 'private', 'PA', 'subscription'],
netflix: ['Netflix, Inc.', 'public', 'US', 'subscription'],
'nextcloud-gmbh': ['Nextcloud GmbH', 'private', 'DE', 'subscription'],
'signal-foundation': ['Signal Technology Foundation', 'nonprofit', 'US', 'none'],
};
const OUTLAW_VENDOR = {
gmail: 'google', outlook: 'microsoft', protonmail: 'proton',
google_drive: 'google', dropbox: 'dropbox', onedrive: 'microsoft',
google_workspace: 'google', microsoft_365: 'microsoft',
slack: 'salesforce', discord: 'discord', zoom: 'zoom', otter: 'otter',
teams: 'microsoft', notion: 'notion', evernote: 'evernote',
obsidian_sync: 'obsidian', whisper_api: 'openai', lastpass: 'lastpass',
'1password': '1password', google_calendar: 'google',
google_photos: 'google', icloud_photos: 'apple', chrome: 'google',
chatgpt: 'openai', gemini: 'google', twitter: 'x-corp',
facebook: 'meta', mint: 'intuit', plaid: 'plaid',
salesforce: 'salesforce', asana: 'asana', trello: 'atlassian',
'23andme': '23andme', fitbit: 'google', ring: 'amazon', nest: 'google',
github: 'microsoft', vercel: 'vercel', nordvpn: 'nord-security',
netflix: 'netflix', windows: 'microsoft', android: 'google',
nextcloud: 'nextcloud-gmbh',
};
const ID_OVERRIDES = { '1password': 'onepassword', '23andme': 'twentythreeandme' };
const slug = (s) => ID_OVERRIDES[s] ?? s.replace(/[^a-z0-9]+/gi, '-').toLowerCase();
// ---------------------------------------------------------------- build
async function build() {
if (existsSync(DB_PATH)) unlinkSync(DB_PATH);
const db = new DatabaseSync(DB_PATH);
db.exec('PRAGMA foreign_keys = ON');
db.exec(readFileSync(join(HERE, 'schema.sql'), 'utf8'));
// vendors
const insVendor = db.prepare(`INSERT OR REPLACE INTO vendor
(id, name, legal_entity, entity_type, jurisdiction, business_model) VALUES (?,?,?,?,?,?)`);
for (const [id, [name, type, juris, model]] of Object.entries(VENDORS)) {
insVendor.run(id, name.replace(/\s*\(.*\)$/, ''), name, type, juris, model);
}
// categories and legacy roster, imported from the built core package so the
// database is guaranteed to match what the site shipped with.
const { categories } = await import(join(CORE, 'dist', 'data', 'categories.js'));
const { outlaws } = await import(join(CORE, 'dist', 'data', 'outlaws.js'));
const insCat = db.prepare(`INSERT OR REPLACE INTO category (id,label,sensitivity,nextcloud) VALUES (?,?,?,?)`);
for (const c of categories) insCat.run(c.id, c.label, c.sensitivity, c.nc);
const insProd = db.prepare(`INSERT OR IGNORE INTO product
(id,vendor_id,name,category_id,platform,sovereign,boss,art_slot,monthly_cost,notes)
VALUES (?,?,?,?,?,?,?,?,?,?)`);
let migrated = 0;
for (const o of outlaws) {
const vendorId = OUTLAW_VENDOR[o.id];
if (!vendorId) { console.warn(` ! no vendor mapped for outlaw '${o.id}'`); continue; }
insProd.run(slug(o.id), vendorId, o.name, o.category, 'web', o.sovereign ? 1 : 0,
o.boss ? 1 : 0, o.faceArtSlot, o.monthlyCost ?? null,
`Legacy editorial placeholder. Not yet assessed. Original policy=${o.policy} infra=${o.infra}.`);
migrated++;
}
// seeds
const seeds = readdirSync(SEED_DIR).filter((f) => f.endsWith('.sql')).sort();
for (const f of seeds) db.exec(readFileSync(join(SEED_DIR, f), 'utf8'));
db.close();
return { migrated, seeds };
}
// ---------------------------------------------------------------- verify
function verify() {
const db = new DatabaseSync(DB_PATH, { readOnly: true });
const fails = [];
const ok = (cond, msg) => { if (!cond) fails.push(msg); };
const dims = db.prepare('SELECT id, weight FROM dimension').all();
const weightSum = dims.reduce((a, d) => a + d.weight, 0);
ok(weightSum === 100, `dimension weights sum to ${weightSum}, expected 100`);
const assessments = db.prepare('SELECT * FROM assessment').all();
ok(assessments.length > 0, 'no assessments present');
for (const a of assessments) {
const scores = db.prepare('SELECT s.*, d.weight FROM score s JOIN dimension d ON d.id = s.dimension_id WHERE s.assessment_id = ?').all(a.id);
ok(scores.length === dims.length, `${a.id}: ${scores.length} scores, expected ${dims.length}`);
const recomputed = scores.reduce((acc, s) => acc + s.value * s.weight, 0) / 10;
ok(Math.abs(recomputed - a.weighted_total) < 0.05,
`${a.id}: stored total ${a.weighted_total} but recomputed ${recomputed.toFixed(2)}`);
ok(GRADES(a.weighted_total) === a.grade,
`${a.id}: grade ${a.grade} does not match band for ${a.weighted_total}`);
for (const s of scores) ok(!!s.rationale && s.rationale.length > 20,
`${a.id}/${s.dimension_id}: score has no written rationale`);
const t12 = db.prepare('SELECT COUNT(*) AS n FROM evidence WHERE assessment_id = ? AND tier <= 2').get(a.id).n;
ok(t12 >= 3, `${a.id}: only ${t12} tier-1/2 evidence rows, need 3`);
const t4 = db.prepare('SELECT COUNT(*) AS n FROM evidence WHERE assessment_id = ? AND tier = 4 AND dimension_id IS NOT NULL').get(a.id).n;
ok(t4 === 0, `${a.id}: ${t4} tier-4 claims attached to a scored dimension`);
const lowest = scores.reduce((m, s) => (s.value < m.value ? s : m), scores[0]);
ok(lowest.dimension_id === a.weakest_dimension,
`${a.id}: weakest_dimension is ${a.weakest_dimension} but lowest score is ${lowest.dimension_id}`);
}
// The instrument's caps must be real, not documentation.
for (const a of assessments) {
const v = db.prepare("SELECT value FROM score WHERE assessment_id = ? AND dimension_id = 'verifiability'").get(a.id)?.value;
if (a.reproducible_build === 0) ok(v <= 3, `${a.id}: no reproducible build but verifiability ${v} > 3`);
if (a.reproducible_build === 1) ok(v <= 6, `${a.id}: experimental reproducible build but verifiability ${v} > 6`);
}
const scores = db.prepare('SELECT * FROM v_product_score').all();
for (const s of scores) {
if (s.weighted_total === null) continue;
ok(s.policy >= 0 && s.policy <= 100, `${s.product}: policy ${s.policy} out of range`);
ok(s.infra >= 0 && s.infra <= 100, `${s.product}: infra ${s.infra} out of range`);
}
const cov = db.prepare('SELECT * FROM v_coverage').all();
const assessed = cov.filter((c) => c.status !== 'unassessed').length;
const total = cov.length;
db.close();
return { fails, scores, cov, assessed, total };
}
// ---------------------------------------------------------------- export
function exportSite() {
const db = new DatabaseSync(DB_PATH, { readOnly: true });
mkdirSync(GEN_DIR, { recursive: true });
const products = db.prepare(`
SELECT p.id, p.name, p.platform, p.category_id, p.vendor_id, p.sovereign, p.boss, p.art_slot,
p.monthly_cost, v.name AS vendor, v.entity_type, v.jurisdiction, v.business_model,
s.weighted_total, s.grade, s.policy, s.infra, s.weakest_dimension, s.headline, s.assessed_on
FROM product p
JOIN vendor v ON v.id = p.vendor_id
LEFT JOIN v_product_score s ON s.product_id = p.id
ORDER BY p.category_id, p.name`).all();
const assessments = db.prepare(`
SELECT a.id, a.product_id, a.version, a.source_commit, a.assessed_on, a.method_version,
a.reproducible_build, a.weighted_total, a.grade, a.weakest_dimension, a.headline, a.not_tested
FROM assessment a ORDER BY a.assessed_on DESC`).all();
const scores = db.prepare(`
SELECT s.assessment_id, s.dimension_id, s.value, s.rationale, d.label, d.weight
FROM score s JOIN dimension d ON d.id = s.dimension_id
ORDER BY d.weight DESC`).all();
const evidence = db.prepare(`
SELECT id, assessment_id, dimension_id, claim, tier, kind, artifact, sha256, observed_at
FROM evidence ORDER BY assessment_id, tier, id`).all();
const findings = db.prepare(`
SELECT id, assessment_id, severity, title, detail, status, cve
FROM finding ORDER BY assessment_id,
CASE severity WHEN 'critical' THEN 0 WHEN 'high' THEN 1 WHEN 'medium' THEN 2
WHEN 'low' THEN 3 ELSE 4 END`).all();
const dimensions = db.prepare('SELECT * FROM dimension ORDER BY weight DESC').all();
db.close();
const payload = {
generated_at: new Date().toISOString(),
method_version: 'btr-0.1',
dimensions, products, assessments, scores, evidence, findings,
};
writeFileSync(join(GEN_DIR, 'catalog.json'), JSON.stringify(payload, null, 2));
const ts = `// GENERATED FILE - do not edit by hand.
// Source of truth: packages/core/db/bigtech.db
// Rebuild: npm run db:build
export interface CatalogProduct {
id: string; name: string; platform: string; category_id: string; vendor_id: string;
vendor: string; entity_type: string; jurisdiction: string; business_model: string;
sovereign: number; boss: number; art_slot: string | null; monthly_cost: number | null;
weighted_total: number | null; grade: string | null; policy: number | null;
infra: number | null; weakest_dimension: string | null; headline: string | null;
assessed_on: string | null;
}
export interface CatalogAssessment {
id: string; product_id: string; version: string; source_commit: string | null;
assessed_on: string; method_version: string; reproducible_build: number;
weighted_total: number; grade: string; weakest_dimension: string; headline: string;
not_tested: string | null;
}
export interface CatalogScore {
assessment_id: string; dimension_id: string; value: number;
rationale: string; label: string; weight: number;
}
export interface CatalogEvidence {
id: number; assessment_id: string; dimension_id: string | null; claim: string;
tier: number; kind: string | null; artifact: string | null;
sha256: string | null; observed_at: string | null;
}
export interface CatalogFinding {
id: number; assessment_id: string; severity: string; title: string;
detail: string | null; status: string; cve: string | null;
}
export interface CatalogDimension {
id: string; label: string; weight: number; anchor_10: string; anchor_0: string;
}
export const catalog = ${JSON.stringify(payload, null, 2)} as {
generated_at: string;
method_version: string;
dimensions: CatalogDimension[];
products: CatalogProduct[];
assessments: CatalogAssessment[];
scores: CatalogScore[];
evidence: CatalogEvidence[];
findings: CatalogFinding[];
};
/** Products with a real assessment. Everything else is a placeholder. */
export const assessed = catalog.products.filter((p) => p.weighted_total !== null);
/** The full roster the game renders, measured where we have measured it. */
export const products = catalog.products;
export function productById(id: string): CatalogProduct | undefined {
return catalog.products.find((p) => p.id === id);
}
`;
writeFileSync(join(GEN_DIR, 'catalog.ts'), ts);
return payload;
}
// ---------------------------------------------------------------- run
const mode = process.argv[2] ?? 'all';
if (mode === 'verify') {
const r = verify();
report(r);
} else {
const { migrated, seeds } = await build();
console.log(`built ${DB_PATH}`);
console.log(` legacy roster absorbed: ${migrated} products (unassessed)`);
console.log(` seeds applied: ${seeds.join(', ')}`);
const r = verify();
const payload = exportSite();
console.log(` exported: src/data/generated/catalog.{ts,json} (${payload.products.length} products)`);
report(r);
}
function report({ fails, cov, assessed, total }) {
console.log('');
console.log('VERIFY');
if (fails.length === 0) console.log(' all instrument checks passed');
else for (const f of fails) console.log(` FAIL ${f}`);
console.log(` coverage: ${assessed} assessed / ${total} products (${((assessed / total) * 100).toFixed(1)}%)`);
console.log('');
console.log('SCORED');
for (const c of cov.filter((x) => x.status !== 'unassessed')) {
console.log(` ${c.name} [${c.platform}] ${c.weighted_total}/100 grade ${c.status}`);
}
if (fails.length) process.exitCode = 1;
}