Skip to main content

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}