///|
/// Shannon entropy over a candidate run, in bits per character.
fn shannon_entropy(text : String, start : Int, end : Int) -> Double {
let buckets : Array[Int] = []
for _ in 0..<128 {
buckets.push(0)
}
let mut total = 0
for k = start; k < end; k = k + 1 {
let c = text[k].to_int()
if c < 128 {
buckets[c] += 1
total += 1
}
}
if total == 0 {
return 0.0
}
let mut bits = 0.0
for count in buckets {
if count > 0 {
let p = count.to_double() / total.to_double()
bits -= p * @math.log2(p)
}
}
bits
}
///|
fn is_entropy_char(c : Int) -> Bool {
is_ascii_alnum(c) || c == 43 || c == 47 || c == 61 || c == 45 || c == 95
}
///|
/// High-entropy secret detection, opt-in: runs of at least 40
/// credential-alphabet characters whose Shannon entropy reaches 4.75
/// bits per character. Realistic English (about 4.5) and base64-encoded
/// prose (about 4.7) stay below the bar; generated keys and secrets sit
/// above it. Reported at medium confidence — entropy is evidence, not
/// proof.
fn scan_high_entropy(text : String, out : Array[Finding]) -> Unit {
let mut i = 0
while i < text.length() {
if is_entropy_char(text[i].to_int()) {
let start = i
while i < text.length() && is_entropy_char(text[i].to_int()) {
i += 1
}
if i - start >= 40 && shannon_entropy(text, start, i) >= 4.75 {
push_finding(out, HighEntropySecret, start, i, Medium)
}
} else {
i += 1
}
}
}