#[cfg(feature = "entropy")]
pub(crate) mod bpe;
mod isolated;
pub(crate) mod keywords;
pub(crate) mod plausibility;
pub(crate) mod policy;
pub(crate) mod scanner;
pub(crate) mod avx512;
pub(crate) mod fast;
#[cfg(target_arch = "aarch64")]
pub(crate) mod fast_neon;
#[cfg(target_arch = "x86_64")]
pub(crate) mod fast_x86;
#[cfg(feature = "entropy")]
pub(crate) use scanner::KEYWORD_FREE_LABEL;
pub use scanner::{find_entropy_secrets, find_entropy_secrets_with_threshold};
pub const LOW_ENTROPY_THRESHOLD: f64 = 3.0;
pub const HIGH_ENTROPY_THRESHOLD: f64 = 4.5;
pub(crate) const ISOLATED_BARE_ENTROPY_LABEL: &str = "none (isolated-token)";
pub const VERY_HIGH_ENTROPY_THRESHOLD: f64 = 5.8;
pub(crate) const FIRST_SOURCE_LINE_NUMBER: usize = 1;
#[derive(serde::Deserialize)]
struct ConfigFileExtensionsFile {
extensions: Vec<String>,
stem_only_extensions: Vec<String>,
}
fn parse_config_file_extensions(raw: &str) -> Result<(Vec<Vec<u8>>, Vec<Vec<u8>>), String> {
toml::from_str::<ConfigFileExtensionsFile>(raw)
.map(|parsed| {
(
parsed
.extensions
.into_iter()
.map(String::into_bytes)
.collect(),
parsed
.stem_only_extensions
.into_iter()
.map(String::into_bytes)
.collect(),
)
})
.map_err(|error| error.to_string())
}
static CONFIG_EXTENSION_LISTS: std::sync::LazyLock<(Vec<Vec<u8>>, Vec<Vec<u8>>)> =
std::sync::LazyLock::new(|| {
match parse_config_file_extensions(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/rules/config-file-extensions.toml"
))) {
Ok(lists) => lists,
Err(error) => panic!(
"rules/config-file-extensions.toml is invalid: {error}. Fix the bundled Tier-B \
config-file-extension list."
),
}
});
fn config_file_extensions() -> &'static [Vec<u8>] {
&CONFIG_EXTENSION_LISTS.0
}
fn extra_stem_config_extensions() -> &'static [Vec<u8>] {
&CONFIG_EXTENSION_LISTS.1
}
#[derive(serde::Deserialize)]
struct CredentialFileNamesFile {
prefix_match: Vec<String>,
exact_or_config_ext: Vec<String>,
}
fn parse_credential_file_names(raw: &str) -> Result<(Vec<Vec<u8>>, Vec<Vec<u8>>), String> {
let parsed: CredentialFileNamesFile = toml::from_str(raw).map_err(|error| error.to_string())?;
if parsed.prefix_match.is_empty() || parsed.exact_or_config_ext.is_empty() {
return Err("prefix_match and exact_or_config_ext must both be non-empty".to_string());
}
Ok((
parsed
.prefix_match
.into_iter()
.map(String::into_bytes)
.collect(),
parsed
.exact_or_config_ext
.into_iter()
.map(String::into_bytes)
.collect(),
))
}
static CREDENTIAL_FILE_NAME_LISTS: std::sync::LazyLock<(Vec<Vec<u8>>, Vec<Vec<u8>>)> =
std::sync::LazyLock::new(|| {
match parse_credential_file_names(include_str!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/rules/credential-file-names.toml"
))) {
Ok(lists) => lists,
Err(error) => panic!(
"rules/credential-file-names.toml is invalid: {error}. Fix the bundled Tier-B \
credential-file-name list."
),
}
});
pub fn shannon_entropy(data: &[u8]) -> f64 {
if data.len() > 1024 {
return shannon_entropy_uncached(data);
}
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static CACHE: RefCell<HashMap<u64, f64>> = RefCell::new(HashMap::with_capacity(256));
}
let hash = crate::util_hash::hash_fast(data);
crate::util_hash::memoize_by_hash(
&CACHE,
hash,
crate::util_hash::DEFAULT_MAX_CACHE_ENTRIES,
|| shannon_entropy_uncached(data),
)
}
fn shannon_entropy_uncached(data: &[u8]) -> f64 {
crate::entropy::fast::shannon_entropy_simd(data)
}
pub(crate) fn unique_byte_count(data: &[u8]) -> usize {
let mut seen = [false; 256];
let mut count = 0usize;
for &byte in data {
let slot = &mut seen[byte as usize];
if !*slot {
*slot = true;
count += 1;
}
}
count
}
pub fn normalized_entropy(data: &[u8]) -> f64 {
if data.is_empty() {
return 0.0;
}
let unique_chars = unique_byte_count(data);
if unique_chars <= 1 {
return 0.0;
}
let max_entropy = (unique_chars as f64).log2();
if max_entropy == 0.0 {
return 0.0;
}
shannon_entropy(data) / max_entropy
}
#[derive(Debug, Clone)]
pub struct EntropyMatch {
pub value: String,
pub entropy: f64,
pub keyword: String,
pub line: usize,
pub offset: usize,
}
#[derive(Debug, Clone)]
pub(crate) struct ClassifiedEntropyMatch {
pub(crate) matched: EntropyMatch,
pub(crate) is_credential_context: bool,
pub(crate) is_same_line_credential_context: bool,
}
pub fn is_entropy_appropriate(path: Option<&str>, allow_source_files: bool) -> bool {
is_entropy_appropriate_inner(path, allow_source_files, false)
}
pub fn is_entropy_appropriate_with_content(
path: Option<&str>,
allow_source_files: bool,
text: &str,
secret_keywords: &[String],
) -> bool {
if is_entropy_appropriate(path, allow_source_files) {
return true;
}
let has_secret_keyword_line =
content_has_secret_keyword_line(path, allow_source_files, text.lines(), secret_keywords);
is_entropy_appropriate_inner(path, allow_source_files, has_secret_keyword_line)
}
fn content_has_secret_keyword_line<'a>(
path: Option<&str>,
allow_source_files: bool,
mut lines: impl Iterator<Item = &'a str>,
secret_keywords: &[String],
) -> bool {
if crate::decode::caesar::is_program_source_code_path(path) && !allow_source_files {
lines.any(keywords::line_has_credential_assignment_surface)
} else {
lines.any(|line| keywords::is_keyword_assignment_line(line, secret_keywords))
}
}
pub(crate) fn is_entropy_appropriate_inner(
path: Option<&str>,
allow_source_files: bool,
has_secret_keyword_line: bool,
) -> bool {
let Some(path) = path else { return true };
let bytes = path.as_bytes();
let ends_ci = |suffix: &[u8]| -> bool {
bytes.len() >= suffix.len()
&& bytes[bytes.len() - suffix.len()..].eq_ignore_ascii_case(suffix)
};
for extension in [b".lock".as_slice(), b".map"] {
if ends_ci(extension) {
return false;
}
}
if ends_ci(b".json") && !has_secret_keyword_line {
return false;
}
if ends_ci(b".min.js") || ends_ci(b".min.css") {
return false;
}
if allow_source_files {
return true;
}
let last_sep = bytes
.iter()
.rposition(|&b| b == b'/' || b == b'\\')
.map(|i| i + 1)
.unwrap_or(0); let filename = &bytes[last_sep..];
for stem in [
b"Cargo.toml".as_slice(),
b"package.json",
b"pyproject.toml",
b"composer.json",
b"Pipfile",
b"Gemfile",
b"pom.xml",
b"build.gradle",
b"build.gradle.kts",
b"build.sbt",
b"mix.exs",
] {
if filename.eq_ignore_ascii_case(stem) {
return false;
}
}
for extension in config_file_extensions() {
if ends_ci(extension) {
return true;
}
}
for name in &CREDENTIAL_FILE_NAME_LISTS.0 {
let starts_ci =
filename.len() >= name.len() && filename[..name.len()].eq_ignore_ascii_case(name);
if starts_ci {
return true;
}
}
for name in &CREDENTIAL_FILE_NAME_LISTS.1 {
if filename.eq_ignore_ascii_case(name) {
return true;
}
if filename.len() > name.len() && filename[..name.len()].eq_ignore_ascii_case(name) {
let tail = &filename[name.len()..];
for ext in config_file_extensions()
.iter()
.chain(extra_stem_config_extensions())
{
if tail.len() >= ext.len()
&& tail[tail.len() - ext.len()..].eq_ignore_ascii_case(ext)
{
return true;
}
}
}
}
has_secret_keyword_line
}
#[cfg(test)]
#[path = "../../tests/unit/entropy_inline.rs"]
mod tests;