[object Object]

← back to Domain Sniper

audit-noise: surface candidate NOISE_PATTERNS by suffix tally + add cellt.net

44a0139a9f36d0959480325cda6520ce5b8c8665 · 2026-05-12 17:54:14 -0700 · steve

Post-bounce, brand-typo-hits.jsonl grew by 6 new pay.cellt.net hits —
'cellt' is Levenshtein-2 from 'callr' (same false-positive shape as
beutl.in). Added /\.cellt\.net$/i to NOISE_PATTERNS and bounced
watcher (new PIDs 41799/41800).

audit-noise.js is a read-only tool: groups every brand-typo-hits row by
2-label and 3-label suffix, marks each row [filtered] vs [candidate]
against the current NOISE_PATTERNS, and emits paste-ready regex lines
for any 3+ hit suffix not yet filtered. Wired as 'npm run audit:noise'.
Current top-5 candidates are all bare-punycode IDNs (legit Wikimedia-style
homoglyph monitoring — not noise to suppress).

Files touched

Diff

commit 44a0139a9f36d0959480325cda6520ce5b8c8665
Author: steve <steve@designerwallcoverings.com>
Date:   Tue May 12 17:54:14 2026 -0700

    audit-noise: surface candidate NOISE_PATTERNS by suffix tally + add cellt.net
    
    Post-bounce, brand-typo-hits.jsonl grew by 6 new pay.cellt.net hits —
    'cellt' is Levenshtein-2 from 'callr' (same false-positive shape as
    beutl.in). Added /\.cellt\.net$/i to NOISE_PATTERNS and bounced
    watcher (new PIDs 41799/41800).
    
    audit-noise.js is a read-only tool: groups every brand-typo-hits row by
    2-label and 3-label suffix, marks each row [filtered] vs [candidate]
    against the current NOISE_PATTERNS, and emits paste-ready regex lines
    for any 3+ hit suffix not yet filtered. Wired as 'npm run audit:noise'.
    Current top-5 candidates are all bare-punycode IDNs (legit Wikimedia-style
    homoglyph monitoring — not noise to suppress).
---
 audit-noise.js        | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++
 brand-typo-watcher.js |  1 +
 package.json          |  1 +
 3 files changed, 77 insertions(+)

diff --git a/audit-noise.js b/audit-noise.js
new file mode 100755
index 0000000..631583e
--- /dev/null
+++ b/audit-noise.js
@@ -0,0 +1,75 @@
+#!/usr/bin/env node
+// audit-noise.js — surface top candidate noise patterns from brand-typo-hits.jsonl.
+//
+// Groups every hit by the last 2 + 3 labels of the domain (so subdomain churn
+// rolls up to the registrable parent), tallies occurrences, and flags any
+// suffix that is NOT already in brand-typo-watcher's NOISE_PATTERNS as a
+// candidate to add. Read-only — never writes back.
+//
+// Usage:  node audit-noise.js [N]   (default top-30)
+
+const fs = require('fs');
+const path = require('path');
+
+const FILE = path.join(__dirname, 'data', 'brand-typo-hits.jsonl');
+const TOP_N = parseInt(process.argv[2] || '30', 10);
+
+// Mirror the NOISE_PATTERNS from brand-typo-watcher (keep in sync if you change either).
+const CURRENT_FILTER = [
+  /\.haplorrhini\.com$/i,
+  /\.beutl\.in$/i,
+  /\.cellt\.net$/i,
+];
+const isFiltered = (suffix) => CURRENT_FILTER.some((r) => r.test('x.' + suffix));
+
+if (!fs.existsSync(FILE)) {
+  console.error(`no hits file at ${FILE}`);
+  process.exit(2);
+}
+
+const tally2 = new Map();
+const tally3 = new Map();
+let total = 0;
+const lines = fs.readFileSync(FILE, 'utf8').split('\n').filter(Boolean);
+for (const ln of lines) {
+  let r; try { r = JSON.parse(ln); } catch { continue; }
+  const d = (r.domain || '').toLowerCase().replace(/^\*\./, '');
+  if (!d) continue;
+  total++;
+  const parts = d.split('.');
+  if (parts.length >= 2) {
+    const s = parts.slice(-2).join('.');
+    tally2.set(s, (tally2.get(s) || 0) + 1);
+  }
+  if (parts.length >= 3) {
+    const s = parts.slice(-3).join('.');
+    tally3.set(s, (tally3.get(s) || 0) + 1);
+  }
+}
+
+const ranked2 = [...tally2.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOP_N);
+const ranked3 = [...tally3.entries()].sort((a, b) => b[1] - a[1]).slice(0, TOP_N);
+
+console.log(`\n  audit-noise :: ${lines.length} hit rows  /  ${total} domain entries\n`);
+console.log(`  top ${TOP_N} 2-label suffixes (registrable parent):`);
+console.log(`  ` + '-'.repeat(72));
+for (const [s, n] of ranked2) {
+  const flag = isFiltered(s) ? '\x1b[32m[filtered]\x1b[0m' : '\x1b[33m[candidate]\x1b[0m';
+  console.log(`  ${String(n).padStart(5)}  ${s.padEnd(40)}  ${flag}`);
+}
+
+console.log(`\n  top ${TOP_N} 3-label suffixes (rolled-up subdomains):`);
+console.log(`  ` + '-'.repeat(72));
+for (const [s, n] of ranked3) {
+  const flag = isFiltered(s) ? '\x1b[32m[filtered]\x1b[0m' : '\x1b[33m[candidate]\x1b[0m';
+  console.log(`  ${String(n).padStart(5)}  ${s.padEnd(40)}  ${flag}`);
+}
+
+const candidates = ranked2.filter(([s]) => !isFiltered(s) && tally2.get(s) >= 3);
+if (candidates.length) {
+  console.log(`\n  candidate NOISE_PATTERNS to consider adding (≥3 hits, not yet filtered):`);
+  for (const [s, n] of candidates.slice(0, 10)) {
+    console.log(`    /\\.${s.replace(/\./g, '\\.')}$/i,    // ${n} hits`);
+  }
+  console.log('');
+}
diff --git a/brand-typo-watcher.js b/brand-typo-watcher.js
index 067d6d9..5cde839 100644
--- a/brand-typo-watcher.js
+++ b/brand-typo-watcher.js
@@ -44,6 +44,7 @@ const NOTIFY = process.env.NOTIFY !== '0';
 const NOISE_PATTERNS = [
   /\.haplorrhini\.com$/i,         // Let's Encrypt internal HTTP-01 challenge subdomains
   /\.beutl\.in$/i,                // Beutl video-editor app — Levenshtein-2 collision with "butlr"
+  /\.cellt\.net$/i,               // pay.cellt.net (Korean payment service) — Lev-2 collision with "callr"
 ];
 function isKnownNoise(d) { return NOISE_PATTERNS.some((r) => r.test(d)); }
 
diff --git a/package.json b/package.json
index fcfeb54..aedd5eb 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,7 @@
     "honeypot:v2:generate": "node honeypot-v2.js generate",
     "honeypot:v2:check": "node honeypot-v2.js check",
     "inspect": "node inspect.js",
+    "audit:noise": "node audit-noise.js",
     "register": "node register.js"
   },
   "dependencies": {

← 855ea94 readme: v1 vs v2 head-to-head + 4.20h slice 0-1 re-check #2  ·  back to Domain Sniper  ·  brand-typo-watcher: idle counter + stale-log marker in per-l fa1502d →