EntropyEvaluator.java
/*
* SPDX-License-Identifier: Apache-2.0
* Copyright 2026 Nikolas Charalambidis
*/
package dev.nichar.facepalm.engine.evaluator;
import dev.nichar.facepalm.engine.FileContext;
import dev.nichar.facepalm.engine.Finding;
import jakarta.annotation.Nonnull;
import jakarta.annotation.Nullable;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
import javax.inject.Named;
import javax.inject.Singleton;
/**
* Evaluates a secret's randomness using Shannon entropy.
* High entropy often signals genuine credentials, while low entropy indicates repetitive or dummy data.
*/
@Named
@Singleton
class EntropyEvaluator implements FindingEvaluator {
@Override
public void evaluate(@Nonnull final Finding finding, @Nonnull final FileContext context) {
// Skip entropy checks for high-signal formats like Private Keys.
if (finding.getPatternName().contains("Private Key") || finding.getSecretValue().contains("-----")) {
return;
}
final var entropy = getShannonEntropy(finding.getSecretValue());
if (entropy > 4.5) {
// Random strings typical of API keys or passwords.
finding.log(String.format(Locale.ROOT, "High Entropy (%.2f)", entropy), 10, 20);
} else if (entropy < 3.0) {
// Repetitive or predictable strings likely to be noise.
finding.log(String.format(Locale.ROOT, "Low Entropy (%.2f)", entropy), 0, -40);
}
}
/**
* Calculates Shannon entropy for the given string.
* Values above 4.5 typically indicate high randomness.
*/
private double getShannonEntropy(@Nullable final String string) {
if (string == null || string.isEmpty()) {
return 0;
}
final Map<Character, Integer> freq = new HashMap<>();
for (final var ch : string.toCharArray()) {
freq.merge(ch, 1, Integer::sum);
}
var entropy = 0.0;
for (final int count : freq.values()) {
final double p = (double) count / string.length();
entropy -= p * (Math.log(p) / Math.log(2));
}
return entropy;
}
}