use keyhog_core::{CredentialShape, DetectorSpec};
pub(crate) const PEM_BEGIN_MARKER: &str = "-----BEGIN";
pub(crate) fn is_pem_block(value: &str) -> bool {
value.starts_with(PEM_BEGIN_MARKER)
}
pub(crate) fn credential_url_userinfo_password(value: &str) -> Option<&str> {
let scheme_end = value.find("://")?;
let scheme_tail = value.as_bytes()[..scheme_end]
.iter()
.rev()
.take_while(|byte| byte.is_ascii_alphabetic() || **byte == b'+')
.count();
if scheme_tail < 2 {
return None;
}
let rest = &value[scheme_end + 3..];
let userinfo_end = rest
.find(|c: char| c == '/' || c == '?' || c == '#' || c.is_ascii_whitespace())
.unwrap_or(rest.len());
let userinfo = &rest[..userinfo_end];
let at = userinfo.find('@')?;
let colon = userinfo[..at].find(':')?;
Some(&userinfo[colon + 1..at])
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub(crate) struct CredentialShapeRule {
exact_length: Option<usize>,
prefix: Option<String>,
body_min_length: Option<usize>,
body_max_length: Option<usize>,
}
impl CredentialShapeRule {
pub(crate) fn allows(&self, credential: &str) -> bool {
if self
.exact_length
.is_some_and(|expected| credential.len() != expected)
{
return false;
}
if let Some(prefix) = self.prefix.as_deref() {
let Some(body) = credential.strip_prefix(prefix) else {
return true;
};
if self
.body_min_length
.is_some_and(|minimum| body.len() < minimum)
{
return false;
}
if self
.body_max_length
.is_some_and(|maximum| body.len() > maximum)
{
return false;
}
}
true
}
fn from_spec(shape: &CredentialShape) -> Self {
Self {
exact_length: shape.exact_length,
prefix: shape.prefix.clone(),
body_min_length: shape.body_min_length,
body_max_length: shape.body_max_length,
}
}
#[cfg(test)]
pub(crate) fn exact_length_for_test(exact_length: usize) -> Self {
Self {
exact_length: Some(exact_length),
prefix: None,
body_min_length: None,
body_max_length: None,
}
}
#[cfg(test)]
pub(crate) fn prefix_body_range_for_test(
prefix: &str,
body_min_length: usize,
body_max_length: usize,
) -> Self {
Self {
exact_length: None,
prefix: Some(prefix.to_string()),
body_min_length: Some(body_min_length),
body_max_length: Some(body_max_length),
}
}
}
pub(crate) fn compile_detector_shape_rule(
detector: &DetectorSpec,
) -> Result<Option<CredentialShapeRule>, String> {
let Some(shape) = detector.credential_shape.as_ref() else {
return Ok(None);
};
shape.validate(&detector.id)?;
Ok(Some(CredentialShapeRule::from_spec(shape)))
}
pub(crate) fn hydrate_detector_shape_rule(
detector: &crate::execution_pack::detector_plan::DetectorPlanRecord,
) -> Result<Option<CredentialShapeRule>, String> {
let Some(shape) = detector.credential_shape.as_ref() else {
return Ok(None);
};
shape.validate(&detector.id)?;
Ok(Some(CredentialShapeRule::from_spec(shape)))
}
#[cfg(test)]
#[path = "../tests/unit/credential_shapes.rs"]
mod tests;