syncable_cli/analyzer/kubelint/templates/
ports.rs1use 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
8pub 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
70pub 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
129pub 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
201pub 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}