mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-09-07 08:27:12 +00:00
181 lines
5.9 KiB
Rust
181 lines
5.9 KiB
Rust
#![expect(
|
|
clippy::disallowed_methods,
|
|
reason = "build script: runs at compile time outside any runtime"
|
|
)]
|
|
|
|
use std::fmt::Write;
|
|
use std::path::Path;
|
|
use std::{env, fs};
|
|
|
|
use serde::Deserialize;
|
|
|
|
#[derive(Deserialize)]
|
|
struct Config {
|
|
allowlist: Option<GlobalAllowlist>,
|
|
rules: Vec<Rule>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct GlobalAllowlist {
|
|
regexes: Option<Vec<String>>,
|
|
stopwords: Option<Vec<String>>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct Rule {
|
|
id: String,
|
|
regex: String,
|
|
#[serde(default)]
|
|
keywords: Vec<String>,
|
|
entropy: Option<f64>,
|
|
#[serde(default)]
|
|
allowlist: Option<RuleAllowlist>,
|
|
#[allow(
|
|
dead_code,
|
|
reason = "The source TOML carries descriptions even though codegen ignores them."
|
|
)]
|
|
description: Option<String>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase")]
|
|
struct RuleAllowlist {
|
|
regexes: Option<Vec<String>>,
|
|
stopwords: Option<Vec<String>>,
|
|
regex_target: Option<String>,
|
|
}
|
|
|
|
fn escape_rust_string(s: &str) -> String {
|
|
let mut out = String::with_capacity(s.len() + 10);
|
|
for ch in s.chars() {
|
|
match ch {
|
|
'\\' => out.push_str("\\\\"),
|
|
'"' => out.push_str("\\\""),
|
|
'\n' => out.push_str("\\n"),
|
|
'\r' => out.push_str("\\r"),
|
|
'\t' => out.push_str("\\t"),
|
|
c => out.push(c),
|
|
}
|
|
}
|
|
out
|
|
}
|
|
|
|
fn main() {
|
|
println!("cargo:rerun-if-changed=data/gitleaks.toml");
|
|
|
|
let toml_path = Path::new("data/gitleaks.toml");
|
|
let toml_content = fs::read_to_string(toml_path).expect("failed to read gitleaks.toml");
|
|
let config: Config = toml::from_str(&toml_content).expect("failed to parse gitleaks.toml");
|
|
|
|
let out_dir = env::var("OUT_DIR").expect("OUT_DIR should be set for build scripts");
|
|
let out_path = Path::new(&out_dir).join("rules_generated.rs");
|
|
|
|
let mut code = String::new();
|
|
|
|
// Global allowlist regexes
|
|
code.push_str("#[allow(unreachable_pub, reason = \"Generated constants are included through a private module boundary.\")]\npub const GLOBAL_ALLOWLIST_REGEXES: &[&str] = &[\n");
|
|
if let Some(ref al) = config.allowlist {
|
|
if let Some(ref regexes) = al.regexes {
|
|
for r in regexes {
|
|
let _ = writeln!(code, " \"{}\",", escape_rust_string(r));
|
|
}
|
|
}
|
|
}
|
|
code.push_str("];\n\n");
|
|
|
|
// Global allowlist stopwords
|
|
code.push_str(
|
|
"#[allow(unreachable_pub, reason = \"Generated constants are included through a private module boundary.\")]\npub const GLOBAL_ALLOWLIST_STOPWORDS: &[&str] = &[\n",
|
|
);
|
|
if let Some(ref al) = config.allowlist {
|
|
if let Some(ref stopwords) = al.stopwords {
|
|
for sw in stopwords {
|
|
let _ = writeln!(code, " \"{}\",", escape_rust_string(sw));
|
|
}
|
|
}
|
|
}
|
|
code.push_str("];\n\n");
|
|
|
|
// Rule definitions
|
|
code.push_str("#[allow(dead_code, unreachable_pub, reason = \"Generated metadata is included through a private module boundary and not every field is read directly.\")]\n");
|
|
code.push_str("pub struct RuleDef {\n");
|
|
code.push_str(" pub id: &'static str,\n");
|
|
code.push_str(" pub regex_pattern: &'static str,\n");
|
|
code.push_str(" pub keywords: &'static [&'static str],\n");
|
|
code.push_str(" pub entropy: Option<f64>,\n");
|
|
code.push_str(" pub allowlist_regexes: &'static [&'static str],\n");
|
|
code.push_str(" pub allowlist_stopwords: &'static [&'static str],\n");
|
|
code.push_str(" pub allowlist_regex_target: Option<&'static str>,\n");
|
|
code.push_str("}\n\n");
|
|
|
|
code.push_str("#[allow(unreachable_pub, reason = \"Generated constants are included through a private module boundary.\")]\npub const RULES: &[RuleDef] = &[\n");
|
|
|
|
for rule in &config.rules {
|
|
code.push_str(" RuleDef {\n");
|
|
let _ = writeln!(code, " id: \"{}\",", escape_rust_string(&rule.id));
|
|
let _ = writeln!(
|
|
code,
|
|
" regex_pattern: \"{}\",",
|
|
escape_rust_string(&rule.regex)
|
|
);
|
|
|
|
// Keywords
|
|
code.push_str(" keywords: &[");
|
|
for kw in &rule.keywords {
|
|
let _ = write!(code, "\"{}\", ", escape_rust_string(kw));
|
|
}
|
|
code.push_str("],\n");
|
|
|
|
// Entropy
|
|
match rule.entropy {
|
|
Some(e) => {
|
|
let _ = writeln!(code, " entropy: Some({e:.1}),");
|
|
}
|
|
None => code.push_str(" entropy: None,\n"),
|
|
}
|
|
|
|
// Allowlist regexes
|
|
code.push_str(" allowlist_regexes: &[");
|
|
if let Some(ref al) = rule.allowlist {
|
|
if let Some(ref regexes) = al.regexes {
|
|
for r in regexes {
|
|
let _ = write!(code, "\"{}\", ", escape_rust_string(r));
|
|
}
|
|
}
|
|
}
|
|
code.push_str("],\n");
|
|
|
|
// Allowlist stopwords
|
|
code.push_str(" allowlist_stopwords: &[");
|
|
if let Some(ref al) = rule.allowlist {
|
|
if let Some(ref stopwords) = al.stopwords {
|
|
for sw in stopwords {
|
|
let _ = write!(code, "\"{}\", ", escape_rust_string(sw));
|
|
}
|
|
}
|
|
}
|
|
code.push_str("],\n");
|
|
|
|
// Allowlist regex target
|
|
if let Some(ref al) = rule.allowlist {
|
|
if let Some(ref target) = al.regex_target {
|
|
let _ = writeln!(
|
|
code,
|
|
" allowlist_regex_target: Some(\"{}\"),",
|
|
escape_rust_string(target)
|
|
);
|
|
} else {
|
|
code.push_str(" allowlist_regex_target: None,\n");
|
|
}
|
|
} else {
|
|
code.push_str(" allowlist_regex_target: None,\n");
|
|
}
|
|
|
|
code.push_str(" },\n");
|
|
}
|
|
|
|
code.push_str("];\n");
|
|
|
|
fs::write(&out_path, code).expect("failed to write generated rules");
|
|
}
|