[object Object]

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