bigtech-sucks/packages/core/db/hooks/probe.mjs

245 lines
11 KiB
JavaScript
Raw Permalink Normal View History

#!/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('');
}