[object Object]

← back to Domain Sniper

dashboard.js: wire ct-source-ctlog (CT_SOURCE=ctlog default)

da5c6535407a72b007af221d0c1a1a9ccb15d7e4 · 2026-05-12 16:21:24 -0700 · Steve Abrams

Mirrors brand-typo-watcher refactor — extracts handleCertEvent() so both
ctlog and certstream paths share the classify+broadcast+log flow.

Dashboard PID 80709 confirmed 6 HOT events flowing in first 20s after
restart (vs zero for 2h58m on dead certstream). Browser UI at :9895 now
shows live activity.

Files touched

Diff

commit da5c6535407a72b007af221d0c1a1a9ccb15d7e4
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Tue May 12 16:21:24 2026 -0700

    dashboard.js: wire ct-source-ctlog (CT_SOURCE=ctlog default)
    
    Mirrors brand-typo-watcher refactor — extracts handleCertEvent() so both
    ctlog and certstream paths share the classify+broadcast+log flow.
    
    Dashboard PID 80709 confirmed 6 HOT events flowing in first 20s after
    restart (vs zero for 2h58m on dead certstream). Browser UI at :9895 now
    shows live activity.
---
 dashboard.js | 69 +++++++++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 50 insertions(+), 19 deletions(-)

diff --git a/dashboard.js b/dashboard.js
index 644803d..eb24097 100644
--- a/dashboard.js
+++ b/dashboard.js
@@ -16,9 +16,16 @@ const http = require('http');
 const WebSocket = require('ws');
 const path = require('path');
 const fs = require('fs');
+const ctLogSource = require('./ct-source-ctlog');
 
 const PORT = parseInt(process.env.DASH_PORT || '9895', 10);
 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, 'dashboard-hits.jsonl');
 
@@ -72,40 +79,64 @@ function appendHit(rec) {
   fs.appendFileSync(HITS_LOG, JSON.stringify(rec) + '\n');
 }
 
+function handleCertEvent(m) {
+  if (m.message_type !== 'certificate_update') return;
+  const all = (m.data && m.data.leaf_cert && m.data.leaf_cert.all_domains) || [];
+  for (const d of all) {
+    stats.seen++;
+    const c = classify(d);
+    if (c.brand) stats.brand++;
+    if (c.hot) stats.hot++;
+    if (c.hot || c.brand) {
+      const rec = { type: 'hit', ts: Date.now(), domain: d, hot: c.hot, brand: c.brand, source: m.data.log_source || 'certstream' };
+      broadcast(rec);
+      appendHit(rec);
+    }
+  }
+}
+
 let reconnectDelay = 1000;
-function connectCT() {
+function connectCertstream() {
+  console.log(`[${new Date().toISOString()}] CT_SOURCE=certstream — connecting to ${CERTSTREAM_URL}`);
   const cs = new WebSocket(CERTSTREAM_URL, { handshakeTimeout: 15_000 });
   cs.on('open', () => {
     console.log(`[${new Date().toISOString()}] CertStream connected`);
     reconnectDelay = 1000;
   });
   cs.on('message', (raw) => {
-    let m;
-    try { m = JSON.parse(raw.toString()); } catch { return; }
-    if (m.message_type !== 'certificate_update') return;
-    const all = (m.data && m.data.leaf_cert && m.data.leaf_cert.all_domains) || [];
-    for (const d of all) {
-      stats.seen++;
-      const c = classify(d);
-      if (c.brand) stats.brand++;
-      if (c.hot) stats.hot++;
-      // Don't flood the browser with the full firehose — only emit interesting hits.
-      // (Otherwise the browser would render ~50/sec and choke.)
-      if (c.hot || c.brand) {
-        const rec = { type: 'hit', ts: Date.now(), domain: d, hot: c.hot, brand: c.brand };
-        broadcast(rec);
-        appendHit(rec);
-      }
-    }
+    let m; try { m = JSON.parse(raw.toString()); } catch { return; }
+    handleCertEvent(m);
   });
   cs.on('close', (code) => {
     console.error(`[${new Date().toISOString()}] CT closed (${code}) — reconnect in ${reconnectDelay}ms`);
-    setTimeout(connectCT, reconnectDelay);
+    setTimeout(connectCertstream, reconnectDelay);
     reconnectDelay = Math.min(reconnectDelay * 2, 30_000);
   });
   cs.on('error', (e) => console.error(`[${new Date().toISOString()}] CT error: ${e.message}`));
 }
 
+function connectCtLog() {
+  console.log(`[${new Date().toISOString()}] CT_SOURCE=ctlog log=${CT_LOG} poll=${CTLOG_POLL_MS}ms batch=${CTLOG_BATCH}`);
+  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(`[${new Date().toISOString()}] ctlog ${st.type}: ${st.msg || JSON.stringify(st)}`);
+      } else if (st.type === 'seeded') {
+        console.log(`[${new Date().toISOString()}] ctlog seeded at tree_size=${st.position}`);
+      }
+    },
+  });
+}
+
+function connectCT() {
+  if (CT_SOURCE === 'certstream') return connectCertstream();
+  return connectCtLog();
+}
+
 setInterval(() => broadcast({ type: 'stats', ...stats }), 2000);
 
 server.listen(PORT, '127.0.0.1', () => {

← 9bc1439 brand-typo-watcher: wire ct-source-ctlog (CT_SOURCE=ctlog de  ·  back to Domain Sniper  ·  ct-source-ctlog: parse PRECERT_ENTRY (TBSCertificate) for SA 2c5c65f →