use clap::ValueEnum;
#[derive(Clone, Copy, Debug, Default, ValueEnum, PartialEq, Eq)]
pub enum Preset {
#[default]
Minimal,
RustQuality,
Secrets,
JsConsole,
PythonDebug,
}
impl Preset {
pub fn name(&self) -> &'static str {
match self {
Preset::Minimal => "minimal",
Preset::RustQuality => "rust-quality",
Preset::Secrets => "secrets",
Preset::JsConsole => "js-console",
Preset::PythonDebug => "python-debug",
}
}
#[allow(dead_code)]
pub fn description(&self) -> &'static str {
match self {
Preset::Minimal => "Minimal starter configuration with basic settings",
Preset::RustQuality => "Rust best practices (no unwrap, no dbg, no todo, no print)",
Preset::Secrets => "Credential and secret detection patterns",
Preset::JsConsole => "JavaScript/TypeScript console and debugger detection",
Preset::PythonDebug => "Python print, breakpoint, and pdb detection",
}
}
pub fn generate(&self) -> String {
match self {
Preset::Minimal => generate_minimal(),
Preset::RustQuality => generate_rust_quality(),
Preset::Secrets => generate_secrets(),
Preset::JsConsole => generate_js_console(),
Preset::PythonDebug => generate_python_debug(),
}
}
}
fn generate_minimal() -> String {
r#"# diffguard.toml - Minimal preset
# Generated by: diffguard init --preset minimal
#
# This is a starter configuration. Add rules below or use a different preset:
# diffguard init --preset rust-quality # Rust best practices
# diffguard init --preset secrets # Secret detection
# diffguard init --preset js-console # JavaScript/TypeScript
# diffguard init --preset python-debug # Python debugging
[defaults]
base = "origin/main"
scope = "added" # added | changed | modified | deleted (changed kept for compatibility)
fail_on = "error" # error | warn | never
max_findings = 200
diff_context = 0
# Example rule (uncomment and customize):
# [[rule]]
# id = "example.no_todo"
# severity = "warn"
# message = "TODO comments should be resolved before merging."
# patterns = ["\\bTODO\\b", "\\bFIXME\\b"]
# paths = ["**/*"]
# exclude_paths = []
# ignore_comments = false
# ignore_strings = true
"#
.to_string()
}
fn generate_rust_quality() -> String {
r#"# diffguard.toml - Rust Quality preset
# Generated by: diffguard init --preset rust-quality
#
# Enforces Rust best practices for production code.
[defaults]
base = "origin/main"
scope = "added"
fail_on = "error"
max_findings = 200
diff_context = 0
[[rule]]
id = "rust.no_unwrap"
severity = "error"
message = "Avoid unwrap() in production code - use proper error handling."
languages = ["rust"]
patterns = ["\\.unwrap\\("]
paths = ["**/*.rs"]
exclude_paths = ["**/tests/**", "**/benches/**", "**/examples/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "rust.no_expect"
severity = "warn"
message = "Consider replacing expect() with proper error handling."
languages = ["rust"]
patterns = ["\\.expect\\("]
paths = ["**/*.rs"]
exclude_paths = ["**/tests/**", "**/benches/**", "**/examples/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "rust.no_dbg"
severity = "error"
message = "Remove dbg!() macro before merging."
languages = ["rust"]
patterns = ["\\bdbg!\\("]
paths = ["**/*.rs"]
exclude_paths = ["**/tests/**", "**/benches/**", "**/examples/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "rust.no_println"
severity = "warn"
message = "Remove println!/eprintln! before merging - use proper logging."
languages = ["rust"]
patterns = ["\\bprintln!\\(", "\\beprintln!\\("]
paths = ["**/*.rs"]
exclude_paths = ["**/tests/**", "**/benches/**", "**/examples/**", "**/bin/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "rust.no_todo"
severity = "warn"
message = "Resolve TODO/FIXME comments before merging."
languages = ["rust"]
patterns = ["\\bTODO\\b", "\\bFIXME\\b", "\\bHACK\\b"]
paths = ["**/*.rs"]
exclude_paths = []
ignore_comments = false
ignore_strings = true
[[rule]]
id = "rust.no_unimplemented"
severity = "error"
message = "Replace unimplemented!()/todo!() with proper implementation."
languages = ["rust"]
patterns = ["\\bunimplemented!\\(", "\\btodo!\\("]
paths = ["**/*.rs"]
exclude_paths = ["**/tests/**", "**/benches/**", "**/examples/**"]
ignore_comments = true
ignore_strings = true
"#
.to_string()
}
fn generate_secrets() -> String {
r#"# diffguard.toml - Secrets Detection preset
# Generated by: diffguard init --preset secrets
#
# Detects potential secrets and credentials in code.
# Note: This uses pattern matching and may have false positives.
# Always review findings manually.
[defaults]
base = "origin/main"
scope = "added"
fail_on = "error"
max_findings = 200
diff_context = 0
[[rule]]
id = "secrets.api_key"
severity = "error"
message = "Potential API key detected - use environment variables instead."
patterns = [
"(?i)(api[_-]?key|apikey)\\s*[=:]\\s*['\"][^'\"]{8,}['\"]",
"(?i)(api[_-]?key|apikey)\\s*=\\s*['\"][^'\"]{8,}['\"]",
]
paths = ["**/*"]
exclude_paths = ["**/*.md", "**/docs/**", "**/*.example", "**/*.sample"]
ignore_comments = true
ignore_strings = false
[[rule]]
id = "secrets.secret_key"
severity = "error"
message = "Potential secret key detected - use environment variables instead."
patterns = [
"(?i)(secret[_-]?key|secretkey)\\s*[=:]\\s*['\"][^'\"]{8,}['\"]",
"(?i)(client[_-]?secret)\\s*[=:]\\s*['\"][^'\"]{8,}['\"]",
]
paths = ["**/*"]
exclude_paths = ["**/*.md", "**/docs/**", "**/*.example", "**/*.sample"]
ignore_comments = true
ignore_strings = false
[[rule]]
id = "secrets.password"
severity = "error"
message = "Potential hardcoded password detected - use environment variables instead."
patterns = [
"(?i)(password|passwd|pwd)\\s*[=:]\\s*['\"][^'\"]{4,}['\"]",
]
paths = ["**/*"]
exclude_paths = ["**/*.md", "**/docs/**", "**/*.example", "**/*.sample", "**/*.test.*", "**/tests/**"]
ignore_comments = true
ignore_strings = false
[[rule]]
id = "secrets.private_key"
severity = "error"
message = "Potential private key detected - never commit private keys."
patterns = [
"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----",
"-----BEGIN PGP PRIVATE KEY BLOCK-----",
]
paths = ["**/*"]
exclude_paths = ["**/*.md", "**/docs/**"]
ignore_comments = false
ignore_strings = false
[[rule]]
id = "secrets.aws_credentials"
severity = "error"
message = "Potential AWS credentials detected - use IAM roles or environment variables."
patterns = [
"(?i)aws[_-]?(access[_-]?key[_-]?id|secret[_-]?access[_-]?key)\\s*[=:]\\s*['\"][A-Za-z0-9/+=]{16,}['\"]",
"AKIA[0-9A-Z]{16}",
]
paths = ["**/*"]
exclude_paths = ["**/*.md", "**/docs/**", "**/*.example", "**/*.sample"]
ignore_comments = true
ignore_strings = false
[[rule]]
id = "secrets.jwt_token"
severity = "error"
message = "Potential JWT token detected - tokens should not be committed."
patterns = [
"eyJ[A-Za-z0-9_-]{10,}\\.eyJ[A-Za-z0-9_-]{10,}\\.[A-Za-z0-9_-]{10,}",
]
paths = ["**/*"]
exclude_paths = ["**/*.md", "**/docs/**", "**/tests/**"]
ignore_comments = true
ignore_strings = false
[[rule]]
id = "secrets.generic_token"
severity = "warn"
message = "Potential token or secret detected - review if this should be committed."
patterns = [
"(?i)(token|auth[_-]?token|bearer)\\s*[=:]\\s*['\"][^'\"]{20,}['\"]",
]
paths = ["**/*"]
exclude_paths = ["**/*.md", "**/docs/**", "**/*.example", "**/*.sample", "**/tests/**"]
ignore_comments = true
ignore_strings = false
"#
.to_string()
}
fn generate_js_console() -> String {
r#"# diffguard.toml - JavaScript/TypeScript Console preset
# Generated by: diffguard init --preset js-console
#
# Detects debugging statements in JavaScript and TypeScript code.
[defaults]
base = "origin/main"
scope = "added"
fail_on = "error"
max_findings = 200
diff_context = 0
[[rule]]
id = "js.no_console_log"
severity = "warn"
message = "Remove console.log() before merging - use a proper logger."
languages = ["javascript", "typescript"]
patterns = ["\\bconsole\\.log\\s*\\("]
paths = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.mjs", "**/*.cjs"]
exclude_paths = ["**/tests/**", "**/*.test.*", "**/*.spec.*", "**/node_modules/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "js.no_console_debug"
severity = "warn"
message = "Remove console.debug() before merging."
languages = ["javascript", "typescript"]
patterns = ["\\bconsole\\.(debug|info|trace)\\s*\\("]
paths = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.mjs", "**/*.cjs"]
exclude_paths = ["**/tests/**", "**/*.test.*", "**/*.spec.*", "**/node_modules/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "js.no_console_error"
severity = "info"
message = "Consider using a proper error logging service instead of console.error()."
languages = ["javascript", "typescript"]
patterns = ["\\bconsole\\.(error|warn)\\s*\\("]
paths = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.mjs", "**/*.cjs"]
exclude_paths = ["**/tests/**", "**/*.test.*", "**/*.spec.*", "**/node_modules/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "js.no_debugger"
severity = "error"
message = "Remove debugger statement before merging."
languages = ["javascript", "typescript"]
patterns = ["\\bdebugger\\s*;?"]
paths = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.mjs", "**/*.cjs"]
exclude_paths = ["**/node_modules/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "js.no_alert"
severity = "error"
message = "Remove alert()/confirm()/prompt() before merging."
languages = ["javascript", "typescript"]
patterns = ["\\b(alert|confirm|prompt)\\s*\\("]
paths = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.mjs", "**/*.cjs"]
exclude_paths = ["**/tests/**", "**/*.test.*", "**/*.spec.*", "**/node_modules/**"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "js.no_todo"
severity = "warn"
message = "Resolve TODO/FIXME comments before merging."
languages = ["javascript", "typescript"]
patterns = ["\\bTODO\\b", "\\bFIXME\\b", "\\bHACK\\b"]
paths = ["**/*.js", "**/*.ts", "**/*.jsx", "**/*.tsx", "**/*.mjs", "**/*.cjs"]
exclude_paths = ["**/node_modules/**"]
ignore_comments = false
ignore_strings = true
"#
.to_string()
}
fn generate_python_debug() -> String {
r#"# diffguard.toml - Python Debug preset
# Generated by: diffguard init --preset python-debug
#
# Detects debugging statements in Python code.
[defaults]
base = "origin/main"
scope = "added"
fail_on = "error"
max_findings = 200
diff_context = 0
[[rule]]
id = "python.no_print"
severity = "warn"
message = "Remove print() before merging - use proper logging."
languages = ["python"]
patterns = ["\\bprint\\s*\\("]
paths = ["**/*.py"]
exclude_paths = ["**/tests/**", "**/test_*.py", "**/*_test.py", "**/conftest.py"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "python.no_pdb"
severity = "error"
message = "Remove pdb import/usage before merging."
languages = ["python"]
patterns = ["\\bimport\\s+pdb\\b", "\\bfrom\\s+pdb\\s+import\\b", "\\bpdb\\.set_trace\\s*\\("]
paths = ["**/*.py"]
exclude_paths = []
ignore_comments = true
ignore_strings = true
[[rule]]
id = "python.no_breakpoint"
severity = "error"
message = "Remove breakpoint() before merging."
languages = ["python"]
patterns = ["\\bbreakpoint\\s*\\("]
paths = ["**/*.py"]
exclude_paths = []
ignore_comments = true
ignore_strings = true
[[rule]]
id = "python.no_ipdb"
severity = "error"
message = "Remove ipdb import/usage before merging."
languages = ["python"]
patterns = ["\\bimport\\s+ipdb\\b", "\\bfrom\\s+ipdb\\s+import\\b", "\\bipdb\\.set_trace\\s*\\("]
paths = ["**/*.py"]
exclude_paths = []
ignore_comments = true
ignore_strings = true
[[rule]]
id = "python.no_pudb"
severity = "error"
message = "Remove pudb import/usage before merging."
languages = ["python"]
patterns = ["\\bimport\\s+pudb\\b", "\\bfrom\\s+pudb\\s+import\\b"]
paths = ["**/*.py"]
exclude_paths = []
ignore_comments = true
ignore_strings = true
[[rule]]
id = "python.no_ic"
severity = "warn"
message = "Remove icecream (ic) debugging before merging."
languages = ["python"]
patterns = ["\\bfrom\\s+icecream\\s+import\\b", "\\bic\\s*\\("]
paths = ["**/*.py"]
exclude_paths = ["**/tests/**", "**/test_*.py", "**/*_test.py"]
ignore_comments = true
ignore_strings = true
[[rule]]
id = "python.no_todo"
severity = "warn"
message = "Resolve TODO/FIXME comments before merging."
languages = ["python"]
patterns = ["\\bTODO\\b", "\\bFIXME\\b", "\\bHACK\\b"]
paths = ["**/*.py"]
exclude_paths = []
ignore_comments = false
ignore_strings = true
"#
.to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use diffguard_types::ConfigFile;
#[test]
fn test_minimal_preset_generates_valid_toml() {
let content = Preset::Minimal.generate();
let result: Result<ConfigFile, _> = toml::from_str(&content);
let err = result.as_ref().err();
assert!(result.is_ok(), "Failed to parse minimal preset: {:?}", err);
}
#[test]
fn test_rust_quality_preset_generates_valid_toml() {
let content = Preset::RustQuality.generate();
let result: Result<ConfigFile, _> = toml::from_str(&content);
let err = result.as_ref().err();
assert!(
result.is_ok(),
"Failed to parse rust-quality preset: {:?}",
err
);
let config = result.unwrap();
assert!(
!config.rule.is_empty(),
"rust-quality preset should have rules"
);
assert!(config.rule.iter().any(|r| r.id == "rust.no_unwrap"));
assert!(config.rule.iter().any(|r| r.id == "rust.no_dbg"));
}
#[test]
fn test_secrets_preset_generates_valid_toml() {
let content = Preset::Secrets.generate();
let result: Result<ConfigFile, _> = toml::from_str(&content);
let err = result.as_ref().err();
assert!(result.is_ok(), "Failed to parse secrets preset: {:?}", err);
let config = result.unwrap();
assert!(!config.rule.is_empty(), "secrets preset should have rules");
assert!(config.rule.iter().any(|r| r.id == "secrets.api_key"));
}
#[test]
fn test_js_console_preset_generates_valid_toml() {
let content = Preset::JsConsole.generate();
let result: Result<ConfigFile, _> = toml::from_str(&content);
let err = result.as_ref().err();
assert!(
result.is_ok(),
"Failed to parse js-console preset: {:?}",
err
);
let config = result.unwrap();
assert!(
!config.rule.is_empty(),
"js-console preset should have rules"
);
assert!(config.rule.iter().any(|r| r.id == "js.no_console_log"));
assert!(config.rule.iter().any(|r| r.id == "js.no_debugger"));
}
#[test]
fn test_python_debug_preset_generates_valid_toml() {
let content = Preset::PythonDebug.generate();
let result: Result<ConfigFile, _> = toml::from_str(&content);
let err = result.as_ref().err();
assert!(
result.is_ok(),
"Failed to parse python-debug preset: {:?}",
err
);
let config = result.unwrap();
assert!(
!config.rule.is_empty(),
"python-debug preset should have rules"
);
assert!(config.rule.iter().any(|r| r.id == "python.no_print"));
assert!(config.rule.iter().any(|r| r.id == "python.no_pdb"));
}
#[test]
fn test_all_presets_have_defaults() {
for preset in [
Preset::Minimal,
Preset::RustQuality,
Preset::Secrets,
Preset::JsConsole,
Preset::PythonDebug,
] {
let content = preset.generate();
let config: ConfigFile =
toml::from_str(&content).expect("Preset should parse as valid TOML");
let msg = format!("{:?} preset should have some defaults configured", preset);
assert!(
config.defaults.base.is_some() || config.defaults.scope.is_some(),
"{msg}"
);
}
}
#[test]
fn test_preset_names() {
assert_eq!(Preset::Minimal.name(), "minimal");
assert_eq!(Preset::RustQuality.name(), "rust-quality");
assert_eq!(Preset::Secrets.name(), "secrets");
assert_eq!(Preset::JsConsole.name(), "js-console");
assert_eq!(Preset::PythonDebug.name(), "python-debug");
}
#[test]
fn test_preset_descriptions() {
for preset in [
Preset::Minimal,
Preset::RustQuality,
Preset::Secrets,
Preset::JsConsole,
Preset::PythonDebug,
] {
assert!(
!preset.description().is_empty(),
"{:?} should have a description",
preset
);
}
}
}