← 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 →