mod analyzer;
mod kb;
mod model;
mod parser;
mod report;
mod sarif;
mod sigma;
use std::io::{IsTerminal, Read};
use std::process::ExitCode;
use clap::Parser;
#[derive(Parser, Debug)]
#[command(name = "opseclint", version, about, long_about = None)]
struct Cli {
path: Option<String>,
#[arg(short, long)]
command: Option<String>,
#[arg(long, value_enum, default_value = "linux-auditd")]
platform: kb::Platform,
#[arg(long)]
json: bool,
#[arg(long, conflicts_with = "json")]
sarif: bool,
#[arg(long, default_value_t = 0)]
min: u8,
#[arg(long)]
ci: bool,
#[arg(long, default_value_t = 50)]
threshold: u8,
#[arg(long)]
no_color: bool,
#[arg(long, value_name = "DIR")]
sigma: Option<String>,
#[arg(long)]
no_sigma_cache: bool,
}
fn read_input(cli: &Cli) -> std::io::Result<String> {
if let Some(cmd) = &cli.command {
return Ok(cmd.clone());
}
if let Some(path) = &cli.path {
return std::fs::read_to_string(path);
}
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf)
}
fn main() -> ExitCode {
let cli = Cli::parse();
let kb = match kb::load(cli.platform) {
Ok(kb) => kb,
Err(e) => {
eprintln!("opseclint: failed to load knowledge base: {e}");
return ExitCode::from(2);
}
};
let input = match read_input(&cli) {
Ok(s) => s,
Err(e) => {
eprintln!("opseclint: failed to read input: {e}");
return ExitCode::from(2);
}
};
let mut report = analyzer::analyze(&input, &kb);
if cli.min > 0 {
report.findings.retain(|f| f.noise >= cli.min);
}
if let Some(dir) = &cli.sigma {
let product = cli.platform.sigma_product();
match sigma::load_cached(std::path::Path::new(dir), product, !cli.no_sigma_cache) {
Ok((index, from_cache)) => {
let enriched = sigma::enrich(&mut report, &index);
if !cli.json && !cli.sarif {
eprintln!(
"opseclint: sigma — {} rule(s) from {} file(s){}; enriched {} finding(s)",
index.rules_indexed,
index.files_scanned,
if from_cache { " [cached]" } else { "" },
enriched
);
}
}
Err(e) => {
eprintln!(
"opseclint: could not read sigma dir '{dir}': {e} (using seed references)"
);
}
}
}
if cli.sarif {
let source_uri = cli.path.clone().unwrap_or_else(|| {
if cli.command.is_some() {
"<command>"
} else {
"stdin"
}
.to_string()
});
println!("{}", sarif::render(&report, &source_uri));
} else if cli.json {
println!("{}", report::render_json(&report));
} else {
let color = !cli.no_color && std::io::stdout().is_terminal();
print!("{}", report::render_human(&report, color));
}
if cli.ci && report.max_noise >= cli.threshold {
if !cli.json {
eprintln!(
"\nopseclint: CI gate failed — loudest action {} (>= threshold {})",
report::severity_word(report.max_severity()),
cli.threshold
);
}
return ExitCode::from(1);
}
ExitCode::SUCCESS
}