← back to Domain Sniper
brand-typo-watcher: wire ct-source-ctlog (CT_SOURCE=ctlog default)
9bc14390bd35bd4a9d84bc60f5bf6545b5b86c2a · 2026-05-12 16:17:09 -0700 · Steve Abrams
certstream.calidog.io WebSocket flow retained as fallback (CT_SOURCE=certstream).
Extracts handleCertEvent() so both sources reuse the same brand-match path.
PID 54555 confirmed seen=52 in first minute after restart — real CT firehose
flowing through Argon2026h1 direct poll.
Also marks 3h-window honeypot spot-check: 0/10 baits sniped at 2.65h elapsed.
Files touched
Diff
commit 9bc14390bd35bd4a9d84bc60f5bf6545b5b86c2a
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 12 16:17:09 2026 -0700
brand-typo-watcher: wire ct-source-ctlog (CT_SOURCE=ctlog default)
certstream.calidog.io WebSocket flow retained as fallback (CT_SOURCE=certstream).
Extracts handleCertEvent() so both sources reuse the same brand-match path.
PID 54555 confirmed seen=52 in first minute after restart — real CT firehose
flowing through Argon2026h1 direct poll.
Also marks 3h-window honeypot spot-check: 0/10 baits sniped at 2.65h elapsed.
---
brand-typo-watcher.js | 90 +++++++++++++++++++++++++++++++++------------------
1 file changed, 58 insertions(+), 32 deletions(-)
diff --git a/brand-typo-watcher.js b/brand-typo-watcher.js
index 49853e6..f37587c 100644
--- a/brand-typo-watcher.js
+++ b/brand-typo-watcher.js
@@ -15,8 +15,15 @@ const WebSocket = require('ws');
const { spawn } = require('child_process');
const fs = require('fs');
const path = require('path');
+const ctLogSource = require('./ct-source-ctlog');
const CERTSTREAM_URL = process.env.CERTSTREAM_URL || 'wss://certstream.calidog.io/';
+// CT_SOURCE=ctlog → direct CT-log polling (default, since certstream is dead)
+// CT_SOURCE=certstream → original WebSocket flow (kept as fallback)
+const CT_SOURCE = process.env.CT_SOURCE || 'ctlog';
+const CT_LOG = process.env.CT_LOG || 'argon';
+const CTLOG_POLL_MS = parseInt(process.env.CTLOG_POLL_MS || '5000', 10);
+const CTLOG_BATCH = parseInt(process.env.CTLOG_BATCH || '32', 10);
const DATA_DIR = path.join(__dirname, 'data');
const HITS_LOG = path.join(DATA_DIR, 'brand-typo-hits.jsonl');
const MAX_DISTANCE = parseInt(process.env.MAX_DISTANCE || '2', 10);
@@ -117,53 +124,72 @@ function logHit(rec) {
fs.appendFileSync(HITS_LOG, JSON.stringify(rec) + '\n');
}
+function handleCertEvent(msg) {
+ if (msg.message_type !== 'certificate_update') return;
+ const domains = (msg.data && msg.data.leaf_cert && msg.data.leaf_cert.all_domains) || [];
+ for (const d of domains) {
+ count.seen++;
+ const m = matchBrand(d);
+ const puny = isPunycode(d);
+ if (puny) count.puny++;
+ if (m) {
+ count.hits++;
+ if (m.matchType === 'SUBSTRING') count.substring++;
+ else count.typo++;
+ const rec = { ts: ts(), domain: d, ...m, isPunycode: puny, cert_index: msg.data.cert_index, source: msg.data.log_source || 'certstream' };
+ const tag = m.matchType === 'SUBSTRING' ? '\x1b[31m[SUBSTRING]\x1b[0m' : `\x1b[33m[TYPO d=${m.distance}]\x1b[0m`;
+ const puntag = puny ? ' \x1b[35m⚠ PUNYCODE\x1b[0m' : '';
+ console.log(`${tag} ${d} ~ ${m.keyword}${puntag}`);
+ logHit(rec);
+ notify(`domain-sniper hit: ${m.keyword}`, `${d} (distance ${m.distance})${puny ? ' — PUNYCODE/IDN' : ''}`);
+ } else if (puny) {
+ logHit({ ts: ts(), domain: d, matchType: 'BARE_PUNYCODE', cert_index: msg.data.cert_index });
+ }
+ }
+}
+
let reconnectDelay = 1000;
-function connect() {
- console.error(`[${ts()}] connecting to ${CERTSTREAM_URL}`);
+function connectCertstream() {
+ console.error(`[${ts()}] CT_SOURCE=certstream — connecting to ${CERTSTREAM_URL}`);
const ws = new WebSocket(CERTSTREAM_URL, { handshakeTimeout: 15_000 });
-
ws.on('open', () => {
console.error(`[${ts()}] connected — watching ${BRAND_KEYWORDS.length} brands (max-distance ${MAX_DISTANCE}, notify=${NOTIFY})`);
reconnectDelay = 1000;
});
-
ws.on('message', (raw) => {
- let msg;
- try { msg = JSON.parse(raw.toString()); } catch { return; }
- if (msg.message_type !== 'certificate_update') return;
- const domains = (msg.data && msg.data.leaf_cert && msg.data.leaf_cert.all_domains) || [];
- for (const d of domains) {
- count.seen++;
- const m = matchBrand(d);
- const puny = isPunycode(d);
- if (puny) count.puny++;
- if (m) {
- count.hits++;
- if (m.matchType === 'SUBSTRING') count.substring++;
- else count.typo++;
- const rec = { ts: ts(), domain: d, ...m, isPunycode: puny, cert_index: msg.data.cert_index };
- const tag = m.matchType === 'SUBSTRING' ? '\x1b[31m[SUBSTRING]\x1b[0m' : `\x1b[33m[TYPO d=${m.distance}]\x1b[0m`;
- const puntag = puny ? ' \x1b[35m⚠ PUNYCODE\x1b[0m' : '';
- console.log(`${tag} ${d} ~ ${m.keyword}${puntag}`);
- logHit(rec);
- notify(`domain-sniper hit: ${m.keyword}`, `${d} (distance ${m.distance})${puny ? ' — PUNYCODE/IDN' : ''}`);
- } else if (puny) {
- // Bare-puny domains without brand match: log to a separate side channel for spot-review.
- // These are NOT brand hits but they're worth scanning weekly.
- logHit({ ts: ts(), domain: d, matchType: 'BARE_PUNYCODE', cert_index: msg.data.cert_index });
- }
- }
+ let msg; try { msg = JSON.parse(raw.toString()); } catch { return; }
+ handleCertEvent(msg);
});
-
ws.on('close', (code) => {
console.error(`[${ts()}] closed (${code}). reconnect in ${reconnectDelay}ms`);
- setTimeout(connect, reconnectDelay);
+ setTimeout(connectCertstream, reconnectDelay);
reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
});
-
ws.on('error', (e) => console.error(`[${ts()}] error: ${e.message}`));
}
+function connectCtLog() {
+ console.error(`[${ts()}] CT_SOURCE=ctlog — polling log=${CT_LOG} poll=${CTLOG_POLL_MS}ms batch=${CTLOG_BATCH}, watching ${BRAND_KEYWORDS.length} brands (max-distance ${MAX_DISTANCE}, notify=${NOTIFY})`);
+ ctLogSource.start({
+ logKey: CT_LOG,
+ pollMs: CTLOG_POLL_MS,
+ batchSize: CTLOG_BATCH,
+ onEvent: handleCertEvent,
+ onStatus: (st) => {
+ if (st.type === 'sth_err' || st.type === 'fetch_err' || st.type === 'error') {
+ console.error(`[${ts()}] ctlog ${st.type}: ${st.msg || JSON.stringify(st)}`);
+ } else if (st.type === 'seeded') {
+ console.error(`[${ts()}] seeded at tree_size=${st.position}`);
+ }
+ },
+ });
+}
+
+function connect() {
+ if (CT_SOURCE === 'certstream') return connectCertstream();
+ return connectCtLog();
+}
+
setInterval(() => {
console.error(`[${ts()}] seen=${count.seen} hits=${count.hits} (substring=${count.substring} typo=${count.typo}) puny=${count.puny}`);
}, 60_000);
← b00757f ct-source-ctlog.js: WORKING direct CT-log polling — bypasses
·
back to Domain Sniper
·
dashboard.js: wire ct-source-ctlog (CT_SOURCE=ctlog default) da5c653 →