use std::collections::BTreeMap;
use super::artifact::{Prop, Type};
pub(crate) const DANGEROUS_FIELDS: &[&str] = &[
"password",
"password_hash",
"secret",
"token",
"access_token",
"refresh_token",
"api_key",
"private_key",
"session_id",
"cookie",
"authorization",
"credential",
];
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct DangerousField {
pub(crate) page: String,
pub(crate) field: String,
}
pub(crate) fn dangerous_fields(
pages: &BTreeMap<String, super::artifact::Page>,
) -> Vec<DangerousField> {
let mut hits = Vec::new();
for (page, schema) in pages {
scan(page, "", &schema.props.fields, &mut hits);
}
hits
}
fn scan(page: &str, prefix: &str, fields: &BTreeMap<String, Prop>, hits: &mut Vec<DangerousField>) {
for (name, prop) in fields {
let path = if prefix.is_empty() {
name.clone()
} else {
format!("{prefix}.{name}")
};
if DANGEROUS_FIELDS.iter().any(|danger| name == danger) {
hits.push(DangerousField {
page: page.to_owned(),
field: path.clone(),
});
}
if let Type::Object { fields } = &prop.ty {
scan(page, &path, fields, hits);
}
if let Type::Array { item } = &prop.ty
&& let Type::Object { fields } = item.as_ref()
{
scan(page, &path, fields, hits);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contracts::artifact::{Page, Props};
fn prop(ty: Type) -> Prop {
Prop { required: true, ty }
}
#[test]
fn flags_password_field() {
let mut fields = BTreeMap::new();
fields.insert("password".to_owned(), prop(Type::String));
let mut pages = BTreeMap::new();
pages.insert(
"Dashboard".to_owned(),
Page {
props: Props { fields },
},
);
let hits = dangerous_fields(&pages);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].field, "password");
}
#[test]
fn flags_nested_secret_in_array() {
let inner = BTreeMap::from([("token".to_owned(), prop(Type::String))]);
let mut fields = BTreeMap::new();
fields.insert(
"users".to_owned(),
prop(Type::Array {
item: Box::new(Type::Object { fields: inner }),
}),
);
let mut pages = BTreeMap::new();
pages.insert(
"Dashboard".to_owned(),
Page {
props: Props { fields },
},
);
let hits = dangerous_fields(&pages);
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].field, "users.token");
}
#[test]
fn passes_clean_fields() {
let mut fields = BTreeMap::new();
fields.insert("name".to_owned(), prop(Type::String));
let mut pages = BTreeMap::new();
pages.insert(
"Dashboard".to_owned(),
Page {
props: Props { fields },
},
);
assert!(dangerous_fields(&pages).is_empty());
}
}