use std::ops::Range;
use std::path::Path;
use serde::Deserialize;
mod connstr;
mod credential;
mod entropy;
mod path;
mod pii;
mod placeholder;
#[cfg(feature = "privacy-filter")]
pub mod privacy_filter;
mod regex;
mod ruleset;
mod uri;
mod value;
pub use connstr::ConnectionStringDetector;
pub(crate) use credential::normalize_key as credential_key_normalize;
pub use credential::{CredentialAssignmentDetector, CredentialKeyDetector};
pub use entropy::{EntropyConfig, EntropyDetector, shannon_entropy};
pub use path::{PathConfig, PathDetector};
pub use pii::{AddressDetector, EmailConfig, EmailDetector, PhoneDetector};
pub use placeholder::{PlaceholderConfig, Placeholders, is_placeholder};
#[cfg(feature = "privacy-filter")]
pub use privacy_filter::{ModelConfig, PrivacyFilterConfig, PrivacyFilterDetector, ViterbiConfig};
pub(crate) use regex::describe_regex_error;
pub use regex::{RegexConfig, RegexDetector};
pub use ruleset::{BETTERLEAKS_RULESET, RuleSource, RulesetConfig, RulesetDetector};
pub use uri::CredentialedUriDetector;
pub use value::{ValueConfig, ValueDetector};
pub trait Detector: Send + Sync {
fn name(&self) -> &str;
fn detect(&self, value: &str, ctx: &LeafContext<'_>, out: &mut Vec<Detection>);
fn document_scope(&self) -> bool {
false
}
fn detect_document(
&self,
values: &[DocumentValue<'_>],
out: &mut [Vec<Detection>],
) -> Result<(), crate::Error> {
for (value, out) in values.iter().zip(out) {
self.detect(value.value, &value.ctx, out);
}
Ok(())
}
}
#[derive(Debug, Clone, Copy)]
pub struct DocumentValue<'a> {
pub value: &'a str,
pub ctx: LeafContext<'a>,
}
#[derive(Debug, Clone, Copy, Default)]
pub struct LeafContext<'a> {
pub key: Option<&'a str>,
pub path: &'a str,
pub credential_context: bool,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Detection {
pub range: Range<usize>,
pub label: String,
}
impl Detection {
pub fn new(range: Range<usize>, label: impl Into<String>) -> Self {
Self {
range,
label: label.into(),
}
}
}
#[derive(Debug, Clone)]
pub enum DetectorConfig {
Entropy(EntropyConfig),
Ruleset(RulesetConfig),
Regex(RegexConfig),
Value(ValueConfig),
Path(PathConfig),
CredentialedUri,
ConnectionString,
CredentialAssignment,
CredentialKey,
PiiEmail(EmailConfig),
PiiPhone,
PiiAddress,
#[cfg(feature = "privacy-filter")]
PrivacyFilter(Box<PrivacyFilterConfig>),
}
fn boxed_unless(skip: bool, detector: impl Detector + 'static) -> Vec<Box<dyn Detector>> {
if skip {
Vec::new()
} else {
vec![Box::new(detector)]
}
}
pub const DETECTOR_NAMES: &[&str] = &[
"entropy",
"ruleset",
"regex",
"value",
"path",
"credentialed_uri",
"connection_string",
"credential_assignment",
"credential_key",
"pii:email",
"pii:phone",
"pii:address",
"privacy_filter",
];
impl<'de> Deserialize<'de> for DetectorConfig {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_any(DetectorVisitor)
}
}
struct DetectorVisitor;
const CONFIGURED: [&str; 6] = ["entropy", "ruleset", "regex", "value", "path", "pii:email"];
#[cfg(not(feature = "privacy-filter"))]
const PRIVACY_FILTER_MISSING: &str = "the privacy_filter detector is not compiled into this build \
(it needs the `privacy-filter` feature)";
fn unknown_detector<E: serde::de::Error>(name: &str) -> E {
E::custom(format!(
"unknown detector {name:?} (expected one of {})",
DETECTOR_NAMES.join(", ")
))
}
impl<'de> serde::de::Visitor<'de> for DetectorVisitor {
type Value = DetectorConfig;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("a detector name, or a map of one detector name to its settings")
}
fn visit_str<E: serde::de::Error>(self, name: &str) -> Result<Self::Value, E> {
match name {
"credentialed_uri" => Ok(DetectorConfig::CredentialedUri),
"connection_string" => Ok(DetectorConfig::ConnectionString),
"credential_assignment" => Ok(DetectorConfig::CredentialAssignment),
"credential_key" => Ok(DetectorConfig::CredentialKey),
"pii:phone" => Ok(DetectorConfig::PiiPhone),
"pii:address" => Ok(DetectorConfig::PiiAddress),
#[cfg(feature = "privacy-filter")]
"privacy_filter" => Ok(DetectorConfig::PrivacyFilter(Box::default())),
#[cfg(not(feature = "privacy-filter"))]
"privacy_filter" => Err(E::custom(PRIVACY_FILTER_MISSING)),
name if CONFIGURED.contains(&name) => Err(E::custom(format!(
"the {name} detector needs settings: write `{name}:` and indent them under it"
))),
other => Err(unknown_detector(other)),
}
}
fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {
use serde::de::Error;
let Some(name) = map.next_key::<String>()? else {
return Err(A::Error::custom("an empty map names no detector"));
};
let detector = match name.as_str() {
"entropy" => DetectorConfig::Entropy(map.next_value()?),
"ruleset" => DetectorConfig::Ruleset(map.next_value()?),
"regex" => DetectorConfig::Regex(map.next_value()?),
"value" => DetectorConfig::Value(map.next_value()?),
"path" => DetectorConfig::Path(map.next_value()?),
"pii:email" => DetectorConfig::PiiEmail(map.next_value()?),
#[cfg(feature = "privacy-filter")]
"privacy_filter" => DetectorConfig::PrivacyFilter(Box::new(
map.next_value::<Option<_>>()?.unwrap_or_default(),
)),
#[cfg(not(feature = "privacy-filter"))]
"privacy_filter" => return Err(A::Error::custom(PRIVACY_FILTER_MISSING)),
other => {
let detector = self.visit_str(other)?;
map.next_value::<serde::de::IgnoredAny>()?;
detector
}
};
if map.next_key::<String>()?.is_some() {
return Err(A::Error::custom(format!(
"{name}: each entry of `detectors` names one detector; \
start the next one with its own `-`"
)));
}
Ok(detector)
}
}
impl DetectorConfig {
pub fn name(&self) -> &'static str {
match self {
Self::Entropy(_) => "entropy",
Self::Ruleset(_) => "ruleset",
Self::Regex(_) => "regex",
Self::Value(_) => "value",
Self::Path(_) => "path",
Self::CredentialedUri => "credentialed_uri",
Self::ConnectionString => "connection_string",
Self::CredentialAssignment => "credential_assignment",
Self::CredentialKey => "credential_key",
Self::PiiEmail(_) => "pii:email",
Self::PiiPhone => "pii:phone",
Self::PiiAddress => "pii:address",
#[cfg(feature = "privacy-filter")]
Self::PrivacyFilter(_) => "privacy_filter",
}
}
pub fn detectors(
&self,
placeholders: &Placeholders,
) -> Result<Vec<Box<dyn Detector>>, crate::Error> {
Ok(match self {
Self::Entropy(config) => vec![Box::new(EntropyDetector::new(config)?)],
Self::Ruleset(config) => vec![Box::new(RulesetDetector::new(config, placeholders)?)],
Self::Regex(config) => config
.detectors()?
.into_iter()
.map(|d| Box::new(d) as Box<dyn Detector>)
.collect(),
Self::Value(config) => {
let detector = ValueDetector::new(&config.values)?;
boxed_unless(detector.is_empty(), detector)
}
Self::Path(config) => {
let detector = PathDetector::new(&config.paths);
boxed_unless(detector.is_empty(), detector)
}
Self::CredentialedUri => vec![Box::new(CredentialedUriDetector::new(placeholders))],
Self::ConnectionString => vec![Box::new(ConnectionStringDetector::new(placeholders))],
Self::CredentialAssignment => {
vec![Box::new(CredentialAssignmentDetector::new(placeholders))]
}
Self::CredentialKey => vec![Box::new(CredentialKeyDetector::new(placeholders))],
Self::PiiEmail(config) => vec![Box::new(EmailDetector::new(config))],
Self::PiiPhone => vec![Box::new(PhoneDetector)],
Self::PiiAddress => vec![Box::new(AddressDetector)],
#[cfg(feature = "privacy-filter")]
Self::PrivacyFilter(config) => vec![Box::new(PrivacyFilterDetector::new(config)?)],
})
}
pub fn resolve_paths(&mut self, base: &Path) {
match self {
Self::Ruleset(config) => {
for source in &mut config.rules {
if let RuleSource::Path(path) = source
&& path.is_relative()
{
*path = base.join(&*path);
}
}
}
#[cfg(feature = "privacy-filter")]
Self::PrivacyFilter(config) => {
if let Some(dir) = config.model_dir.as_mut().filter(|d| d.is_relative()) {
*dir = base.join(&*dir);
}
}
_ => {}
}
}
}