1use serde::{Deserialize, Serialize};
10use std::cmp::Ordering;
11use std::fmt;
12use std::path::PathBuf;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, Serialize, Deserialize)]
19#[serde(rename_all = "lowercase")]
20pub enum Severity {
21 Error,
23 #[default]
25 Warning,
26 Info,
28}
29
30impl Severity {
31 pub fn parse(s: &str) -> Option<Self> {
33 match s.to_lowercase().as_str() {
34 "error" => Some(Self::Error),
35 "warning" => Some(Self::Warning),
36 "info" => Some(Self::Info),
37 _ => None,
38 }
39 }
40
41 pub fn as_str(&self) -> &'static str {
43 match self {
44 Self::Error => "error",
45 Self::Warning => "warning",
46 Self::Info => "info",
47 }
48 }
49}
50
51impl fmt::Display for Severity {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 write!(f, "{}", self.as_str())
54 }
55}
56
57impl Ord for Severity {
58 fn cmp(&self, other: &Self) -> Ordering {
59 let self_val = match self {
61 Self::Error => 0,
62 Self::Warning => 1,
63 Self::Info => 2,
64 };
65 let other_val = match other {
66 Self::Error => 0,
67 Self::Warning => 1,
68 Self::Info => 2,
69 };
70 other_val.cmp(&self_val)
72 }
73}
74
75impl PartialOrd for Severity {
76 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
77 Some(self.cmp(other))
78 }
79}
80
81#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
83pub struct RuleCode(pub String);
84
85impl RuleCode {
86 pub fn new(code: impl Into<String>) -> Self {
88 Self(code.into())
89 }
90
91 pub fn as_str(&self) -> &str {
93 &self.0
94 }
95
96 pub fn is_security_check(&self) -> bool {
98 const SECURITY_CHECKS: &[&str] = &[
99 "privileged-container",
100 "privilege-escalation",
101 "run-as-non-root",
102 "read-only-root-fs",
103 "drop-net-raw-capability",
104 "hostnetwork",
105 "hostpid",
106 "hostipc",
107 "host-mounts",
108 "writable-host-mount",
109 "docker-sock",
110 "unsafe-proc-mount",
111 "access-to-secrets",
112 "access-to-create-pods",
113 "cluster-admin-role-binding",
114 "wildcard-in-rules",
115 ];
116 SECURITY_CHECKS.contains(&self.0.as_str())
117 }
118
119 pub fn is_best_practice_check(&self) -> bool {
121 const BEST_PRACTICE_CHECKS: &[&str] = &[
122 "latest-tag",
123 "no-liveness-probe",
124 "no-readiness-probe",
125 "unset-cpu-requirements",
126 "unset-memory-requirements",
127 "minimum-replicas",
128 "no-anti-affinity",
129 "no-rolling-update-strategy",
130 "default-service-account",
131 ];
132 BEST_PRACTICE_CHECKS.contains(&self.0.as_str())
133 }
134}
135
136impl fmt::Display for RuleCode {
137 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
138 write!(f, "{}", self.0)
139 }
140}
141
142impl From<&str> for RuleCode {
143 fn from(s: &str) -> Self {
144 Self::new(s)
145 }
146}
147
148impl From<String> for RuleCode {
149 fn from(s: String) -> Self {
150 Self(s)
151 }
152}
153
154impl AsRef<str> for RuleCode {
155 fn as_ref(&self) -> &str {
156 &self.0
157 }
158}
159
160#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
165pub struct Diagnostic {
166 pub message: String,
168 pub remediation: Option<String>,
170}
171
172impl Diagnostic {
173 pub fn new(message: impl Into<String>) -> Self {
175 Self {
176 message: message.into(),
177 remediation: None,
178 }
179 }
180
181 pub fn with_remediation(message: impl Into<String>, remediation: impl Into<String>) -> Self {
183 Self {
184 message: message.into(),
185 remediation: Some(remediation.into()),
186 }
187 }
188}
189
190impl From<String> for Diagnostic {
191 fn from(message: String) -> Self {
192 Self::new(message)
193 }
194}
195
196impl From<&str> for Diagnostic {
197 fn from(message: &str) -> Self {
198 Self::new(message)
199 }
200}
201
202#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
207pub struct CheckFailure {
208 pub code: RuleCode,
210 pub severity: Severity,
212 pub message: String,
214 pub file_path: PathBuf,
216 pub object_name: String,
218 pub object_kind: String,
220 pub object_namespace: Option<String>,
222 pub line: Option<u32>,
224 pub remediation: Option<String>,
226}
227
228impl CheckFailure {
229 pub fn new(
231 code: impl Into<RuleCode>,
232 severity: Severity,
233 message: impl Into<String>,
234 file_path: impl Into<PathBuf>,
235 object_name: impl Into<String>,
236 object_kind: impl Into<String>,
237 ) -> Self {
238 Self {
239 code: code.into(),
240 severity,
241 message: message.into(),
242 file_path: file_path.into(),
243 object_name: object_name.into(),
244 object_kind: object_kind.into(),
245 object_namespace: None,
246 line: None,
247 remediation: None,
248 }
249 }
250
251 pub fn with_namespace(mut self, namespace: impl Into<String>) -> Self {
253 self.object_namespace = Some(namespace.into());
254 self
255 }
256
257 pub fn with_line(mut self, line: u32) -> Self {
259 self.line = Some(line);
260 self
261 }
262
263 pub fn with_remediation(mut self, remediation: impl Into<String>) -> Self {
265 self.remediation = Some(remediation.into());
266 self
267 }
268
269 pub fn object_identifier(&self) -> String {
271 match &self.object_namespace {
272 Some(ns) => format!("{}/{}", ns, self.object_name),
273 None => self.object_name.clone(),
274 }
275 }
276}
277
278impl Ord for CheckFailure {
279 fn cmp(&self, other: &Self) -> Ordering {
280 match self.file_path.cmp(&other.file_path) {
282 Ordering::Equal => match (self.line, other.line) {
283 (Some(a), Some(b)) => match a.cmp(&b) {
284 Ordering::Equal => self.severity.cmp(&other.severity),
285 other => other,
286 },
287 (Some(_), None) => Ordering::Less,
288 (None, Some(_)) => Ordering::Greater,
289 (None, None) => self.severity.cmp(&other.severity),
290 },
291 other => other,
292 }
293 }
294}
295
296impl PartialOrd for CheckFailure {
297 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
298 Some(self.cmp(other))
299 }
300}
301
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
304pub enum ObjectKind {
305 Deployment,
307 StatefulSet,
308 DaemonSet,
309 ReplicaSet,
310 Pod,
311 Job,
312 CronJob,
313
314 Service,
316 Ingress,
317 NetworkPolicy,
318
319 Role,
321 ClusterRole,
322 RoleBinding,
323 ClusterRoleBinding,
324 ServiceAccount,
325
326 HorizontalPodAutoscaler,
328 PodDisruptionBudget,
329
330 PersistentVolumeClaim,
332
333 DeploymentConfig,
335 SecurityContextConstraints,
336
337 ServiceMonitor,
339
340 ScaledObject,
342
343 Any,
345}
346
347impl ObjectKind {
348 pub fn as_str(&self) -> &'static str {
350 match self {
351 Self::Deployment => "Deployment",
352 Self::StatefulSet => "StatefulSet",
353 Self::DaemonSet => "DaemonSet",
354 Self::ReplicaSet => "ReplicaSet",
355 Self::Pod => "Pod",
356 Self::Job => "Job",
357 Self::CronJob => "CronJob",
358 Self::Service => "Service",
359 Self::Ingress => "Ingress",
360 Self::NetworkPolicy => "NetworkPolicy",
361 Self::Role => "Role",
362 Self::ClusterRole => "ClusterRole",
363 Self::RoleBinding => "RoleBinding",
364 Self::ClusterRoleBinding => "ClusterRoleBinding",
365 Self::ServiceAccount => "ServiceAccount",
366 Self::HorizontalPodAutoscaler => "HorizontalPodAutoscaler",
367 Self::PodDisruptionBudget => "PodDisruptionBudget",
368 Self::PersistentVolumeClaim => "PersistentVolumeClaim",
369 Self::DeploymentConfig => "DeploymentConfig",
370 Self::SecurityContextConstraints => "SecurityContextConstraints",
371 Self::ServiceMonitor => "ServiceMonitor",
372 Self::ScaledObject => "ScaledObject",
373 Self::Any => "Any",
374 }
375 }
376
377 pub fn from_kind(kind: &str) -> Option<Self> {
379 match kind {
380 "Deployment" => Some(Self::Deployment),
381 "StatefulSet" => Some(Self::StatefulSet),
382 "DaemonSet" => Some(Self::DaemonSet),
383 "ReplicaSet" => Some(Self::ReplicaSet),
384 "Pod" => Some(Self::Pod),
385 "Job" => Some(Self::Job),
386 "CronJob" => Some(Self::CronJob),
387 "Service" => Some(Self::Service),
388 "Ingress" => Some(Self::Ingress),
389 "NetworkPolicy" => Some(Self::NetworkPolicy),
390 "Role" => Some(Self::Role),
391 "ClusterRole" => Some(Self::ClusterRole),
392 "RoleBinding" => Some(Self::RoleBinding),
393 "ClusterRoleBinding" => Some(Self::ClusterRoleBinding),
394 "ServiceAccount" => Some(Self::ServiceAccount),
395 "HorizontalPodAutoscaler" => Some(Self::HorizontalPodAutoscaler),
396 "PodDisruptionBudget" => Some(Self::PodDisruptionBudget),
397 "PersistentVolumeClaim" => Some(Self::PersistentVolumeClaim),
398 "DeploymentConfig" => Some(Self::DeploymentConfig),
399 "SecurityContextConstraints" => Some(Self::SecurityContextConstraints),
400 "ServiceMonitor" => Some(Self::ServiceMonitor),
401 "ScaledObject" => Some(Self::ScaledObject),
402 _ => None,
403 }
404 }
405
406 pub fn is_deployment_like(&self) -> bool {
408 matches!(
409 self,
410 Self::Deployment
411 | Self::StatefulSet
412 | Self::DaemonSet
413 | Self::ReplicaSet
414 | Self::Pod
415 | Self::Job
416 | Self::CronJob
417 | Self::DeploymentConfig
418 )
419 }
420
421 pub fn is_job_like(&self) -> bool {
423 matches!(self, Self::Job | Self::CronJob)
424 }
425}
426
427impl fmt::Display for ObjectKind {
428 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
429 write!(f, "{}", self.as_str())
430 }
431}
432
433#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
435pub struct ObjectKindsDesc {
436 pub object_kinds: Vec<String>,
439}
440
441impl ObjectKindsDesc {
442 pub fn new(kinds: &[&str]) -> Self {
444 Self {
445 object_kinds: kinds.iter().map(|s| (*s).to_string()).collect(),
446 }
447 }
448
449 pub fn matches(&self, kind: &ObjectKind) -> bool {
451 if self.object_kinds.is_empty() {
453 return kind.is_deployment_like();
454 }
455
456 for k in &self.object_kinds {
457 match k.as_str() {
458 "DeploymentLike" if kind.is_deployment_like() => return true,
459 "JobLike" if kind.is_job_like() => return true,
460 "Any" => return true,
461 _ if k == kind.as_str() => return true,
462 _ => continue,
463 }
464 }
465 false
466 }
467}
468
469impl Default for ObjectKindsDesc {
470 fn default() -> Self {
471 Self {
472 object_kinds: vec!["DeploymentLike".to_string()],
473 }
474 }
475}
476
477#[cfg(test)]
478mod tests {
479 use super::*;
480
481 #[test]
482 fn test_severity_ordering() {
483 assert!(Severity::Error > Severity::Warning);
484 assert!(Severity::Warning > Severity::Info);
485 }
486
487 #[test]
488 fn test_severity_from_str() {
489 assert_eq!(Severity::parse("error"), Some(Severity::Error));
490 assert_eq!(Severity::parse("WARNING"), Some(Severity::Warning));
491 assert_eq!(Severity::parse("Info"), Some(Severity::Info));
492 assert_eq!(Severity::parse("invalid"), None);
493 }
494
495 #[test]
496 fn test_rule_code() {
497 let code = RuleCode::new("privileged-container");
498 assert!(code.is_security_check());
499 assert!(!code.is_best_practice_check());
500
501 let code = RuleCode::new("latest-tag");
502 assert!(!code.is_security_check());
503 assert!(code.is_best_practice_check());
504 }
505
506 #[test]
507 fn test_check_failure_ordering() {
508 let f1 = CheckFailure::new(
509 "check1",
510 Severity::Warning,
511 "msg1",
512 "a.yaml",
513 "obj1",
514 "Deployment",
515 )
516 .with_line(10);
517 let f2 = CheckFailure::new(
518 "check2",
519 Severity::Error,
520 "msg2",
521 "a.yaml",
522 "obj2",
523 "Service",
524 )
525 .with_line(5);
526 let f3 = CheckFailure::new("check3", Severity::Info, "msg3", "b.yaml", "obj3", "Pod");
527
528 let mut failures = vec![f1.clone(), f2.clone(), f3.clone()];
529 failures.sort();
530
531 assert_eq!(failures[0].file_path.to_str(), Some("a.yaml"));
533 assert_eq!(failures[0].line, Some(5));
534 assert_eq!(failures[1].file_path.to_str(), Some("a.yaml"));
535 assert_eq!(failures[1].line, Some(10));
536 assert_eq!(failures[2].file_path.to_str(), Some("b.yaml"));
537 }
538
539 #[test]
540 fn test_object_kind_matching() {
541 let desc = ObjectKindsDesc::new(&["DeploymentLike"]);
542 assert!(desc.matches(&ObjectKind::Deployment));
543 assert!(desc.matches(&ObjectKind::StatefulSet));
544 assert!(desc.matches(&ObjectKind::DaemonSet));
545 assert!(desc.matches(&ObjectKind::Job));
546 assert!(!desc.matches(&ObjectKind::Service));
547
548 let desc = ObjectKindsDesc::new(&["Service", "Ingress"]);
549 assert!(desc.matches(&ObjectKind::Service));
550 assert!(desc.matches(&ObjectKind::Ingress));
551 assert!(!desc.matches(&ObjectKind::Deployment));
552 }
553
554 #[test]
555 fn test_diagnostic() {
556 let d = Diagnostic::new("container is privileged");
557 assert_eq!(d.message, "container is privileged");
558 assert!(d.remediation.is_none());
559
560 let d = Diagnostic::with_remediation("issue", "fix it");
561 assert_eq!(d.message, "issue");
562 assert_eq!(d.remediation, Some("fix it".to_string()));
563 }
564}