use crate::domain::request::AnalysisConfig;
use crate::domain::types::{Finding, Suppression};
use std::path::Path;
pub fn is_suppressed(finding: &Finding, config: &AnalysisConfig) -> bool {
for s in &config.suppressions {
if suppression_matches(finding, s) {
return true;
}
}
for p in &config.approved_patterns {
if let Some(ref rule) = p.rule_id {
if rule != &finding.rule_id && rule != "*" {
continue;
}
}
if let Some(ref glob) = p.path_glob {
let path = finding.location.file.to_string_lossy();
if !path_matches_glob(&path, glob) {
continue;
}
}
if let Some(ref needle) = p.code_contains {
let _ = needle;
}
return true;
}
false
}
fn suppression_matches(finding: &Finding, s: &Suppression) -> bool {
if let Some(ref rule) = s.rule_id {
if rule != &finding.rule_id && rule != "*" {
return false;
}
}
if let Some(ref file) = s.file {
if finding.location.file != *file
&& !finding
.location
.file
.ends_with(file.as_path())
{
return false;
}
}
if let Some(line) = s.line {
if finding.location.start_line != line {
return false;
}
}
true
}
pub fn path_matches_glob(path: &str, pattern: &str) -> bool {
let path = path.replace('\\', "/");
let pattern = pattern.replace('\\', "/");
if pattern == "**" || pattern == "*" {
return true;
}
if pattern.starts_with("**/") && pattern.ends_with("/**") {
let mid = &pattern[3..pattern.len() - 3];
if mid.is_empty() {
return true;
}
return path == mid
|| path.starts_with(&format!("{mid}/"))
|| path.ends_with(&format!("/{mid}"))
|| path.contains(&format!("/{mid}/"));
}
if let Some(rest) = pattern.strip_prefix("**/") {
let rest = rest.trim_start_matches('/');
return path == rest
|| path.ends_with(rest)
|| path.ends_with(&format!("/{rest}"))
|| path.contains(&format!("/{rest}"))
|| path.contains(rest);
}
if let Some(prefix) = pattern.strip_suffix("/**") {
return path == prefix || path.starts_with(&format!("{prefix}/"));
}
if let Some(suf) = pattern.strip_prefix('*') {
return path.ends_with(suf);
}
path == pattern || path.ends_with(&pattern) || path.ends_with(&format!("/{pattern}"))
}
pub fn parse_inline_suppressions(source: &str, path: &Path) -> Vec<Suppression> {
let mut out = Vec::new();
for (idx, line) in source.lines().enumerate() {
let line_no = (idx + 1) as u32;
for marker in ["xbp-analysis: ignore", "xbp-audit: ignore", "xbp-aspirator: ignore"] {
if let Some(pos) = line.find(marker) {
let rest = line[pos + marker.len()..].trim();
let rule = rest.split_whitespace().next().unwrap_or("*");
out.push(Suppression {
rule_id: Some(rule.to_string()),
file: Some(path.to_path_buf()),
line: Some(line_no),
reason: Some("inline".into()),
pattern: None,
});
out.push(Suppression {
rule_id: Some(rule.to_string()),
file: Some(path.to_path_buf()),
line: Some(line_no + 1),
reason: Some("inline-next".into()),
pattern: None,
});
}
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::types::{Confidence, LanguageId, Severity, SourceLocation};
#[test]
fn glob_double_star_suffix() {
assert!(path_matches_glob("crates/foo/src/lib.rs", "**/src/lib.rs"));
assert!(path_matches_glob("a/b/target/x", "**/target/**"));
}
#[test]
fn suppress_by_rule_and_line() {
let finding = Finding::builder("ignored-failure", LanguageId::rust())
.severity(Severity::High)
.confidence(Confidence::High)
.location(SourceLocation::single_line("src/a.rs", 10, 1))
.explanation("x")
.failure_scenario("y")
.remediation("z")
.build();
let mut config = AnalysisConfig::default_for_rust();
config.suppressions.push(Suppression {
rule_id: Some("ignored-failure".into()),
file: Some("src/a.rs".into()),
line: Some(10),
reason: None,
pattern: None,
});
assert!(is_suppressed(&finding, &config));
}
}