← 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); });