foxguard 0.12.0

A security scanner as fast as a linter, written in Rust. 200+ built-in rules across 12 source languages.
Documentation
//! Generate `www/src/data/rules.ts` from the Rust rule registry.
//!
//! Run with:
//!
//! ```sh
//! cargo run --bin gen_rules_ts > www/src/data/rules.ts
//! ```
//!
//! The CI `rule-inventory-check` job regenerates the file and diffs it
//! against the committed copy, so the Rust registry is the single source
//! of truth for rule metadata on the website.

use foxguard::rules::{Rule, RuleRegistry};
use foxguard::{Language, Severity};

/// Languages in the order they should appear in the generated file.
const LANGUAGE_ORDER: &[Language] = &[
    Language::JavaScript,
    Language::Python,
    Language::Go,
    Language::Ruby,
    Language::Java,
    Language::Php,
    Language::Rust,
    Language::CSharp,
    Language::Swift,
    Language::Kotlin,
    Language::Solidity,
    Language::Haskell,
    Language::NginxConf,
    Language::ApacheConf,
    Language::HAProxyConf,
    Language::Dockerfile,
    Language::Manifest,
];

fn language_slug(language: Language) -> &'static str {
    match language {
        Language::JavaScript => "js",
        Language::Python => "py",
        Language::Go => "go",
        Language::Ruby => "rb",
        Language::Java => "java",
        Language::Php => "php",
        Language::Rust => "rs",
        Language::CSharp => "cs",
        Language::Swift => "swift",
        Language::Kotlin => "kt",
        Language::C => "c",
        Language::Hcl => "hcl",
        Language::Solidity => "sol",
        Language::Yaml => "yaml",
        Language::NginxConf => "nginxconf",
        Language::ApacheConf => "apacheconf",
        Language::HAProxyConf => "haproxyconf",
        Language::Dockerfile => "dockerfile",
        Language::Manifest => "manifest",
        Language::Bash => "bash",
        Language::Ocaml => "ocaml",
        Language::Scala => "scala",
        Language::Elixir => "elixir",
        Language::Json => "json",
        Language::Apex => "apex",
        Language::Clojure => "clojure",
        Language::Html => "html",
        Language::Xml => "xml",
        Language::Dart => "dart",
        Language::Haskell => "hs",
        // Regex-mode rules fan out per language; this binary generates rules
        // for concrete languages only.
        Language::Regex => "regex",
    }
}

fn language_display_name(language: Language) -> &'static str {
    match language {
        Language::JavaScript => "JavaScript / TypeScript",
        Language::Python => "Python",
        Language::Go => "Go",
        Language::Ruby => "Ruby",
        Language::Java => "Java",
        Language::Php => "PHP",
        Language::Rust => "Rust",
        Language::CSharp => "C#",
        Language::Swift => "Swift",
        Language::Kotlin => "Kotlin",
        Language::C => "C",
        Language::Hcl => "HCL / Terraform",
        Language::Solidity => "Solidity",
        Language::Yaml => "YAML",
        Language::NginxConf => "Nginx",
        Language::ApacheConf => "Apache",
        Language::HAProxyConf => "HAProxy",
        Language::Dockerfile => "Dockerfile",
        Language::Manifest => "Manifest",
        Language::Bash => "Bash",
        Language::Ocaml => "OCaml",
        Language::Scala => "Scala",
        Language::Elixir => "Elixir",
        Language::Json => "JSON",
        Language::Apex => "Apex",
        Language::Clojure => "Clojure",
        Language::Html => "HTML",
        Language::Xml => "XML",
        Language::Dart => "Dart",
        Language::Haskell => "Haskell",
        Language::Regex => "Regex",
    }
}

fn language_array_name(language: Language) -> &'static str {
    match language {
        Language::JavaScript => "jsRules",
        Language::Python => "pyRules",
        Language::Go => "goRules",
        Language::Ruby => "rubyRules",
        Language::Java => "javaRules",
        Language::Php => "phpRules",
        Language::Rust => "rustRules",
        Language::CSharp => "csharpRules",
        Language::Swift => "swiftRules",
        Language::Kotlin => "kotlinRules",
        Language::C => "cRules",
        Language::Hcl => "hclRules",
        Language::Solidity => "solidityRules",
        Language::Yaml => "yamlRules",
        Language::NginxConf => "nginxconfRules",
        Language::ApacheConf => "apacheconfRules",
        Language::HAProxyConf => "haproxyconfRules",
        Language::Dockerfile => "dockerfileRules",
        Language::Manifest => "manifestRules",
        Language::Bash => "bashRules",
        Language::Ocaml => "ocamlRules",
        Language::Scala => "scalaRules",
        Language::Elixir => "elixirRules",
        Language::Json => "jsonRules",
        Language::Apex => "apexRules",
        Language::Clojure => "clojureRules",
        Language::Html => "htmlRules",
        Language::Xml => "xmlRules",
        Language::Dart => "dartRules",
        Language::Haskell => "haskellRules",
        Language::Regex => "regexRules",
    }
}

fn severity_str(severity: Severity) -> &'static str {
    match severity {
        Severity::Low => "low",
        Severity::Medium => "medium",
        Severity::High => "high",
        Severity::Critical => "critical",
    }
}

/// Escape a string for use inside a single-quoted TypeScript string literal.
fn escape_single_quoted(s: &str) -> String {
    let mut out = String::with_capacity(s.len());
    for c in s.chars() {
        match c {
            '\\' => out.push_str("\\\\"),
            '\'' => out.push_str("\\'"),
            '\n' => out.push_str("\\n"),
            '\r' => out.push_str("\\r"),
            _ => out.push(c),
        }
    }
    out
}

fn emit_rule_group(language: Language, rules: &[&dyn Rule]) {
    println!("const {}: Rule[] = [", language_array_name(language));
    for rule in rules {
        let id = escape_single_quoted(rule.id());
        let cwe = escape_single_quoted(rule.cwe().unwrap_or(""));
        let desc = escape_single_quoted(rule.description());
        let severity = severity_str(rule.severity());
        println!(
            "  {{ id: '{}', cwe: '{}', desc: '{}', severity: '{}' }},",
            id, cwe, desc, severity
        );
    }
    println!("];");
}

fn main() {
    let registry = RuleRegistry::new();
    let all = registry.all_rules();

    // Group by language, sort each group by rule ID for stable output.
    let grouped: Vec<(Language, Vec<&dyn Rule>)> = LANGUAGE_ORDER
        .iter()
        .copied()
        .map(|lang| {
            let mut rules: Vec<&dyn Rule> = all
                .iter()
                .map(|r| r.as_ref())
                .filter(|r| r.language() == lang)
                .collect();
            rules.sort_by(|a, b| a.id().cmp(b.id()));
            (lang, rules)
        })
        .collect();

    // Header
    println!(
        "// DO NOT EDIT — generated by `cargo run --bin gen_rules_ts > www/src/data/rules.ts`."
    );
    println!("// Source of truth: the Rust rule registry in `src/rules/mod.rs`.");
    println!("// To add or update rules, edit the Rust source and regenerate this file.");
    println!();
    println!("export interface Rule {{");
    println!("  id: string;");
    println!("  cwe: string;");
    println!("  desc: string;");
    println!("  severity: 'critical' | 'high' | 'medium' | 'low';");
    println!("}}");
    println!();
    println!("export interface RuleGroup {{");
    println!("  name: string;");
    println!("  slug: string;");
    println!("  rules: Rule[];");
    println!("}}");
    println!();

    for (language, rules) in &grouped {
        emit_rule_group(*language, rules);
        println!();
    }

    println!("export const ruleGroups: RuleGroup[] = [");
    for (language, _) in &grouped {
        println!(
            "  {{ name: '{}', slug: '{}', rules: {} }},",
            escape_single_quoted(language_display_name(*language)),
            language_slug(*language),
            language_array_name(*language),
        );
    }
    println!("];");
    println!();
    println!("export const totalRules = ruleGroups.reduce((sum, g) => sum + g.rules.length, 0);");
    println!(
        "export const productLanguageCount = {};",
        LANGUAGE_ORDER.len()
    );
}