syncable_cli/analyzer/helmlint/rules/
hl4xxx.rs

1//! HL4xxx - Security Rules
2//!
3//! Rules for validating container and Kubernetes security settings.
4
5use crate::analyzer::helmlint::parser::template::TemplateToken;
6use crate::analyzer::helmlint::rules::{LintContext, Rule};
7use crate::analyzer::helmlint::types::{CheckFailure, RuleCategory, Severity};
8
9/// Get all HL4xxx rules.
10pub fn rules() -> Vec<Box<dyn Rule>> {
11    vec![
12        Box::new(HL4001),
13        Box::new(HL4002),
14        Box::new(HL4003),
15        Box::new(HL4004),
16        Box::new(HL4005),
17        Box::new(HL4006),
18        Box::new(HL4011),
19        Box::new(HL4012),
20    ]
21}
22
23/// HL4001: Container running as root
24pub struct HL4001;
25
26impl Rule for HL4001 {
27    fn code(&self) -> &'static str {
28        "HL4001"
29    }
30
31    fn severity(&self) -> Severity {
32        Severity::Warning
33    }
34
35    fn name(&self) -> &'static str {
36        "container-runs-as-root"
37    }
38
39    fn description(&self) -> &'static str {
40        "Container may run as root user"
41    }
42
43    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
44        let mut failures = Vec::new();
45
46        // Check values.yaml for runAsNonRoot settings
47        if let Some(values) = ctx.values {
48            // Look for securityContext settings
49            let has_run_as_non_root = values
50                .defined_paths
51                .iter()
52                .any(|p| p.to_lowercase().contains("runasnonroot"));
53
54            let has_run_as_user = values
55                .defined_paths
56                .iter()
57                .any(|p| p.to_lowercase().contains("runasuser"));
58
59            if !has_run_as_non_root && !has_run_as_user {
60                failures.push(CheckFailure::new(
61                    "HL4001",
62                    Severity::Warning,
63                    "No runAsNonRoot or runAsUser setting found. Container may run as root",
64                    "values.yaml",
65                    1,
66                    RuleCategory::Security,
67                ));
68            }
69        }
70
71        // Check templates for hardcoded security contexts
72        for template in ctx.templates {
73            let content = template
74                .tokens
75                .iter()
76                .filter_map(|t| match t {
77                    TemplateToken::Text { content, .. } => Some(content.as_str()),
78                    _ => None,
79                })
80                .collect::<Vec<_>>()
81                .join("");
82
83            // Check for runAsUser: 0 (root)
84            if content.contains("runAsUser: 0") || content.contains("runAsUser:0") {
85                failures.push(CheckFailure::new(
86                    "HL4001",
87                    Severity::Warning,
88                    "Container is configured to run as root (runAsUser: 0)",
89                    &template.path,
90                    1,
91                    RuleCategory::Security,
92                ));
93            }
94        }
95
96        failures
97    }
98}
99
100/// HL4002: Privileged container
101pub struct HL4002;
102
103impl Rule for HL4002 {
104    fn code(&self) -> &'static str {
105        "HL4002"
106    }
107
108    fn severity(&self) -> Severity {
109        Severity::Error
110    }
111
112    fn name(&self) -> &'static str {
113        "privileged-container"
114    }
115
116    fn description(&self) -> &'static str {
117        "Container runs in privileged mode"
118    }
119
120    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
121        let mut failures = Vec::new();
122
123        // Check values.yaml
124        if let Some(values) = ctx.values {
125            for path in &values.defined_paths {
126                if path.to_lowercase().contains("privileged") {
127                    if let Some(value) = values.get(path) {
128                        if is_truthy(value) {
129                            let line = values.line_for_path(path).unwrap_or(1);
130                            failures.push(CheckFailure::new(
131                                "HL4002",
132                                Severity::Error,
133                                format!("Privileged mode enabled at '{}'", path),
134                                "values.yaml",
135                                line,
136                                RuleCategory::Security,
137                            ));
138                        }
139                    }
140                }
141            }
142        }
143
144        // Check templates for hardcoded privileged: true
145        for template in ctx.templates {
146            for token in &template.tokens {
147                if let TemplateToken::Text { content, line } = token {
148                    if content.contains("privileged: true") {
149                        failures.push(CheckFailure::new(
150                            "HL4002",
151                            Severity::Error,
152                            "Container is configured with privileged: true",
153                            &template.path,
154                            *line,
155                            RuleCategory::Security,
156                        ));
157                    }
158                }
159            }
160        }
161
162        failures
163    }
164}
165
166/// HL4003: HostPath volume mount
167pub struct HL4003;
168
169impl Rule for HL4003 {
170    fn code(&self) -> &'static str {
171        "HL4003"
172    }
173
174    fn severity(&self) -> Severity {
175        Severity::Warning
176    }
177
178    fn name(&self) -> &'static str {
179        "hostpath-volume"
180    }
181
182    fn description(&self) -> &'static str {
183        "Using hostPath volumes can expose host filesystem"
184    }
185
186    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
187        let mut failures = Vec::new();
188
189        for template in ctx.templates {
190            for token in &template.tokens {
191                if let TemplateToken::Text { content, line } = token {
192                    if content.contains("hostPath:") {
193                        failures.push(CheckFailure::new(
194                            "HL4003",
195                            Severity::Warning,
196                            "Using hostPath volume mount. This can expose the host filesystem to the container",
197                            &template.path,
198                            *line,
199                            RuleCategory::Security,
200                        ));
201                    }
202                }
203            }
204        }
205
206        failures
207    }
208}
209
210/// HL4004: HostNetwork enabled
211pub struct HL4004;
212
213impl Rule for HL4004 {
214    fn code(&self) -> &'static str {
215        "HL4004"
216    }
217
218    fn severity(&self) -> Severity {
219        Severity::Warning
220    }
221
222    fn name(&self) -> &'static str {
223        "host-network"
224    }
225
226    fn description(&self) -> &'static str {
227        "Using host network can bypass network policies"
228    }
229
230    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
231        let mut failures = Vec::new();
232
233        // Check values.yaml
234        if let Some(values) = ctx.values {
235            for path in &values.defined_paths {
236                if path.to_lowercase().contains("hostnetwork") {
237                    if let Some(value) = values.get(path) {
238                        if is_truthy(value) {
239                            let line = values.line_for_path(path).unwrap_or(1);
240                            failures.push(CheckFailure::new(
241                                "HL4004",
242                                Severity::Warning,
243                                format!("Host network enabled at '{}'", path),
244                                "values.yaml",
245                                line,
246                                RuleCategory::Security,
247                            ));
248                        }
249                    }
250                }
251            }
252        }
253
254        // Check templates
255        for template in ctx.templates {
256            for token in &template.tokens {
257                if let TemplateToken::Text { content, line } = token {
258                    if content.contains("hostNetwork: true") {
259                        failures.push(CheckFailure::new(
260                            "HL4004",
261                            Severity::Warning,
262                            "Pod uses host network. This bypasses network policies",
263                            &template.path,
264                            *line,
265                            RuleCategory::Security,
266                        ));
267                    }
268                }
269            }
270        }
271
272        failures
273    }
274}
275
276/// HL4005: HostPID enabled
277pub struct HL4005;
278
279impl Rule for HL4005 {
280    fn code(&self) -> &'static str {
281        "HL4005"
282    }
283
284    fn severity(&self) -> Severity {
285        Severity::Warning
286    }
287
288    fn name(&self) -> &'static str {
289        "host-pid"
290    }
291
292    fn description(&self) -> &'static str {
293        "Using host PID namespace can expose host processes"
294    }
295
296    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
297        let mut failures = Vec::new();
298
299        for template in ctx.templates {
300            for token in &template.tokens {
301                if let TemplateToken::Text { content, line } = token {
302                    if content.contains("hostPID: true") {
303                        failures.push(CheckFailure::new(
304                            "HL4005",
305                            Severity::Warning,
306                            "Pod uses host PID namespace. This can expose host processes",
307                            &template.path,
308                            *line,
309                            RuleCategory::Security,
310                        ));
311                    }
312                }
313            }
314        }
315
316        failures
317    }
318}
319
320/// HL4006: Missing securityContext
321pub struct HL4006;
322
323impl Rule for HL4006 {
324    fn code(&self) -> &'static str {
325        "HL4006"
326    }
327
328    fn severity(&self) -> Severity {
329        Severity::Info
330    }
331
332    fn name(&self) -> &'static str {
333        "missing-security-context"
334    }
335
336    fn description(&self) -> &'static str {
337        "Container or pod is missing securityContext"
338    }
339
340    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
341        let mut failures = Vec::new();
342
343        // Check if values.yaml has any security context settings
344        if let Some(values) = ctx.values {
345            let has_security_context = values
346                .defined_paths
347                .iter()
348                .any(|p| p.to_lowercase().contains("securitycontext"));
349
350            if !has_security_context {
351                failures.push(CheckFailure::new(
352                    "HL4006",
353                    Severity::Info,
354                    "No securityContext configuration found in values.yaml",
355                    "values.yaml",
356                    1,
357                    RuleCategory::Security,
358                ));
359            }
360        }
361
362        failures
363    }
364}
365
366/// HL4011: Secret in environment variable
367pub struct HL4011;
368
369impl Rule for HL4011 {
370    fn code(&self) -> &'static str {
371        "HL4011"
372    }
373
374    fn severity(&self) -> Severity {
375        Severity::Warning
376    }
377
378    fn name(&self) -> &'static str {
379        "secret-in-env"
380    }
381
382    fn description(&self) -> &'static str {
383        "Sensitive value passed via environment variable instead of mounted secret"
384    }
385
386    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
387        let mut failures = Vec::new();
388
389        // Look for environment variables with sensitive names and direct values
390        let sensitive_patterns = [
391            "PASSWORD",
392            "SECRET",
393            "TOKEN",
394            "API_KEY",
395            "APIKEY",
396            "PRIVATE_KEY",
397            "CREDENTIALS",
398        ];
399
400        for template in ctx.templates {
401            for token in &template.tokens {
402                if let TemplateToken::Text { content, line } = token {
403                    // Check if this looks like an env definition with a sensitive name
404                    for pattern in &sensitive_patterns {
405                        let search = format!("name: {}", pattern);
406                        let search_lower = format!("name: {}", pattern.to_lowercase());
407                        if (content.contains(&search) || content.contains(&search_lower))
408                            && content.contains("value:")
409                            && !content.contains("valueFrom:")
410                            && !content.contains("secretKeyRef:")
411                        {
412                            failures.push(CheckFailure::new(
413                                "HL4011",
414                                Severity::Warning,
415                                format!(
416                                    "Environment variable matching '{}' should use secretKeyRef instead of direct value",
417                                    pattern
418                                ),
419                                &template.path,
420                                *line,
421                                RuleCategory::Security,
422                            ));
423                        }
424                    }
425                }
426            }
427        }
428
429        failures
430    }
431}
432
433/// HL4012: Hardcoded credentials detected
434pub struct HL4012;
435
436impl Rule for HL4012 {
437    fn code(&self) -> &'static str {
438        "HL4012"
439    }
440
441    fn severity(&self) -> Severity {
442        Severity::Error
443    }
444
445    fn name(&self) -> &'static str {
446        "hardcoded-credentials"
447    }
448
449    fn description(&self) -> &'static str {
450        "Hardcoded credentials or secrets detected in templates"
451    }
452
453    fn check(&self, ctx: &LintContext) -> Vec<CheckFailure> {
454        let mut failures = Vec::new();
455
456        // Credential types to check for
457        let credential_types = [
458            ("password:", "password"),
459            ("secret:", "secret"),
460            ("apikey:", "API key"),
461            ("token:", "token"),
462        ];
463
464        for template in ctx.templates {
465            for token in &template.tokens {
466                if let TemplateToken::Text { content, line } = token {
467                    let lower_content = content.to_lowercase();
468
469                    for (pattern, cred_type) in &credential_types {
470                        // Check for patterns that look like credentials
471                        if lower_content.contains(pattern) {
472                            // Make sure it's not using a template variable
473                            let has_template_var = content.contains("{{") && content.contains("}}");
474                            let is_empty = content.contains("\"\"") || content.contains("''");
475
476                            if !has_template_var && !is_empty {
477                                // Additional check: line should have an actual value
478                                let parts: Vec<&str> = content.split(':').collect();
479                                if parts.len() >= 2 {
480                                    let value_part = parts[1].trim();
481                                    if !value_part.is_empty()
482                                        && !value_part.starts_with('{')
483                                        && !value_part.starts_with('$')
484                                        && value_part != "\"\""
485                                        && value_part != "''"
486                                    {
487                                        failures.push(CheckFailure::new(
488                                            "HL4012",
489                                            Severity::Error,
490                                            format!(
491                                                "Possible hardcoded {} detected. Use Secrets instead",
492                                                cred_type
493                                            ),
494                                            &template.path,
495                                            *line,
496                                            RuleCategory::Security,
497                                        ));
498                                        break;
499                                    }
500                                }
501                            }
502                        }
503                    }
504                }
505            }
506        }
507
508        failures
509    }
510}
511
512/// Check if a YAML value is truthy.
513fn is_truthy(value: &serde_yaml::Value) -> bool {
514    match value {
515        serde_yaml::Value::Bool(b) => *b,
516        serde_yaml::Value::String(s) => {
517            let lower = s.to_lowercase();
518            lower == "true" || lower == "yes" || lower == "1"
519        }
520        serde_yaml::Value::Number(n) => n.as_i64().map(|i| i != 0).unwrap_or(false),
521        _ => false,
522    }
523}
524
525#[cfg(test)]
526mod tests {
527    use super::*;
528
529    #[test]
530    fn test_is_truthy() {
531        assert!(is_truthy(&serde_yaml::Value::Bool(true)));
532        assert!(!is_truthy(&serde_yaml::Value::Bool(false)));
533        assert!(is_truthy(&serde_yaml::Value::String("true".to_string())));
534        assert!(is_truthy(&serde_yaml::Value::String("yes".to_string())));
535        assert!(!is_truthy(&serde_yaml::Value::String("false".to_string())));
536        assert!(is_truthy(&serde_yaml::Value::Number(1.into())));
537        assert!(!is_truthy(&serde_yaml::Value::Number(0.into())));
538    }
539
540    #[test]
541    fn test_rules_exist() {
542        let all_rules = rules();
543        assert!(!all_rules.is_empty());
544    }
545}