[object Object]

← back to Domain Sniper

honeypot-v2: wire actual lure body (still env-gated)

4893045e2a72adea1da46060d4fdc4607d0fd0f1 · 2026-05-12 17:45:00 -0700 · steve

Two-phase lure: DoH-availability pre-check first, then bucket queries
fired only against the available subset. Skipped baits get queryResult
SKIPPED:not-available rather than null. Resumable — persists batch JSON
after every item write. Sets lured/luredAt on completion and refuses to
re-lure an already-lured batch.

Env gate (LURE_CONFIRM_LEAK_2026=yes) still mandatory — verified the
refusal path still triggers without it. YOLO autonomous loops cannot
set this env.

Files touched

Diff

commit 4893045e2a72adea1da46060d4fdc4607d0fd0f1
Author: steve <steve@designerwallcoverings.com>
Date:   Tue May 12 17:45:00 2026 -0700

    honeypot-v2: wire actual lure body (still env-gated)
    
    Two-phase lure: DoH-availability pre-check first, then bucket queries
    fired only against the available subset. Skipped baits get queryResult
    SKIPPED:not-available rather than null. Resumable — persists batch JSON
    after every item write. Sets lured/luredAt on completion and refuses to
    re-lure an already-lured batch.
    
    Env gate (LURE_CONFIRM_LEAK_2026=yes) still mandatory — verified the
    refusal path still triggers without it. YOLO autonomous loops cannot
    set this env.
---
 honeypot-v2.js | 92 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 90 insertions(+), 2 deletions(-)

diff --git a/honeypot-v2.js b/honeypot-v2.js
index 88e6658..57ab088 100755
--- a/honeypot-v2.js
+++ b/honeypot-v2.js
@@ -28,6 +28,7 @@
 const fs = require('fs');
 const path = require('path');
 const https = require('https');
+const { spawnSync } = require('child_process');
 
 const DATA_DIR = path.join(__dirname, 'data');
 
@@ -145,6 +146,94 @@ function lureRefusal(cohortKey, file) {
   process.exit(2);
 }
 
+// ---------- query helpers (only invoked from lure() under the env gate) ----------
+function dohAvailNs(domain) {
+  return new Promise((r) => {
+    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);
+          r({ available: j.Status === 3 || (j.Status === 0 && !taken) });
+        } catch { r({ available: false, err: 'parse' }); }
+      });
+    }).on('error', (e) => r({ available: false, err: e.message }));
+  });
+}
+
+function whoisQ(server, domain) {
+  const r = spawnSync('whois', ['-h', server, domain], { encoding: 'utf8', timeout: 15_000 });
+  const out = ((r.stdout || '') + (r.stderr || '')).toLowerCase();
+  if (out.match(/no match|not found|no data found|domain not found|available|free/)) return 'AVAILABLE';
+  if (out.match(/registry domain id|registrar:|creation date:|name server:|nserver/)) return 'TAKEN';
+  return 'UNKNOWN';
+}
+
+function godaddyApi(domain) {
+  const key = process.env.GODADDY_KEY;
+  const secret = process.env.GODADDY_SECRET;
+  if (!key || !secret) return Promise.resolve('SKIPPED:no-godaddy-key');
+  return new Promise((resolve) => {
+    https.get({
+      hostname: 'api.godaddy.com',
+      path: '/v1/domains/available?domain=' + encodeURIComponent(domain),
+      headers: { Authorization: `sso-key ${key}:${secret}` },
+    }, (res) => {
+      let d = ''; res.on('data', (c) => d += c);
+      res.on('end', () => {
+        try { const j = JSON.parse(d); resolve(j.available === true ? 'AVAILABLE' : 'TAKEN'); }
+        catch { resolve(`HTTP-${res.statusCode}`); }
+      });
+    }).on('error', (e) => resolve('ERROR:' + e.message));
+  });
+}
+
+function fireBucketQuery(method, domain) {
+  switch (method) {
+    case 'whois-verisign-com': return Promise.resolve(whoisQ('whois.verisign-grs.com', domain));
+    case 'whois-google-app':   return Promise.resolve(whoisQ('whois.nic.google',       domain));
+    case 'whois-nic-co':       return Promise.resolve(whoisQ('whois.nic.co',           domain));
+    case 'godaddy-api-com':    return godaddyApi(domain);
+    case 'none':               return Promise.resolve('CONTROL');
+    default:                   return Promise.resolve('UNKNOWN-METHOD');
+  }
+}
+
+async function lure(cohortKey, file) {
+  if (!file) { console.error('usage: lure <cohort> <batch.json>'); process.exit(2); }
+  const batch = JSON.parse(fs.readFileSync(file, 'utf8'));
+  if (batch.lured) { console.error(`  REFUSED: batch already lured at ${batch.luredAt}. Generate a fresh one.`); process.exit(2); }
+  console.log(`\n  honeypot-v2 LURE  cohort=${batch.cohort}  batchTs=${batch.batchTs}`);
+  console.log(`  ${batch.items.length} baits across ${new Set(batch.items.map(i=>i.bucket)).size} buckets\n`);
+  console.log(`  phase 1: DoH-availability pre-check (only available baits get lured) ...\n`);
+  let avail = 0;
+  for (const it of batch.items) {
+    const r = await dohAvailNs(it.domain);
+    it.preCheckAvailable = !!r.available;
+    it.preCheckedAt = new Date().toISOString();
+    if (r.available) avail++;
+    process.stdout.write(`  [${it.bucket}] ${it.domain.padEnd(16)} ${r.available ? 'AVAILABLE' : 'taken/err'}\n`);
+    await new Promise((rr) => setTimeout(rr, 200));
+  }
+  fs.writeFileSync(file, JSON.stringify(batch, null, 2));
+  console.log(`\n  ${avail} of ${batch.items.length} baits are DoH-available — these get lured.`);
+  console.log(`  phase 2: firing bucket queries (only on the ${avail} available baits) ...\n`);
+  for (const it of batch.items) {
+    if (!it.preCheckAvailable) { it.queryResult = 'SKIPPED:not-available'; continue; }
+    it.queriedAt = new Date().toISOString();
+    try { it.queryResult = await fireBucketQuery(it.method, it.domain); }
+    catch (e) { it.queryResult = 'ERROR:' + e.message; }
+    console.log(`  [${it.bucket}] ${it.domain.padEnd(16)} ${it.method.padEnd(22)} ${it.queryResult}`);
+    fs.writeFileSync(file, JSON.stringify(batch, null, 2));
+    await new Promise((rr) => setTimeout(rr, 1500 + Math.random() * 1200));
+  }
+  batch.lured = true;
+  batch.luredAt = new Date().toISOString();
+  fs.writeFileSync(file, JSON.stringify(batch, null, 2));
+  console.log(`\n  bait deployed. ${avail} honeypots in the wild.`);
+  console.log(`  wait 24-48h, then:  node honeypot-v2.js check ${path.relative(__dirname, file)}\n`);
+}
+
 async function check(file) {
   if (!file) { console.error('usage: node honeypot-v2.js check <path/to/honeypot-v2-*.json>'); process.exit(2); }
   const batch = JSON.parse(fs.readFileSync(file, 'utf8'));
@@ -190,8 +279,7 @@ if (cmd === 'generate') {
   generate(cohortArg);
 } else if (cmd === 'lure') {
   if (process.env.LURE_CONFIRM_LEAK_2026 === 'yes') {
-    console.error('lure: gated path reached, but full lure execution lives in honeypot.js v1 currently. Wire up before re-running.');
-    process.exit(2);
+    lure(cohortArg, fileArg).catch((e) => { console.error(e); process.exit(1); });
   } else {
     lureRefusal(cohortArg, fileArg);
   }

← 89086bf honeypot-v2: check shows availability tally + readme baselin  ·  back to Domain Sniper  ·  readme: v1 vs v2 head-to-head + 4.20h slice 0-1 re-check #2 855ea94 →