forked from mirrors/kingfisher
115 lines
3.4 KiB
Rust
115 lines
3.4 KiB
Rust
use std::{
|
|
collections::HashSet,
|
|
fs,
|
|
path::{Path, PathBuf},
|
|
sync::Arc,
|
|
};
|
|
|
|
use anyhow::{Context, Result};
|
|
use chrono::Local;
|
|
use serde::{Deserialize, Serialize};
|
|
use tracing::debug;
|
|
|
|
use crate::{findings_store::FindingsStore, matcher::compute_finding_fingerprint};
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
pub struct BaselineFile {
|
|
#[serde(rename = "ExactFindings", default)]
|
|
pub exact_findings: ExactFindings,
|
|
}
|
|
|
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
|
pub struct ExactFindings {
|
|
#[serde(default)]
|
|
pub matches: Vec<BaselineFinding>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
|
pub struct BaselineFinding {
|
|
pub filepath: String,
|
|
pub fingerprint: String,
|
|
pub linenum: usize,
|
|
pub lastupdated: String,
|
|
}
|
|
|
|
pub fn load_baseline(path: &Path) -> Result<BaselineFile> {
|
|
let data = fs::read_to_string(path).context("read baseline file")?;
|
|
Ok(serde_yaml::from_str(&data).context("parse baseline yaml")?)
|
|
}
|
|
|
|
pub fn save_baseline(path: &Path, baseline: &BaselineFile) -> Result<()> {
|
|
let data = serde_yaml::to_string(baseline).context("serialize baseline")?;
|
|
fs::write(path, data).context("write baseline file")
|
|
}
|
|
|
|
fn normalize_path(p: &Path, roots: &[PathBuf]) -> String {
|
|
for root in roots {
|
|
if let Ok(stripped) = p.strip_prefix(root) {
|
|
if let Some(name) = root.file_name() {
|
|
return PathBuf::from(name).join(stripped).to_string_lossy().into();
|
|
}
|
|
}
|
|
}
|
|
p.to_string_lossy().into()
|
|
}
|
|
|
|
fn compute_hash(secret: &str, path: &str) -> String {
|
|
let fp = compute_finding_fingerprint(secret, path, 0, 0);
|
|
format!("{:016x}", fp)
|
|
}
|
|
|
|
fn extract_secret(m: &crate::matcher::Match) -> String {
|
|
m.groups
|
|
.captures
|
|
.get(1)
|
|
.or_else(|| m.groups.captures.get(0))
|
|
.map(|c| c.value.to_string())
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
pub fn apply_baseline(
|
|
store: &mut FindingsStore,
|
|
baseline_path: &Path,
|
|
manage: bool,
|
|
roots: &[PathBuf],
|
|
) -> Result<()> {
|
|
let mut baseline = if baseline_path.exists() {
|
|
load_baseline(baseline_path)?
|
|
} else {
|
|
BaselineFile::default()
|
|
};
|
|
|
|
let mut known: HashSet<String> =
|
|
baseline.exact_findings.matches.iter().map(|m| m.fingerprint.clone()).collect();
|
|
|
|
let mut new_entries = Vec::new();
|
|
for arc_msg in store.get_matches_mut() {
|
|
let (origin, _blob, m) = Arc::make_mut(arc_msg);
|
|
let file_path = origin.iter().filter_map(|o| o.full_path()).next();
|
|
if let Some(fp) = file_path {
|
|
let normalized = normalize_path(&fp, roots);
|
|
let secret = extract_secret(m);
|
|
let hash = compute_hash(&secret, &normalized);
|
|
if known.contains(&hash) {
|
|
debug!("Skipping {} due to baseline (hash {})", normalized, hash);
|
|
m.visible = false;
|
|
} else if manage {
|
|
known.insert(hash.clone());
|
|
let entry = BaselineFinding {
|
|
filepath: normalized,
|
|
fingerprint: hash,
|
|
linenum: m.location.source_span.start.line,
|
|
lastupdated: Local::now().to_rfc2822(),
|
|
};
|
|
new_entries.push(entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
if manage && !new_entries.is_empty() {
|
|
baseline.exact_findings.matches.extend(new_entries);
|
|
save_baseline(baseline_path, &baseline)?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|