Skip to main content

diffguard_types/
lib.rs

1//! Data types (config + receipts) for diffguard.
2//!
3//! This crate is intentionally "dumb": pure DTOs with serde + schemars.
4
5use std::collections::HashMap;
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10// ── Schema Identifiers ─────────────────────────────────────────
11pub const CHECK_SCHEMA_V1: &str = "diffguard.check.v1";
12pub const SENSOR_REPORT_SCHEMA_V1: &str = "sensor.report.v1";
13
14// ── Frozen Vocabulary ──────────────────────────────────────────
15// Check IDs
16pub const CHECK_ID_PATTERN: &str = "diffguard.pattern";
17pub const CHECK_ID_INTERNAL: &str = "diffguard.internal";
18
19// Reason tokens (snake_case)
20pub const REASON_NO_DIFF_INPUT: &str = "no_diff_input";
21pub const REASON_MISSING_BASE: &str = "missing_base";
22pub const REASON_GIT_UNAVAILABLE: &str = "git_unavailable";
23pub const REASON_TOOL_ERROR: &str = "tool_error";
24/// Deprecated: no longer emitted in verdict.reasons (redundant with verdict.counts).
25/// Retained for backward-compatible vocabulary validation.
26pub const REASON_HAS_ERROR: &str = "has_error";
27/// Deprecated: no longer emitted in verdict.reasons (redundant with verdict.counts).
28/// Retained for backward-compatible vocabulary validation.
29pub const REASON_HAS_WARNING: &str = "has_warning";
30pub const REASON_TRUNCATED: &str = "truncated";
31
32// Tool error code (R1 survivability)
33pub const CODE_TOOL_RUNTIME_ERROR: &str = "tool.runtime_error";
34
35// Capability names
36pub const CAP_GIT: &str = "git";
37
38// Capability statuses
39pub const CAP_STATUS_AVAILABLE: &str = "available";
40pub const CAP_STATUS_UNAVAILABLE: &str = "unavailable";
41pub const CAP_STATUS_SKIPPED: &str = "skipped";
42
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
44#[serde(rename_all = "snake_case")]
45pub enum Severity {
46    Info,
47    Warn,
48    Error,
49}
50
51impl Severity {
52    pub fn as_str(self) -> &'static str {
53        match self {
54            Severity::Info => "info",
55            Severity::Warn => "warn",
56            Severity::Error => "error",
57        }
58    }
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
62#[serde(rename_all = "snake_case")]
63pub enum Scope {
64    Added,
65    Changed,
66    Modified,
67    Deleted,
68}
69
70impl Scope {
71    pub fn as_str(self) -> &'static str {
72        match self {
73            Scope::Added => "added",
74            Scope::Changed => "changed",
75            Scope::Modified => "modified",
76            Scope::Deleted => "deleted",
77        }
78    }
79}
80
81#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
82#[serde(rename_all = "snake_case")]
83pub enum FailOn {
84    Error,
85    Warn,
86    Never,
87}
88
89impl FailOn {
90    pub fn as_str(self) -> &'static str {
91        match self {
92            FailOn::Error => "error",
93            FailOn::Warn => "warn",
94            FailOn::Never => "never",
95        }
96    }
97}
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
100#[serde(rename_all = "snake_case")]
101pub enum MatchMode {
102    /// Emit a finding when at least one pattern matches (default behavior).
103    #[default]
104    Any,
105    /// Emit a finding when none of the patterns match within the scoped file.
106    Absent,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
110pub struct ToolMeta {
111    pub name: String,
112    pub version: String,
113}
114
115#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
116pub struct DiffMeta {
117    pub base: String,
118    pub head: String,
119    pub context_lines: u32,
120    pub scope: Scope,
121    pub files_scanned: u32,
122    pub lines_scanned: u32,
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
126pub struct Finding {
127    pub rule_id: String,
128    pub severity: Severity,
129    pub message: String,
130    pub path: String,
131    pub line: u32,
132    #[serde(default, skip_serializing_if = "Option::is_none")]
133    pub column: Option<u32>,
134    pub match_text: String,
135    pub snippet: String,
136}
137
138#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
139#[serde(rename_all = "snake_case")]
140pub enum VerdictStatus {
141    Pass,
142    Warn,
143    Fail,
144    /// For cockpit mode when inputs are missing or check cannot run.
145    Skip,
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
149pub struct VerdictCounts {
150    pub info: u32,
151    pub warn: u32,
152    pub error: u32,
153    /// Number of matches suppressed via inline directives.
154    #[serde(default, skip_serializing_if = "is_zero")]
155    pub suppressed: u32,
156}
157
158fn is_zero(n: &u32) -> bool {
159    *n == 0
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
163pub struct Verdict {
164    pub status: VerdictStatus,
165    pub counts: VerdictCounts,
166    pub reasons: Vec<String>,
167}
168
169#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
170pub struct CheckReceipt {
171    pub schema: String,
172    pub tool: ToolMeta,
173    pub diff: DiffMeta,
174    pub findings: Vec<Finding>,
175    pub verdict: Verdict,
176    #[serde(default, skip_serializing_if = "Option::is_none")]
177    pub timing: Option<TimingMetrics>,
178}
179
180/// Timing metrics for performance analysis.
181#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
182pub struct TimingMetrics {
183    pub total_ms: u64,
184    pub diff_parse_ms: u64,
185    pub rule_compile_ms: u64,
186    pub evaluation_ms: u64,
187}
188
189/// The on-disk configuration file.
190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
191pub struct ConfigFile {
192    /// Include other config files. Paths are relative to this config file's directory.
193    /// Rules are merged: later definitions override earlier ones by rule ID.
194    #[serde(default, skip_serializing_if = "Vec::is_empty")]
195    pub includes: Vec<String>,
196
197    #[serde(default)]
198    pub defaults: Defaults,
199
200    #[serde(default)]
201    pub rule: Vec<RuleConfig>,
202}
203
204impl ConfigFile {
205    pub fn built_in() -> Self {
206        Self {
207            includes: vec![],
208            defaults: Defaults::default(),
209            rule: vec![
210                // ============================================================
211                // Rust rules
212                // ============================================================
213                RuleConfig {
214                    id: "rust.no_unwrap".to_string(),
215                    severity: Severity::Error,
216                    message: "Avoid unwrap/expect in production code.".to_string(),
217                    languages: vec!["rust".to_string()],
218                    patterns: vec!["\\.unwrap\\(".to_string(), "\\.expect\\(".to_string()],
219                    paths: vec!["**/*.rs".to_string()],
220                    exclude_paths: vec![
221                        "**/tests/**".to_string(),
222                        "**/benches/**".to_string(),
223                        "**/examples/**".to_string(),
224                    ],
225                    ignore_comments: true,
226                    ignore_strings: true,
227                    match_mode: Default::default(),
228                    multiline: false,
229                    multiline_window: None,
230                    context_patterns: vec![],
231                    context_window: None,
232                    escalate_patterns: vec![],
233                    escalate_window: None,
234                    escalate_to: None,
235                    depends_on: vec![],
236                    help: Some(
237                        "Use the ? operator to propagate errors, or use expect() with a \
238                        meaningful message that explains the invariant. Consider using \
239                        anyhow or thiserror for structured error handling."
240                            .to_string(),
241                    ),
242                    url: Some(
243                        "https://doc.rust-lang.org/book/ch09-02-recoverable-errors-with-result.html"
244                            .to_string(),
245                    ),
246                    tags: vec!["safety".to_string()],
247                    test_cases: vec![],
248                },
249                RuleConfig {
250                    id: "rust.no_dbg".to_string(),
251                    severity: Severity::Warn,
252                    message: "Remove dbg!/println! before merging.".to_string(),
253                    languages: vec!["rust".to_string()],
254                    patterns: vec![
255                        "\\bdbg!\\(".to_string(),
256                        "\\bprintln!\\(".to_string(),
257                        "\\beprintln!\\(".to_string(),
258                    ],
259                    paths: vec!["**/*.rs".to_string()],
260                    exclude_paths: vec![
261                        "**/tests/**".to_string(),
262                        "**/benches/**".to_string(),
263                        "**/examples/**".to_string(),
264                    ],
265                    ignore_comments: true,
266                    ignore_strings: true,
267                    match_mode: Default::default(),
268                    multiline: false,
269                    multiline_window: None,
270                    context_patterns: vec![],
271                    context_window: None,
272                    escalate_patterns: vec![],
273                    escalate_window: None,
274                    escalate_to: None,
275                    depends_on: vec![],
276                    help: Some(
277                        "Remove debug output before merging. For logging, use the log or \
278                        tracing crate instead. If you need to keep the output, consider \
279                        using conditional compilation with #[cfg(debug_assertions)]."
280                            .to_string(),
281                    ),
282                    url: Some("https://doc.rust-lang.org/std/macro.dbg.html".to_string()),
283                    tags: vec!["debug".to_string()],
284                    test_cases: vec![],
285                },
286                RuleConfig {
287                    id: "rust.no_todo".to_string(),
288                    severity: Severity::Warn,
289                    message: "Resolve TODO/FIXME comments before merging.".to_string(),
290                    languages: vec!["rust".to_string()],
291                    patterns: vec![
292                        r"\bTODO\b".to_string(),
293                        r"\bFIXME\b".to_string(),
294                        r"\btodo!\s*\(".to_string(),
295                        r"\bunimplemented!\s*\(".to_string(),
296                    ],
297                    paths: vec!["**/*.rs".to_string()],
298                    exclude_paths: vec![],
299                    ignore_comments: false,
300                    ignore_strings: true,
301                    match_mode: Default::default(),
302                    multiline: false,
303                    multiline_window: None,
304                    context_patterns: vec![],
305                    context_window: None,
306                    escalate_patterns: vec![],
307                    escalate_window: None,
308                    escalate_to: None,
309                    depends_on: vec![],
310                    help: Some(
311                        "Address TODO/FIXME comments before merging, or create tracking \
312                        issues for planned work. The todo! and unimplemented! macros will \
313                        panic at runtime."
314                            .to_string(),
315                    ),
316                    url: None,
317                    tags: vec!["style".to_string()],
318                    test_cases: vec![],
319                },
320                // ============================================================
321                // Python rules
322                // ============================================================
323                RuleConfig {
324                    id: "python.no_print".to_string(),
325                    severity: Severity::Warn,
326                    message: "Remove print() before merging.".to_string(),
327                    languages: vec!["python".to_string()],
328                    patterns: vec![r"\bprint\s*\(".to_string()],
329                    paths: vec!["**/*.py".to_string()],
330                    exclude_paths: vec!["**/tests/**".to_string(), "**/test_*.py".to_string()],
331                    ignore_comments: true,
332                    ignore_strings: true,
333                    match_mode: Default::default(),
334                    multiline: false,
335                    multiline_window: None,
336                    context_patterns: vec![],
337                    context_window: None,
338                    escalate_patterns: vec![],
339                    escalate_window: None,
340                    escalate_to: None,
341                    depends_on: vec![],
342                    help: Some(
343                        "Use the logging module instead of print() for production code. \
344                        Configure logging levels appropriately (DEBUG, INFO, WARNING, ERROR)."
345                            .to_string(),
346                    ),
347                    url: Some("https://docs.python.org/3/library/logging.html".to_string()),
348                    tags: vec!["debug".to_string()],
349                    test_cases: vec![],
350                },
351                RuleConfig {
352                    id: "python.no_pdb".to_string(),
353                    severity: Severity::Error,
354                    message: "Remove debugger statements before merging.".to_string(),
355                    languages: vec!["python".to_string()],
356                    patterns: vec![
357                        r"\bimport\s+pdb\b".to_string(),
358                        r"\bpdb\.set_trace\s*\(".to_string(),
359                    ],
360                    paths: vec!["**/*.py".to_string()],
361                    exclude_paths: vec![],
362                    ignore_comments: true,
363                    ignore_strings: true,
364                    match_mode: Default::default(),
365                    multiline: false,
366                    multiline_window: None,
367                    context_patterns: vec![],
368                    context_window: None,
369                    escalate_patterns: vec![],
370                    escalate_window: None,
371                    escalate_to: None,
372                    depends_on: vec![],
373                    help: Some(
374                        "Remove pdb debugger statements before merging. These will cause \
375                        the application to pause and wait for interactive input in production."
376                            .to_string(),
377                    ),
378                    url: Some("https://docs.python.org/3/library/pdb.html".to_string()),
379                    tags: vec!["debug".to_string()],
380                    test_cases: vec![],
381                },
382                RuleConfig {
383                    id: "python.no_breakpoint".to_string(),
384                    severity: Severity::Error,
385                    message: "Remove breakpoint() calls before merging.".to_string(),
386                    languages: vec!["python".to_string()],
387                    patterns: vec![r"\bbreakpoint\s*\(".to_string()],
388                    paths: vec!["**/*.py".to_string()],
389                    exclude_paths: vec![],
390                    ignore_comments: true,
391                    ignore_strings: true,
392                    match_mode: Default::default(),
393                    multiline: false,
394                    multiline_window: None,
395                    context_patterns: vec![],
396                    context_window: None,
397                    escalate_patterns: vec![],
398                    escalate_window: None,
399                    escalate_to: None,
400                    depends_on: vec![],
401                    help: Some(
402                        "Remove breakpoint() calls before merging. The breakpoint() function \
403                        (Python 3.7+) invokes the debugger and will pause execution in production."
404                            .to_string(),
405                    ),
406                    url: Some("https://docs.python.org/3/library/functions.html#breakpoint".to_string()),
407                    tags: vec!["debug".to_string()],
408                    test_cases: vec![],
409                },
410                // ============================================================
411                // JavaScript/TypeScript rules
412                // ============================================================
413                RuleConfig {
414                    id: "js.no_console".to_string(),
415                    severity: Severity::Warn,
416                    message: "Remove console.log before merging.".to_string(),
417                    languages: vec!["javascript".to_string(), "typescript".to_string()],
418                    patterns: vec![r"\bconsole\.(log|debug|info)\s*\(".to_string()],
419                    paths: vec![
420                        "**/*.js".to_string(),
421                        "**/*.ts".to_string(),
422                        "**/*.jsx".to_string(),
423                        "**/*.tsx".to_string(),
424                    ],
425                    exclude_paths: vec![
426                        "**/tests/**".to_string(),
427                        "**/*.test.*".to_string(),
428                        "**/*.spec.*".to_string(),
429                    ],
430                    ignore_comments: true,
431                    ignore_strings: true,
432                    match_mode: Default::default(),
433                    multiline: false,
434                    multiline_window: None,
435                    context_patterns: vec![],
436                    context_window: None,
437                    escalate_patterns: vec![],
438                    escalate_window: None,
439                    escalate_to: None,
440                    depends_on: vec![],
441                    help: Some(
442                        "Use a proper logging library (e.g., winston, pino, bunyan) instead \
443                        of console.log. For client-side code, consider using a logger that \
444                        can be disabled in production builds."
445                            .to_string(),
446                    ),
447                    url: Some(
448                        "https://developer.mozilla.org/en-US/docs/Web/API/console".to_string(),
449                    ),
450                    tags: vec!["debug".to_string()],
451                    test_cases: vec![],
452                },
453                RuleConfig {
454                    id: "js.no_debugger".to_string(),
455                    severity: Severity::Error,
456                    message: "Remove debugger statements before merging.".to_string(),
457                    languages: vec!["javascript".to_string(), "typescript".to_string()],
458                    patterns: vec![r"\bdebugger\b".to_string()],
459                    paths: vec![
460                        "**/*.js".to_string(),
461                        "**/*.ts".to_string(),
462                        "**/*.jsx".to_string(),
463                        "**/*.tsx".to_string(),
464                    ],
465                    exclude_paths: vec![],
466                    ignore_comments: true,
467                    ignore_strings: true,
468                    match_mode: Default::default(),
469                    multiline: false,
470                    multiline_window: None,
471                    context_patterns: vec![],
472                    context_window: None,
473                    escalate_patterns: vec![],
474                    escalate_window: None,
475                    escalate_to: None,
476                    depends_on: vec![],
477                    help: Some(
478                        "Remove debugger statements before merging. These will pause \
479                        execution in the browser's developer tools, which is not intended \
480                        for production code."
481                            .to_string(),
482                    ),
483                    url: Some(
484                        "https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/debugger"
485                            .to_string(),
486                    ),
487                    tags: vec!["debug".to_string()],
488                    test_cases: vec![],
489                },
490                // ============================================================
491                // Ruby rules
492                // ============================================================
493                RuleConfig {
494                    id: "ruby.no_binding_pry".to_string(),
495                    severity: Severity::Error,
496                    message: "Remove binding.pry before merging.".to_string(),
497                    languages: vec!["ruby".to_string()],
498                    patterns: vec![r"\bbinding\.pry\b".to_string()],
499                    paths: vec!["**/*.rb".to_string(), "**/*.rake".to_string()],
500                    exclude_paths: vec!["**/test/**".to_string(), "**/spec/**".to_string()],
501                    ignore_comments: true,
502                    ignore_strings: true,
503                    match_mode: Default::default(),
504                    multiline: false,
505                    multiline_window: None,
506                    context_patterns: vec![],
507                    context_window: None,
508                    escalate_patterns: vec![],
509                    escalate_window: None,
510                    escalate_to: None,
511                    depends_on: vec![],
512                    help: Some(
513                        "Remove binding.pry debugger statements before merging. These will \
514                        pause execution and open an interactive REPL in production."
515                            .to_string(),
516                    ),
517                    url: Some("https://github.com/pry/pry".to_string()),
518                    tags: vec!["debug".to_string()],
519                    test_cases: vec![],
520                },
521                RuleConfig {
522                    id: "ruby.no_byebug".to_string(),
523                    severity: Severity::Error,
524                    message: "Remove byebug statements before merging.".to_string(),
525                    languages: vec!["ruby".to_string()],
526                    patterns: vec![r"\bbyebug\b".to_string()],
527                    paths: vec!["**/*.rb".to_string(), "**/*.rake".to_string()],
528                    exclude_paths: vec!["**/test/**".to_string(), "**/spec/**".to_string()],
529                    ignore_comments: true,
530                    ignore_strings: true,
531                    match_mode: Default::default(),
532                    multiline: false,
533                    multiline_window: None,
534                    context_patterns: vec![],
535                    context_window: None,
536                    escalate_patterns: vec![],
537                    escalate_window: None,
538                    escalate_to: None,
539                    depends_on: vec![],
540                    help: Some(
541                        "Remove byebug debugger statements before merging. These will \
542                        pause execution and open an interactive debugger in production."
543                            .to_string(),
544                    ),
545                    url: Some("https://github.com/deivid-rodriguez/byebug".to_string()),
546                    tags: vec!["debug".to_string()],
547                    test_cases: vec![],
548                },
549                // ============================================================
550                // Java rules
551                // ============================================================
552                RuleConfig {
553                    id: "java.no_sout".to_string(),
554                    severity: Severity::Warn,
555                    message: "Remove System.out.println before merging.".to_string(),
556                    languages: vec!["java".to_string()],
557                    patterns: vec![r"\bSystem\.out\.println\s*\(".to_string()],
558                    paths: vec!["**/*.java".to_string()],
559                    exclude_paths: vec!["**/test/**".to_string(), "**/tests/**".to_string()],
560                    ignore_comments: true,
561                    ignore_strings: true,
562                    match_mode: Default::default(),
563                    multiline: false,
564                    multiline_window: None,
565                    context_patterns: vec![],
566                    context_window: None,
567                    escalate_patterns: vec![],
568                    escalate_window: None,
569                    escalate_to: None,
570                    depends_on: vec![],
571                    help: Some(
572                        "Use a logging framework (e.g., SLF4J, Log4j, java.util.logging) instead \
573                        of System.out.println for production code. Logging frameworks provide \
574                        log levels, formatting, and configurable output destinations."
575                            .to_string(),
576                    ),
577                    url: Some("https://www.slf4j.org/".to_string()),
578                    tags: vec!["debug".to_string()],
579                    test_cases: vec![],
580                },
581                // ============================================================
582                // C# rules
583                // ============================================================
584                RuleConfig {
585                    id: "csharp.no_console".to_string(),
586                    severity: Severity::Warn,
587                    message: "Remove Console.WriteLine before merging.".to_string(),
588                    languages: vec!["csharp".to_string()],
589                    patterns: vec![r"\bConsole\.WriteLine\s*\(".to_string()],
590                    paths: vec!["**/*.cs".to_string()],
591                    exclude_paths: vec!["**/Tests/**".to_string(), "**/*.Tests/**".to_string()],
592                    ignore_comments: true,
593                    ignore_strings: true,
594                    match_mode: Default::default(),
595                    multiline: false,
596                    multiline_window: None,
597                    context_patterns: vec![],
598                    context_window: None,
599                    escalate_patterns: vec![],
600                    escalate_window: None,
601                    escalate_to: None,
602                    depends_on: vec![],
603                    help: Some(
604                        "Use a logging framework (e.g., Serilog, NLog, Microsoft.Extensions.Logging) \
605                        instead of Console.WriteLine for production code. Logging frameworks provide \
606                        structured logging, log levels, and configurable sinks."
607                            .to_string(),
608                    ),
609                    url: Some("https://learn.microsoft.com/en-us/dotnet/core/extensions/logging".to_string()),
610                    tags: vec!["debug".to_string()],
611                    test_cases: vec![],
612                },
613                // ============================================================
614                // Go rules
615                // ============================================================
616                RuleConfig {
617                    id: "go.no_fmt_print".to_string(),
618                    severity: Severity::Warn,
619                    message: "Remove fmt.Print* before merging.".to_string(),
620                    languages: vec!["go".to_string()],
621                    patterns: vec![r"\bfmt\.(Print|Println|Printf)\s*\(".to_string()],
622                    paths: vec!["**/*.go".to_string()],
623                    exclude_paths: vec!["**/*_test.go".to_string()],
624                    ignore_comments: true,
625                    ignore_strings: true,
626                    match_mode: Default::default(),
627                    multiline: false,
628                    multiline_window: None,
629                    context_patterns: vec![],
630                    context_window: None,
631                    escalate_patterns: vec![],
632                    escalate_window: None,
633                    escalate_to: None,
634                    depends_on: vec![],
635                    help: Some(
636                        "Use the log package or a structured logging library (e.g., zap, \
637                        zerolog, logrus) instead of fmt.Print* for production code."
638                            .to_string(),
639                    ),
640                    url: Some("https://pkg.go.dev/log".to_string()),
641                    tags: vec!["debug".to_string()],
642                    test_cases: vec![],
643                },
644                RuleConfig {
645                    id: "go.no_panic".to_string(),
646                    severity: Severity::Warn,
647                    message: "Avoid panic() in production code.".to_string(),
648                    languages: vec!["go".to_string()],
649                    patterns: vec![r"\bpanic\s*\(".to_string()],
650                    paths: vec!["**/*.go".to_string()],
651                    exclude_paths: vec!["**/*_test.go".to_string()],
652                    ignore_comments: true,
653                    ignore_strings: true,
654                    match_mode: Default::default(),
655                    multiline: false,
656                    multiline_window: None,
657                    context_patterns: vec![],
658                    context_window: None,
659                    escalate_patterns: vec![],
660                    escalate_window: None,
661                    escalate_to: None,
662                    depends_on: vec![],
663                    help: Some(
664                        "Return errors instead of panicking. Use panic only for truly \
665                        unrecoverable situations. Consider using errors.New() or fmt.Errorf() \
666                        to create descriptive error values that callers can handle gracefully."
667                            .to_string(),
668                    ),
669                    url: Some("https://go.dev/doc/effective_go#errors".to_string()),
670                    tags: vec!["safety".to_string()],
671                    test_cases: vec![],
672                },
673                // ============================================================
674                // Kotlin rules
675                // ============================================================
676                RuleConfig {
677                    id: "kotlin.no_println".to_string(),
678                    severity: Severity::Warn,
679                    message: "Remove println() before merging.".to_string(),
680                    languages: vec!["kotlin".to_string()],
681                    patterns: vec![r"\bprintln\s*\(".to_string()],
682                    paths: vec!["**/*.kt".to_string(), "**/*.kts".to_string()],
683                    exclude_paths: vec!["**/test/**".to_string(), "**/tests/**".to_string()],
684                    ignore_comments: true,
685                    ignore_strings: true,
686                    match_mode: Default::default(),
687                    multiline: false,
688                    multiline_window: None,
689                    context_patterns: vec![],
690                    context_window: None,
691                    escalate_patterns: vec![],
692                    escalate_window: None,
693                    escalate_to: None,
694                    depends_on: vec![],
695                    help: Some(
696                        "Use a logging framework (e.g., SLF4J, Logback, kotlin-logging) instead \
697                        of println() for production code. Logging frameworks provide log levels, \
698                        structured output, and configurable destinations."
699                            .to_string(),
700                    ),
701                    url: Some("https://www.slf4j.org/".to_string()),
702                    tags: vec!["debug".to_string()],
703                    test_cases: vec![],
704                },
705                // ============================================================
706                // Secret/Credential detection rules
707                // ============================================================
708                RuleConfig {
709                    id: "secrets.aws_access_key".to_string(),
710                    severity: Severity::Error,
711                    message: "Potential AWS Access Key ID detected.".to_string(),
712                    languages: vec![],
713                    patterns: vec![r"AKIA[0-9A-Z]{16}".to_string()],
714                    paths: vec![],
715                    exclude_paths: vec![],
716                    ignore_comments: false,
717                    ignore_strings: false,
718                    match_mode: Default::default(),
719                    multiline: false,
720                    multiline_window: None,
721                    context_patterns: vec![],
722                    context_window: None,
723                    escalate_patterns: vec![],
724                    escalate_window: None,
725                    escalate_to: None,
726                    depends_on: vec![],
727                    help: Some(
728                        "AWS Access Key IDs should never be committed to source control. \
729                        Use environment variables, AWS IAM roles, or a secrets manager \
730                        (e.g., AWS Secrets Manager, HashiCorp Vault) to manage credentials."
731                            .to_string(),
732                    ),
733                    url: Some("https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html".to_string()),
734                    tags: vec!["security".to_string()],
735                    test_cases: vec![],
736                },
737                RuleConfig {
738                    id: "secrets.github_token".to_string(),
739                    severity: Severity::Error,
740                    message: "Potential GitHub token detected.".to_string(),
741                    languages: vec![],
742                    patterns: vec![r"(ghp_|gho_|ghu_|ghs_|ghr_)[a-zA-Z0-9]{36}".to_string()],
743                    paths: vec![],
744                    exclude_paths: vec![],
745                    ignore_comments: false,
746                    ignore_strings: false,
747                    match_mode: Default::default(),
748                    multiline: false,
749                    multiline_window: None,
750                    context_patterns: vec![],
751                    context_window: None,
752                    escalate_patterns: vec![],
753                    escalate_window: None,
754                    escalate_to: None,
755                    depends_on: vec![],
756                    help: Some(
757                        "GitHub tokens should never be committed to source control. \
758                        Use environment variables or GitHub Actions secrets to manage tokens. \
759                        If a token was accidentally committed, revoke it immediately."
760                            .to_string(),
761                    ),
762                    url: Some("https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens".to_string()),
763                    tags: vec!["security".to_string()],
764                    test_cases: vec![],
765                },
766                RuleConfig {
767                    id: "secrets.generic_api_key".to_string(),
768                    severity: Severity::Error,
769                    message: "Potential API key detected.".to_string(),
770                    languages: vec![],
771                    patterns: vec![r#"(?i)(api[_-]?key|apikey)\s*[:=]\s*["'][^"']{16,}["']"#.to_string()],
772                    paths: vec![],
773                    exclude_paths: vec![
774                        "**/*.md".to_string(),
775                        "**/README*".to_string(),
776                        "**/CHANGELOG*".to_string(),
777                    ],
778                    ignore_comments: false,
779                    ignore_strings: false,
780                    match_mode: Default::default(),
781                    multiline: false,
782                    multiline_window: None,
783                    context_patterns: vec![],
784                    context_window: None,
785                    escalate_patterns: vec![],
786                    escalate_window: None,
787                    escalate_to: None,
788                    depends_on: vec![],
789                    help: Some(
790                        "API keys should not be hardcoded in source files. \
791                        Use environment variables or a secrets manager to inject credentials \
792                        at runtime. Consider using .env files (excluded from version control) \
793                        for local development."
794                            .to_string(),
795                    ),
796                    url: None,
797                    tags: vec!["security".to_string()],
798                    test_cases: vec![],
799                },
800                RuleConfig {
801                    id: "secrets.private_key".to_string(),
802                    severity: Severity::Error,
803                    message: "Private key detected.".to_string(),
804                    languages: vec![],
805                    patterns: vec![r"-----BEGIN (RSA |EC |DSA |OPENSSH )?PRIVATE KEY-----".to_string()],
806                    paths: vec![],
807                    exclude_paths: vec![
808                        "**/*.md".to_string(),
809                        "**/README*".to_string(),
810                    ],
811                    ignore_comments: false,
812                    ignore_strings: false,
813                    match_mode: Default::default(),
814                    multiline: false,
815                    multiline_window: None,
816                    context_patterns: vec![],
817                    context_window: None,
818                    escalate_patterns: vec![],
819                    escalate_window: None,
820                    escalate_to: None,
821                    depends_on: vec![],
822                    help: Some(
823                        "Private keys must never be committed to source control. \
824                        Store private keys securely using a secrets manager, encrypted storage, \
825                        or environment variables. If a private key was accidentally committed, \
826                        consider it compromised and generate a new key pair."
827                            .to_string(),
828                    ),
829                    url: None,
830                    tags: vec!["security".to_string()],
831                    test_cases: vec![],
832                },
833                RuleConfig {
834                    id: "secrets.slack_token".to_string(),
835                    severity: Severity::Error,
836                    message: "Potential Slack token detected.".to_string(),
837                    languages: vec![],
838                    patterns: vec![r"xox[baprs]-[0-9a-zA-Z]{10,}".to_string()],
839                    paths: vec![],
840                    exclude_paths: vec![
841                        "**/*.md".to_string(),
842                        "**/README*".to_string(),
843                    ],
844                    ignore_comments: false,
845                    ignore_strings: false,
846                    match_mode: Default::default(),
847                    multiline: false,
848                    multiline_window: None,
849                    context_patterns: vec![],
850                    context_window: None,
851                    escalate_patterns: vec![],
852                    escalate_window: None,
853                    escalate_to: None,
854                    depends_on: vec![],
855                    help: Some(
856                        "Slack tokens should never be committed to source control. \
857                        Use environment variables or a secrets manager. If a token was \
858                        accidentally committed, revoke it in your Slack workspace settings."
859                            .to_string(),
860                    ),
861                    url: Some("https://api.slack.com/authentication/token-types".to_string()),
862                    tags: vec!["security".to_string()],
863                    test_cases: vec![],
864                },
865                RuleConfig {
866                    id: "secrets.stripe_key".to_string(),
867                    severity: Severity::Error,
868                    message: "Potential Stripe API key detected.".to_string(),
869                    languages: vec![],
870                    patterns: vec![r"(sk|rk)_live_[0-9a-zA-Z]{24,}".to_string()],
871                    paths: vec![],
872                    exclude_paths: vec![
873                        "**/*.md".to_string(),
874                        "**/README*".to_string(),
875                    ],
876                    ignore_comments: false,
877                    ignore_strings: false,
878                    match_mode: Default::default(),
879                    multiline: false,
880                    multiline_window: None,
881                    context_patterns: vec![],
882                    context_window: None,
883                    escalate_patterns: vec![],
884                    escalate_window: None,
885                    escalate_to: None,
886                    depends_on: vec![],
887                    help: Some(
888                        "Stripe live API keys should never be committed to source control. \
889                        Use environment variables or a secrets manager. If a key was \
890                        accidentally committed, rotate it immediately in your Stripe dashboard."
891                            .to_string(),
892                    ),
893                    url: Some("https://stripe.com/docs/keys".to_string()),
894                    tags: vec!["security".to_string()],
895                    test_cases: vec![],
896                },
897                RuleConfig {
898                    id: "secrets.google_api_key".to_string(),
899                    severity: Severity::Error,
900                    message: "Potential Google API key detected.".to_string(),
901                    languages: vec![],
902                    patterns: vec![r"AIza[0-9A-Za-z\-_]{35}".to_string()],
903                    paths: vec![],
904                    exclude_paths: vec![
905                        "**/*.md".to_string(),
906                        "**/README*".to_string(),
907                    ],
908                    ignore_comments: false,
909                    ignore_strings: false,
910                    match_mode: Default::default(),
911                    multiline: false,
912                    multiline_window: None,
913                    context_patterns: vec![],
914                    context_window: None,
915                    escalate_patterns: vec![],
916                    escalate_window: None,
917                    escalate_to: None,
918                    depends_on: vec![],
919                    help: Some(
920                        "Google API keys should not be committed to source control. \
921                        Use environment variables or Google Cloud Secret Manager. \
922                        Restrict the key's allowed APIs and referrers in the Google Cloud Console."
923                            .to_string(),
924                    ),
925                    url: Some("https://cloud.google.com/docs/authentication/api-keys".to_string()),
926                    tags: vec!["security".to_string()],
927                    test_cases: vec![],
928                },
929                RuleConfig {
930                    id: "secrets.twilio_key".to_string(),
931                    severity: Severity::Error,
932                    message: "Potential Twilio API key detected.".to_string(),
933                    languages: vec![],
934                    patterns: vec![r"SK[0-9a-fA-F]{32}".to_string()],
935                    paths: vec![],
936                    exclude_paths: vec![
937                        "**/*.md".to_string(),
938                        "**/README*".to_string(),
939                    ],
940                    ignore_comments: false,
941                    ignore_strings: false,
942                    match_mode: Default::default(),
943                    multiline: false,
944                    multiline_window: None,
945                    context_patterns: vec![],
946                    context_window: None,
947                    escalate_patterns: vec![],
948                    escalate_window: None,
949                    escalate_to: None,
950                    depends_on: vec![],
951                    help: Some(
952                        "Twilio API keys should not be committed to source control. \
953                        Use environment variables or a secrets manager. If compromised, \
954                        revoke the key in your Twilio console."
955                            .to_string(),
956                    ),
957                    url: Some("https://www.twilio.com/docs/iam/api-keys".to_string()),
958                    tags: vec!["security".to_string()],
959                    test_cases: vec![],
960                },
961                RuleConfig {
962                    id: "secrets.npm_token".to_string(),
963                    severity: Severity::Error,
964                    message: "Potential npm token detected.".to_string(),
965                    languages: vec![],
966                    patterns: vec![r"npm_[0-9a-zA-Z]{36}".to_string()],
967                    paths: vec![],
968                    exclude_paths: vec![
969                        "**/*.md".to_string(),
970                        "**/README*".to_string(),
971                    ],
972                    ignore_comments: false,
973                    ignore_strings: false,
974                    match_mode: Default::default(),
975                    multiline: false,
976                    multiline_window: None,
977                    context_patterns: vec![],
978                    context_window: None,
979                    escalate_patterns: vec![],
980                    escalate_window: None,
981                    escalate_to: None,
982                    depends_on: vec![],
983                    help: Some(
984                        "npm tokens should not be committed to source control. \
985                        Use environment variables or npm's built-in .npmrc configuration. \
986                        If compromised, revoke the token on npmjs.com."
987                            .to_string(),
988                    ),
989                    url: Some("https://docs.npmjs.com/about-access-tokens".to_string()),
990                    tags: vec!["security".to_string()],
991                    test_cases: vec![],
992                },
993                RuleConfig {
994                    id: "secrets.pypi_token".to_string(),
995                    severity: Severity::Error,
996                    message: "Potential PyPI token detected.".to_string(),
997                    languages: vec![],
998                    patterns: vec![r"pypi-[0-9a-zA-Z_-]{50,}".to_string()],
999                    paths: vec![],
1000                    exclude_paths: vec![
1001                        "**/*.md".to_string(),
1002                        "**/README*".to_string(),
1003                    ],
1004                    ignore_comments: false,
1005                    ignore_strings: false,
1006                    match_mode: Default::default(),
1007                    multiline: false,
1008                    multiline_window: None,
1009                    context_patterns: vec![],
1010                    context_window: None,
1011                    escalate_patterns: vec![],
1012                    escalate_window: None,
1013                    escalate_to: None,
1014                    depends_on: vec![],
1015                    help: Some(
1016                        "PyPI tokens should not be committed to source control. \
1017                        Use environment variables or a secrets manager. \
1018                        If compromised, revoke the token on pypi.org."
1019                            .to_string(),
1020                    ),
1021                    url: Some("https://pypi.org/help/#apitoken".to_string()),
1022                    tags: vec!["security".to_string()],
1023                    test_cases: vec![],
1024                },
1025                RuleConfig {
1026                    id: "secrets.password_assignment".to_string(),
1027                    severity: Severity::Warn,
1028                    message: "Potential hardcoded password detected.".to_string(),
1029                    languages: vec![],
1030                    patterns: vec![r#"(?i)(password|passwd|pwd)\s*[:=]\s*["'][^"']{8,}["']"#.to_string()],
1031                    paths: vec![],
1032                    exclude_paths: vec![
1033                        "**/*.md".to_string(),
1034                        "**/README*".to_string(),
1035                        "**/*.example*".to_string(),
1036                        "**/*test*".to_string(),
1037                    ],
1038                    ignore_comments: true,
1039                    ignore_strings: false,
1040                    match_mode: Default::default(),
1041                    multiline: false,
1042                    multiline_window: None,
1043                    context_patterns: vec![],
1044                    context_window: None,
1045                    escalate_patterns: vec![],
1046                    escalate_window: None,
1047                    escalate_to: None,
1048                    depends_on: vec![],
1049                    help: Some(
1050                        "Passwords should not be hardcoded in source files. \
1051                        Use environment variables, a secrets manager, or secure configuration \
1052                        files excluded from version control."
1053                            .to_string(),
1054                    ),
1055                    url: None,
1056                    tags: vec!["security".to_string()],
1057                    test_cases: vec![],
1058                },
1059                RuleConfig {
1060                    id: "secrets.jwt_token".to_string(),
1061                    severity: Severity::Warn,
1062                    message: "Potential JWT token detected.".to_string(),
1063                    languages: vec![],
1064                    patterns: vec![r"eyJ[a-zA-Z0-9_-]{10,}\.eyJ[a-zA-Z0-9_-]{10,}\.[a-zA-Z0-9_-]{10,}".to_string()],
1065                    paths: vec![],
1066                    exclude_paths: vec![
1067                        "**/*.md".to_string(),
1068                        "**/README*".to_string(),
1069                        "**/*test*".to_string(),
1070                    ],
1071                    ignore_comments: true,
1072                    ignore_strings: false,
1073                    match_mode: Default::default(),
1074                    multiline: false,
1075                    multiline_window: None,
1076                    context_patterns: vec![],
1077                    context_window: None,
1078                    escalate_patterns: vec![],
1079                    escalate_window: None,
1080                    escalate_to: None,
1081                    depends_on: vec![],
1082                    help: Some(
1083                        "JWT tokens should not be hardcoded in source files. \
1084                        They may contain sensitive claims or grant unauthorized access. \
1085                        Generate tokens dynamically at runtime."
1086                            .to_string(),
1087                    ),
1088                    url: Some("https://jwt.io/introduction".to_string()),
1089                    tags: vec!["security".to_string()],
1090                    test_cases: vec![],
1091                },
1092                // ============================================================
1093                // Security-focused rules
1094                // ============================================================
1095                RuleConfig {
1096                    id: "security.hardcoded_ipv4".to_string(),
1097                    severity: Severity::Warn,
1098                    message: "Hardcoded IPv4 address detected.".to_string(),
1099                    languages: vec![],
1100                    patterns: vec![r"\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\b".to_string()],
1101                    paths: vec![],
1102                    exclude_paths: vec![
1103                        "**/*.md".to_string(),
1104                        "**/README*".to_string(),
1105                        "**/*test*".to_string(),
1106                        "**/Dockerfile*".to_string(),
1107                    ],
1108                    ignore_comments: true,
1109                    ignore_strings: false,
1110                    match_mode: Default::default(),
1111                    multiline: false,
1112                    multiline_window: None,
1113                    context_patterns: vec![],
1114                    context_window: None,
1115                    escalate_patterns: vec![],
1116                    escalate_window: None,
1117                    escalate_to: None,
1118                    depends_on: vec![],
1119                    help: Some(
1120                        "Hardcoded IP addresses make code inflexible and can expose internal \
1121                        network topology. Use configuration files, environment variables, \
1122                        or DNS names instead."
1123                            .to_string(),
1124                    ),
1125                    url: None,
1126                    tags: vec!["security".to_string()],
1127                    test_cases: vec![],
1128                },
1129                RuleConfig {
1130                    id: "security.http_url".to_string(),
1131                    severity: Severity::Warn,
1132                    message: "Non-HTTPS URL detected.".to_string(),
1133                    languages: vec![],
1134                    patterns: vec![r#"["']http://[^"']+["']"#.to_string()],
1135                    paths: vec![],
1136                    exclude_paths: vec![
1137                        "**/*.md".to_string(),
1138                        "**/README*".to_string(),
1139                        "**/*test*".to_string(),
1140                        "**/localhost*".to_string(),
1141                    ],
1142                    ignore_comments: true,
1143                    ignore_strings: false,
1144                    match_mode: Default::default(),
1145                    multiline: false,
1146                    multiline_window: None,
1147                    context_patterns: vec![],
1148                    context_window: None,
1149                    escalate_patterns: vec![],
1150                    escalate_window: None,
1151                    escalate_to: None,
1152                    depends_on: vec![],
1153                    help: Some(
1154                        "Use HTTPS instead of HTTP for secure communication. \
1155                        HTTP transmits data in plaintext, making it vulnerable to \
1156                        man-in-the-middle attacks."
1157                            .to_string(),
1158                    ),
1159                    url: None,
1160                    tags: vec!["security".to_string()],
1161                    test_cases: vec![],
1162                },
1163                RuleConfig {
1164                    id: "js.no_eval".to_string(),
1165                    severity: Severity::Error,
1166                    message: "Avoid eval() - potential code injection risk.".to_string(),
1167                    languages: vec!["javascript".to_string(), "typescript".to_string()],
1168                    patterns: vec![r"\beval\s*\(".to_string(), r"\bFunction\s*\(".to_string()],
1169                    paths: vec![
1170                        "**/*.js".to_string(),
1171                        "**/*.ts".to_string(),
1172                        "**/*.jsx".to_string(),
1173                        "**/*.tsx".to_string(),
1174                    ],
1175                    exclude_paths: vec!["**/*test*".to_string()],
1176                    ignore_comments: true,
1177                    ignore_strings: true,
1178                    match_mode: Default::default(),
1179                    multiline: false,
1180                    multiline_window: None,
1181                    context_patterns: vec![],
1182                    context_window: None,
1183                    escalate_patterns: vec![],
1184                    escalate_window: None,
1185                    escalate_to: None,
1186                    depends_on: vec![],
1187                    help: Some(
1188                        "eval() and the Function constructor execute arbitrary code, \
1189                        creating severe security risks. Use safer alternatives like \
1190                        JSON.parse() for data or template literals for strings."
1191                            .to_string(),
1192                    ),
1193                    url: Some("https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/eval#never_use_direct_eval!".to_string()),
1194                    tags: vec!["security".to_string()],
1195                    test_cases: vec![],
1196                },
1197                RuleConfig {
1198                    id: "python.no_eval".to_string(),
1199                    severity: Severity::Error,
1200                    message: "Avoid eval()/exec() - potential code injection risk.".to_string(),
1201                    languages: vec!["python".to_string()],
1202                    patterns: vec![r"\beval\s*\(".to_string(), r"\bexec\s*\(".to_string()],
1203                    paths: vec!["**/*.py".to_string()],
1204                    exclude_paths: vec!["**/*test*".to_string()],
1205                    ignore_comments: true,
1206                    ignore_strings: true,
1207                    match_mode: Default::default(),
1208                    multiline: false,
1209                    multiline_window: None,
1210                    context_patterns: vec![],
1211                    context_window: None,
1212                    escalate_patterns: vec![],
1213                    escalate_window: None,
1214                    escalate_to: None,
1215                    depends_on: vec![],
1216                    help: Some(
1217                        "eval() and exec() execute arbitrary Python code, creating severe \
1218                        security risks. Use ast.literal_eval() for safe literal evaluation \
1219                        or find alternative approaches."
1220                            .to_string(),
1221                    ),
1222                    url: Some("https://docs.python.org/3/library/functions.html#eval".to_string()),
1223                    tags: vec!["security".to_string()],
1224                    test_cases: vec![],
1225                },
1226                RuleConfig {
1227                    id: "ruby.no_eval".to_string(),
1228                    severity: Severity::Error,
1229                    message: "Avoid eval/instance_eval - potential code injection risk.".to_string(),
1230                    languages: vec!["ruby".to_string()],
1231                    patterns: vec![r"\beval\s*[\(\s]".to_string(), r"\binstance_eval\s*[\(\s{]".to_string()],
1232                    paths: vec!["**/*.rb".to_string(), "**/*.rake".to_string()],
1233                    exclude_paths: vec!["**/*test*".to_string(), "**/spec/**".to_string()],
1234                    ignore_comments: true,
1235                    ignore_strings: true,
1236                    match_mode: Default::default(),
1237                    multiline: false,
1238                    multiline_window: None,
1239                    context_patterns: vec![],
1240                    context_window: None,
1241                    escalate_patterns: vec![],
1242                    escalate_window: None,
1243                    escalate_to: None,
1244                    depends_on: vec![],
1245                    help: Some(
1246                        "eval and instance_eval execute arbitrary Ruby code, creating severe \
1247                        security risks. Use safer metaprogramming techniques like \
1248                        define_method or public_send."
1249                            .to_string(),
1250                    ),
1251                    url: None,
1252                    tags: vec!["security".to_string()],
1253                    test_cases: vec![],
1254                },
1255                RuleConfig {
1256                    id: "php.no_eval".to_string(),
1257                    severity: Severity::Error,
1258                    message: "Avoid eval()/create_function() - potential code injection risk.".to_string(),
1259                    languages: vec!["php".to_string()],
1260                    patterns: vec![r"\beval\s*\(".to_string(), r"\bcreate_function\s*\(".to_string()],
1261                    paths: vec!["**/*.php".to_string()],
1262                    exclude_paths: vec!["**/*test*".to_string()],
1263                    ignore_comments: true,
1264                    ignore_strings: true,
1265                    match_mode: Default::default(),
1266                    multiline: false,
1267                    multiline_window: None,
1268                    context_patterns: vec![],
1269                    context_window: None,
1270                    escalate_patterns: vec![],
1271                    escalate_window: None,
1272                    escalate_to: None,
1273                    depends_on: vec![],
1274                    help: Some(
1275                        "eval() and create_function() execute arbitrary PHP code, creating \
1276                        severe security risks. Use anonymous functions or other safe alternatives."
1277                            .to_string(),
1278                    ),
1279                    url: Some("https://www.php.net/manual/en/function.eval.php".to_string()),
1280                    tags: vec!["security".to_string()],
1281                    test_cases: vec![],
1282                },
1283                RuleConfig {
1284                    id: "shell.no_eval".to_string(),
1285                    severity: Severity::Error,
1286                    message: "Avoid eval in shell scripts - potential code injection risk.".to_string(),
1287                    languages: vec!["shell".to_string()],
1288                    patterns: vec![r"\beval\s+".to_string()],
1289                    paths: vec![
1290                        "**/*.sh".to_string(),
1291                        "**/*.bash".to_string(),
1292                        "**/*.zsh".to_string(),
1293                    ],
1294                    exclude_paths: vec!["**/*test*".to_string()],
1295                    ignore_comments: true,
1296                    ignore_strings: true,
1297                    match_mode: Default::default(),
1298                    multiline: false,
1299                    multiline_window: None,
1300                    context_patterns: vec![],
1301                    context_window: None,
1302                    escalate_patterns: vec![],
1303                    escalate_window: None,
1304                    escalate_to: None,
1305                    depends_on: vec![],
1306                    help: Some(
1307                        "eval in shell scripts executes arbitrary commands, creating \
1308                        severe security risks especially with user input. Use safer \
1309                        alternatives like arrays or direct command execution."
1310                            .to_string(),
1311                    ),
1312                    url: None,
1313                    tags: vec!["security".to_string()],
1314                    test_cases: vec![],
1315                },
1316                RuleConfig {
1317                    id: "security.sql_concat".to_string(),
1318                    severity: Severity::Warn,
1319                    message: "Potential SQL injection - avoid string concatenation in queries.".to_string(),
1320                    languages: vec![],
1321                    patterns: vec![
1322                        r#"(?i)(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*\+.*["']"#.to_string(),
1323                        r#"(?i)(SELECT|INSERT|UPDATE|DELETE|FROM|WHERE).*["'].*\+"#.to_string(),
1324                    ],
1325                    paths: vec![],
1326                    exclude_paths: vec![
1327                        "**/*test*".to_string(),
1328                        "**/*.md".to_string(),
1329                    ],
1330                    ignore_comments: true,
1331                    ignore_strings: false,
1332                    match_mode: Default::default(),
1333                    multiline: false,
1334                    multiline_window: None,
1335                    context_patterns: vec![],
1336                    context_window: None,
1337                    escalate_patterns: vec![],
1338                    escalate_window: None,
1339                    escalate_to: None,
1340                    depends_on: vec![],
1341                    help: Some(
1342                        "String concatenation in SQL queries can lead to SQL injection attacks. \
1343                        Use parameterized queries or prepared statements instead."
1344                            .to_string(),
1345                    ),
1346                    url: Some("https://cheatsheetseries.owasp.org/cheatsheets/Query_Parameterization_Cheat_Sheet.html".to_string()),
1347                    tags: vec!["security".to_string()],
1348                    test_cases: vec![],
1349                },
1350            ],
1351        }
1352    }
1353}
1354
1355#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1356pub struct Defaults {
1357    #[serde(default, skip_serializing_if = "Option::is_none")]
1358    pub base: Option<String>,
1359
1360    #[serde(default, skip_serializing_if = "Option::is_none")]
1361    pub head: Option<String>,
1362
1363    #[serde(default, skip_serializing_if = "Option::is_none")]
1364    pub scope: Option<Scope>,
1365
1366    #[serde(default, skip_serializing_if = "Option::is_none")]
1367    pub fail_on: Option<FailOn>,
1368
1369    #[serde(default, skip_serializing_if = "Option::is_none")]
1370    pub max_findings: Option<u32>,
1371
1372    #[serde(default, skip_serializing_if = "Option::is_none")]
1373    pub diff_context: Option<u32>,
1374}
1375
1376impl Default for Defaults {
1377    fn default() -> Self {
1378        Self {
1379            base: Some("origin/main".to_string()),
1380            head: Some("HEAD".to_string()),
1381            scope: Some(Scope::Added),
1382            fail_on: Some(FailOn::Error),
1383            max_findings: Some(200),
1384            diff_context: Some(0),
1385        }
1386    }
1387}
1388
1389#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1390pub struct RuleConfig {
1391    pub id: String,
1392    pub severity: Severity,
1393    pub message: String,
1394
1395    /// Optional language tags (e.g. "rust"). Empty means "all".
1396    #[serde(default)]
1397    pub languages: Vec<String>,
1398
1399    /// One or more regex patterns.
1400    pub patterns: Vec<String>,
1401
1402    /// Include path globs. Empty means "all".
1403    #[serde(default)]
1404    pub paths: Vec<String>,
1405
1406    /// Exclude path globs.
1407    #[serde(default)]
1408    pub exclude_paths: Vec<String>,
1409
1410    #[serde(default)]
1411    pub ignore_comments: bool,
1412
1413    #[serde(default)]
1414    pub ignore_strings: bool,
1415
1416    /// Matching mode:
1417    /// - `any` (default): emit when patterns match
1418    /// - `absent`: emit when patterns do not match in the scoped file
1419    #[serde(default, skip_serializing_if = "is_match_mode_any")]
1420    pub match_mode: MatchMode,
1421
1422    /// Enable multi-line matching across consecutive scoped lines.
1423    #[serde(default, skip_serializing_if = "is_false")]
1424    pub multiline: bool,
1425
1426    /// Number of consecutive scoped lines to include in a multiline window.
1427    /// If omitted and `multiline=true`, a default of 2 lines is used.
1428    #[serde(default, skip_serializing_if = "Option::is_none")]
1429    pub multiline_window: Option<u32>,
1430
1431    /// Optional context patterns that must match near a primary match.
1432    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1433    pub context_patterns: Vec<String>,
1434
1435    /// Context search window (lines before/after the matched line).
1436    /// If omitted and `context_patterns` are set, a default of 3 is used.
1437    #[serde(default, skip_serializing_if = "Option::is_none")]
1438    pub context_window: Option<u32>,
1439
1440    /// Optional patterns that escalate severity when found near a match.
1441    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1442    pub escalate_patterns: Vec<String>,
1443
1444    /// Escalation search window (lines before/after the matched line).
1445    /// If omitted and `escalate_patterns` are set, a default of 0 (same line) is used.
1446    #[serde(default, skip_serializing_if = "Option::is_none")]
1447    pub escalate_window: Option<u32>,
1448
1449    /// Escalation target severity. Defaults to `error` when escalation patterns match.
1450    #[serde(default, skip_serializing_if = "Option::is_none")]
1451    pub escalate_to: Option<Severity>,
1452
1453    /// Rule dependencies. This rule is only evaluated in files where all dependencies matched.
1454    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1455    pub depends_on: Vec<String>,
1456
1457    /// Optional help text explaining how to fix violations.
1458    #[serde(default, skip_serializing_if = "Option::is_none")]
1459    pub help: Option<String>,
1460
1461    /// Optional URL with more information about the rule.
1462    #[serde(default, skip_serializing_if = "Option::is_none")]
1463    pub url: Option<String>,
1464
1465    /// Tags for grouping/filtering rules (e.g., "debug", "security", "style").
1466    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1467    pub tags: Vec<String>,
1468
1469    /// Test cases for validating this rule.
1470    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1471    pub test_cases: Vec<RuleTestCase>,
1472}
1473
1474/// A test case for validating a rule's behavior.
1475#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1476pub struct RuleTestCase {
1477    /// The input line to test against the rule.
1478    pub input: String,
1479
1480    /// Whether the rule should match this input.
1481    pub should_match: bool,
1482
1483    /// Optional: override ignore_comments for this test case.
1484    #[serde(default, skip_serializing_if = "Option::is_none")]
1485    pub ignore_comments: Option<bool>,
1486
1487    /// Optional: override ignore_strings for this test case.
1488    #[serde(default, skip_serializing_if = "Option::is_none")]
1489    pub ignore_strings: Option<bool>,
1490
1491    /// Optional: specify a language for preprocessing (e.g., "rust", "python").
1492    #[serde(default, skip_serializing_if = "Option::is_none")]
1493    pub language: Option<String>,
1494
1495    /// Optional: description of what this test case validates.
1496    #[serde(default, skip_serializing_if = "Option::is_none")]
1497    pub description: Option<String>,
1498}
1499
1500fn is_false(v: &bool) -> bool {
1501    !*v
1502}
1503
1504fn is_match_mode_any(mode: &MatchMode) -> bool {
1505    matches!(mode, MatchMode::Any)
1506}
1507
1508// ============================================================================
1509// Per-directory override types
1510// ============================================================================
1511
1512/// Per-directory override configuration (.diffguard.toml).
1513///
1514/// These files can be placed in any directory to override rule behavior
1515/// for files in that directory and its subdirectories.
1516#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1517pub struct DirectoryOverrideConfig {
1518    /// Rule-specific overrides.
1519    #[serde(default, rename = "rule")]
1520    pub rules: Vec<RuleOverride>,
1521}
1522
1523/// Override settings for a specific rule in a directory.
1524#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1525pub struct RuleOverride {
1526    /// The rule ID to override (e.g., "rust.no_unwrap").
1527    pub id: String,
1528
1529    /// Set to false to disable this rule for this directory.
1530    #[serde(default, skip_serializing_if = "Option::is_none")]
1531    pub enabled: Option<bool>,
1532
1533    /// Override the severity for this directory.
1534    #[serde(default, skip_serializing_if = "Option::is_none")]
1535    pub severity: Option<Severity>,
1536
1537    /// Additional paths to exclude within this directory.
1538    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1539    pub exclude_paths: Vec<String>,
1540}
1541
1542// ============================================================================
1543// sensor.report.v1 types (Cockpit ecosystem integration)
1544// ============================================================================
1545
1546/// The `sensor.report.v1` envelope for Cockpit ecosystem integration.
1547#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1548pub struct SensorReport {
1549    /// Schema identifier, always "sensor.report.v1".
1550    pub schema: String,
1551    /// Tool metadata.
1552    pub tool: ToolMeta,
1553    /// Run timing and capability information.
1554    pub run: RunMeta,
1555    /// Overall verdict.
1556    pub verdict: Verdict,
1557    /// Findings in sensor format.
1558    pub findings: Vec<SensorFinding>,
1559    /// List of artifacts produced.
1560    #[serde(default, skip_serializing_if = "Vec::is_empty")]
1561    pub artifacts: Vec<Artifact>,
1562    /// Additional data payload (diff metadata, etc.).
1563    #[serde(default, skip_serializing_if = "Option::is_none")]
1564    pub data: Option<serde_json::Value>,
1565}
1566
1567/// Run timing and capability status.
1568#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1569pub struct RunMeta {
1570    /// ISO 8601 timestamp when the run started.
1571    pub started_at: String,
1572    /// ISO 8601 timestamp when the run ended.
1573    pub ended_at: String,
1574    /// Duration in milliseconds.
1575    pub duration_ms: u64,
1576    /// Capability status map (e.g., "git" -> available/unavailable).
1577    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1578    pub capabilities: HashMap<String, CapabilityStatus>,
1579}
1580
1581/// Status of a capability (e.g., git availability).
1582#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1583pub struct CapabilityStatus {
1584    /// Status: "available", "unavailable", or "skipped".
1585    pub status: String,
1586    /// Stable token reason (e.g., "missing_base", "tool_error").
1587    #[serde(default, skip_serializing_if = "Option::is_none")]
1588    pub reason: Option<String>,
1589    /// Human-readable detail for diagnostics.
1590    #[serde(default, skip_serializing_if = "Option::is_none")]
1591    pub detail: Option<String>,
1592}
1593
1594/// A finding in sensor.report.v1 format.
1595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1596pub struct SensorFinding {
1597    /// Check identifier (constant: "diffguard.pattern").
1598    pub check_id: String,
1599    /// Rule code (maps from rule_id, e.g., "rust.no_unwrap").
1600    pub code: String,
1601    /// Finding severity.
1602    pub severity: Severity,
1603    /// Human-readable message.
1604    pub message: String,
1605    /// Location in the source.
1606    pub location: SensorLocation,
1607    /// Stable fingerprint (full SHA-256, 64 hex chars).
1608    pub fingerprint: String,
1609    /// Optional help text.
1610    #[serde(default, skip_serializing_if = "Option::is_none")]
1611    pub help: Option<String>,
1612    /// Optional URL for more information.
1613    #[serde(default, skip_serializing_if = "Option::is_none")]
1614    pub url: Option<String>,
1615    /// Additional data (match_text, snippet).
1616    #[serde(default, skip_serializing_if = "Option::is_none")]
1617    pub data: Option<serde_json::Value>,
1618}
1619
1620/// Location in sensor.report.v1 format.
1621#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1622pub struct SensorLocation {
1623    /// Repo-relative path with forward slashes.
1624    pub path: String,
1625    /// Line number (1-based).
1626    pub line: u32,
1627    /// Optional column number (1-based).
1628    #[serde(default, skip_serializing_if = "Option::is_none")]
1629    pub column: Option<u32>,
1630}
1631
1632/// An artifact produced by the sensor.
1633#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1634pub struct Artifact {
1635    /// Path to the artifact file.
1636    pub path: String,
1637    /// Format of the artifact (e.g., "json", "sarif", "markdown").
1638    pub format: String,
1639}
1640
1641#[cfg(test)]
1642mod tests {
1643    use super::*;
1644
1645    #[test]
1646    fn severity_scope_failon_as_str() {
1647        assert_eq!(Severity::Info.as_str(), "info");
1648        assert_eq!(Severity::Warn.as_str(), "warn");
1649        assert_eq!(Severity::Error.as_str(), "error");
1650
1651        assert_eq!(Scope::Added.as_str(), "added");
1652        assert_eq!(Scope::Changed.as_str(), "changed");
1653        assert_eq!(Scope::Modified.as_str(), "modified");
1654        assert_eq!(Scope::Deleted.as_str(), "deleted");
1655
1656        assert_eq!(FailOn::Error.as_str(), "error");
1657        assert_eq!(FailOn::Warn.as_str(), "warn");
1658        assert_eq!(FailOn::Never.as_str(), "never");
1659    }
1660
1661    #[test]
1662    fn defaults_match_expected_values() {
1663        let defaults = Defaults::default();
1664        assert_eq!(defaults.base.as_deref(), Some("origin/main"));
1665        assert_eq!(defaults.head.as_deref(), Some("HEAD"));
1666        assert_eq!(defaults.scope, Some(Scope::Added));
1667        assert_eq!(defaults.fail_on, Some(FailOn::Error));
1668        assert_eq!(defaults.max_findings, Some(200));
1669        assert_eq!(defaults.diff_context, Some(0));
1670    }
1671
1672    #[test]
1673    fn verdict_counts_suppressed_is_omitted_when_zero() {
1674        let counts = VerdictCounts::default();
1675        let value = serde_json::to_value(&counts).expect("serialize verdict counts");
1676        let obj = value.as_object().expect("counts should be object");
1677        assert!(!obj.contains_key("suppressed"));
1678
1679        let with_suppressed = VerdictCounts {
1680            suppressed: 2,
1681            ..VerdictCounts::default()
1682        };
1683        let value = serde_json::to_value(&with_suppressed).expect("serialize verdict counts");
1684        let obj = value.as_object().expect("counts should be object");
1685        assert_eq!(obj.get("suppressed").and_then(|v| v.as_u64()), Some(2));
1686    }
1687
1688    #[test]
1689    fn built_in_config_contains_expected_rules_and_unique_ids() {
1690        let cfg = ConfigFile::built_in();
1691        assert!(cfg.rule.len() > 10, "built-in rules should be non-trivial");
1692
1693        let ids: std::collections::HashSet<&str> = cfg.rule.iter().map(|r| r.id.as_str()).collect();
1694        assert_eq!(
1695            ids.len(),
1696            cfg.rule.len(),
1697            "built-in rule IDs should be unique"
1698        );
1699
1700        for expected in [
1701            "rust.no_unwrap",
1702            "rust.no_dbg",
1703            "python.no_print",
1704            "js.no_console",
1705            "ruby.no_binding_pry",
1706            "security.hardcoded_ipv4",
1707        ] {
1708            assert!(
1709                ids.contains(expected),
1710                "expected built-in rule '{expected}'"
1711            );
1712        }
1713
1714        assert_eq!(cfg.defaults, Defaults::default());
1715    }
1716}