repolens/config/mod.rs
1//! # Configuration Module
2//!
3//! This module handles all configuration-related functionality for RepoLens,
4//! including loading configuration files, managing presets, and providing
5//! rule-specific settings.
6//!
7//! ## Configuration Priority
8//!
9//! Configuration is loaded with the following priority (highest to lowest):
10//!
11//! 1. CLI flags (handled by clap)
12//! 2. Environment variables (`REPOLENS_*`)
13//! 3. Configuration file (`.repolens.toml`)
14//! 4. Default values
15//!
16//! ## Configuration File
17//!
18//! The default configuration file is `.repolens.toml` in the project root.
19//!
20//! ```toml
21//! preset = "opensource"
22//!
23//! [actions]
24//! gitignore = true
25//! contributing = true
26//!
27//! [actions.license]
28//! enabled = true
29//! license_type = "MIT"
30//! author = "Your Name"
31//!
32//! [actions.branch_protection]
33//! enabled = true
34//! required_approvals = 1
35//!
36//! ["rules.secrets"]
37//! ignore_patterns = ["test_*"]
38//! ignore_files = ["*.test.ts"]
39//!
40//! ["rules.custom"."no-todo"]
41//! pattern = "TODO"
42//! severity = "warning"
43//! files = ["**/*.rs"]
44//! message = "TODO comment found"
45//! ```
46//!
47//! ## Environment Variables
48//!
49//! | Variable | Description |
50//! |----------|-------------|
51//! | `REPOLENS_PRESET` | Override preset (opensource, enterprise, strict) |
52//! | `REPOLENS_CONFIG` | Path to configuration file |
53//! | `REPOLENS_VERBOSE` | Verbosity level (0-3) |
54//! | `REPOLENS_NO_CACHE` | Disable caching (true/false) |
55//! | `REPOLENS_GITHUB_TOKEN` | GitHub API token |
56//!
57//! ## Examples
58//!
59//! ### Loading Configuration
60//!
61//! ```rust,no_run
62//! use repolens::config::Config;
63//!
64//! // Load from default location or environment
65//! let config = Config::load_or_default().expect("Failed to load config");
66//!
67//! // Check preset
68//! println!("Using preset: {}", config.preset);
69//! ```
70//!
71//! ### Creating from Preset
72//!
73//! ```rust
74//! use repolens::config::{Config, Preset};
75//!
76//! let config = Config::from_preset(Preset::Enterprise);
77//! assert_eq!(config.preset, "enterprise");
78//! ```
79//!
80//! ### Checking Rule Configuration
81//!
82//! ```rust
83//! use repolens::config::Config;
84//!
85//! let config = Config::default();
86//!
87//! // Check if a rule is enabled
88//! if config.is_rule_enabled("SEC001") {
89//! println!("Secret detection is enabled");
90//! }
91//! ```
92
93pub mod loader;
94pub mod presets;
95
96pub use loader::get_env_verbosity;
97pub use loader::Config;
98pub use presets::Preset;
99
100use serde::{Deserialize, Serialize};
101use std::collections::HashMap;
102
103// Re-export CacheConfig from cache module for convenience
104pub use crate::cache::CacheConfig;
105
106// Re-export HooksConfig from hooks module for convenience
107pub use crate::hooks::HooksConfig;
108
109/// Configuration for individual audit rules.
110///
111/// Allows enabling/disabling rules and overriding their default severity.
112///
113/// # Examples
114///
115/// ```toml
116/// [rules.SEC001]
117/// enabled = false
118/// severity = "warning"
119/// ```
120///
121/// ```rust
122/// use repolens::config::RuleConfig;
123///
124/// let rule = RuleConfig {
125/// enabled: true,
126/// severity: Some("critical".to_string()),
127/// };
128/// ```
129#[derive(Debug, Clone, Serialize, Deserialize, Default)]
130pub struct RuleConfig {
131 /// Whether the rule is enabled. Defaults to `true`.
132 #[serde(default = "default_true")]
133 pub enabled: bool,
134
135 /// Severity override (critical, warning, info).
136 /// If `None`, the rule's default severity is used.
137 pub severity: Option<String>,
138}
139
140fn default_true() -> bool {
141 true
142}
143
144/// Configuration for secrets detection.
145///
146/// Controls which patterns and files are scanned for secrets,
147/// and allows defining custom secret patterns.
148///
149/// # Examples
150///
151/// ```toml
152/// ["rules.secrets"]
153/// ignore_patterns = ["test_*", "*_mock"]
154/// ignore_files = ["*.test.ts", "fixtures/**"]
155/// custom_patterns = ["MY_SECRET_\\w+"]
156/// ```
157#[derive(Debug, Clone, Serialize, Deserialize, Default)]
158pub struct SecretsConfig {
159 /// Patterns to ignore when scanning for secrets.
160 /// Supports glob patterns like `test_*` or `*_mock`.
161 #[serde(default)]
162 pub ignore_patterns: Vec<String>,
163
164 /// Files to ignore when scanning for secrets.
165 /// Supports glob patterns like `*.test.ts` or `vendor/**`.
166 #[serde(default)]
167 pub ignore_files: Vec<String>,
168
169 /// Custom regex patterns to detect as secrets.
170 /// Added to the default secret detection patterns.
171 #[serde(default)]
172 pub custom_patterns: Vec<String>,
173}
174
175/// Configuration for URL validation.
176///
177/// Used primarily in enterprise mode to allow internal URLs
178/// that would otherwise be flagged as potential issues.
179///
180/// # Examples
181///
182/// ```toml
183/// ["rules.urls"]
184/// allowed_internal = ["https://internal.company.com/*", "http://localhost:*"]
185/// ```
186#[derive(Debug, Clone, Serialize, Deserialize, Default)]
187pub struct UrlConfig {
188 /// Allowed internal URLs (for enterprise mode).
189 /// Supports glob patterns for URL matching.
190 #[serde(default)]
191 pub allowed_internal: Vec<String>,
192}
193
194/// Configuration for remediation actions.
195///
196/// Controls which automated fixes and file generations are enabled
197/// when running `repolens apply`.
198///
199/// # Examples
200///
201/// ```toml
202/// [actions]
203/// gitignore = true
204/// contributing = true
205/// code_of_conduct = true
206/// security_policy = true
207///
208/// [actions.license]
209/// enabled = true
210/// license_type = "MIT"
211///
212/// [actions.branch_protection]
213/// enabled = true
214/// required_approvals = 2
215/// ```
216#[derive(Debug, Clone, Serialize, Deserialize)]
217pub struct ActionsConfig {
218 /// Whether to update `.gitignore` with recommended entries.
219 #[serde(default = "default_true")]
220 pub gitignore: bool,
221
222 /// License file generation configuration.
223 #[serde(default)]
224 pub license: LicenseConfig,
225
226 /// Whether to create `CONTRIBUTING.md` if missing.
227 #[serde(default = "default_true")]
228 pub contributing: bool,
229
230 /// Whether to create `CODE_OF_CONDUCT.md` if missing.
231 #[serde(default = "default_true")]
232 pub code_of_conduct: bool,
233
234 /// Whether to create `SECURITY.md` if missing.
235 #[serde(default = "default_true")]
236 pub security_policy: bool,
237
238 /// GitHub branch protection rule configuration.
239 #[serde(default)]
240 pub branch_protection: BranchProtectionConfig,
241
242 /// GitHub repository settings configuration.
243 #[serde(default)]
244 pub github_settings: GitHubSettingsConfig,
245}
246
247impl Default for ActionsConfig {
248 fn default() -> Self {
249 Self {
250 gitignore: true,
251 license: LicenseConfig::default(),
252 contributing: true,
253 code_of_conduct: true,
254 security_policy: true,
255 branch_protection: BranchProtectionConfig::default(),
256 github_settings: GitHubSettingsConfig::default(),
257 }
258 }
259}
260
261/// Configuration for LICENSE file generation.
262///
263/// Controls the license type and metadata used when generating
264/// a LICENSE file.
265///
266/// # Supported License Types
267///
268/// - `MIT` - MIT License (default)
269/// - `Apache-2.0` - Apache License 2.0
270/// - `GPL-3.0` - GNU General Public License v3.0
271/// - `BSD-3-Clause` - BSD 3-Clause License
272/// - `UNLICENSED` - Proprietary/No License
273///
274/// # Examples
275///
276/// ```toml
277/// [actions.license]
278/// enabled = true
279/// license_type = "Apache-2.0"
280/// author = "Your Name"
281/// year = "2024"
282/// ```
283#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct LicenseConfig {
285 /// Whether to create LICENSE file if missing.
286 #[serde(default = "default_true")]
287 pub enabled: bool,
288
289 /// License type (MIT, Apache-2.0, GPL-3.0, etc.).
290 /// Defaults to "MIT".
291 #[serde(default = "default_license_type")]
292 pub license_type: String,
293
294 /// Author name for license. If not set, attempts to
295 /// detect from git configuration.
296 #[serde(default)]
297 pub author: Option<String>,
298
299 /// Year for license. Defaults to current year if not specified.
300 #[serde(default)]
301 pub year: Option<String>,
302}
303
304impl Default for LicenseConfig {
305 fn default() -> Self {
306 Self {
307 enabled: true,
308 license_type: "MIT".to_string(),
309 author: None,
310 year: None,
311 }
312 }
313}
314
315fn default_license_type() -> String {
316 "MIT".to_string()
317}
318
319/// Configuration for GitHub branch protection rules.
320///
321/// These settings are applied via the GitHub API when running
322/// `repolens apply` with appropriate permissions.
323///
324/// # Examples
325///
326/// ```toml
327/// [actions.branch_protection]
328/// enabled = true
329/// branch = "main"
330/// required_approvals = 2
331/// require_status_checks = true
332/// block_force_push = true
333/// require_signed_commits = true
334/// ```
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct BranchProtectionConfig {
337 /// Whether to enable branch protection rules.
338 #[serde(default = "default_true")]
339 pub enabled: bool,
340
341 /// Branch to protect. Defaults to "main".
342 #[serde(default = "default_branch")]
343 pub branch: String,
344
345 /// Number of required pull request approvals.
346 /// Defaults to 1, enterprise preset uses 2.
347 #[serde(default = "default_approvals")]
348 pub required_approvals: u32,
349
350 /// Whether to require status checks to pass before merging.
351 #[serde(default = "default_true")]
352 pub require_status_checks: bool,
353
354 /// Whether to block force pushes to the protected branch.
355 #[serde(default = "default_true")]
356 pub block_force_push: bool,
357
358 /// Whether to require signed commits.
359 /// Defaults to `false`, enterprise/strict presets enable this.
360 #[serde(default)]
361 pub require_signed_commits: bool,
362}
363
364impl Default for BranchProtectionConfig {
365 fn default() -> Self {
366 Self {
367 enabled: true,
368 branch: "main".to_string(),
369 required_approvals: 1,
370 require_status_checks: true,
371 block_force_push: true,
372 require_signed_commits: false,
373 }
374 }
375}
376
377fn default_branch() -> String {
378 "main".to_string()
379}
380
381fn default_approvals() -> u32 {
382 1
383}
384
385/// Configuration for GitHub repository settings.
386///
387/// These settings are applied via the GitHub API when running
388/// `repolens apply` with appropriate permissions.
389///
390/// # Examples
391///
392/// ```toml
393/// [actions.github_settings]
394/// discussions = true
395/// issues = true
396/// wiki = false
397/// vulnerability_alerts = true
398/// automated_security_fixes = true
399/// ```
400#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct GitHubSettingsConfig {
402 /// Whether to enable GitHub Discussions for the repository.
403 #[serde(default = "default_true")]
404 pub discussions: bool,
405
406 /// Whether to enable GitHub Issues for the repository.
407 #[serde(default = "default_true")]
408 pub issues: bool,
409
410 /// Whether to enable GitHub Wiki for the repository.
411 /// Defaults to `false` as wikis are often unused.
412 #[serde(default)]
413 pub wiki: bool,
414
415 /// Whether to enable Dependabot vulnerability alerts.
416 #[serde(default = "default_true")]
417 pub vulnerability_alerts: bool,
418
419 /// Whether to enable Dependabot automatic security fixes.
420 #[serde(default = "default_true")]
421 pub automated_security_fixes: bool,
422}
423
424impl Default for GitHubSettingsConfig {
425 fn default() -> Self {
426 Self {
427 discussions: true,
428 issues: true,
429 wiki: false,
430 vulnerability_alerts: true,
431 automated_security_fixes: true,
432 }
433 }
434}
435
436/// Configuration for file template generation.
437///
438/// These values are used when generating files like LICENSE,
439/// CONTRIBUTING.md, and other template-based files.
440///
441/// # Examples
442///
443/// ```toml
444/// [templates]
445/// license_author = "Your Company"
446/// license_year = "2024"
447/// project_name = "My Project"
448/// project_description = "A description of the project"
449/// ```
450#[derive(Debug, Clone, Serialize, Deserialize, Default)]
451pub struct TemplatesConfig {
452 /// Author name for license and other templates.
453 /// Overrides auto-detected git user name.
454 pub license_author: Option<String>,
455
456 /// Year for license templates.
457 /// Defaults to current year if not specified.
458 pub license_year: Option<String>,
459
460 /// Project name override.
461 /// Defaults to repository/directory name if not specified.
462 pub project_name: Option<String>,
463
464 /// Project description for generated files.
465 pub project_description: Option<String>,
466}
467
468/// Configuration for a custom audit rule.
469///
470/// Custom rules allow defining project-specific checks using either
471/// regex patterns or shell commands.
472///
473/// # Pattern-based Rules
474///
475/// ```toml
476/// ["rules.custom"."no-todo"]
477/// pattern = "TODO|FIXME"
478/// severity = "warning"
479/// files = ["**/*.rs", "**/*.py"]
480/// message = "Found TODO/FIXME comment"
481/// remediation = "Complete the task or remove the comment"
482/// ```
483///
484/// # Command-based Rules
485///
486/// ```toml
487/// ["rules.custom"."has-makefile"]
488/// command = "test -f Makefile"
489/// severity = "info"
490/// message = "Makefile not found"
491/// invert = true
492/// ```
493#[derive(Debug, Clone, Serialize, Deserialize)]
494pub struct CustomRule {
495 /// Regex pattern to match in file contents.
496 /// Required if `command` is not set.
497 #[serde(default)]
498 pub pattern: Option<String>,
499
500 /// Shell command to execute for the check.
501 /// The rule triggers if the command returns exit code 0
502 /// (or non-zero if `invert` is true).
503 /// Required if `pattern` is not set.
504 #[serde(default)]
505 pub command: Option<String>,
506
507 /// Severity level: "critical", "warning", or "info".
508 /// Defaults to "warning".
509 #[serde(default = "default_custom_severity")]
510 pub severity: String,
511
512 /// File glob patterns to scan (only used with `pattern`).
513 /// If empty, all files are scanned.
514 #[serde(default)]
515 pub files: Vec<String>,
516
517 /// Custom message shown when the rule triggers.
518 pub message: Option<String>,
519
520 /// Detailed description of the issue.
521 pub description: Option<String>,
522
523 /// Suggested steps to fix the issue.
524 pub remediation: Option<String>,
525
526 /// If true, inverts the matching logic:
527 /// - For patterns: triggers when pattern is NOT found
528 /// - For commands: triggers when command returns non-zero
529 #[serde(default)]
530 pub invert: bool,
531}
532
533fn default_custom_severity() -> String {
534 "warning".to_string()
535}
536
537/// Container for custom rule definitions.
538///
539/// Custom rules are defined under the `["rules.custom"]` section
540/// in the configuration file.
541///
542/// # Examples
543///
544/// ```toml
545/// ["rules.custom"."rule-id"]
546/// pattern = "some_pattern"
547/// severity = "warning"
548/// message = "Issue found"
549/// ```
550#[derive(Debug, Clone, Serialize, Deserialize, Default)]
551pub struct CustomRulesConfig {
552 /// Map of rule ID to rule configuration.
553 /// Rule IDs should be kebab-case (e.g., "no-todo", "require-tests").
554 #[serde(flatten)]
555 pub rules: HashMap<String, CustomRule>,
556}
557
558/// Configuration for dependency license compliance checking.
559///
560/// Allows specifying which licenses are allowed or denied for
561/// project dependencies.
562///
563/// # Examples
564///
565/// ```toml
566/// ["rules.licenses"]
567/// enabled = true
568/// allowed_licenses = ["MIT", "Apache-2.0", "BSD-3-Clause"]
569/// denied_licenses = ["GPL-3.0", "AGPL-3.0"]
570/// ```
571#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct LicenseComplianceConfig {
573 /// Whether license compliance checking is enabled.
574 #[serde(default = "default_true")]
575 pub enabled: bool,
576
577 /// List of allowed SPDX license identifiers.
578 /// If empty, all known licenses are allowed (unless in `denied_licenses`).
579 /// Example: `["MIT", "Apache-2.0", "BSD-3-Clause"]`
580 #[serde(default)]
581 pub allowed_licenses: Vec<String>,
582
583 /// List of denied SPDX license identifiers.
584 /// Dependencies with these licenses will be flagged.
585 /// Example: `["GPL-3.0", "AGPL-3.0"]`
586 #[serde(default)]
587 pub denied_licenses: Vec<String>,
588}
589
590impl Default for LicenseComplianceConfig {
591 fn default() -> Self {
592 Self {
593 enabled: true,
594 allowed_licenses: Vec::new(),
595 denied_licenses: Vec::new(),
596 }
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603
604 #[test]
605 fn test_rule_config_default() {
606 let config = RuleConfig::default();
607 assert!(!config.enabled); // Default for bool is false
608 assert!(config.severity.is_none());
609 }
610
611 #[test]
612 fn test_rule_config_deserialize() {
613 let toml_str = r#"
614 enabled = true
615 severity = "critical"
616 "#;
617 let config: RuleConfig = toml::from_str(toml_str).unwrap();
618 assert!(config.enabled);
619 assert_eq!(config.severity, Some("critical".to_string()));
620 }
621
622 #[test]
623 fn test_secrets_config_default() {
624 let config = SecretsConfig::default();
625 assert!(config.ignore_patterns.is_empty());
626 assert!(config.ignore_files.is_empty());
627 assert!(config.custom_patterns.is_empty());
628 }
629
630 #[test]
631 fn test_url_config_default() {
632 let config = UrlConfig::default();
633 assert!(config.allowed_internal.is_empty());
634 }
635
636 #[test]
637 fn test_actions_config_default() {
638 let config = ActionsConfig::default();
639 assert!(config.gitignore);
640 assert!(config.contributing);
641 assert!(config.code_of_conduct);
642 assert!(config.security_policy);
643 }
644
645 #[test]
646 fn test_license_config_default() {
647 let config = LicenseConfig::default();
648 assert!(config.enabled);
649 assert_eq!(config.license_type, "MIT");
650 assert!(config.author.is_none());
651 assert!(config.year.is_none());
652 }
653
654 #[test]
655 fn test_branch_protection_config_default() {
656 let config = BranchProtectionConfig::default();
657 assert!(config.enabled);
658 assert_eq!(config.branch, "main");
659 assert_eq!(config.required_approvals, 1);
660 assert!(config.require_status_checks);
661 assert!(config.block_force_push);
662 assert!(!config.require_signed_commits);
663 }
664
665 #[test]
666 fn test_github_settings_config_default() {
667 let config = GitHubSettingsConfig::default();
668 assert!(config.discussions);
669 assert!(config.issues);
670 assert!(!config.wiki);
671 assert!(config.vulnerability_alerts);
672 assert!(config.automated_security_fixes);
673 }
674
675 #[test]
676 fn test_templates_config_default() {
677 let config = TemplatesConfig::default();
678 assert!(config.license_author.is_none());
679 assert!(config.license_year.is_none());
680 assert!(config.project_name.is_none());
681 assert!(config.project_description.is_none());
682 }
683
684 #[test]
685 fn test_custom_rule_deserialize() {
686 let toml_str = r#"
687 pattern = "TODO|FIXME"
688 severity = "warning"
689 files = ["*.rs", "*.py"]
690 message = "Found TODO comment"
691 description = "TODO comments should be addressed"
692 remediation = "Complete the task or remove the comment"
693 invert = false
694 "#;
695 let rule: CustomRule = toml::from_str(toml_str).unwrap();
696 assert_eq!(rule.pattern, Some("TODO|FIXME".to_string()));
697 assert_eq!(rule.severity, "warning");
698 assert_eq!(rule.files.len(), 2);
699 assert!(!rule.invert);
700 }
701
702 #[test]
703 fn test_custom_rule_with_command() {
704 let toml_str = r#"
705 command = "test -f Makefile"
706 severity = "info"
707 message = "Makefile not found"
708 invert = true
709 "#;
710 let rule: CustomRule = toml::from_str(toml_str).unwrap();
711 assert!(rule.pattern.is_none());
712 assert_eq!(rule.command, Some("test -f Makefile".to_string()));
713 assert!(rule.invert);
714 }
715
716 #[test]
717 fn test_custom_rules_config_default() {
718 let config = CustomRulesConfig::default();
719 assert!(config.rules.is_empty());
720 }
721
722 #[test]
723 fn test_default_true_function() {
724 assert!(default_true());
725 }
726
727 #[test]
728 fn test_default_license_type_function() {
729 assert_eq!(default_license_type(), "MIT");
730 }
731
732 #[test]
733 fn test_default_branch_function() {
734 assert_eq!(default_branch(), "main");
735 }
736
737 #[test]
738 fn test_default_approvals_function() {
739 assert_eq!(default_approvals(), 1);
740 }
741
742 #[test]
743 fn test_default_custom_severity_function() {
744 assert_eq!(default_custom_severity(), "warning");
745 }
746
747 #[test]
748 fn test_license_compliance_config_default() {
749 let config = LicenseComplianceConfig::default();
750 assert!(config.enabled);
751 assert!(config.allowed_licenses.is_empty());
752 assert!(config.denied_licenses.is_empty());
753 }
754
755 #[test]
756 fn test_license_compliance_config_deserialize() {
757 let toml_str = r#"
758 enabled = true
759 allowed_licenses = ["MIT", "Apache-2.0"]
760 denied_licenses = ["GPL-3.0"]
761 "#;
762 let config: LicenseComplianceConfig = toml::from_str(toml_str).unwrap();
763 assert!(config.enabled);
764 assert_eq!(config.allowed_licenses.len(), 2);
765 assert_eq!(config.denied_licenses.len(), 1);
766 assert_eq!(config.allowed_licenses[0], "MIT");
767 assert_eq!(config.denied_licenses[0], "GPL-3.0");
768 }
769
770 #[test]
771 fn test_license_compliance_config_deserialize_defaults() {
772 let toml_str = r#""#;
773 let config: LicenseComplianceConfig = toml::from_str(toml_str).unwrap();
774 assert!(config.enabled);
775 assert!(config.allowed_licenses.is_empty());
776 assert!(config.denied_licenses.is_empty());
777 }
778}