← 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(`[31m[BRAND][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 };
- console.log(`[33m[HOT][0m ${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 →