[object Object]

← back to Domain Sniper

watch-ct: migrate to ct-source-ctlog (6-log fan-out, default)

02d8f7f7d6760df91440a25d86b356d421a73df7 · 2026-05-12 18:23:21 -0700 · steve

watch-ct.js was the only watcher still on the dead certstream WebSocket.
Mirrored the same CT_SOURCE/CT_LOGS/CT_LOG/CTLOG_POLL_MS/CTLOG_BATCH env
shape used by brand-typo-watcher.js and dashboard.js. handleCertEvent()
extracted as the shared cert sink so both the certstream fallback and
the ctlog flow consume it. Default now: 6-log direct polling
(argon/xenon/wyvern/sphinx/elephant/tiger), CertStream kept as opt-in
fallback via CT_SOURCE=certstream.

Replaced raw ESC bytes (0x1b) in console.log with \x1b escapes for
greppability. Runtime output identical.

Also recorded 4.74h slice 8-9 RECHECK#2: 0/10. Cumulative 0/160.

Files touched

Diff

commit 02d8f7f7d6760df91440a25d86b356d421a73df7
Author: steve <steve@designerwallcoverings.com>
Date:   Tue May 12 18:23:21 2026 -0700

    watch-ct: migrate to ct-source-ctlog (6-log fan-out, default)
    
    watch-ct.js was the only watcher still on the dead certstream WebSocket.
    Mirrored the same CT_SOURCE/CT_LOGS/CT_LOG/CTLOG_POLL_MS/CTLOG_BATCH env
    shape used by brand-typo-watcher.js and dashboard.js. handleCertEvent()
    extracted as the shared cert sink so both the certstream fallback and
    the ctlog flow consume it. Default now: 6-log direct polling
    (argon/xenon/wyvern/sphinx/elephant/tiger), CertStream kept as opt-in
    fallback via CT_SOURCE=certstream.
    
    Replaced raw ESC bytes (0x1b) in console.log with \x1b escapes for
    greppability. Runtime output identical.
    
    Also recorded 4.74h slice 8-9 RECHECK#2: 0/10. Cumulative 0/160.
---
 watch-ct.js | 100 +++++++++++++++++++++++++++++++++++++-----------------------
 1 file changed, 62 insertions(+), 38 deletions(-)

diff --git a/watch-ct.js b/watch-ct.js
index 2b63615..35a87bf 100644
--- a/watch-ct.js
+++ b/watch-ct.js
@@ -1,25 +1,30 @@
 #!/usr/bin/env node
 // Live Certificate Transparency watcher.
-// Source: wss://certstream.calidog.io/  — public WebSocket of every TLS cert issued.
+// Default source: direct CT-log polling via ct-source-ctlog (6-log fan-out).
+// Fallback: CERTSTREAM_URL via WebSocket (CT_SOURCE=certstream).
 // Use cases:
-//   1. INTEL — see what kinds of names other people are registering right now (patterns to grab next)
+//   1. INTEL — see what kinds of names other people are registering right now
 //   2. BRAND DEFENSE — alert if someone registers a typo of one of Steve's brands
-//   3. TIMING — measure the latency between WHOIS-check and cert-issue to calibrate aggregator behavior
+//   3. TIMING — measure latency between WHOIS-check and cert-issue
 //
 // Reminder: certs = ALREADY REGISTERED domains. This is not a "drops" feed.
-// For actual snipe targets (pending-delete), see watch-drops.js (Step 2).
+// For actual snipe targets (pending-delete), see watch-drops.js.
 
 const WebSocket = require('ws');
 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/';
+const CT_SOURCE = process.env.CT_SOURCE || 'ctlog';
+const CT_LOG = process.env.CT_LOG || 'argon';
+const CT_LOGS = (process.env.CT_LOGS || 'argon,xenon,wyvern,sphinx,elephant,tiger').split(',').map((s) => s.trim()).filter(Boolean);
+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, 'ct-hits.jsonl');
 const STATS_INTERVAL_MS = 30_000;
 
-// Brand-defense keywords. Any new cert whose domain CONTAINS one of these substrings is a hit.
-// Edit freely — keep them lowercase.
 const BRAND_KEYWORDS = (process.env.BRAND_KEYWORDS || [
   'designerwallcoverings',
   'philipperomano',
@@ -39,7 +44,6 @@ const BRAND_KEYWORDS = (process.env.BRAND_KEYWORDS || [
   'glassbeaded',
 ].join(',')).split(',').map((s) => s.trim().toLowerCase()).filter(Boolean);
 
-// "Hot pattern" filter — short, no digits, no hyphens, mainstream TLD.
 const HOT_TLDS = new Set(['com', 'app', 'io', 'co', 'ai', 'dev']);
 const MAX_STEM_LEN = 8;
 
@@ -49,11 +53,9 @@ function looksHot(domain) {
   if (parts.length < 2) return false;
   const tld = parts[parts.length - 1].toLowerCase();
   if (!HOT_TLDS.has(tld)) return false;
-  // For multi-label hosts (foo.bar.app), only consider the registrable label (= parts[-2]).
   const stem = parts[parts.length - 2].toLowerCase();
   if (stem.length > MAX_STEM_LEN || stem.length < 3) return false;
   if (/[-0-9]/.test(stem)) return false;
-  // Pronounceability heuristic: must have both a vowel and a consonant.
   if (!/[aeiou]/.test(stem)) return false;
   if (!/[bcdfghjklmnpqrstvwxyz]/.test(stem)) return false;
   return true;
@@ -83,52 +85,74 @@ function ts() {
   return new Date().toISOString();
 }
 
+function handleCertEvent(msg) {
+  if (msg.message_type !== 'certificate_update') return;
+  const domains = (msg.data && msg.data.leaf_cert && msg.data.leaf_cert.all_domains) || [];
+  const seenAt = ts();
+  for (const d of domains) {
+    count.total++;
+    const brand = brandMatch(d);
+    const hot = looksHot(d);
+    if (brand) {
+      count.brand++;
+      const line = { ts: seenAt, type: 'BRAND', domain: d, keyword: brand, cert_index: msg.data.cert_index, source: msg.data.log_source || 'certstream' };
+      console.log(`\x1b[31m[BRAND]\x1b[0m ${d}  (matches: ${brand})`);
+      appendHit(line);
+    } else if (hot) {
+      count.hot++;
+      const line = { ts: seenAt, type: 'HOT', domain: d, cert_index: msg.data.cert_index, source: msg.data.log_source || 'certstream' };
+      console.log(`\x1b[33m[HOT]\x1b[0m   ${d}`);
+      appendHit(line);
+    }
+  }
+}
+
 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 for hot + brand hits`);
     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) || [];
-    const seenAt = ts();
-    for (const d of domains) {
-      count.total++;
-      const brand = brandMatch(d);
-      const hot = looksHot(d);
-      if (brand) {
-        count.brand++;
-        const line = { ts: seenAt, type: 'BRAND', domain: d, keyword: brand, cert_index: msg.data.cert_index };
-        console.log(`[BRAND] ${d}  (matches: ${brand})`);
-        appendHit(line);
-      } else if (hot) {
-        count.hot++;
-        const line = { ts: seenAt, type: 'HOT', domain: d, cert_index: msg.data.cert_index };
-        console.log(`[HOT]   ${d}`);
-        appendHit(line);
-      }
-    }
+    let msg; try { msg = JSON.parse(raw.toString()); } catch { return; }
+    handleCertEvent(msg);
   });
-
   ws.on('close', (code) => {
     console.error(`[${ts()}] connection closed (code ${code}). reconnecting in ${reconnectDelay}ms`);
-    setTimeout(connect, reconnectDelay);
+    setTimeout(connectCertstream, reconnectDelay);
     reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
   });
+  ws.on('error', (err) => console.error(`[${ts()}] ws error: ${err.message}`));
+}
 
-  ws.on('error', (err) => {
-    console.error(`[${ts()}] ws error: ${err.message}`);
+function connectCtLog() {
+  const opts = CT_LOGS.length > 1
+    ? { logKeys: CT_LOGS, pollMs: CTLOG_POLL_MS, batchSize: CTLOG_BATCH }
+    : { logKey: CT_LOG, pollMs: CTLOG_POLL_MS, batchSize: CTLOG_BATCH };
+  console.error(`[${ts()}] CT_SOURCE=ctlog ${CT_LOGS.length > 1 ? 'multi=[' + CT_LOGS.join(',') + ']' : 'log=' + CT_LOG} poll=${CTLOG_POLL_MS}ms batch=${CTLOG_BATCH}`);
+  ctLogSource.start({
+    ...opts,
+    onEvent: handleCertEvent,
+    onStatus: (st) => {
+      if (st.type === 'sth_err' || st.type === 'fetch_err' || st.type === 'error') {
+        console.error(`[${ts()}] ctlog ${st.type} on ${st.log || '?'}: ${st.msg || JSON.stringify(st)}`);
+      } else if (st.type === 'seeded') {
+        console.error(`[${ts()}] seeded ${st.log} at tree_size=${st.position}`);
+      } else if (st.type === 'resumed') {
+        console.error(`[${ts()}] resumed ${st.log} at cursor=${st.position}`);
+      }
+    },
   });
 }
 
+function connect() {
+  if (CT_SOURCE === 'certstream') return connectCertstream();
+  return connectCtLog();
+}
+
 setInterval(() => {
   console.error(`[${ts()}] seen=${count.total}  hot=${count.hot}  brand=${count.brand}`);
   flushHits();

← a353d85 watchdog: opt-in launchd plist + zsh script that bounces das  ·  back to Domain Sniper  ·  ct-source-certspotter: mark both adapters STALLED with react d315f31 →