use std::{ffi::OsString, path::PathBuf};
use anyhow::{Context, bail, ensure};
use kingfisher_rules::Rules;
use kingfisher_scanner::{Confidence, RulesDatabase, get_builtin_rules};
use serde_json::json;
const USAGE: &str = "inspect_rules [EXACT_RULE_ID] [--rules-path PATH] [--no-builtins]
[--id-prefix PREFIX] [--with-validation] [--with-revocation] [--field NAME]
Omit the ID to list catalog summaries. Repeat --rules-path or --field.
Fields: pattern, detection_regex, validation, revocation, depends_on_rule,
betterleaks_filter, pattern_requirements, examples, references.";
fn text(value: OsString) -> anyhow::Result<String> {
value.into_string().map_err(|_| anyhow::anyhow!("rule IDs/options must be valid Unicode"))
}
fn main() -> anyhow::Result<()> {
let mut args = std::env::args_os().skip(1);
let mut rule_id = None;
let mut paths = Vec::new();
let mut builtins = true;
let mut prefix = String::new();
let mut with_validation = false;
let mut with_revocation = false;
let mut fields = Vec::new();
while let Some(arg) = args.next() {
let arg = text(arg)?;
match arg.as_str() {
"--help" | "-h" => {
println!("{USAGE}");
return Ok(());
}
"--rules-path" => {
paths.push(PathBuf::from(args.next().context("--rules-path requires a path")?));
}
"--no-builtins" => builtins = false,
"--id-prefix" => prefix = text(args.next().context("--id-prefix requires a prefix")?)?,
"--with-validation" => with_validation = true,
"--with-revocation" => with_revocation = true,
"--field" => {
let field = text(args.next().context("--field requires a name")?)?;
ensure!(
matches!(
field.as_str(),
"pattern"
| "detection_regex"
| "validation"
| "revocation"
| "depends_on_rule"
| "betterleaks_filter"
| "pattern_requirements"
| "examples"
| "references"
),
"unknown field: {field}\n{USAGE}"
);
fields.push(field);
}
_ if arg.starts_with('-') => bail!("unknown option: {arg}\n{USAGE}"),
_ => {
ensure!(rule_id.is_none(), "supply only one exact rule ID\n{USAGE}");
rule_id = Some(arg);
}
}
}
ensure!(fields.is_empty() || rule_id.is_some(), "--field requires an exact rule ID");
ensure!(
rule_id.is_none() || (prefix.is_empty() && !with_validation && !with_revocation),
"catalog filters apply when the rule ID is omitted"
);
let mut rules = if builtins { get_builtin_rules(Some(Confidence::Low))? } else { Rules::new() };
if !paths.is_empty() {
rules.update(Rules::from_paths(paths, Confidence::Low)?);
}
let database = RulesDatabase::from_rule_collection(rules)?;
if let Some(id) = rule_id {
let (index, rule) = database
.rules()
.iter()
.enumerate()
.find(|(_, rule)| rule.id() == id)
.with_context(|| format!("unknown exact rule ID: {id}"))?;
let mut detail = serde_json::to_value(rule.syntax())?;
detail["detection_regex"] = json!(database.anchored_regexes()[index].as_str());
if !fields.is_empty() {
let mut selected = json!({"id": rule.id()});
for field in fields {
selected[&field] = detail[&field].clone();
}
detail = selected;
}
println!("{}", serde_json::to_string_pretty(&detail)?);
} else {
for rule in database.rules() {
let syntax = rule.syntax();
if !rule.id().starts_with(&prefix)
|| (with_validation && syntax.validation.is_none())
|| (with_revocation && syntax.revocation.is_none())
{
continue;
}
println!(
"{}",
json!({
"id": rule.id(), "name": rule.name(), "visible": syntax.visible,
"validation": syntax.validation.is_some(),
"revocation": syntax.revocation.is_some(),
})
);
}
}
Ok(())
}