1use super::manifest_validator::{
7 ManifestValidationContext, ManifestValidationRule, ValidationOutcome,
8};
9use super::rule_id::{CoreRuleId, RuleIdentifier};
10
11pub struct InputNamingConventionRule;
13
14impl ManifestValidationRule for InputNamingConventionRule {
15 fn id(&self) -> RuleIdentifier {
16 RuleIdentifier::Core(CoreRuleId::InputNamingConvention)
17 }
18
19 fn description(&self) -> &'static str {
20 "Validates that inputs follow naming conventions"
21 }
22
23 fn check(&self, ctx: &ManifestValidationContext) -> ValidationOutcome {
24 if ctx.input_name.contains('-') {
26 return ValidationOutcome::Warning {
27 message: format!(
28 "Input '{}' contains hyphens. Consider using underscores for consistency",
29 ctx.full_name
30 ),
31 suggestion: Some(format!("Rename to '{}'", ctx.full_name.replace('-', "_"))),
32 };
33 }
34
35 if ctx.input_name.chars().any(|c| c.is_uppercase()) {
36 return ValidationOutcome::Warning {
37 message: format!(
38 "Input '{}' contains uppercase letters. Consider using lowercase for consistency",
39 ctx.full_name
40 ),
41 suggestion: Some(format!(
42 "Rename to '{}'",
43 ctx.full_name.to_lowercase()
44 )),
45 };
46 }
47
48 ValidationOutcome::Pass
49 }
50}
51
52pub struct CliInputOverrideRule;
54
55impl ManifestValidationRule for CliInputOverrideRule {
56 fn id(&self) -> RuleIdentifier {
57 RuleIdentifier::Core(CoreRuleId::CliInputOverride)
58 }
59
60 fn description(&self) -> &'static str {
61 "Warns when CLI inputs override environment values"
62 }
63
64 fn check(&self, ctx: &ManifestValidationContext) -> ValidationOutcome {
65 match (
66 ctx.cli_inputs.iter().find(|(k, _)| k == ctx.input_name),
67 ctx.effective_inputs.get(ctx.input_name),
68 ) {
69 (Some((_, cli_value)), Some(env_value)) if cli_value != env_value => {
70 ValidationOutcome::Warning {
71 message: format!("CLI input '{}' overrides environment value", ctx.input_name),
72 suggestion: Some(format!(
73 "CLI value '{}' will be used instead of environment value '{}'",
74 cli_value, env_value
75 )),
76 }
77 }
78 _ => ValidationOutcome::Pass,
79 }
80 }
81}
82
83pub struct SensitiveDataRule;
85
86impl ManifestValidationRule for SensitiveDataRule {
87 fn id(&self) -> RuleIdentifier {
88 RuleIdentifier::Core(CoreRuleId::SensitiveData)
89 }
90
91 fn description(&self) -> &'static str {
92 "Detects potential sensitive data in inputs"
93 }
94
95 fn check(&self, ctx: &ManifestValidationContext) -> ValidationOutcome {
96 const SENSITIVE_PATTERNS: &[&str] = &[
97 "password",
98 "passwd",
99 "secret",
100 "token",
101 "key",
102 "credential",
103 "private",
104 "auth",
105 "apikey",
106 "api_key",
107 "access_key",
108 ];
109
110 let lower_name = ctx.input_name.to_lowercase();
111
112 if !SENSITIVE_PATTERNS.iter().any(|&p| lower_name.contains(p)) {
113 return ValidationOutcome::Pass;
114 }
115
116 let Some(value) = ctx.effective_inputs.get(ctx.input_name) else {
117 return ValidationOutcome::Pass;
118 };
119
120 if value.starts_with('<') && value.ends_with('>') {
121 return ValidationOutcome::Warning {
122 message: format!(
123 "Input '{}' appears to contain sensitive data with placeholder value",
124 ctx.full_name
125 ),
126 suggestion: Some("Ensure this value is properly set before deployment".to_string()),
127 };
128 }
129
130 if !value.starts_with("${") && !value.starts_with("input.") {
131 return ValidationOutcome::Warning {
132 message: format!("Input '{}' may contain hardcoded sensitive data", ctx.full_name),
133 suggestion: Some(
134 "Consider using environment variables or secure secret management".to_string(),
135 ),
136 };
137 }
138
139 ValidationOutcome::Pass
140 }
141}
142
143pub struct NoDefaultValuesRule;
145
146impl ManifestValidationRule for NoDefaultValuesRule {
147 fn id(&self) -> RuleIdentifier {
148 RuleIdentifier::Core(CoreRuleId::NoDefaultValues)
149 }
150
151 fn description(&self) -> &'static str {
152 "Ensures production environments don't use default values"
153 }
154
155 fn check(&self, ctx: &ManifestValidationContext) -> ValidationOutcome {
156 if !matches!(ctx.environment, Some("production" | "prod")) {
158 return ValidationOutcome::Pass;
159 }
160
161 match (
162 ctx.manifest.environments.get("defaults").and_then(|d| d.get(ctx.input_name)),
163 ctx.effective_inputs.get(ctx.input_name),
164 ) {
165 (Some(default_value), Some(env_value)) if default_value == env_value => {
166 ValidationOutcome::Warning {
167 message: format!(
168 "Production environment is using default value for '{}'",
169 ctx.full_name
170 ),
171 suggestion: Some(
172 "Define an explicit value for production environment".to_string(),
173 ),
174 }
175 }
176 _ => ValidationOutcome::Pass,
177 }
178 }
179}
180
181pub struct RequiredProductionInputsRule;
183
184impl ManifestValidationRule for RequiredProductionInputsRule {
185 fn id(&self) -> RuleIdentifier {
186 RuleIdentifier::Core(CoreRuleId::RequiredProductionInputs)
187 }
188
189 fn description(&self) -> &'static str {
190 "Ensures required inputs are present in production"
191 }
192
193 fn check(&self, ctx: &ManifestValidationContext) -> ValidationOutcome {
194 const REQUIRED_PATTERNS: &[&str] = &[
195 "api_url",
196 "api_endpoint",
197 "base_url",
198 "api_token",
199 "api_key",
200 "auth_token",
201 "chain_id",
202 "network_id",
203 ];
204
205 if !matches!(ctx.environment, Some("production" | "prod")) {
207 return ValidationOutcome::Pass;
208 }
209
210 let lower_name = ctx.input_name.to_lowercase();
211
212 if REQUIRED_PATTERNS.iter().any(|&p| lower_name.contains(p))
213 && !ctx.effective_inputs.contains_key(ctx.input_name)
214 {
215 ValidationOutcome::Error {
216 message: format!(
217 "Required production input '{}' is not defined",
218 ctx.full_name
219 ),
220 context: Some(
221 "Production environments must define all API endpoints and authentication tokens".to_string()
222 ),
223 suggestion: Some(
224 "Add this input to your production environment configuration".to_string()
225 ),
226 documentation_link: Some(
227 "https://docs.txtx.sh/deployment/production".to_string()
228 ),
229 }
230 } else {
231 ValidationOutcome::Pass
232 }
233 }
234}
235
236pub fn get_linter_rules() -> Vec<Box<dyn ManifestValidationRule>> {
238 vec![
239 Box::new(InputNamingConventionRule),
240 Box::new(CliInputOverrideRule),
241 Box::new(SensitiveDataRule),
242 ]
243}
244
245pub fn get_strict_linter_rules() -> Vec<Box<dyn ManifestValidationRule>> {
247 vec![
248 Box::new(InputNamingConventionRule),
249 Box::new(CliInputOverrideRule),
250 Box::new(SensitiveDataRule),
251 Box::new(NoDefaultValuesRule),
252 Box::new(RequiredProductionInputsRule),
253 ]
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259 use crate::manifest::WorkspaceManifest;
260 use std::collections::{HashMap, HashSet};
261 use txtx_addon_kit::indexmap::IndexMap;
262
263 fn create_test_context<'a>(
264 input_name: &'a str,
265 full_name: &'a str,
266 manifest: &'a WorkspaceManifest,
267 effective_inputs: &'a HashMap<String, String>,
268 ) -> ManifestValidationContext<'a> {
269 ManifestValidationContext {
270 input_name,
271 full_name,
272 manifest,
273 environment: Some("production"),
274 effective_inputs,
275 cli_inputs: &[],
276 content: "",
277 file_path: "test.tx",
278 active_addons: HashSet::new(),
279 }
280 }
281
282 #[test]
283 fn test_naming_convention_rule() {
284 let manifest = WorkspaceManifest {
285 name: "test".to_string(),
286 id: "test".to_string(),
287 runbooks: vec![],
288 environments: IndexMap::new(),
289 location: None,
290 };
291
292 let inputs = HashMap::new();
293 let rule = InputNamingConventionRule;
294
295 let ctx = create_test_context("api-key", "input.api-key", &manifest, &inputs);
297 match rule.check(&ctx) {
298 ValidationOutcome::Warning { message, .. } => {
299 assert!(message.contains("hyphens"));
300 }
301 _ => panic!("Expected warning for hyphenated name"),
302 }
303
304 let ctx = create_test_context("ApiKey", "input.ApiKey", &manifest, &inputs);
306 match rule.check(&ctx) {
307 ValidationOutcome::Warning { message, .. } => {
308 assert!(message.contains("uppercase"));
309 }
310 _ => panic!("Expected warning for uppercase name"),
311 }
312
313 let ctx = create_test_context("api_key", "input.api_key", &manifest, &inputs);
315 match rule.check(&ctx) {
316 ValidationOutcome::Pass => {}
317 _ => panic!("Expected pass for valid name"),
318 }
319 }
320
321 #[test]
322 fn test_sensitive_data_rule() {
323 let manifest = WorkspaceManifest {
324 name: "test".to_string(),
325 id: "test".to_string(),
326 runbooks: vec![],
327 environments: IndexMap::new(),
328 location: None,
329 };
330
331 let mut inputs = HashMap::new();
332 inputs.insert("api_key".to_string(), "hardcoded123".to_string());
333
334 let rule = SensitiveDataRule;
335 let ctx = create_test_context("api_key", "input.api_key", &manifest, &inputs);
336
337 match rule.check(&ctx) {
338 ValidationOutcome::Warning { message, .. } => {
339 assert!(message.contains("hardcoded sensitive data"));
340 }
341 _ => panic!("Expected warning for hardcoded sensitive data"),
342 }
343 }
344}