use std::path::Path;
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct Config {
pub allowlist: AllowlistConfig,
pub strict: StrictConfig,
pub scanners: ScannersConfig,
pub semgrep: SemgrepConfig,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct AllowlistConfig {
pub registries: Vec<String>,
pub domains: Vec<String>,
}
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct StrictConfig {
pub enabled: bool,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct ScannersConfig {
pub shellcheck: bool,
pub semgrep: bool,
pub secrets: bool,
pub prompt: bool,
pub bash_patterns: bool,
#[serde(alias = "typescript_patterns")]
pub typescript: bool,
pub package_install: bool,
pub malicious_urls: bool,
pub obfuscation: bool,
pub pii: bool,
pub frontmatter: bool,
pub agent_frontmatter: bool,
}
#[derive(Debug, Clone, Default, serde::Deserialize, serde::Serialize)]
#[serde(default)]
pub struct SemgrepConfig {
pub config: Option<String>,
pub metrics: bool,
pub version_check: bool,
}
impl AllowlistConfig {
pub fn normalize(&mut self) {
for s in &mut self.registries {
*s = s.to_lowercase();
}
for s in &mut self.domains {
*s = s.to_lowercase();
}
}
}
impl Default for AllowlistConfig {
fn default() -> Self {
AllowlistConfig {
registries: vec![
"registry.npmjs.org".to_string(),
"pypi.org".to_string(),
"files.pythonhosted.org".to_string(),
],
domains: vec![
"registry.npmjs.org".to_string(),
"npmjs.org".to_string(),
"github.com".to_string(),
"githubusercontent.com".to_string(),
"pypi.org".to_string(),
],
}
}
}
impl Default for ScannersConfig {
fn default() -> Self {
ScannersConfig {
shellcheck: true,
semgrep: true,
secrets: true,
prompt: true,
bash_patterns: true,
typescript: true,
package_install: true,
malicious_urls: true,
obfuscation: true,
pii: true,
frontmatter: true,
agent_frontmatter: true,
}
}
}
impl Config {
pub fn load(path: Option<&Path>) -> Result<Config, String> {
let (config_path, is_explicit) = if let Some(p) = path {
(p.to_path_buf(), true)
} else {
(
Path::new("oxidized-agentic-audit.toml").to_path_buf(),
false,
)
};
match std::fs::read_to_string(&config_path) {
Ok(content) => {
let mut config: Config = toml::from_str(&content).map_err(|e| {
format!("Failed to parse config {}: {}", config_path.display(), e)
})?;
config.allowlist.normalize();
Ok(config)
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound && !is_explicit => {
Ok(Config::default())
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Err(format!("Config file not found: {}", config_path.display()))
}
Err(e) => Err(format!(
"Failed to read config {}: {}",
config_path.display(),
e
)),
}
}
pub fn is_scanner_enabled(&self, name: &str) -> bool {
match name {
"shellcheck" => self.scanners.shellcheck,
"semgrep" => self.scanners.semgrep,
"secrets" => self.scanners.secrets,
"prompt" => self.scanners.prompt,
"bash_patterns" => self.scanners.bash_patterns,
"typescript" | "typescript_patterns" => self.scanners.typescript,
"package_install" => self.scanners.package_install,
"malicious_urls" => self.scanners.malicious_urls,
"obfuscation" => self.scanners.obfuscation,
"pii" => self.scanners.pii,
"frontmatter" => self.scanners.frontmatter,
"agent_frontmatter" => self.scanners.agent_frontmatter,
_ => true,
}
}
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct SuppressionFile {
pub suppress: Vec<Suppression>,
}
#[derive(Debug, Clone, serde::Deserialize, serde::Serialize)]
pub struct Suppression {
pub rule: String,
pub file: String,
pub lines: Option<String>,
pub reason: String,
pub ticket: Option<String>,
}
pub fn load_suppressions(skill_path: &Path) -> Vec<Suppression> {
let ignore_path = skill_path.join(".oxidized-agentic-audit-ignore");
if !ignore_path.exists() {
return vec![];
}
let content = match std::fs::read_to_string(&ignore_path) {
Ok(c) => c,
Err(_) => return vec![],
};
match toml::from_str::<SuppressionFile>(&content) {
Ok(file) => file
.suppress
.into_iter()
.filter(|s| {
let suppression_path = std::path::Path::new(&s.file);
let has_parent_dir = suppression_path
.components()
.any(|c| matches!(c, std::path::Component::ParentDir));
let is_absolute = suppression_path.is_absolute();
if has_parent_dir || is_absolute {
eprintln!(
"Warning: ignoring suppression with unsafe path in file field: {}",
s.file
);
}
!has_parent_dir && !is_absolute
})
.collect(),
Err(e) => {
eprintln!("Warning: failed to parse .oxidized-agentic-audit-ignore: {e}");
vec![]
}
}
}