1use std::collections::HashMap;
6
7use schemars::JsonSchema;
8use serde::{Deserialize, Serialize};
9
10pub const CHECK_SCHEMA_V1: &str = "diffguard.check.v1";
12pub const SENSOR_REPORT_SCHEMA_V1: &str = "sensor.report.v1";
13
14pub const CHECK_ID_PATTERN: &str = "diffguard.pattern";
17pub const CHECK_ID_INTERNAL: &str = "diffguard.internal";
18
19pub 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";
24pub const REASON_HAS_ERROR: &str = "has_error";
27pub const REASON_HAS_WARNING: &str = "has_warning";
30pub const REASON_TRUNCATED: &str = "truncated";
31
32pub const CODE_TOOL_RUNTIME_ERROR: &str = "tool.runtime_error";
34
35pub const CAP_GIT: &str = "git";
37
38pub 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 #[default]
104 Any,
105 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 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 #[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#[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
191pub struct ConfigFile {
192 #[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 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 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 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 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 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 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 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 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 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 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 #[serde(default)]
1397 pub languages: Vec<String>,
1398
1399 pub patterns: Vec<String>,
1401
1402 #[serde(default)]
1404 pub paths: Vec<String>,
1405
1406 #[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 #[serde(default, skip_serializing_if = "is_match_mode_any")]
1420 pub match_mode: MatchMode,
1421
1422 #[serde(default, skip_serializing_if = "is_false")]
1424 pub multiline: bool,
1425
1426 #[serde(default, skip_serializing_if = "Option::is_none")]
1429 pub multiline_window: Option<u32>,
1430
1431 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1433 pub context_patterns: Vec<String>,
1434
1435 #[serde(default, skip_serializing_if = "Option::is_none")]
1438 pub context_window: Option<u32>,
1439
1440 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1442 pub escalate_patterns: Vec<String>,
1443
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1447 pub escalate_window: Option<u32>,
1448
1449 #[serde(default, skip_serializing_if = "Option::is_none")]
1451 pub escalate_to: Option<Severity>,
1452
1453 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1455 pub depends_on: Vec<String>,
1456
1457 #[serde(default, skip_serializing_if = "Option::is_none")]
1459 pub help: Option<String>,
1460
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1463 pub url: Option<String>,
1464
1465 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1467 pub tags: Vec<String>,
1468
1469 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1471 pub test_cases: Vec<RuleTestCase>,
1472}
1473
1474#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1476pub struct RuleTestCase {
1477 pub input: String,
1479
1480 pub should_match: bool,
1482
1483 #[serde(default, skip_serializing_if = "Option::is_none")]
1485 pub ignore_comments: Option<bool>,
1486
1487 #[serde(default, skip_serializing_if = "Option::is_none")]
1489 pub ignore_strings: Option<bool>,
1490
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1493 pub language: Option<String>,
1494
1495 #[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#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema, Default)]
1517pub struct DirectoryOverrideConfig {
1518 #[serde(default, rename = "rule")]
1520 pub rules: Vec<RuleOverride>,
1521}
1522
1523#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1525pub struct RuleOverride {
1526 pub id: String,
1528
1529 #[serde(default, skip_serializing_if = "Option::is_none")]
1531 pub enabled: Option<bool>,
1532
1533 #[serde(default, skip_serializing_if = "Option::is_none")]
1535 pub severity: Option<Severity>,
1536
1537 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1539 pub exclude_paths: Vec<String>,
1540}
1541
1542#[derive(Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
1548pub struct SensorReport {
1549 pub schema: String,
1551 pub tool: ToolMeta,
1553 pub run: RunMeta,
1555 pub verdict: Verdict,
1557 pub findings: Vec<SensorFinding>,
1559 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1561 pub artifacts: Vec<Artifact>,
1562 #[serde(default, skip_serializing_if = "Option::is_none")]
1564 pub data: Option<serde_json::Value>,
1565}
1566
1567#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1569pub struct RunMeta {
1570 pub started_at: String,
1572 pub ended_at: String,
1574 pub duration_ms: u64,
1576 #[serde(default, skip_serializing_if = "HashMap::is_empty")]
1578 pub capabilities: HashMap<String, CapabilityStatus>,
1579}
1580
1581#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1583pub struct CapabilityStatus {
1584 pub status: String,
1586 #[serde(default, skip_serializing_if = "Option::is_none")]
1588 pub reason: Option<String>,
1589 #[serde(default, skip_serializing_if = "Option::is_none")]
1591 pub detail: Option<String>,
1592}
1593
1594#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1596pub struct SensorFinding {
1597 pub check_id: String,
1599 pub code: String,
1601 pub severity: Severity,
1603 pub message: String,
1605 pub location: SensorLocation,
1607 pub fingerprint: String,
1609 #[serde(default, skip_serializing_if = "Option::is_none")]
1611 pub help: Option<String>,
1612 #[serde(default, skip_serializing_if = "Option::is_none")]
1614 pub url: Option<String>,
1615 #[serde(default, skip_serializing_if = "Option::is_none")]
1617 pub data: Option<serde_json::Value>,
1618}
1619
1620#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1622pub struct SensorLocation {
1623 pub path: String,
1625 pub line: u32,
1627 #[serde(default, skip_serializing_if = "Option::is_none")]
1629 pub column: Option<u32>,
1630}
1631
1632#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
1634pub struct Artifact {
1635 pub path: String,
1637 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}