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.
244 lines
11 KiB
JavaScript
244 lines
11 KiB
JavaScript
#!/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(/<script[^>]+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 <url> [--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('');
|
|
}
|