Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions lib/typo/typo.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,6 @@ func Generate(domain string, cfg []strategy.Strategy, logger slog.Logger) ([]typ
}
}

// TODO: Issue #15 here the strategy name is preserved.
// On the return pass that through to the HTTPResult object for results storing
results, err := typogenerator.Fuzz(sld, cfg...)
if err != nil {
return results, err
Expand Down
18 changes: 13 additions & 5 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,16 @@ import (
"time"
)

// candidate pairs a permutation with the strategy that generated it.
type candidate struct {
Permutation string
StrategyName string
}

// Output is the shape of what is returned to the results.json and thus site
type Output struct {
Domain string `json:"domain"`
Strategy string `json:"strategy"`
Resolvable bool `json:"resolvable"`
HasMail bool `json:"has_mail"`
DNS verify.DNSResult `json:"dns"`
Expand Down Expand Up @@ -90,27 +97,28 @@ func main() {

ctx := context.Background()

in := make(chan string)
in := make(chan candidate)
out := make(chan Output)

var wg sync.WaitGroup
for i := 0; i < *workers; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for d := range in {
for c := range in {
for _, tld := range tldsOverride {
v, err := verify.VerifyDomain(ctx, d+"."+tld, vCfg)
v, err := verify.VerifyDomain(ctx, c.Permutation+"."+tld, vCfg)
if err != nil {
continue
}
// Simple triage: only emit domains that show signs of being real
// Simple triage: only emit domains that show signs of being "real"
if !v.Resolvable && !v.HasMail {
continue
}

out <- Output{
Domain: v.ASCII,
Strategy: c.StrategyName,
Resolvable: v.Resolvable,
HasMail: v.HasMail,
DNS: v.DNS,
Expand All @@ -125,7 +133,7 @@ func main() {
go func() {
for _, d := range candidates {
for _, p := range d.Permutations {
in <- p // the actual typo permutation
in <- candidate{Permutation: p, StrategyName: d.StrategyName}
}
}
close(in)
Expand Down
7 changes: 0 additions & 7 deletions site/home.html
Original file line number Diff line number Diff line change
Expand Up @@ -63,13 +63,6 @@ <h2>Filters</h2>
<label>Variant class</label>
<select id="variantFilter">
<option value="">All</option>
<option value="tld">TLD-only</option>
<option value="insert">Insert</option>
<option value="delete">Delete</option>
<option value="substitute">Substitute</option>
<option value="transpose">Transpose</option>
<option value="other">Other/complex</option>
<option value="unknown">Unknown (no base domain)</option>
</select>
</div>
<div>
Expand Down
38 changes: 14 additions & 24 deletions site/js/state.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,15 @@ function normalizeRecord(r){
.concat(dns.AAAA||[])
.filter(Boolean);

const v = classifyVariant(CFG.baseDomain, d); // TODO: this doens't seem to do anything
const cand = parseDomainParts(d);
const scored = scoreRecord(r, CFG);

return {
_raw:r,
domain:d,
resolvable: !!r.resolvable,
variantClass: v.variantClass,
tld: v.tld || "",
editDistance: v.editDistance,
tldOnly: v.tldOnly,
variantClass: r.strategy || "unknown",
tld: cand.tld || "",
score: scored.score,
tags: scored.tags,
ips: ips.join(" "),
Expand Down Expand Up @@ -126,7 +124,7 @@ function sortView(){

VIEW.sort((a,b)=>{
let av = a[k], bv = b[k];
if(k==="score" || k==="httpStatusCode" || k==="editDistance"){
if(k==="score" || k==="httpStatusCode"){
av = Number(av||0); bv = Number(bv||0);
return (av-bv)*mul;
}
Expand Down Expand Up @@ -158,7 +156,7 @@ function render(){
tr.appendChild(dom);

const vc = document.createElement("td");
vc.innerHTML = `<span class="pill"><strong>${r.variantClass}</strong>${r.editDistance!==null?`<span class="mono">d=${r.editDistance}</span>`:""}</span>`;
vc.innerHTML = `<span class="pill"><strong>${r.variantClass}</strong></span>`;
tr.appendChild(vc);

const tld = document.createElement("td");
Expand Down Expand Up @@ -209,6 +207,13 @@ function render(){
tb.appendChild(tr);
}

// update variant dropdown
const variants = Array.from(new Set(RAW.map(r=>r.variantClass).filter(Boolean))).sort();
const vSel = $("variantFilter");
const vCurrent = vSel.value;
vSel.innerHTML = '<option value="">All</option>' + variants.map(v=>`<option value="${v}">${v}</option>`).join("");
vSel.value = variants.includes(vCurrent) ? vCurrent : "";

// update TLD dropdown
const tlds = Array.from(new Set(RAW.map(r=>r.tld).filter(Boolean))).sort();
const sel = $("tldFilter");
Expand Down Expand Up @@ -371,7 +376,7 @@ function fingerprintIndicators(r){
}

// Sinkhole IP indicator
const sinkholes = parseList($("sinkholeIps").value);
const sinkholes = splitAny($("sinkholeIps").value);
if(sinkholes.length && r.ips){
const hit = sinkholes.find(ip => (" "+r.ips+" ").includes(ip));
if(hit) out.push(`<span class="pill"><strong style="color:var(--warn)">sinkhole-hit</strong></span> Matches sinkhole IP: <span class="mono">${escapeHtml(hit)}</span>.`);
Expand All @@ -380,7 +385,7 @@ function fingerprintIndicators(r){
// TLS issuer familiarity / entropy heuristic (only if issuer present)
const issuer = safe(r.tlsIssuer);
if(issuer){
const known = parseList($("knownIssuers").value).some(k => issuer.toLowerCase().includes(k.toLowerCase()));
const known = splitAny($("knownIssuers").value).some(k => issuer.toLowerCase().includes(k.toLowerCase()));
if(!known){
out.push(`<span class="pill"><strong style="color:var(--warn)">unfamiliar-issuer</strong></span> TLS issuer not in known list.`);
}
Expand All @@ -398,21 +403,6 @@ function fingerprintIndicators(r){
return out;
}

// Shannon entropy of a string (for heuristic use only).
function shannonEntropy(str){
const s = (str||"");
if(!s) return 0;
const freq = new Map();
for(const ch of s){
freq.set(ch, (freq.get(ch)||0)+1);
}
let ent = 0;
for(const [_, count] of freq){
const p = count / s.length;
ent -= p * Math.log2(p);
}
return ent;
}
function renderGroups(){
const byVariant = {};
const byTld = {};
Expand Down
61 changes: 0 additions & 61 deletions site/js/utilities.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,67 +39,6 @@ function registrableHint(domain){
return parts.slice(-2).join(".");
}

function levenshtein(a,b){
a = a||""; b = b||"";
const n=a.length, m=b.length;
const dp = Array.from({length:n+1}, ()=>Array(m+1).fill(0));
for(let i=0;i<=n;i++) dp[i][0]=i;
for(let j=0;j<=m;j++) dp[0][j]=j;
for(let i=1;i<=n;i++){
for(let j=1;j<=m;j++){
const cost = a[i-1]===b[j-1]?0:1;
dp[i][j] = Math.min(
dp[i-1][j]+1,
dp[i][j-1]+1,
dp[i-1][j-1]+cost
);
// transposition (Damerau-lite)
if(i>1 && j>1 && a[i-1]===b[j-2] && a[i-2]===b[j-1]){
dp[i][j] = Math.min(dp[i][j], dp[i-2][j-2]+1);
}
}
}
return dp[n][m];
}

function classifyVariant(baseDomain, candidateDomain){
const base = parseDomainParts(baseDomain||"");
const cand = parseDomainParts(candidateDomain||"");
if(!baseDomain) return {variantClass:"unknown", editDistance:null, tld:cand.tld, tldOnly:false};

// Compare SLDs only; treat TLD-only separately
const baseSLD = base.sld;
const candSLD = cand.sld;
const tldOnly = (baseSLD === candSLD) && (base.tld !== cand.tld);
const dist = levenshtein(baseSLD, candSLD);

if(tldOnly) return {variantClass:"tld", editDistance:0, tld:cand.tld, tldOnly:true};

// quick classifiers for single-edit categories
if(dist === 1){
if(baseSLD.length + 1 === candSLD.length) return {variantClass:"insert", editDistance:1, tld:cand.tld, tldOnly:false};
if(baseSLD.length - 1 === candSLD.length) return {variantClass:"delete", editDistance:1, tld:cand.tld, tldOnly:false};
if(baseSLD.length === candSLD.length) return {variantClass:"substitute", editDistance:1, tld:cand.tld, tldOnly:false};
}

// transpose check: distance 1 with same length often captures adjacent transpositions already, but be explicit
if(baseSLD.length === candSLD.length){
let diffs = [];
for(let i=0;i<baseSLD.length;i++){
if(baseSLD[i] !== candSLD[i]) diffs.push(i);
if(diffs.length>2) break;
}
if(diffs.length===2){
const [i,j]=diffs;
if(j===i+1 && baseSLD[i]===candSLD[j] && baseSLD[j]===candSLD[i]){
return {variantClass:"transpose", editDistance:dist, tld:cand.tld, tldOnly:false};
}
}
}

return {variantClass: dist<=2 ? "other" : "other", editDistance:dist, tld:cand.tld, tldOnly:false};
}

function scoreRecord(r, cfg){
const sinkholes = cfg.sinkholes;
const indicators = cfg.indicators;
Expand Down
Loading