devops_validate/rules/
loader.rs1use super::engine::{Rule, RuleEngine};
6
7const 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
81const 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
90pub fn load_builtin_rules() -> RuleEngine {
92 let mut rules = Vec::new();
93
94 if let Ok(k8s_rules) = parse_rules_yaml(K8S_RULES_YAML) {
96 rules.extend(k8s_rules);
97 }
98
99 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
107pub 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
117fn 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 let first = &rules[0];
153 assert_eq!(first.id, "k8s/replicas-1");
154 assert_eq!(first.severity, "warning");
155 }
156}