Skip to main content

devops_validate/rules/
loader.rs

1//! Rule loader for built-in and custom rules
2//!
3//! Loads rules from embedded YAML or external sources.
4
5use super::engine::{Rule, RuleEngine};
6
7/// Embedded K8s best-practice rules
8const K8S_RULES_YAML: &str = r#"
9rules:
10  - id: k8s/replicas-1
11    condition: '$.spec.replicas == 1'
12    severity: warning
13    message: 'replicas=1 — consider >=2 for high availability'
14
15  - id: k8s/no-resource-limits
16    condition: '$.spec.template.spec.containers[0].resources.limits == null'
17    severity: warning
18    message: 'Container has no resource limits — may cause OOM kills'
19
20  - id: k8s/no-liveness-probe
21    condition: '$.spec.template.spec.containers[0].livenessProbe == null'
22    severity: warning
23    message: 'Container has no livenessProbe — Kubernetes will not detect hangs'
24
25  - id: k8s/no-readiness-probe
26    condition: '$.spec.template.spec.containers[0].readinessProbe == null'
27    severity: warning
28    message: 'Container has no readinessProbe — traffic may be sent to unready pods'
29
30  - id: k8s/latest-image-tag
31    condition: '$.spec.template.spec.containers[0].image contains ":latest"'
32    severity: warning
33    message: 'Image uses :latest tag — pin a specific version for reproducibility'
34
35  - id: k8s/no-resource-requests
36    condition: '$.spec.template.spec.containers[0].resources.requests == null'
37    severity: info
38    message: 'Container has no resource requests — scheduler cannot make optimal placement decisions'
39
40  - id: k8s/always-pull-policy
41    condition: '$.spec.template.spec.containers[0].imagePullPolicy == "Always"'
42    severity: info
43    message: 'imagePullPolicy=Always adds latency to every pod start — use IfNotPresent with a pinned tag'
44
45  - id: k8s/no-security-context
46    condition: '$.spec.template.spec.containers[0].securityContext == null'
47    severity: hint
48    message: 'No securityContext set — consider runAsNonRoot, readOnlyRootFilesystem, allowPrivilegeEscalation: false'
49
50  - id: k8s/host-network
51    condition: '$.spec.template.spec.hostNetwork == true'
52    severity: warning
53    message: 'hostNetwork=true — pods share the host network namespace (security risk)'
54
55  - id: k8s/privileged-container
56    condition: '$.spec.template.spec.containers[0].securityContext.privileged == true'
57    severity: warning
58    message: 'privileged=true — container has full host access (security risk)'
59
60  - id: k8s/run-as-root
61    condition: '$.spec.template.spec.containers[0].securityContext.runAsNonRoot != true'
62    severity: info
63    message: 'Container may run as root — consider runAsNonRoot: true'
64
65  - id: k8s/service-type-loadbalancer
66    condition: '$.spec.type == "LoadBalancer"'
67    severity: warning
68    message: 'type=LoadBalancer creates a cloud load balancer — ensure this is intentional (cost implications)'
69
70  - id: k8s/service-empty-selector
71    condition: '$.spec.selector == null'
72    severity: warning
73    message: 'Service has no selector — will match no pods'
74
75  - id: k8s/hpa-min-gt-max
76    condition: '$.spec.minReplicas > $.spec.maxReplicas'
77    severity: error
78    message: 'minReplicas cannot be greater than maxReplicas'
79"#;
80
81/// Embedded GitLab CI rules
82const GITLAB_CI_RULES_YAML: &str = r#"
83rules:
84  - id: gitlab-ci/no-stages
85    condition: '$.stages == null'
86    severity: info
87    message: 'No stages defined — using default stages: build, test, deploy'
88"#;
89
90/// Load built-in rules for all supported types
91pub fn load_builtin_rules() -> RuleEngine {
92    let mut rules = Vec::new();
93
94    // Load K8s rules
95    if let Ok(k8s_rules) = parse_rules_yaml(K8S_RULES_YAML) {
96        rules.extend(k8s_rules);
97    }
98
99    // Load GitLab CI rules
100    if let Ok(gitlab_rules) = parse_rules_yaml(GITLAB_CI_RULES_YAML) {
101        rules.extend(gitlab_rules);
102    }
103
104    RuleEngine::with_rules(rules)
105}
106
107/// Load rules for a specific type
108pub fn load_rules_for_type(yaml_type: &str) -> RuleEngine {
109    let rules = match yaml_type.split('/').next().unwrap_or(yaml_type) {
110        "k8s" => parse_rules_yaml(K8S_RULES_YAML).unwrap_or_default(),
111        "gitlab-ci" => parse_rules_yaml(GITLAB_CI_RULES_YAML).unwrap_or_default(),
112        _ => Vec::new(),
113    };
114    RuleEngine::with_rules(rules)
115}
116
117/// Parse rules from YAML string
118fn parse_rules_yaml(yaml: &str) -> Result<Vec<Rule>, String> {
119    #[derive(serde::Deserialize)]
120    struct RulesDoc {
121        rules: Vec<Rule>,
122    }
123
124    let doc: RulesDoc =
125        serde_yaml::from_str(yaml).map_err(|e| format!("Failed to parse rules YAML: {}", e))?;
126
127    Ok(doc.rules)
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_load_builtin_rules() {
136        let engine = load_builtin_rules();
137        assert!(engine.rule_count() > 0);
138    }
139
140    #[test]
141    fn test_load_k8s_rules() {
142        let engine = load_rules_for_type("k8s/deployment");
143        assert!(engine.rule_count() > 0);
144    }
145
146    #[test]
147    fn test_parse_rules_yaml() {
148        let rules = parse_rules_yaml(K8S_RULES_YAML).unwrap();
149        assert!(!rules.is_empty());
150
151        // Check first rule
152        let first = &rules[0];
153        assert_eq!(first.id, "k8s/replicas-1");
154        assert_eq!(first.severity, "warning");
155    }
156}