← back to Domain Sniper
spotcheck.js
111 lines
#!/usr/bin/env node
// spotcheck.js — small DoH-only sweep over a slice of a honeypot batch.
//
// Reads the most recent data/honeypot-*.json batch (or one named via the
// HONEYPOT_BATCH env var), picks 2 baits per bucket from the requested slice
// (0-1, 2-3, 4-5, 6-7, 8-9, or 'all' for the whole batch), DoH-checks each
// for availability, and appends one summary row to data/honeypot-spotcheck.jsonl.
//
// Usage:
// node spotcheck.js 0-1 # 10 baits (2 per bucket A/B/C/D/E)
// node spotcheck.js all # all 50 baits
// HONEYPOT_BATCH=foo.json node spotcheck.js 2-3
//
// Why: we'd been pasting the equivalent 12-line node -e inline every tick.
// Extracting it makes the YOLO loop tighter and lets `node inspect.js`
// keep its clean ASCII chart.
const fs = require('fs');
const path = require('path');
const https = require('https');
const DATA_DIR = path.join(__dirname, 'data');
const OUT_FILE = path.join(DATA_DIR, 'honeypot-spotcheck.jsonl');
function newestHoneypot() {
if (process.env.HONEYPOT_BATCH) return path.resolve(process.env.HONEYPOT_BATCH);
// Pick v1 lure batches only:
// - filename starts with honeypot-YYYY-... (excludes honeypot-v2-*)
// - ends in -<ms>Z.json (excludes -results.json sidecars)
const files = fs.readdirSync(DATA_DIR).filter((f) => /^honeypot-\d{4}-\d{2}-\d{2}T[\d-]+Z\.json$/.test(f));
if (!files.length) throw new Error('no honeypot-*.json batch found in data/');
files.sort();
return path.join(DATA_DIR, files[files.length - 1]);
}
const sliceArg = (process.argv[2] || '0-1').trim();
const sliceRanges = {
'0-1': [0, 2], '2-3': [2, 4], '4-5': [4, 6], '6-7': [6, 8], '8-9': [8, 10],
all: [0, 10],
};
const range = sliceRanges[sliceArg];
if (!range) {
console.error(`unknown slice: ${sliceArg}. valid: ${Object.keys(sliceRanges).join(', ')}`);
process.exit(2);
}
function dohNs(domain) {
return new Promise((resolve) => {
https.get(
'https://cloudflare-dns.com/dns-query?name=' + encodeURIComponent(domain) + '&type=NS',
{ headers: { accept: 'application/dns-json' }, timeout: 5000 },
(res) => {
let body = ''; res.on('data', (c) => body += c);
res.on('end', () => {
try {
const j = JSON.parse(body);
const taken = (j.Answer || []).length > 0 || (j.Authority || []).some((a) => a.type === 6);
resolve({ status: j.Status, taken, stillAvail: j.Status === 3 || (j.Status === 0 && !taken) });
} catch { resolve({ status: -1, taken: false, stillAvail: false }); }
});
},
).on('error', () => resolve({ status: -2, taken: false, stillAvail: false }));
});
}
(async () => {
const batchPath = newestHoneypot();
const batch = JSON.parse(fs.readFileSync(batchPath, 'utf8'));
// batchTs format on disk is `YYYY-MM-DDTHH-MM-SS-mmmZ` (filesystem-safe).
// Convert to ISO: keep the first two dashes (date), turn the next three into : : .
const iso = String(batch.batchTs).replace(/^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})-(\d+)Z$/, '$1-$2-$3T$4:$5:$6.$7Z');
const elapsedH = (Date.now() - new Date(iso).getTime()) / 3.6e6;
const byBucket = {};
for (const it of batch.items) (byBucket[it.bucket] = byBucket[it.bucket] || []).push(it);
const sample = [];
for (const [k, v] of Object.entries(byBucket)) sample.push(...v.slice(range[0], range[1]).map((x) => ({ ...x, bucket: k })));
const tally = {};
const items = [];
for (const it of sample) {
const r = await dohNs(it.domain);
const sniped = it.preCheckAvailable && !r.stillAvail;
tally[it.bucket] = tally[it.bucket] || { sniped: 0, total: 0, names: [] };
tally[it.bucket].total++;
if (sniped) { tally[it.bucket].sniped++; tally[it.bucket].names.push(it.domain); }
items.push({ bucket: it.bucket, domain: it.domain, status: r.status, stillAvail: r.stillAvail, sniped });
await new Promise((r) => setTimeout(r, 200));
}
const rec = {
spotcheckAt: new Date().toISOString(),
batchPath: path.relative(__dirname, batchPath),
batchTs: batch.batchTs,
elapsedHours: +elapsedH.toFixed(2),
sampleSize: sample.length,
sampleSlice: sliceArg,
tally,
items,
};
fs.appendFileSync(OUT_FILE, JSON.stringify(rec) + '\n');
console.log(`\n elapsed: ${rec.elapsedHours}h | slice ${sliceArg} | batch ${path.basename(batchPath)}`);
for (const [k, v] of Object.entries(tally)) {
console.log(` ${k} ${v.sniped}/${v.total} ${v.names.join(', ') || '(none)'}`);
}
const totalSniped = Object.values(tally).reduce((a, b) => a + b.sniped, 0);
const totalChecked = Object.values(tally).reduce((a, b) => a + b.total, 0);
console.log(`\n this slice: ${totalSniped}/${totalChecked} sniped\n`);
})().catch((e) => { console.error(e); process.exit(1); });