syncable_cli/analyzer/kubelint/templates/
ports.rs

1//! Port-related check templates.
2
3use crate::analyzer::kubelint::context::Object;
4use crate::analyzer::kubelint::extract;
5use crate::analyzer::kubelint::templates::{CheckFunc, ParameterDesc, Template, TemplateError};
6use crate::analyzer::kubelint::types::{Diagnostic, ObjectKindsDesc};
7
8/// Template for detecting privileged ports (< 1024).
9pub struct PrivilegedPortsTemplate;
10
11impl Template for PrivilegedPortsTemplate {
12    fn key(&self) -> &str {
13        "privileged-ports"
14    }
15
16    fn human_name(&self) -> &str {
17        "Privileged Ports"
18    }
19
20    fn description(&self) -> &str {
21        "Detects containers using privileged ports (< 1024)"
22    }
23
24    fn supported_object_kinds(&self) -> ObjectKindsDesc {
25        ObjectKindsDesc::default()
26    }
27
28    fn parameters(&self) -> Vec<ParameterDesc> {
29        Vec::new()
30    }
31
32    fn instantiate(
33        &self,
34        _params: &serde_yaml::Value,
35    ) -> Result<Box<dyn CheckFunc>, TemplateError> {
36        Ok(Box::new(PrivilegedPortsCheck))
37    }
38}
39
40struct PrivilegedPortsCheck;
41
42impl CheckFunc for PrivilegedPortsCheck {
43    fn check(&self, object: &Object) -> Vec<Diagnostic> {
44        let mut diagnostics = Vec::new();
45
46        if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) {
47            for container in extract::container::all_containers(pod_spec) {
48                for port in &container.ports {
49                    if port.container_port < 1024 {
50                        diagnostics.push(Diagnostic {
51                            message: format!(
52                                "Container '{}' uses privileged port {}",
53                                container.name, port.container_port
54                            ),
55                            remediation: Some(
56                                "Use ports >= 1024 to avoid requiring NET_BIND_SERVICE \
57                                 capability. Map privileged ports via Service if needed."
58                                    .to_string(),
59                            ),
60                        });
61                    }
62                }
63            }
64        }
65
66        diagnostics
67    }
68}
69
70/// Template for detecting SSH port usage.
71pub struct SSHPortTemplate;
72
73impl Template for SSHPortTemplate {
74    fn key(&self) -> &str {
75        "ssh-port"
76    }
77
78    fn human_name(&self) -> &str {
79        "SSH Port"
80    }
81
82    fn description(&self) -> &str {
83        "Detects containers exposing SSH port (22)"
84    }
85
86    fn supported_object_kinds(&self) -> ObjectKindsDesc {
87        ObjectKindsDesc::default()
88    }
89
90    fn parameters(&self) -> Vec<ParameterDesc> {
91        Vec::new()
92    }
93
94    fn instantiate(
95        &self,
96        _params: &serde_yaml::Value,
97    ) -> Result<Box<dyn CheckFunc>, TemplateError> {
98        Ok(Box::new(SSHPortCheck))
99    }
100}
101
102struct SSHPortCheck;
103
104impl CheckFunc for SSHPortCheck {
105    fn check(&self, object: &Object) -> Vec<Diagnostic> {
106        let mut diagnostics = Vec::new();
107
108        if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) {
109            for container in extract::container::all_containers(pod_spec) {
110                for port in &container.ports {
111                    if port.container_port == 22 {
112                        diagnostics.push(Diagnostic {
113                            message: format!("Container '{}' exposes SSH port 22", container.name),
114                            remediation: Some(
115                                "SSH access in containers is generally discouraged. \
116                                 Use kubectl exec for debugging or remove SSH."
117                                    .to_string(),
118                            ),
119                        });
120                    }
121                }
122            }
123        }
124
125        diagnostics
126    }
127}
128
129/// Template for validating liveness probe port matches container port.
130pub struct LivenessPortTemplate;
131
132impl Template for LivenessPortTemplate {
133    fn key(&self) -> &str {
134        "liveness-port"
135    }
136
137    fn human_name(&self) -> &str {
138        "Liveness Probe Port"
139    }
140
141    fn description(&self) -> &str {
142        "Validates that liveness probe port matches an exposed container port"
143    }
144
145    fn supported_object_kinds(&self) -> ObjectKindsDesc {
146        ObjectKindsDesc::default()
147    }
148
149    fn parameters(&self) -> Vec<ParameterDesc> {
150        Vec::new()
151    }
152
153    fn instantiate(
154        &self,
155        _params: &serde_yaml::Value,
156    ) -> Result<Box<dyn CheckFunc>, TemplateError> {
157        Ok(Box::new(LivenessPortCheck))
158    }
159}
160
161struct LivenessPortCheck;
162
163impl CheckFunc for LivenessPortCheck {
164    fn check(&self, object: &Object) -> Vec<Diagnostic> {
165        let mut diagnostics = Vec::new();
166
167        if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) {
168            for container in extract::container::containers(pod_spec) {
169                if let Some(probe) = &container.liveness_probe {
170                    let probe_port = probe
171                        .http_get
172                        .as_ref()
173                        .map(|h| h.port)
174                        .or_else(|| probe.tcp_socket.as_ref().map(|t| t.port));
175
176                    if let Some(port_num) = probe_port {
177                        let has_matching_port =
178                            container.ports.iter().any(|p| p.container_port == port_num);
179
180                        if !has_matching_port && !container.ports.is_empty() {
181                            diagnostics.push(Diagnostic {
182                                message: format!(
183                                    "Container '{}' liveness probe uses port {} which is not exposed",
184                                    container.name, port_num
185                                ),
186                                remediation: Some(
187                                    "Ensure the liveness probe port matches an exposed container port."
188                                        .to_string(),
189                                ),
190                            });
191                        }
192                    }
193                }
194            }
195        }
196
197        diagnostics
198    }
199}
200
201/// Template for validating readiness probe port matches container port.
202pub struct ReadinessPortTemplate;
203
204impl Template for ReadinessPortTemplate {
205    fn key(&self) -> &str {
206        "readiness-port"
207    }
208
209    fn human_name(&self) -> &str {
210        "Readiness Probe Port"
211    }
212
213    fn description(&self) -> &str {
214        "Validates that readiness probe port matches an exposed container port"
215    }
216
217    fn supported_object_kinds(&self) -> ObjectKindsDesc {
218        ObjectKindsDesc::default()
219    }
220
221    fn parameters(&self) -> Vec<ParameterDesc> {
222        Vec::new()
223    }
224
225    fn instantiate(
226        &self,
227        _params: &serde_yaml::Value,
228    ) -> Result<Box<dyn CheckFunc>, TemplateError> {
229        Ok(Box::new(ReadinessPortCheck))
230    }
231}
232
233struct ReadinessPortCheck;
234
235impl CheckFunc for ReadinessPortCheck {
236    fn check(&self, object: &Object) -> Vec<Diagnostic> {
237        let mut diagnostics = Vec::new();
238
239        if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) {
240            for container in extract::container::containers(pod_spec) {
241                if let Some(probe) = &container.readiness_probe {
242                    let probe_port = probe
243                        .http_get
244                        .as_ref()
245                        .map(|h| h.port)
246                        .or_else(|| probe.tcp_socket.as_ref().map(|t| t.port));
247
248                    if let Some(port_num) = probe_port {
249                        let has_matching_port =
250                            container.ports.iter().any(|p| p.container_port == port_num);
251
252                        if !has_matching_port && !container.ports.is_empty() {
253                            diagnostics.push(Diagnostic {
254                                message: format!(
255                                    "Container '{}' readiness probe uses port {} which is not exposed",
256                                    container.name, port_num
257                                ),
258                                remediation: Some(
259                                    "Ensure the readiness probe port matches an exposed container port."
260                                        .to_string(),
261                                ),
262                            });
263                        }
264                    }
265                }
266            }
267        }
268
269        diagnostics
270    }
271}
272
273#[cfg(test)]
274mod tests {
275    use super::*;
276    use crate::analyzer::kubelint::parser::yaml::parse_yaml;
277
278    #[test]
279    fn test_privileged_port_detected() {
280        let yaml = r#"
281apiVersion: apps/v1
282kind: Deployment
283metadata:
284  name: priv-port
285spec:
286  template:
287    spec:
288      containers:
289      - name: nginx
290        image: nginx:1.21.0
291        ports:
292        - containerPort: 80
293"#;
294        let objects = parse_yaml(yaml).unwrap();
295        let check = PrivilegedPortsCheck;
296        let diagnostics = check.check(&objects[0]);
297        assert_eq!(diagnostics.len(), 1);
298        assert!(diagnostics[0].message.contains("80"));
299    }
300
301    #[test]
302    fn test_non_privileged_port_ok() {
303        let yaml = r#"
304apiVersion: apps/v1
305kind: Deployment
306metadata:
307  name: non-priv-port
308spec:
309  template:
310    spec:
311      containers:
312      - name: app
313        image: myapp:1.0
314        ports:
315        - containerPort: 8080
316"#;
317        let objects = parse_yaml(yaml).unwrap();
318        let check = PrivilegedPortsCheck;
319        let diagnostics = check.check(&objects[0]);
320        assert!(diagnostics.is_empty());
321    }
322
323    #[test]
324    fn test_ssh_port_detected() {
325        let yaml = r#"
326apiVersion: apps/v1
327kind: Deployment
328metadata:
329  name: ssh-container
330spec:
331  template:
332    spec:
333      containers:
334      - name: ssh
335        image: ssh:latest
336        ports:
337        - containerPort: 22
338"#;
339        let objects = parse_yaml(yaml).unwrap();
340        let check = SSHPortCheck;
341        let diagnostics = check.check(&objects[0]);
342        assert_eq!(diagnostics.len(), 1);
343        assert!(diagnostics[0].message.contains("SSH"));
344    }
345}