Case Study

threadr

OSINT tool for security professionals. Maps relationships between digital identities to help defenders understand their attack surface.

ReactTypeScriptHonoBullMQNeo4jRedisSQLiteTailwindDockerVitest
00 / Live Demo

What a scan looks like

One email address. 17 plugins. 47 nodes discovered in under 30 seconds.

threadr
0 nodes0 edges
01 / Engineering

What I had to think about

The plugin system is plumbing. These are the parts where the maths matters.

02 / The Code

From the source

Three functions from the repo. The maths is the implementation.

Dempster’s Combination Rule
Fuses two mass functions over Θ = {SAME, DIFFERENT}. Conflict mass K normalises the result. Total conflict → pure uncertainty.
function combine(m1: MassFunction, m2: MassFunction): MassFunction {
  // Non-empty intersections
  const sameSame = m1.same * m2.same
  const sameUncertain = m1.same * m2.uncertain
  const uncertainSame = m1.uncertain * m2.same
  const diffDiff = m1.different * m2.different
  const diffUncertain = m1.different * m2.uncertain
  const uncertainDiff = m1.uncertain * m2.different
  const uncertainUncertain = m1.uncertain * m2.uncertain

  // Conflict: {SAME} ∩ {DIFFERENT} = ∅
  const K = m1.same * m2.different + m1.different * m2.same
  const norm = 1 - K
  if (norm <= 0) return { same: 0, different: 0, uncertain: 1 }

  return {
    same: (sameSame + sameUncertain + uncertainSame) / norm,
    different: (diffDiff + diffUncertain + uncertainDiff) / norm,
    uncertain: uncertainUncertain / norm,
  }
}
Lévy Stable Sampling
Chambers-Mallows-Stuck (1976) algorithm. α=1.5 gives infinite variance — defeats Kolmogorov-Smirnov timing correlation that catches Poisson.
function levySample(scale: number, min = 100, max = 30000): number {
  const alpha = 1.5 // heavy tail without Cauchy extremes
  const beta = 1.0  // right-skewed (positive delays)

  // Chambers-Mallows-Stuck algorithm
  const U = (Math.random() - 0.5) * Math.PI
  const W = -Math.log(Math.random() || 1e-10)

  const phi0 = Math.atan(beta * Math.tan(Math.PI * alpha / 2)) / alpha
  const factor = Math.pow(
    Math.cos(U - alpha * phi0) / W,
    (1 - alpha) / alpha
  ) * Math.sin(alpha * (U - phi0))
    / Math.pow(Math.cos(U), 1 / alpha)

  return Math.min(Math.abs(factor) * scale + min, max)
}
k-Anonymity Decoy Guarantee
For every real target, k decoys of the same type are queried. Observer identification probability: 1/(k+1). Cryptographic random selection from curated pools.
function generateDecoys(real: string, type: SeedType, k: number): string[] {
  const pool = POOLS[type].filter(t => t !== real.toLowerCase())
  const count = Math.min(k, pool.length)
  const decoys: string[] = []
  const used = new Set<number>()

  const randomBytes = new Uint32Array(count)
  crypto.getRandomValues(randomBytes)

  for (let i = 0; i < count; i++) {
    let idx = randomBytes[i] % pool.length
    while (used.has(idx)) idx = (idx + 1) % pool.length
    used.add(idx)
    decoys.push(pool[idx])
  }
  return decoys
}

// k=3 → 25% identification probability
// k=5 → 16.7%
function identificationProbability(k: number): number {
  return 1 / (k + 1)
}
0
plugins
0
tests
0
analytics engines
0
anonymity layers