1use std::fs;
2use std::path::Path;
3
4use clap::ValueEnum;
5use serde::{Deserialize, Serialize};
6
7use crate::budget::BudgetTarget;
8use crate::error::Result;
9use crate::limits;
10
11#[derive(Debug, Clone, Deserialize)]
12#[serde(default)]
13pub struct Config {
14 pub output: OutputConfig,
15 pub scanning: ScanningConfig,
16 pub categories: CategoryConfig,
17 pub priority: PriorityWeights,
18 pub priority_rules: Vec<PriorityRule>,
19 pub exclude: ExcludeConfig,
20 pub signatures: SignatureConfig,
21}
22
23pub use crate::limits::{
24 MAX_BUDGET_BYTES as SAFE_MAX_BUDGET_BYTES, MAX_BUDGET_TOKENS as SAFE_MAX_BUDGET_TOKENS,
25 MAX_SCAN_DEPTH as SAFE_MAX_SCAN_DEPTH,
26};
27
28impl Config {
29 pub fn from_str(contents: &str) -> Result<Self> {
30 Ok(toml::from_str(contents)?)
31 }
32
33 pub fn load(path: impl AsRef<Path>) -> Result<Self> {
34 let contents = fs::read_to_string(path)?;
35 Self::from_str(&contents)
36 }
37}
38
39pub fn clamp_budget_target(target: BudgetTarget) -> BudgetTarget {
40 match target {
41 BudgetTarget::Tokens(tokens) => {
42 let clamped = limits::clamp_budget_tokens(tokens);
43 if clamped != tokens {
44 eprintln!(
45 "SECURITY: token budget clamped to {}",
46 SAFE_MAX_BUDGET_TOKENS
47 );
48 }
49 BudgetTarget::Tokens(clamped)
50 }
51 BudgetTarget::Bytes(bytes) => {
52 let clamped = limits::clamp_budget_bytes(bytes);
53 if clamped != bytes {
54 eprintln!(
55 "SECURITY: byte budget clamped to {}",
56 SAFE_MAX_BUDGET_BYTES
57 );
58 }
59 BudgetTarget::Bytes(clamped)
60 }
61 }
62}
63
64pub fn apply_security_overrides(config: &mut Config) {
65 if config.scanning.follow_symlinks {
66 eprintln!("SECURITY: follow_symlinks forced off");
67 config.scanning.follow_symlinks = false;
68 }
69
70 if config.scanning.include_hidden {
71 eprintln!("SECURITY: include_hidden forced off");
72 config.scanning.include_hidden = false;
73 }
74
75 if config.scanning.max_depth == 0 || config.scanning.max_depth > SAFE_MAX_SCAN_DEPTH {
76 eprintln!(
77 "SECURITY: max_depth clamped to {}",
78 SAFE_MAX_SCAN_DEPTH
79 );
80 config.scanning.max_depth = SAFE_MAX_SCAN_DEPTH;
81 }
82}
83
84impl Default for Config {
85 fn default() -> Self {
86 Self {
87 output: OutputConfig::default(),
88 scanning: ScanningConfig::default(),
89 categories: CategoryConfig::default(),
90 priority: PriorityWeights::default(),
91 priority_rules: default_priority_rules(),
92 exclude: ExcludeConfig::default(),
93 signatures: SignatureConfig::default(),
94 }
95 }
96}
97
98#[derive(Debug, Clone, Copy, Deserialize, Serialize, ValueEnum, PartialEq, Eq)]
99#[serde(rename_all = "lowercase")]
100pub enum OutputFormat {
101 Pipe,
102 Full,
103 Json,
104}
105
106impl Default for OutputFormat {
107 fn default() -> Self {
108 Self::Pipe
109 }
110}
111
112#[derive(Debug, Clone, Deserialize)]
113#[serde(default)]
114pub struct OutputConfig {
115 pub format: OutputFormat,
116 pub default_budget_tokens: usize,
117 pub default_budget_bytes: usize,
118}
119
120impl Default for OutputConfig {
121 fn default() -> Self {
122 Self {
123 format: OutputFormat::Pipe,
124 default_budget_tokens: 4000,
125 default_budget_bytes: 16_000,
126 }
127 }
128}
129
130#[derive(Debug, Clone, Deserialize)]
131#[serde(default)]
132pub struct ScanningConfig {
133 pub use_gitignore: bool,
134 pub include_hidden: bool,
135 pub max_depth: usize,
136 pub follow_symlinks: bool,
137 pub no_git_safety: bool,
138}
139
140impl Default for ScanningConfig {
141 fn default() -> Self {
142 Self {
143 use_gitignore: true,
144 include_hidden: false,
145 max_depth: 20,
146 follow_symlinks: false,
147 no_git_safety: true,
148 }
149 }
150}
151
152#[derive(Debug, Clone, Deserialize)]
153#[serde(default)]
154pub struct CategoryConfig {
155 pub code: Category,
156 pub docs: Category,
157 pub config: Category,
158 pub build: Category,
159 pub data: Category,
160}
161
162impl Default for CategoryConfig {
163 fn default() -> Self {
164 Self {
165 code: Category::new(
166 &[
167 "rs", "go", "py", "ts", "tsx", "js", "jsx", "c", "cpp", "h", "hpp", "java",
168 "rb", "ex", "exs",
169 ],
170 100,
171 ),
172 docs: Category::new(&["md", "mdx", "txt", "rst", "adoc"], 90),
173 config: Category::new(&["toml", "yaml", "yml", "json", "ini", "cfg"], 80),
174 build: Category::new(&["lock", "sum"], 20),
175 data: Category::new(&["csv", "sql"], 30),
176 }
177 }
178}
179
180#[derive(Debug, Clone, Deserialize)]
181pub struct Category {
182 pub extensions: Vec<String>,
183 pub priority: i32,
184}
185
186impl Category {
187 fn new(extensions: &[&str], priority: i32) -> Self {
188 Self {
189 extensions: extensions.iter().map(|ext| ext.to_string()).collect(),
190 priority,
191 }
192 }
193}
194
195#[derive(Debug, Clone, Deserialize)]
196pub struct PriorityRule {
197 pub pattern: String,
198 pub priority: i32,
199}
200
201#[derive(Debug, Clone, Deserialize)]
204#[serde(default)]
205pub struct PriorityWeights {
206 pub default_priority: i32,
208 pub entrypoint_boost: i32,
210 pub root_code_boost: i32,
212 pub focus_dir_boost: i32,
214 pub test_penalty: i32,
216 pub fixture_penalty: i32,
218 pub depth_penalty_step: i32,
220 pub max_depth_penalty: i32,
222}
223
224impl Default for PriorityWeights {
225 fn default() -> Self {
226 Self {
227 default_priority: 50,
228 entrypoint_boost: 40,
229 root_code_boost: 20,
230 focus_dir_boost: 15,
231 test_penalty: -40,
232 fixture_penalty: -25,
233 depth_penalty_step: -5,
234 max_depth_penalty: -30,
235 }
236 }
237}
238
239#[derive(Debug, Clone, Deserialize)]
240#[serde(default)]
241pub struct ExcludeConfig {
242 pub patterns: Vec<String>,
243}
244
245impl Default for ExcludeConfig {
246 fn default() -> Self {
247 Self {
248 patterns: vec![
249 "target/".to_string(),
250 "node_modules/".to_string(),
251 "dist/".to_string(),
252 "build/".to_string(),
253 ".git/".to_string(),
254 "__pycache__/".to_string(),
255 "*.pyc".to_string(),
256 ".DS_Store".to_string(),
257 "*.min.js".to_string(),
258 "*.min.css".to_string(),
259 "vendor/".to_string(),
260 ".venv/".to_string(),
261 "venv/".to_string(),
262 ],
263 }
264 }
265}
266
267#[derive(Debug, Clone, Deserialize)]
268#[serde(default)]
269pub struct SignatureConfig {
270 pub enabled: bool,
271 pub languages: Vec<String>,
272 pub include_functions: bool,
273 pub include_structs: bool,
274 pub include_traits: bool,
275 pub include_interfaces: bool,
276 pub include_classes: bool,
277 pub include_types: bool,
278 pub include_constants: bool,
279 pub max_signature_length: usize,
280}
281
282impl Default for SignatureConfig {
283 fn default() -> Self {
284 Self {
285 enabled: true,
286 languages: vec![
287 "rust".to_string(),
288 "go".to_string(),
289 "python".to_string(),
290 "typescript".to_string(),
291 "javascript".to_string(),
292 "c".to_string(),
293 "cpp".to_string(),
294 ],
295 include_functions: true,
296 include_structs: true,
297 include_traits: true,
298 include_interfaces: true,
299 include_classes: true,
300 include_types: true,
301 include_constants: true,
302 max_signature_length: 200,
303 }
304 }
305}
306
307fn default_priority_rules() -> Vec<PriorityRule> {
308 vec![
309 PriorityRule {
310 pattern: "README*".to_string(),
311 priority: 200,
312 },
313 PriorityRule {
314 pattern: "AGENTS.md".to_string(),
315 priority: 200,
316 },
317 PriorityRule {
318 pattern: "CLAUDE.md".to_string(),
319 priority: 200,
320 },
321 PriorityRule {
322 pattern: "Cargo.toml".to_string(),
323 priority: 150,
324 },
325 PriorityRule {
326 pattern: "package.json".to_string(),
327 priority: 150,
328 },
329 PriorityRule {
330 pattern: "go.mod".to_string(),
331 priority: 150,
332 },
333 PriorityRule {
334 pattern: "src/main.*".to_string(),
335 priority: 140,
336 },
337 PriorityRule {
338 pattern: "src/lib.*".to_string(),
339 priority: 140,
340 },
341 PriorityRule {
342 pattern: "**/mod.rs".to_string(),
343 priority: 130,
344 },
345 PriorityRule {
346 pattern: "**/*_test.*".to_string(),
347 priority: 50,
348 },
349 PriorityRule {
350 pattern: "**/test_*".to_string(),
351 priority: 50,
352 },
353 PriorityRule {
354 pattern: "**/*.lock".to_string(),
355 priority: 10,
356 },
357 ]
358}
359
360const SECURITY_EXCLUDE_PATTERNS: &[&str] = &[
361 ".env",
362 ".env.*",
363 "*.pem",
364 "*.key",
365 "credentials*",
366];
367
368pub fn security_exclude_patterns() -> Vec<String> {
369 SECURITY_EXCLUDE_PATTERNS
370 .iter()
371 .map(|pattern| pattern.to_string())
372 .collect()
373}