From f562a946f3090af7903a7863ef724fe6c249074f Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Wed, 31 Dec 2025 02:10:47 +0100 Subject: [PATCH 1/3] feat: rant fmt and lint check --- .DS_Store | Bin 8196 -> 8196 bytes src/agent/persistence.rs | 5 +- src/agent/tools/fetch.rs | 8 +-- src/agent/ui/hooks.rs | 30 ++++----- src/analyzer/helmlint/lint.rs | 5 +- src/analyzer/helmlint/pragma.rs | 13 ++-- src/analyzer/helmlint/rules/hl1xxx.rs | 63 +++++++----------- src/analyzer/helmlint/rules/hl2xxx.rs | 30 +++------ src/analyzer/helmlint/rules/hl3xxx.rs | 5 +- src/analyzer/helmlint/rules/hl4xxx.rs | 36 ++++------ src/analyzer/kubelint/config.rs | 10 ++- .../kubelint/templates/hostnetwork.rs | 15 ++--- src/analyzer/kubelint/templates/misc.rs | 24 +++---- src/analyzer/kubelint/templates/pdb.rs | 10 ++- src/analyzer/kubelint/templates/privileged.rs | 5 +- src/analyzer/kubelint/templates/rbac.rs | 5 +- .../kubelint/templates/unsafeprocmount.rs | 8 +-- src/analyzer/kubelint/templates/validation.rs | 31 ++++----- src/auth/credentials.rs | 5 +- src/auth/device_flow.rs | 4 +- src/lib.rs | 38 ++++++----- 21 files changed, 141 insertions(+), 209 deletions(-) diff --git a/.DS_Store b/.DS_Store index e2cb9d048a56b98bb056b8f7f582bfbadca9237b..5868e7803a8e3c725f815d8b91b29b1fb0df2e8f 100644 GIT binary patch delta 40 wcmZp1XmOa}&nUYwU^hRb>}DQ;MwZPFMbwxlHiT?um-xoAIZ$*P)5L~A02Z7L_y7O^ delta 84 zcmZp1XmOa}&nUMsU^hRb+-4quMixe?$$cV{+PVxW45() { - if index > 0 && index <= sessions.len() { + if let Ok(index) = identifier.parse::() + && index > 0 && index <= sessions.len() { return sessions.into_iter().nth(index - 1); } - } // Try to find by UUID or partial UUID sessions diff --git a/src/agent/tools/fetch.rs b/src/agent/tools/fetch.rs index 30b5b4d3..6c38979a 100644 --- a/src/agent/tools/fetch.rs +++ b/src/agent/tools/fetch.rs @@ -69,9 +69,9 @@ impl WebFetchTool { let robots_url = format!("{}://{}/robots.txt", url.scheme(), url.authority()); // Try to fetch robots.txt (ignore errors - many sites don't have one) - if let Ok(response) = self.client().get(&robots_url).send().await { - if response.status().is_success() { - if let Ok(robots_content) = response.text().await { + if let Ok(response) = self.client().get(&robots_url).send().await + && response.status().is_success() + && let Ok(robots_content) = response.text().await { let path = url.path(); for line in robots_content.lines() { if let Some(disallowed) = line.strip_prefix("Disallow: ") { @@ -97,8 +97,6 @@ impl WebFetchTool { } } } - } - } Ok(()) } diff --git a/src/agent/ui/hooks.rs b/src/agent/ui/hooks.rs index 8618c441..2dbcea8a 100644 --- a/src/agent/ui/hooks.rs +++ b/src/agent/ui/hooks.rs @@ -1193,8 +1193,8 @@ fn format_kubelint_result( let mut lines = Vec::new(); // Check for parse errors first - if let Some(errors) = parse_errors { - if !errors.is_empty() { + if let Some(errors) = parse_errors + && !errors.is_empty() { lines.push(format!( "{}☸ {} parse error{} (files could not be fully analyzed){}", ansi::HIGH, @@ -1235,7 +1235,6 @@ fn format_kubelint_result( return (false, lines); } } - } if total == 0 && parse_errors.map(|e| e.is_empty()).unwrap_or(true) { lines.push(format!( @@ -1326,8 +1325,8 @@ fn format_kubelint_result( } // Then high priority - if shown < MAX_PREVIEW { - if let Some(high_issues) = action_plan + if shown < MAX_PREVIEW + && let Some(high_issues) = action_plan .and_then(|a| a.get("high")) .and_then(|h| h.as_array()) { @@ -1336,11 +1335,10 @@ fn format_kubelint_result( shown += 1; } } - } // Show quick fix hint - if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array()) { - if let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str()) { + if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array()) + && let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str()) { let truncated = if first_fix.len() > 70 { format!("{}...", &first_fix[..67]) } else { @@ -1353,7 +1351,6 @@ fn format_kubelint_result( ansi::RESET )); } - } // Note about remaining issues let remaining = total as usize - shown; @@ -1429,8 +1426,8 @@ fn format_helmlint_result( let mut lines = Vec::new(); // Check for parse errors first - if let Some(errors) = parse_errors { - if !errors.is_empty() { + if let Some(errors) = parse_errors + && !errors.is_empty() { lines.push(format!( "{}⎈ {} parse error{} (chart could not be fully analyzed){}", ansi::HIGH, @@ -1471,7 +1468,6 @@ fn format_helmlint_result( return (false, lines); } } - } if total == 0 && parse_errors.map(|e| e.is_empty()).unwrap_or(true) { lines.push(format!( @@ -1562,8 +1558,8 @@ fn format_helmlint_result( } // Then high priority - if shown < MAX_PREVIEW { - if let Some(high_issues) = action_plan + if shown < MAX_PREVIEW + && let Some(high_issues) = action_plan .and_then(|a| a.get("high")) .and_then(|h| h.as_array()) { @@ -1572,11 +1568,10 @@ fn format_helmlint_result( shown += 1; } } - } // Show quick fix hint - if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array()) { - if let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str()) { + if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array()) + && let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str()) { let truncated = if first_fix.len() > 70 { format!("{}...", &first_fix[..67]) } else { @@ -1589,7 +1584,6 @@ fn format_helmlint_result( ansi::RESET )); } - } // Note about remaining issues let remaining = total as usize - shown; diff --git a/src/analyzer/helmlint/lint.rs b/src/analyzer/helmlint/lint.rs index ca0cee98..68c282fb 100644 --- a/src/analyzer/helmlint/lint.rs +++ b/src/analyzer/helmlint/lint.rs @@ -315,11 +315,10 @@ fn collect_chart_files(path: &Path) -> HashSet { .into_iter() .filter_map(|e| e.ok()) { - if entry.path().is_file() { - if let Ok(relative) = entry.path().strip_prefix(path) { + if entry.path().is_file() + && let Ok(relative) = entry.path().strip_prefix(path) { files.insert(relative.display().to_string()); } - } } files diff --git a/src/analyzer/helmlint/pragma.rs b/src/analyzer/helmlint/pragma.rs index b4cd151d..83969c39 100644 --- a/src/analyzer/helmlint/pragma.rs +++ b/src/analyzer/helmlint/pragma.rs @@ -38,20 +38,17 @@ impl PragmaState { } // Check if the rule is ignored for this specific line - if let Some(ignores) = self.line_ignores.get(&line) { - if ignores.contains(code.as_str()) { + if let Some(ignores) = self.line_ignores.get(&line) + && ignores.contains(code.as_str()) { return true; } - } // Check if previous line has an ignore pragma for this line - if line > 1 { - if let Some(ignores) = self.line_ignores.get(&(line - 1)) { - if ignores.contains(code.as_str()) { + if line > 1 + && let Some(ignores) = self.line_ignores.get(&(line - 1)) + && ignores.contains(code.as_str()) { return true; } - } - } false } diff --git a/src/analyzer/helmlint/rules/hl1xxx.rs b/src/analyzer/helmlint/rules/hl1xxx.rs index 7ad31a4f..eaa8f150 100644 --- a/src/analyzer/helmlint/rules/hl1xxx.rs +++ b/src/analyzer/helmlint/rules/hl1xxx.rs @@ -86,8 +86,8 @@ impl Rule for HL1002 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if !chart.has_valid_api_version() { + if let Some(chart) = ctx.chart_metadata + && !chart.has_valid_api_version() { let version = match &chart.api_version { ApiVersion::Unknown(v) => v.clone(), _ => "unknown".to_string(), @@ -101,7 +101,6 @@ impl Rule for HL1002 { RuleCategory::Structure, )]; } - } vec![] } } @@ -127,8 +126,8 @@ impl Rule for HL1003 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if chart.name.is_empty() { + if let Some(chart) = ctx.chart_metadata + && chart.name.is_empty() { return vec![CheckFailure::new( "HL1003", Severity::Error, @@ -138,7 +137,6 @@ impl Rule for HL1003 { RuleCategory::Structure, )]; } - } vec![] } } @@ -164,8 +162,8 @@ impl Rule for HL1004 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if chart.version.is_empty() { + if let Some(chart) = ctx.chart_metadata + && chart.version.is_empty() { return vec![CheckFailure::new( "HL1004", Severity::Error, @@ -175,7 +173,6 @@ impl Rule for HL1004 { RuleCategory::Structure, )]; } - } vec![] } } @@ -201,8 +198,8 @@ impl Rule for HL1005 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if !chart.version.is_empty() && !is_valid_semver(&chart.version) { + if let Some(chart) = ctx.chart_metadata + && !chart.version.is_empty() && !is_valid_semver(&chart.version) { return vec![CheckFailure::new( "HL1005", Severity::Warning, @@ -215,7 +212,6 @@ impl Rule for HL1005 { RuleCategory::Structure, )]; } - } vec![] } } @@ -241,13 +237,13 @@ impl Rule for HL1006 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if chart.description.is_none() + if let Some(chart) = ctx.chart_metadata + && (chart.description.is_none() || chart .description .as_ref() .map(|d| d.is_empty()) - .unwrap_or(true) + .unwrap_or(true)) { return vec![CheckFailure::new( "HL1006", @@ -258,7 +254,6 @@ impl Rule for HL1006 { RuleCategory::Structure, )]; } - } vec![] } } @@ -284,8 +279,8 @@ impl Rule for HL1007 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if chart.maintainers.is_empty() { + if let Some(chart) = ctx.chart_metadata + && chart.maintainers.is_empty() { return vec![CheckFailure::new( "HL1007", Severity::Info, @@ -295,7 +290,6 @@ impl Rule for HL1007 { RuleCategory::Structure, )]; } - } vec![] } } @@ -321,8 +315,8 @@ impl Rule for HL1008 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if chart.is_deprecated() { + if let Some(chart) = ctx.chart_metadata + && chart.is_deprecated() { return vec![CheckFailure::new( "HL1008", Severity::Warning, @@ -332,7 +326,6 @@ impl Rule for HL1008 { RuleCategory::Structure, )]; } - } vec![] } } @@ -359,11 +352,10 @@ impl Rule for HL1009 { fn check(&self, ctx: &LintContext) -> Vec { // Skip for library charts - if let Some(chart) = ctx.chart_metadata { - if chart.is_library() { + if let Some(chart) = ctx.chart_metadata + && chart.is_library() { return vec![]; } - } let has_templates = ctx .files @@ -466,8 +458,8 @@ impl Rule for HL1012 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if !is_valid_chart_name(&chart.name) { + if let Some(chart) = ctx.chart_metadata + && !is_valid_chart_name(&chart.name) { return vec![CheckFailure::new( "HL1012", Severity::Error, @@ -480,7 +472,6 @@ impl Rule for HL1012 { RuleCategory::Structure, )]; } - } vec![] } } @@ -506,9 +497,9 @@ impl Rule for HL1013 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if let Some(icon) = &chart.icon { - if icon.starts_with("http://") { + if let Some(chart) = ctx.chart_metadata + && let Some(icon) = &chart.icon + && icon.starts_with("http://") { return vec![CheckFailure::new( "HL1013", Severity::Warning, @@ -518,8 +509,6 @@ impl Rule for HL1013 { RuleCategory::Structure, )]; } - } - } vec![] } } @@ -545,9 +534,9 @@ impl Rule for HL1014 { } fn check(&self, ctx: &LintContext) -> Vec { - if let Some(chart) = ctx.chart_metadata { - if let Some(home) = &chart.home { - if home.starts_with("http://") { + if let Some(chart) = ctx.chart_metadata + && let Some(home) = &chart.home + && home.starts_with("http://") { return vec![CheckFailure::new( "HL1014", Severity::Warning, @@ -557,8 +546,6 @@ impl Rule for HL1014 { RuleCategory::Structure, )]; } - } - } vec![] } } diff --git a/src/analyzer/helmlint/rules/hl2xxx.rs b/src/analyzer/helmlint/rules/hl2xxx.rs index 9db1ae56..96b64301 100644 --- a/src/analyzer/helmlint/rules/hl2xxx.rs +++ b/src/analyzer/helmlint/rules/hl2xxx.rs @@ -239,10 +239,10 @@ impl Rule for HL2005 { let lower_path = path.to_lowercase(); let is_port_field = port_patterns.iter().any(|p| lower_path.ends_with(p)); - if is_port_field { - if let Some(value) = values.get(path) { - if let Some(port) = extract_port_number(value) { - if !(1..=65535).contains(&port) { + if is_port_field + && let Some(value) = values.get(path) + && let Some(port) = extract_port_number(value) + && !(1..=65535).contains(&port) { let line = values.line_for_path(path).unwrap_or(1); failures.push(CheckFailure::new( "HL2005", @@ -256,9 +256,6 @@ impl Rule for HL2005 { RuleCategory::Values, )); } - } - } - } } failures @@ -296,9 +293,9 @@ impl Rule for HL2007 { // Look for image.tag or similar patterns for path in &values.defined_paths { let lower_path = path.to_lowercase(); - if lower_path.ends_with(".tag") || lower_path.ends_with("imagetag") { - if let Some(serde_yaml::Value::String(tag)) = values.get(path) { - if tag == "latest" { + if (lower_path.ends_with(".tag") || lower_path.ends_with("imagetag")) + && let Some(serde_yaml::Value::String(tag)) = values.get(path) + && tag == "latest" { let line = values.line_for_path(path).unwrap_or(1); failures.push(CheckFailure::new( "HL2007", @@ -312,8 +309,6 @@ impl Rule for HL2007 { RuleCategory::Values, )); } - } - } } failures @@ -350,10 +345,10 @@ impl Rule for HL2008 { for path in &values.defined_paths { let lower_path = path.to_lowercase(); - if lower_path.ends_with("replicacount") || lower_path.ends_with("replicas") { - if let Some(value) = values.get(path) { - if let Some(count) = extract_number(value) { - if count == 0 { + if (lower_path.ends_with("replicacount") || lower_path.ends_with("replicas")) + && let Some(value) = values.get(path) + && let Some(count) = extract_number(value) + && count == 0 { let line = values.line_for_path(path).unwrap_or(1); failures.push(CheckFailure::new( "HL2008", @@ -367,9 +362,6 @@ impl Rule for HL2008 { RuleCategory::Values, )); } - } - } - } } failures diff --git a/src/analyzer/helmlint/rules/hl3xxx.rs b/src/analyzer/helmlint/rules/hl3xxx.rs index b53b8d0c..f0a25cdd 100644 --- a/src/analyzer/helmlint/rules/hl3xxx.rs +++ b/src/analyzer/helmlint/rules/hl3xxx.rs @@ -304,11 +304,10 @@ impl Rule for HL3008 { fn check(&self, ctx: &LintContext) -> Vec { // Skip for library charts - if let Some(chart) = ctx.chart_metadata { - if chart.is_library() { + if let Some(chart) = ctx.chart_metadata + && chart.is_library() { return vec![]; } - } let has_notes = ctx.files.iter().any(|f| f.ends_with("NOTES.txt")); if !has_notes { diff --git a/src/analyzer/helmlint/rules/hl4xxx.rs b/src/analyzer/helmlint/rules/hl4xxx.rs index 03d32030..2fbcbe6d 100644 --- a/src/analyzer/helmlint/rules/hl4xxx.rs +++ b/src/analyzer/helmlint/rules/hl4xxx.rs @@ -123,9 +123,9 @@ impl Rule for HL4002 { // Check values.yaml if let Some(values) = ctx.values { for path in &values.defined_paths { - if path.to_lowercase().contains("privileged") { - if let Some(value) = values.get(path) { - if is_truthy(value) { + if path.to_lowercase().contains("privileged") + && let Some(value) = values.get(path) + && is_truthy(value) { let line = values.line_for_path(path).unwrap_or(1); failures.push(CheckFailure::new( "HL4002", @@ -136,16 +136,14 @@ impl Rule for HL4002 { RuleCategory::Security, )); } - } - } } } // Check templates for hardcoded privileged: true for template in ctx.templates { for token in &template.tokens { - if let TemplateToken::Text { content, line } = token { - if content.contains("privileged: true") { + if let TemplateToken::Text { content, line } = token + && content.contains("privileged: true") { failures.push(CheckFailure::new( "HL4002", Severity::Error, @@ -155,7 +153,6 @@ impl Rule for HL4002 { RuleCategory::Security, )); } - } } } @@ -188,8 +185,8 @@ impl Rule for HL4003 { for template in ctx.templates { for token in &template.tokens { - if let TemplateToken::Text { content, line } = token { - if content.contains("hostPath:") { + if let TemplateToken::Text { content, line } = token + && content.contains("hostPath:") { failures.push(CheckFailure::new( "HL4003", Severity::Warning, @@ -199,7 +196,6 @@ impl Rule for HL4003 { RuleCategory::Security, )); } - } } } @@ -233,9 +229,9 @@ impl Rule for HL4004 { // Check values.yaml if let Some(values) = ctx.values { for path in &values.defined_paths { - if path.to_lowercase().contains("hostnetwork") { - if let Some(value) = values.get(path) { - if is_truthy(value) { + if path.to_lowercase().contains("hostnetwork") + && let Some(value) = values.get(path) + && is_truthy(value) { let line = values.line_for_path(path).unwrap_or(1); failures.push(CheckFailure::new( "HL4004", @@ -246,16 +242,14 @@ impl Rule for HL4004 { RuleCategory::Security, )); } - } - } } } // Check templates for template in ctx.templates { for token in &template.tokens { - if let TemplateToken::Text { content, line } = token { - if content.contains("hostNetwork: true") { + if let TemplateToken::Text { content, line } = token + && content.contains("hostNetwork: true") { failures.push(CheckFailure::new( "HL4004", Severity::Warning, @@ -265,7 +259,6 @@ impl Rule for HL4004 { RuleCategory::Security, )); } - } } } @@ -298,8 +291,8 @@ impl Rule for HL4005 { for template in ctx.templates { for token in &template.tokens { - if let TemplateToken::Text { content, line } = token { - if content.contains("hostPID: true") { + if let TemplateToken::Text { content, line } = token + && content.contains("hostPID: true") { failures.push(CheckFailure::new( "HL4005", Severity::Warning, @@ -309,7 +302,6 @@ impl Rule for HL4005 { RuleCategory::Security, )); } - } } } diff --git a/src/analyzer/kubelint/config.rs b/src/analyzer/kubelint/config.rs index a22ad294..a81a979a 100644 --- a/src/analyzer/kubelint/config.rs +++ b/src/analyzer/kubelint/config.rs @@ -156,11 +156,10 @@ impl KubelintConfig { let path_str = path.to_string_lossy(); for pattern in &self.ignore_paths { - if let Ok(glob) = glob::Pattern::new(pattern) { - if glob.matches(&path_str) { + if let Ok(glob) = glob::Pattern::new(pattern) + && glob.matches(&path_str) { return true; } - } // Also check simple prefix/suffix matches if path_str.contains(pattern) { return true; @@ -186,11 +185,10 @@ impl KubelintConfig { pub fn load_from_default() -> Option { for filename in &[".kube-linter.yaml", ".kube-linter.yml"] { let path = Path::new(filename); - if path.exists() { - if let Ok(config) = Self::load_from_file(path) { + if path.exists() + && let Ok(config) = Self::load_from_file(path) { return Some(config); } - } } None } diff --git a/src/analyzer/kubelint/templates/hostnetwork.rs b/src/analyzer/kubelint/templates/hostnetwork.rs index 945e0c4b..9a8d1a6e 100644 --- a/src/analyzer/kubelint/templates/hostnetwork.rs +++ b/src/analyzer/kubelint/templates/hostnetwork.rs @@ -43,8 +43,8 @@ impl CheckFunc for HostNetworkCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { - if pod_spec.host_network == Some(true) { + if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) + && pod_spec.host_network == Some(true) { diagnostics.push(Diagnostic { message: "Pod is configured to use the host's network namespace".to_string(), remediation: Some( @@ -54,7 +54,6 @@ impl CheckFunc for HostNetworkCheck { ), }); } - } diagnostics } @@ -98,8 +97,8 @@ impl CheckFunc for HostPIDCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { - if pod_spec.host_pid == Some(true) { + if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) + && pod_spec.host_pid == Some(true) { diagnostics.push(Diagnostic { message: "Pod is configured to use the host's PID namespace".to_string(), remediation: Some( @@ -110,7 +109,6 @@ impl CheckFunc for HostPIDCheck { ), }); } - } diagnostics } @@ -154,8 +152,8 @@ impl CheckFunc for HostIPCCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { - if pod_spec.host_ipc == Some(true) { + if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) + && pod_spec.host_ipc == Some(true) { diagnostics.push(Diagnostic { message: "Pod is configured to use the host's IPC namespace".to_string(), remediation: Some( @@ -165,7 +163,6 @@ impl CheckFunc for HostIPCCheck { ), }); } - } diagnostics } diff --git a/src/analyzer/kubelint/templates/misc.rs b/src/analyzer/kubelint/templates/misc.rs index 607b6173..79fa3450 100644 --- a/src/analyzer/kubelint/templates/misc.rs +++ b/src/analyzer/kubelint/templates/misc.rs @@ -52,8 +52,8 @@ impl CheckFunc for SysctlsCheck { "net.", ]; - if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { - if let Some(sc) = &pod_spec.security_context { + if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) + && let Some(sc) = &pod_spec.security_context { for sysctl in &sc.sysctls { let is_unsafe = unsafe_sysctls .iter() @@ -73,7 +73,6 @@ impl CheckFunc for SysctlsCheck { } } } - } diagnostics } @@ -117,15 +116,15 @@ impl CheckFunc for DnsConfigOptionsCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { - if let Some(dns_config) = &pod_spec.dns_config { + if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) + && let Some(dns_config) = &pod_spec.dns_config { // Check for ndots setting that could cause performance issues for option in &dns_config.options { - if let Some(name) = &option.name { - if name == "ndots" { - if let Some(value) = &option.value { - if let Ok(ndots) = value.parse::() { - if ndots > 5 { + if let Some(name) = &option.name + && name == "ndots" + && let Some(value) = &option.value + && let Ok(ndots) = value.parse::() + && ndots > 5 { diagnostics.push(Diagnostic { message: format!( "DNS ndots is set to {}, which may cause DNS lookup performance issues", @@ -137,13 +136,8 @@ impl CheckFunc for DnsConfigOptionsCheck { ), }); } - } - } - } - } } } - } diagnostics } diff --git a/src/analyzer/kubelint/templates/pdb.rs b/src/analyzer/kubelint/templates/pdb.rs index b9076a3a..4a445726 100644 --- a/src/analyzer/kubelint/templates/pdb.rs +++ b/src/analyzer/kubelint/templates/pdb.rs @@ -43,8 +43,8 @@ impl CheckFunc for PdbMaxUnavailableCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object { - if let Some(max_unavailable) = &pdb.max_unavailable { + if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object + && let Some(max_unavailable) = &pdb.max_unavailable { // Check if it's set to 0 or 0% if max_unavailable == "0" || max_unavailable == "0%" { diagnostics.push(Diagnostic { @@ -59,7 +59,6 @@ impl CheckFunc for PdbMaxUnavailableCheck { }); } } - } diagnostics } @@ -103,8 +102,8 @@ impl CheckFunc for PdbMinAvailableCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object { - if let Some(min_available) = &pdb.min_available { + if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object + && let Some(min_available) = &pdb.min_available { // Check if it's set to 100% if min_available == "100%" { diagnostics.push(Diagnostic { @@ -117,7 +116,6 @@ impl CheckFunc for PdbMinAvailableCheck { }); } } - } diagnostics } diff --git a/src/analyzer/kubelint/templates/privileged.rs b/src/analyzer/kubelint/templates/privileged.rs index 9f6f45fd..05f32226 100644 --- a/src/analyzer/kubelint/templates/privileged.rs +++ b/src/analyzer/kubelint/templates/privileged.rs @@ -45,8 +45,8 @@ impl CheckFunc for PrivilegedCheck { if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { for container in extract::container::all_containers(pod_spec) { - if let Some(sc) = &container.security_context { - if sc.privileged == Some(true) { + if let Some(sc) = &container.security_context + && sc.privileged == Some(true) { diagnostics.push(Diagnostic { message: format!( "Container '{}' is running in privileged mode", @@ -58,7 +58,6 @@ impl CheckFunc for PrivilegedCheck { ), }); } - } } } diff --git a/src/analyzer/kubelint/templates/rbac.rs b/src/analyzer/kubelint/templates/rbac.rs index 846ad541..8f7957e6 100644 --- a/src/analyzer/kubelint/templates/rbac.rs +++ b/src/analyzer/kubelint/templates/rbac.rs @@ -49,8 +49,8 @@ impl CheckFunc for ClusterAdminRoleBindingCheck { _ => None, }; - if let Some(role_ref) = role_ref { - if role_ref.kind == "ClusterRole" && role_ref.name == "cluster-admin" { + if let Some(role_ref) = role_ref + && role_ref.kind == "ClusterRole" && role_ref.name == "cluster-admin" { diagnostics.push(Diagnostic { message: "Binding grants cluster-admin privileges".to_string(), remediation: Some( @@ -60,7 +60,6 @@ impl CheckFunc for ClusterAdminRoleBindingCheck { ), }); } - } diagnostics } diff --git a/src/analyzer/kubelint/templates/unsafeprocmount.rs b/src/analyzer/kubelint/templates/unsafeprocmount.rs index 7d8330a9..05b3e9b3 100644 --- a/src/analyzer/kubelint/templates/unsafeprocmount.rs +++ b/src/analyzer/kubelint/templates/unsafeprocmount.rs @@ -45,9 +45,9 @@ impl CheckFunc for UnsafeProcMountCheck { if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { for container in extract::container::all_containers(pod_spec) { - if let Some(sc) = &container.security_context { - if let Some(proc_mount) = &sc.proc_mount { - if proc_mount == "Unmasked" { + if let Some(sc) = &container.security_context + && let Some(proc_mount) = &sc.proc_mount + && proc_mount == "Unmasked" { diagnostics.push(Diagnostic { message: format!( "Container '{}' has unsafe /proc mount (procMount: Unmasked)", @@ -60,8 +60,6 @@ impl CheckFunc for UnsafeProcMountCheck { ), }); } - } - } } } diff --git a/src/analyzer/kubelint/templates/validation.rs b/src/analyzer/kubelint/templates/validation.rs index 157d2136..e59704d3 100644 --- a/src/analyzer/kubelint/templates/validation.rs +++ b/src/analyzer/kubelint/templates/validation.rs @@ -99,8 +99,8 @@ impl CheckFunc for RestartPolicyCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { - if let Some(policy) = &pod_spec.restart_policy { + if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) + && let Some(policy) = &pod_spec.restart_policy { // For Deployments, StatefulSets, DaemonSets - must be Always match &object.k8s_object { K8sObject::Deployment(_) @@ -124,7 +124,6 @@ impl CheckFunc for RestartPolicyCheck { _ => {} } } - } diagnostics } @@ -566,8 +565,8 @@ impl CheckFunc for JobTtlSecondsAfterFinishedCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let K8sObject::Job(job) = &object.k8s_object { - if job.ttl_seconds_after_finished.is_none() { + if let K8sObject::Job(job) = &object.k8s_object + && job.ttl_seconds_after_finished.is_none() { diagnostics.push(Diagnostic { message: "Job does not have ttlSecondsAfterFinished set".to_string(), remediation: Some( @@ -576,7 +575,6 @@ impl CheckFunc for JobTtlSecondsAfterFinishedCheck { ), }); } - } diagnostics } @@ -620,8 +618,8 @@ impl CheckFunc for PriorityClassNameCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { - if pod_spec.priority_class_name.is_none() { + if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) + && pod_spec.priority_class_name.is_none() { diagnostics.push(Diagnostic { message: "Pod does not have priorityClassName set".to_string(), remediation: Some( @@ -629,7 +627,6 @@ impl CheckFunc for PriorityClassNameCheck { ), }); } - } diagnostics } @@ -690,9 +687,9 @@ impl CheckFunc for ServiceTypeCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let K8sObject::Service(svc) = &object.k8s_object { - if let Some(svc_type) = &svc.type_ { - if self.disallowed.contains(svc_type) { + if let K8sObject::Service(svc) = &object.k8s_object + && let Some(svc_type) = &svc.type_ + && self.disallowed.contains(svc_type) { diagnostics.push(Diagnostic { message: format!("Service uses disallowed type '{}'", svc_type), remediation: Some(format!( @@ -701,8 +698,6 @@ impl CheckFunc for ServiceTypeCheck { )), }); } - } - } diagnostics } @@ -755,9 +750,9 @@ impl CheckFunc for HpaMinReplicasCheck { fn check(&self, object: &Object) -> Vec { let mut diagnostics = Vec::new(); - if let K8sObject::HorizontalPodAutoscaler(hpa) = &object.k8s_object { - if let Some(min) = hpa.min_replicas { - if min < self.min_replicas { + if let K8sObject::HorizontalPodAutoscaler(hpa) = &object.k8s_object + && let Some(min) = hpa.min_replicas + && min < self.min_replicas { diagnostics.push(Diagnostic { message: format!( "HPA minReplicas is {} but should be at least {}", @@ -769,8 +764,6 @@ impl CheckFunc for HpaMinReplicasCheck { )), }); } - } - } diagnostics } diff --git a/src/auth/credentials.rs b/src/auth/credentials.rs index 646add04..c089c46f 100644 --- a/src/auth/credentials.rs +++ b/src/auth/credentials.rs @@ -40,10 +40,7 @@ pub fn get_access_token() -> Option { // Check expiry if let Some(expires_at) = config.syncable_auth.expires_at { - let now = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok()? - .as_secs(); + let now = SystemTime::now().duration_since(UNIX_EPOCH).ok()?.as_secs(); if now > expires_at { return None; // Token expired } diff --git a/src/auth/device_flow.rs b/src/auth/device_flow.rs index 0d69aaf6..5bed012f 100644 --- a/src/auth/device_flow.rs +++ b/src/auth/device_flow.rs @@ -3,7 +3,7 @@ //! Implements the OAuth 2.0 device flow to authenticate CLI users via the Syncable web interface. use super::credentials; -use anyhow::{anyhow, Result}; +use anyhow::{Result, anyhow}; use reqwest::Client; use serde::Deserialize; use std::time::{Duration, Instant}; @@ -100,7 +100,7 @@ pub async fn login(no_browser: bool) -> Result<()> { .verification_uri_complete .as_ref() .unwrap_or(&device_code.verification_uri); - + if let Err(e) = open::that(url) { println!("⚠️ Could not open browser automatically: {}", e); println!(" Please open the URL above manually."); diff --git a/src/lib.rs b/src/lib.rs index a9d0a642..23269e3f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -215,19 +215,23 @@ pub async fn run_command(command: Commands) -> Result<()> { } } Commands::Auth { command } => { - use cli::AuthCommand; use auth::credentials; use auth::device_flow; - + use cli::AuthCommand; + match command { AuthCommand::Login { no_browser } => { device_flow::login(no_browser).await.map_err(|e| { - error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(e.to_string())) + error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed( + e.to_string(), + )) }) } AuthCommand::Logout => { credentials::clear_credentials().map_err(|e| { - error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed(e.to_string())) + error::IaCGeneratorError::Config(error::ConfigError::ParsingFailed( + e.to_string(), + )) })?; println!("✅ Logged out successfully. Credentials cleared."); Ok(()) @@ -263,22 +267,20 @@ pub async fn run_command(command: Commands) -> Result<()> { } Ok(()) } - AuthCommand::Token { raw } => { - match credentials::get_access_token() { - Some(token) => { - if raw { - print!("{}", token); - } else { - println!("Access Token: {}", token); - } - Ok(()) - } - None => { - eprintln!("Not authenticated. Run: sync-ctl auth login"); - std::process::exit(1); + AuthCommand::Token { raw } => match credentials::get_access_token() { + Some(token) => { + if raw { + print!("{}", token); + } else { + println!("Access Token: {}", token); } + Ok(()) } - } + None => { + eprintln!("Not authenticated. Run: sync-ctl auth login"); + std::process::exit(1); + } + }, } } } From b9eece01a5d8ad98c3e1a28e3571f75540b50762 Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Wed, 31 Dec 2025 09:53:24 +0100 Subject: [PATCH 2/3] feat: added authentication, for agent usage --- src/lib.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/lib.rs b/src/lib.rs index 23269e3f..2a969481 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -118,6 +118,16 @@ pub async fn run_command(command: Commands) -> Result<()> { use cli::ChatProvider; use config::load_agent_config; + // Check if user is authenticated with Syncable + if !auth::credentials::is_authenticated() { + println!("\n\x1b[1;33m📢 Sign in to use Syncable Agent\x1b[0m"); + println!(" It's free and costs you nothing!\n"); + println!(" Run: \x1b[1;36msync-ctl auth login\x1b[0m\n"); + return Err(error::IaCGeneratorError::Config( + error::ConfigError::MissingConfig("Syncable authentication required".to_string()) + )); + } + let project_path = path.canonicalize().unwrap_or(path); // Handle --resume flag From 5327abf8bd1fe6b7b2be79d891e1d0202b0bda38 Mon Sep 17 00:00:00 2001 From: Alex Holmberg Date: Wed, 31 Dec 2025 09:58:40 +0100 Subject: [PATCH 3/3] feat: fixed fmt / clappy issues in ci pipeline --- src/agent/persistence.rs | 8 +- src/agent/tools/fetch.rs | 47 ++-- src/agent/ui/hooks.rs | 220 ++++++++-------- src/analyzer/helmlint/lint.rs | 7 +- src/analyzer/helmlint/pragma.rs | 14 +- src/analyzer/helmlint/rules/hl1xxx.rs | 237 +++++++++--------- src/analyzer/helmlint/rules/hl2xxx.rs | 71 +++--- src/analyzer/helmlint/rules/hl3xxx.rs | 7 +- src/analyzer/helmlint/rules/hl4xxx.rs | 116 +++++---- src/analyzer/kubelint/config.rs | 14 +- .../kubelint/templates/hostnetwork.rs | 45 ++-- src/analyzer/kubelint/templates/misc.rs | 72 +++--- src/analyzer/kubelint/templates/pdb.rs | 52 ++-- src/analyzer/kubelint/templates/privileged.rs | 7 +- src/analyzer/kubelint/templates/rbac.rs | 20 +- .../kubelint/templates/unsafeprocmount.rs | 7 +- src/analyzer/kubelint/templates/validation.rs | 100 ++++---- src/lib.rs | 4 +- 18 files changed, 548 insertions(+), 500 deletions(-) diff --git a/src/agent/persistence.rs b/src/agent/persistence.rs index b5fba97b..33572596 100644 --- a/src/agent/persistence.rs +++ b/src/agent/persistence.rs @@ -252,9 +252,11 @@ impl SessionSelector { // Try to parse as numeric index first if let Ok(index) = identifier.parse::() - && index > 0 && index <= sessions.len() { - return sessions.into_iter().nth(index - 1); - } + && index > 0 + && index <= sessions.len() + { + return sessions.into_iter().nth(index - 1); + } // Try to find by UUID or partial UUID sessions diff --git a/src/agent/tools/fetch.rs b/src/agent/tools/fetch.rs index 6c38979a..b4f63eb6 100644 --- a/src/agent/tools/fetch.rs +++ b/src/agent/tools/fetch.rs @@ -71,32 +71,33 @@ impl WebFetchTool { // Try to fetch robots.txt (ignore errors - many sites don't have one) if let Ok(response) = self.client().get(&robots_url).send().await && response.status().is_success() - && let Ok(robots_content) = response.text().await { - let path = url.path(); - for line in robots_content.lines() { - if let Some(disallowed) = line.strip_prefix("Disallow: ") { - let disallowed = disallowed.trim(); - if !disallowed.is_empty() { - let disallowed = if !disallowed.starts_with('/') { - format!("/{}", disallowed) - } else { - disallowed.to_string() - }; - let check_path = if !path.starts_with('/') { - format!("/{}", path) - } else { - path.to_string() - }; - if check_path.starts_with(&disallowed) { - return Err(WebFetchError(format!( - "URL {} cannot be fetched due to robots.txt restrictions", - url - ))); - } - } + && let Ok(robots_content) = response.text().await + { + let path = url.path(); + for line in robots_content.lines() { + if let Some(disallowed) = line.strip_prefix("Disallow: ") { + let disallowed = disallowed.trim(); + if !disallowed.is_empty() { + let disallowed = if !disallowed.starts_with('/') { + format!("/{}", disallowed) + } else { + disallowed.to_string() + }; + let check_path = if !path.starts_with('/') { + format!("/{}", path) + } else { + path.to_string() + }; + if check_path.starts_with(&disallowed) { + return Err(WebFetchError(format!( + "URL {} cannot be fetched due to robots.txt restrictions", + url + ))); } } } + } + } Ok(()) } diff --git a/src/agent/ui/hooks.rs b/src/agent/ui/hooks.rs index 2dbcea8a..89240407 100644 --- a/src/agent/ui/hooks.rs +++ b/src/agent/ui/hooks.rs @@ -1194,47 +1194,48 @@ fn format_kubelint_result( // Check for parse errors first if let Some(errors) = parse_errors - && !errors.is_empty() { - lines.push(format!( - "{}☸ {} parse error{} (files could not be fully analyzed){}", - ansi::HIGH, - errors.len(), - if errors.len() == 1 { "" } else { "s" }, - ansi::RESET - )); - for (i, err) in errors.iter().take(3).enumerate() { - if let Some(err_str) = err.as_str() { - let truncated = if err_str.len() > 70 { - format!("{}...", &err_str[..67]) - } else { - err_str.to_string() - }; - lines.push(format!( - "{} {} {}{}", - ansi::HIGH, - if i == errors.len().min(3) - 1 { - "└" - } else { - "│" - }, - truncated, - ansi::RESET - )); - } - } - if errors.len() > 3 { + && !errors.is_empty() + { + lines.push(format!( + "{}☸ {} parse error{} (files could not be fully analyzed){}", + ansi::HIGH, + errors.len(), + if errors.len() == 1 { "" } else { "s" }, + ansi::RESET + )); + for (i, err) in errors.iter().take(3).enumerate() { + if let Some(err_str) = err.as_str() { + let truncated = if err_str.len() > 70 { + format!("{}...", &err_str[..67]) + } else { + err_str.to_string() + }; lines.push(format!( - "{} +{} more errors{}", - ansi::GRAY, - errors.len() - 3, + "{} {} {}{}", + ansi::HIGH, + if i == errors.len().min(3) - 1 { + "└" + } else { + "│" + }, + truncated, ansi::RESET )); } - // If we only have parse errors and no lint issues, return early - if total == 0 { - return (false, lines); - } } + if errors.len() > 3 { + lines.push(format!( + "{} +{} more errors{}", + ansi::GRAY, + errors.len() - 3, + ansi::RESET + )); + } + // If we only have parse errors and no lint issues, return early + if total == 0 { + return (false, lines); + } + } if total == 0 && parse_errors.map(|e| e.is_empty()).unwrap_or(true) { lines.push(format!( @@ -1329,28 +1330,29 @@ fn format_kubelint_result( && let Some(high_issues) = action_plan .and_then(|a| a.get("high")) .and_then(|h| h.as_array()) - { - for issue in high_issues.iter().take(MAX_PREVIEW - shown) { - lines.push(format_kubelint_issue(issue, "🟠", ansi::HIGH)); - shown += 1; - } + { + for issue in high_issues.iter().take(MAX_PREVIEW - shown) { + lines.push(format_kubelint_issue(issue, "🟠", ansi::HIGH)); + shown += 1; } + } // Show quick fix hint if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array()) - && let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str()) { - let truncated = if first_fix.len() > 70 { - format!("{}...", &first_fix[..67]) - } else { - first_fix.to_string() - }; - lines.push(format!( - "{} → Fix: {}{}", - ansi::INFO_BLUE, - truncated, - ansi::RESET - )); - } + && let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str()) + { + let truncated = if first_fix.len() > 70 { + format!("{}...", &first_fix[..67]) + } else { + first_fix.to_string() + }; + lines.push(format!( + "{} → Fix: {}{}", + ansi::INFO_BLUE, + truncated, + ansi::RESET + )); + } // Note about remaining issues let remaining = total as usize - shown; @@ -1427,47 +1429,48 @@ fn format_helmlint_result( // Check for parse errors first if let Some(errors) = parse_errors - && !errors.is_empty() { - lines.push(format!( - "{}⎈ {} parse error{} (chart could not be fully analyzed){}", - ansi::HIGH, - errors.len(), - if errors.len() == 1 { "" } else { "s" }, - ansi::RESET - )); - for (i, err) in errors.iter().take(3).enumerate() { - if let Some(err_str) = err.as_str() { - let truncated = if err_str.len() > 70 { - format!("{}...", &err_str[..67]) - } else { - err_str.to_string() - }; - lines.push(format!( - "{} {} {}{}", - ansi::HIGH, - if i == errors.len().min(3) - 1 { - "└" - } else { - "│" - }, - truncated, - ansi::RESET - )); - } - } - if errors.len() > 3 { + && !errors.is_empty() + { + lines.push(format!( + "{}⎈ {} parse error{} (chart could not be fully analyzed){}", + ansi::HIGH, + errors.len(), + if errors.len() == 1 { "" } else { "s" }, + ansi::RESET + )); + for (i, err) in errors.iter().take(3).enumerate() { + if let Some(err_str) = err.as_str() { + let truncated = if err_str.len() > 70 { + format!("{}...", &err_str[..67]) + } else { + err_str.to_string() + }; lines.push(format!( - "{} +{} more errors{}", - ansi::GRAY, - errors.len() - 3, + "{} {} {}{}", + ansi::HIGH, + if i == errors.len().min(3) - 1 { + "└" + } else { + "│" + }, + truncated, ansi::RESET )); } - // If we only have parse errors and no lint issues, return early - if total == 0 { - return (false, lines); - } } + if errors.len() > 3 { + lines.push(format!( + "{} +{} more errors{}", + ansi::GRAY, + errors.len() - 3, + ansi::RESET + )); + } + // If we only have parse errors and no lint issues, return early + if total == 0 { + return (false, lines); + } + } if total == 0 && parse_errors.map(|e| e.is_empty()).unwrap_or(true) { lines.push(format!( @@ -1562,28 +1565,29 @@ fn format_helmlint_result( && let Some(high_issues) = action_plan .and_then(|a| a.get("high")) .and_then(|h| h.as_array()) - { - for issue in high_issues.iter().take(MAX_PREVIEW - shown) { - lines.push(format_helmlint_issue(issue, "🟠", ansi::HIGH)); - shown += 1; - } + { + for issue in high_issues.iter().take(MAX_PREVIEW - shown) { + lines.push(format_helmlint_issue(issue, "🟠", ansi::HIGH)); + shown += 1; } + } // Show quick fix hint if let Some(quick_fixes) = v.get("quick_fixes").and_then(|q| q.as_array()) - && let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str()) { - let truncated = if first_fix.len() > 70 { - format!("{}...", &first_fix[..67]) - } else { - first_fix.to_string() - }; - lines.push(format!( - "{} → Fix: {}{}", - ansi::INFO_BLUE, - truncated, - ansi::RESET - )); - } + && let Some(first_fix) = quick_fixes.first().and_then(|f| f.as_str()) + { + let truncated = if first_fix.len() > 70 { + format!("{}...", &first_fix[..67]) + } else { + first_fix.to_string() + }; + lines.push(format!( + "{} → Fix: {}{}", + ansi::INFO_BLUE, + truncated, + ansi::RESET + )); + } // Note about remaining issues let remaining = total as usize - shown; diff --git a/src/analyzer/helmlint/lint.rs b/src/analyzer/helmlint/lint.rs index 68c282fb..5fb9094c 100644 --- a/src/analyzer/helmlint/lint.rs +++ b/src/analyzer/helmlint/lint.rs @@ -316,9 +316,10 @@ fn collect_chart_files(path: &Path) -> HashSet { .filter_map(|e| e.ok()) { if entry.path().is_file() - && let Ok(relative) = entry.path().strip_prefix(path) { - files.insert(relative.display().to_string()); - } + && let Ok(relative) = entry.path().strip_prefix(path) + { + files.insert(relative.display().to_string()); + } } files diff --git a/src/analyzer/helmlint/pragma.rs b/src/analyzer/helmlint/pragma.rs index 83969c39..53a203b2 100644 --- a/src/analyzer/helmlint/pragma.rs +++ b/src/analyzer/helmlint/pragma.rs @@ -39,16 +39,18 @@ impl PragmaState { // Check if the rule is ignored for this specific line if let Some(ignores) = self.line_ignores.get(&line) - && ignores.contains(code.as_str()) { - return true; - } + && ignores.contains(code.as_str()) + { + return true; + } // Check if previous line has an ignore pragma for this line if line > 1 && let Some(ignores) = self.line_ignores.get(&(line - 1)) - && ignores.contains(code.as_str()) { - return true; - } + && ignores.contains(code.as_str()) + { + return true; + } false } diff --git a/src/analyzer/helmlint/rules/hl1xxx.rs b/src/analyzer/helmlint/rules/hl1xxx.rs index eaa8f150..3af285da 100644 --- a/src/analyzer/helmlint/rules/hl1xxx.rs +++ b/src/analyzer/helmlint/rules/hl1xxx.rs @@ -87,20 +87,21 @@ impl Rule for HL1002 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata - && !chart.has_valid_api_version() { - let version = match &chart.api_version { - ApiVersion::Unknown(v) => v.clone(), - _ => "unknown".to_string(), - }; - return vec![CheckFailure::new( - "HL1002", - Severity::Error, - format!("Invalid apiVersion '{}'. Must be v1 or v2", version), - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && !chart.has_valid_api_version() + { + let version = match &chart.api_version { + ApiVersion::Unknown(v) => v.clone(), + _ => "unknown".to_string(), + }; + return vec![CheckFailure::new( + "HL1002", + Severity::Error, + format!("Invalid apiVersion '{}'. Must be v1 or v2", version), + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -127,16 +128,17 @@ impl Rule for HL1003 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata - && chart.name.is_empty() { - return vec![CheckFailure::new( - "HL1003", - Severity::Error, - "Missing required field 'name' in Chart.yaml", - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && chart.name.is_empty() + { + return vec![CheckFailure::new( + "HL1003", + Severity::Error, + "Missing required field 'name' in Chart.yaml", + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -163,16 +165,17 @@ impl Rule for HL1004 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata - && chart.version.is_empty() { - return vec![CheckFailure::new( - "HL1004", - Severity::Error, - "Missing required field 'version' in Chart.yaml", - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && chart.version.is_empty() + { + return vec![CheckFailure::new( + "HL1004", + Severity::Error, + "Missing required field 'version' in Chart.yaml", + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -199,19 +202,21 @@ impl Rule for HL1005 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata - && !chart.version.is_empty() && !is_valid_semver(&chart.version) { - return vec![CheckFailure::new( - "HL1005", - Severity::Warning, - format!( - "Version '{}' is not valid SemVer (expected X.Y.Z format)", - chart.version - ), - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && !chart.version.is_empty() + && !is_valid_semver(&chart.version) + { + return vec![CheckFailure::new( + "HL1005", + Severity::Warning, + format!( + "Version '{}' is not valid SemVer (expected X.Y.Z format)", + chart.version + ), + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -244,16 +249,16 @@ impl Rule for HL1006 { .as_ref() .map(|d| d.is_empty()) .unwrap_or(true)) - { - return vec![CheckFailure::new( - "HL1006", - Severity::Info, - "Chart.yaml is missing a description", - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + { + return vec![CheckFailure::new( + "HL1006", + Severity::Info, + "Chart.yaml is missing a description", + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -280,16 +285,17 @@ impl Rule for HL1007 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata - && chart.maintainers.is_empty() { - return vec![CheckFailure::new( - "HL1007", - Severity::Info, - "Chart.yaml has no maintainers listed", - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && chart.maintainers.is_empty() + { + return vec![CheckFailure::new( + "HL1007", + Severity::Info, + "Chart.yaml has no maintainers listed", + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -316,16 +322,17 @@ impl Rule for HL1008 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata - && chart.is_deprecated() { - return vec![CheckFailure::new( - "HL1008", - Severity::Warning, - "Chart is marked as deprecated", - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && chart.is_deprecated() + { + return vec![CheckFailure::new( + "HL1008", + Severity::Warning, + "Chart is marked as deprecated", + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -353,9 +360,10 @@ impl Rule for HL1009 { fn check(&self, ctx: &LintContext) -> Vec { // Skip for library charts if let Some(chart) = ctx.chart_metadata - && chart.is_library() { - return vec![]; - } + && chart.is_library() + { + return vec![]; + } let has_templates = ctx .files @@ -459,19 +467,20 @@ impl Rule for HL1012 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata - && !is_valid_chart_name(&chart.name) { - return vec![CheckFailure::new( - "HL1012", - Severity::Error, - format!( - "Chart name '{}' contains invalid characters. Use only lowercase letters, numbers, and hyphens", - chart.name - ), - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && !is_valid_chart_name(&chart.name) + { + return vec![CheckFailure::new( + "HL1012", + Severity::Error, + format!( + "Chart name '{}' contains invalid characters. Use only lowercase letters, numbers, and hyphens", + chart.name + ), + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -499,16 +508,17 @@ impl Rule for HL1013 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata && let Some(icon) = &chart.icon - && icon.starts_with("http://") { - return vec![CheckFailure::new( - "HL1013", - Severity::Warning, - "Icon URL should use HTTPS instead of HTTP", - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && icon.starts_with("http://") + { + return vec![CheckFailure::new( + "HL1013", + Severity::Warning, + "Icon URL should use HTTPS instead of HTTP", + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } @@ -536,16 +546,17 @@ impl Rule for HL1014 { fn check(&self, ctx: &LintContext) -> Vec { if let Some(chart) = ctx.chart_metadata && let Some(home) = &chart.home - && home.starts_with("http://") { - return vec![CheckFailure::new( - "HL1014", - Severity::Warning, - "Home URL should use HTTPS instead of HTTP", - "Chart.yaml", - 1, - RuleCategory::Structure, - )]; - } + && home.starts_with("http://") + { + return vec![CheckFailure::new( + "HL1014", + Severity::Warning, + "Home URL should use HTTPS instead of HTTP", + "Chart.yaml", + 1, + RuleCategory::Structure, + )]; + } vec![] } } diff --git a/src/analyzer/helmlint/rules/hl2xxx.rs b/src/analyzer/helmlint/rules/hl2xxx.rs index 96b64301..7cdcbe36 100644 --- a/src/analyzer/helmlint/rules/hl2xxx.rs +++ b/src/analyzer/helmlint/rules/hl2xxx.rs @@ -241,21 +241,22 @@ impl Rule for HL2005 { if is_port_field && let Some(value) = values.get(path) - && let Some(port) = extract_port_number(value) - && !(1..=65535).contains(&port) { - let line = values.line_for_path(path).unwrap_or(1); - failures.push(CheckFailure::new( - "HL2005", - Severity::Error, - format!( - "Invalid port number {} at '{}'. Must be between 1 and 65535", - port, path - ), - "values.yaml", - line, - RuleCategory::Values, - )); - } + && let Some(port) = extract_port_number(value) + && !(1..=65535).contains(&port) + { + let line = values.line_for_path(path).unwrap_or(1); + failures.push(CheckFailure::new( + "HL2005", + Severity::Error, + format!( + "Invalid port number {} at '{}'. Must be between 1 and 65535", + port, path + ), + "values.yaml", + line, + RuleCategory::Values, + )); + } } failures @@ -295,9 +296,10 @@ impl Rule for HL2007 { let lower_path = path.to_lowercase(); if (lower_path.ends_with(".tag") || lower_path.ends_with("imagetag")) && let Some(serde_yaml::Value::String(tag)) = values.get(path) - && tag == "latest" { - let line = values.line_for_path(path).unwrap_or(1); - failures.push(CheckFailure::new( + && tag == "latest" + { + let line = values.line_for_path(path).unwrap_or(1); + failures.push(CheckFailure::new( "HL2007", Severity::Warning, format!( @@ -308,7 +310,7 @@ impl Rule for HL2007 { line, RuleCategory::Values, )); - } + } } failures @@ -347,21 +349,22 @@ impl Rule for HL2008 { let lower_path = path.to_lowercase(); if (lower_path.ends_with("replicacount") || lower_path.ends_with("replicas")) && let Some(value) = values.get(path) - && let Some(count) = extract_number(value) - && count == 0 { - let line = values.line_for_path(path).unwrap_or(1); - failures.push(CheckFailure::new( - "HL2008", - Severity::Warning, - format!( - "Replica count at '{}' is 0. No pods will be created by default", - path - ), - "values.yaml", - line, - RuleCategory::Values, - )); - } + && let Some(count) = extract_number(value) + && count == 0 + { + let line = values.line_for_path(path).unwrap_or(1); + failures.push(CheckFailure::new( + "HL2008", + Severity::Warning, + format!( + "Replica count at '{}' is 0. No pods will be created by default", + path + ), + "values.yaml", + line, + RuleCategory::Values, + )); + } } failures diff --git a/src/analyzer/helmlint/rules/hl3xxx.rs b/src/analyzer/helmlint/rules/hl3xxx.rs index f0a25cdd..139fb66e 100644 --- a/src/analyzer/helmlint/rules/hl3xxx.rs +++ b/src/analyzer/helmlint/rules/hl3xxx.rs @@ -305,9 +305,10 @@ impl Rule for HL3008 { fn check(&self, ctx: &LintContext) -> Vec { // Skip for library charts if let Some(chart) = ctx.chart_metadata - && chart.is_library() { - return vec![]; - } + && chart.is_library() + { + return vec![]; + } let has_notes = ctx.files.iter().any(|f| f.ends_with("NOTES.txt")); if !has_notes { diff --git a/src/analyzer/helmlint/rules/hl4xxx.rs b/src/analyzer/helmlint/rules/hl4xxx.rs index 2fbcbe6d..db002009 100644 --- a/src/analyzer/helmlint/rules/hl4xxx.rs +++ b/src/analyzer/helmlint/rules/hl4xxx.rs @@ -125,17 +125,18 @@ impl Rule for HL4002 { for path in &values.defined_paths { if path.to_lowercase().contains("privileged") && let Some(value) = values.get(path) - && is_truthy(value) { - let line = values.line_for_path(path).unwrap_or(1); - failures.push(CheckFailure::new( - "HL4002", - Severity::Error, - format!("Privileged mode enabled at '{}'", path), - "values.yaml", - line, - RuleCategory::Security, - )); - } + && is_truthy(value) + { + let line = values.line_for_path(path).unwrap_or(1); + failures.push(CheckFailure::new( + "HL4002", + Severity::Error, + format!("Privileged mode enabled at '{}'", path), + "values.yaml", + line, + RuleCategory::Security, + )); + } } } @@ -143,16 +144,17 @@ impl Rule for HL4002 { for template in ctx.templates { for token in &template.tokens { if let TemplateToken::Text { content, line } = token - && content.contains("privileged: true") { - failures.push(CheckFailure::new( - "HL4002", - Severity::Error, - "Container is configured with privileged: true", - &template.path, - *line, - RuleCategory::Security, - )); - } + && content.contains("privileged: true") + { + failures.push(CheckFailure::new( + "HL4002", + Severity::Error, + "Container is configured with privileged: true", + &template.path, + *line, + RuleCategory::Security, + )); + } } } @@ -186,8 +188,9 @@ impl Rule for HL4003 { for template in ctx.templates { for token in &template.tokens { if let TemplateToken::Text { content, line } = token - && content.contains("hostPath:") { - failures.push(CheckFailure::new( + && content.contains("hostPath:") + { + failures.push(CheckFailure::new( "HL4003", Severity::Warning, "Using hostPath volume mount. This can expose the host filesystem to the container", @@ -195,7 +198,7 @@ impl Rule for HL4003 { *line, RuleCategory::Security, )); - } + } } } @@ -231,17 +234,18 @@ impl Rule for HL4004 { for path in &values.defined_paths { if path.to_lowercase().contains("hostnetwork") && let Some(value) = values.get(path) - && is_truthy(value) { - let line = values.line_for_path(path).unwrap_or(1); - failures.push(CheckFailure::new( - "HL4004", - Severity::Warning, - format!("Host network enabled at '{}'", path), - "values.yaml", - line, - RuleCategory::Security, - )); - } + && is_truthy(value) + { + let line = values.line_for_path(path).unwrap_or(1); + failures.push(CheckFailure::new( + "HL4004", + Severity::Warning, + format!("Host network enabled at '{}'", path), + "values.yaml", + line, + RuleCategory::Security, + )); + } } } @@ -249,16 +253,17 @@ impl Rule for HL4004 { for template in ctx.templates { for token in &template.tokens { if let TemplateToken::Text { content, line } = token - && content.contains("hostNetwork: true") { - failures.push(CheckFailure::new( - "HL4004", - Severity::Warning, - "Pod uses host network. This bypasses network policies", - &template.path, - *line, - RuleCategory::Security, - )); - } + && content.contains("hostNetwork: true") + { + failures.push(CheckFailure::new( + "HL4004", + Severity::Warning, + "Pod uses host network. This bypasses network policies", + &template.path, + *line, + RuleCategory::Security, + )); + } } } @@ -292,16 +297,17 @@ impl Rule for HL4005 { for template in ctx.templates { for token in &template.tokens { if let TemplateToken::Text { content, line } = token - && content.contains("hostPID: true") { - failures.push(CheckFailure::new( - "HL4005", - Severity::Warning, - "Pod uses host PID namespace. This can expose host processes", - &template.path, - *line, - RuleCategory::Security, - )); - } + && content.contains("hostPID: true") + { + failures.push(CheckFailure::new( + "HL4005", + Severity::Warning, + "Pod uses host PID namespace. This can expose host processes", + &template.path, + *line, + RuleCategory::Security, + )); + } } } diff --git a/src/analyzer/kubelint/config.rs b/src/analyzer/kubelint/config.rs index a81a979a..f24e14ae 100644 --- a/src/analyzer/kubelint/config.rs +++ b/src/analyzer/kubelint/config.rs @@ -157,9 +157,10 @@ impl KubelintConfig { for pattern in &self.ignore_paths { if let Ok(glob) = glob::Pattern::new(pattern) - && glob.matches(&path_str) { - return true; - } + && glob.matches(&path_str) + { + return true; + } // Also check simple prefix/suffix matches if path_str.contains(pattern) { return true; @@ -186,9 +187,10 @@ impl KubelintConfig { for filename in &[".kube-linter.yaml", ".kube-linter.yml"] { let path = Path::new(filename); if path.exists() - && let Ok(config) = Self::load_from_file(path) { - return Some(config); - } + && let Ok(config) = Self::load_from_file(path) + { + return Some(config); + } } None } diff --git a/src/analyzer/kubelint/templates/hostnetwork.rs b/src/analyzer/kubelint/templates/hostnetwork.rs index 9a8d1a6e..abfc3671 100644 --- a/src/analyzer/kubelint/templates/hostnetwork.rs +++ b/src/analyzer/kubelint/templates/hostnetwork.rs @@ -44,16 +44,17 @@ impl CheckFunc for HostNetworkCheck { let mut diagnostics = Vec::new(); if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) - && pod_spec.host_network == Some(true) { - diagnostics.push(Diagnostic { - message: "Pod is configured to use the host's network namespace".to_string(), - remediation: Some( - "Remove hostNetwork: true unless absolutely necessary. \ + && pod_spec.host_network == Some(true) + { + diagnostics.push(Diagnostic { + message: "Pod is configured to use the host's network namespace".to_string(), + remediation: Some( + "Remove hostNetwork: true unless absolutely necessary. \ Using host network grants access to all network interfaces on the host." - .to_string(), - ), - }); - } + .to_string(), + ), + }); + } diagnostics } @@ -98,17 +99,18 @@ impl CheckFunc for HostPIDCheck { let mut diagnostics = Vec::new(); if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) - && pod_spec.host_pid == Some(true) { - diagnostics.push(Diagnostic { - message: "Pod is configured to use the host's PID namespace".to_string(), - remediation: Some( - "Remove hostPID: true unless absolutely necessary. \ + && pod_spec.host_pid == Some(true) + { + diagnostics.push(Diagnostic { + message: "Pod is configured to use the host's PID namespace".to_string(), + remediation: Some( + "Remove hostPID: true unless absolutely necessary. \ Using host PID allows processes in the container to see and signal all \ processes on the host." - .to_string(), - ), - }); - } + .to_string(), + ), + }); + } diagnostics } @@ -153,8 +155,9 @@ impl CheckFunc for HostIPCCheck { let mut diagnostics = Vec::new(); if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) - && pod_spec.host_ipc == Some(true) { - diagnostics.push(Diagnostic { + && pod_spec.host_ipc == Some(true) + { + diagnostics.push(Diagnostic { message: "Pod is configured to use the host's IPC namespace".to_string(), remediation: Some( "Remove hostIPC: true unless absolutely necessary. \ @@ -162,7 +165,7 @@ impl CheckFunc for HostIPCCheck { .to_string(), ), }); - } + } diagnostics } diff --git a/src/analyzer/kubelint/templates/misc.rs b/src/analyzer/kubelint/templates/misc.rs index 79fa3450..1d2a0845 100644 --- a/src/analyzer/kubelint/templates/misc.rs +++ b/src/analyzer/kubelint/templates/misc.rs @@ -53,26 +53,24 @@ impl CheckFunc for SysctlsCheck { ]; if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) - && let Some(sc) = &pod_spec.security_context { - for sysctl in &sc.sysctls { - let is_unsafe = unsafe_sysctls - .iter() - .any(|prefix| sysctl.name.starts_with(prefix)); - if is_unsafe { - diagnostics.push(Diagnostic { - message: format!( - "Pod uses potentially unsafe sysctl '{}'", - sysctl.name - ), - remediation: Some( - "Ensure this sysctl is allowed by the cluster's PodSecurityPolicy \ + && let Some(sc) = &pod_spec.security_context + { + for sysctl in &sc.sysctls { + let is_unsafe = unsafe_sysctls + .iter() + .any(|prefix| sysctl.name.starts_with(prefix)); + if is_unsafe { + diagnostics.push(Diagnostic { + message: format!("Pod uses potentially unsafe sysctl '{}'", sysctl.name), + remediation: Some( + "Ensure this sysctl is allowed by the cluster's PodSecurityPolicy \ or PodSecurityStandard and is necessary for your workload." - .to_string(), - ), - }); - } + .to_string(), + ), + }); } } + } diagnostics } @@ -117,27 +115,29 @@ impl CheckFunc for DnsConfigOptionsCheck { let mut diagnostics = Vec::new(); if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) - && let Some(dns_config) = &pod_spec.dns_config { - // Check for ndots setting that could cause performance issues - for option in &dns_config.options { - if let Some(name) = &option.name - && name == "ndots" - && let Some(value) = &option.value - && let Ok(ndots) = value.parse::() - && ndots > 5 { - diagnostics.push(Diagnostic { - message: format!( - "DNS ndots is set to {}, which may cause DNS lookup performance issues", - ndots - ), - remediation: Some( - "Consider lowering ndots to 2 or less for better DNS performance." - .to_string(), - ), - }); - } + && let Some(dns_config) = &pod_spec.dns_config + { + // Check for ndots setting that could cause performance issues + for option in &dns_config.options { + if let Some(name) = &option.name + && name == "ndots" + && let Some(value) = &option.value + && let Ok(ndots) = value.parse::() + && ndots > 5 + { + diagnostics.push(Diagnostic { + message: format!( + "DNS ndots is set to {}, which may cause DNS lookup performance issues", + ndots + ), + remediation: Some( + "Consider lowering ndots to 2 or less for better DNS performance." + .to_string(), + ), + }); } } + } diagnostics } diff --git a/src/analyzer/kubelint/templates/pdb.rs b/src/analyzer/kubelint/templates/pdb.rs index 4a445726..3a0d4939 100644 --- a/src/analyzer/kubelint/templates/pdb.rs +++ b/src/analyzer/kubelint/templates/pdb.rs @@ -44,21 +44,22 @@ impl CheckFunc for PdbMaxUnavailableCheck { let mut diagnostics = Vec::new(); if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object - && let Some(max_unavailable) = &pdb.max_unavailable { - // Check if it's set to 0 or 0% - if max_unavailable == "0" || max_unavailable == "0%" { - diagnostics.push(Diagnostic { - message: - "PDB maxUnavailable is set to 0, which blocks all voluntary disruptions" - .to_string(), - remediation: Some( - "Set maxUnavailable to at least 1 or a non-zero percentage to allow \ + && let Some(max_unavailable) = &pdb.max_unavailable + { + // Check if it's set to 0 or 0% + if max_unavailable == "0" || max_unavailable == "0%" { + diagnostics.push(Diagnostic { + message: + "PDB maxUnavailable is set to 0, which blocks all voluntary disruptions" + .to_string(), + remediation: Some( + "Set maxUnavailable to at least 1 or a non-zero percentage to allow \ voluntary disruptions during cluster maintenance." - .to_string(), - ), - }); - } + .to_string(), + ), + }); } + } diagnostics } @@ -103,19 +104,22 @@ impl CheckFunc for PdbMinAvailableCheck { let mut diagnostics = Vec::new(); if let K8sObject::PodDisruptionBudget(pdb) = &object.k8s_object - && let Some(min_available) = &pdb.min_available { - // Check if it's set to 100% - if min_available == "100%" { - diagnostics.push(Diagnostic { - message: "PDB minAvailable is set to 100%, which blocks all voluntary disruptions".to_string(), - remediation: Some( - "Set minAvailable to less than 100% to allow voluntary disruptions \ + && let Some(min_available) = &pdb.min_available + { + // Check if it's set to 100% + if min_available == "100%" { + diagnostics.push(Diagnostic { + message: + "PDB minAvailable is set to 100%, which blocks all voluntary disruptions" + .to_string(), + remediation: Some( + "Set minAvailable to less than 100% to allow voluntary disruptions \ during cluster maintenance." - .to_string(), - ), - }); - } + .to_string(), + ), + }); } + } diagnostics } diff --git a/src/analyzer/kubelint/templates/privileged.rs b/src/analyzer/kubelint/templates/privileged.rs index 05f32226..3057bb83 100644 --- a/src/analyzer/kubelint/templates/privileged.rs +++ b/src/analyzer/kubelint/templates/privileged.rs @@ -46,8 +46,9 @@ impl CheckFunc for PrivilegedCheck { if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) { for container in extract::container::all_containers(pod_spec) { if let Some(sc) = &container.security_context - && sc.privileged == Some(true) { - diagnostics.push(Diagnostic { + && sc.privileged == Some(true) + { + diagnostics.push(Diagnostic { message: format!( "Container '{}' is running in privileged mode", container.name @@ -57,7 +58,7 @@ impl CheckFunc for PrivilegedCheck { Set securityContext.privileged to false.".to_string() ), }); - } + } } } diff --git a/src/analyzer/kubelint/templates/rbac.rs b/src/analyzer/kubelint/templates/rbac.rs index 8f7957e6..1d1318d4 100644 --- a/src/analyzer/kubelint/templates/rbac.rs +++ b/src/analyzer/kubelint/templates/rbac.rs @@ -50,16 +50,18 @@ impl CheckFunc for ClusterAdminRoleBindingCheck { }; if let Some(role_ref) = role_ref - && role_ref.kind == "ClusterRole" && role_ref.name == "cluster-admin" { - diagnostics.push(Diagnostic { - message: "Binding grants cluster-admin privileges".to_string(), - remediation: Some( - "Avoid binding to cluster-admin. Create a more restrictive ClusterRole \ + && role_ref.kind == "ClusterRole" + && role_ref.name == "cluster-admin" + { + diagnostics.push(Diagnostic { + message: "Binding grants cluster-admin privileges".to_string(), + remediation: Some( + "Avoid binding to cluster-admin. Create a more restrictive ClusterRole \ with only the required permissions." - .to_string(), - ), - }); - } + .to_string(), + ), + }); + } diagnostics } diff --git a/src/analyzer/kubelint/templates/unsafeprocmount.rs b/src/analyzer/kubelint/templates/unsafeprocmount.rs index 05b3e9b3..cade100b 100644 --- a/src/analyzer/kubelint/templates/unsafeprocmount.rs +++ b/src/analyzer/kubelint/templates/unsafeprocmount.rs @@ -47,8 +47,9 @@ impl CheckFunc for UnsafeProcMountCheck { for container in extract::container::all_containers(pod_spec) { if let Some(sc) = &container.security_context && let Some(proc_mount) = &sc.proc_mount - && proc_mount == "Unmasked" { - diagnostics.push(Diagnostic { + && proc_mount == "Unmasked" + { + diagnostics.push(Diagnostic { message: format!( "Container '{}' has unsafe /proc mount (procMount: Unmasked)", container.name @@ -59,7 +60,7 @@ impl CheckFunc for UnsafeProcMountCheck { .to_string(), ), }); - } + } } } diff --git a/src/analyzer/kubelint/templates/validation.rs b/src/analyzer/kubelint/templates/validation.rs index e59704d3..0ad14a54 100644 --- a/src/analyzer/kubelint/templates/validation.rs +++ b/src/analyzer/kubelint/templates/validation.rs @@ -100,15 +100,16 @@ impl CheckFunc for RestartPolicyCheck { let mut diagnostics = Vec::new(); if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) - && let Some(policy) = &pod_spec.restart_policy { - // For Deployments, StatefulSets, DaemonSets - must be Always - match &object.k8s_object { - K8sObject::Deployment(_) - | K8sObject::StatefulSet(_) - | K8sObject::DaemonSet(_) - | K8sObject::ReplicaSet(_) => { - if policy != "Always" { - diagnostics.push(Diagnostic { + && let Some(policy) = &pod_spec.restart_policy + { + // For Deployments, StatefulSets, DaemonSets - must be Always + match &object.k8s_object { + K8sObject::Deployment(_) + | K8sObject::StatefulSet(_) + | K8sObject::DaemonSet(_) + | K8sObject::ReplicaSet(_) => { + if policy != "Always" { + diagnostics.push(Diagnostic { message: format!( "Restart policy is '{}' but should be 'Always' for this workload type", policy @@ -119,11 +120,11 @@ impl CheckFunc for RestartPolicyCheck { .to_string(), ), }); - } } - _ => {} } + _ => {} } + } diagnostics } @@ -566,15 +567,16 @@ impl CheckFunc for JobTtlSecondsAfterFinishedCheck { let mut diagnostics = Vec::new(); if let K8sObject::Job(job) = &object.k8s_object - && job.ttl_seconds_after_finished.is_none() { - diagnostics.push(Diagnostic { - message: "Job does not have ttlSecondsAfterFinished set".to_string(), - remediation: Some( - "Set ttlSecondsAfterFinished to automatically clean up finished Jobs." - .to_string(), - ), - }); - } + && job.ttl_seconds_after_finished.is_none() + { + diagnostics.push(Diagnostic { + message: "Job does not have ttlSecondsAfterFinished set".to_string(), + remediation: Some( + "Set ttlSecondsAfterFinished to automatically clean up finished Jobs." + .to_string(), + ), + }); + } diagnostics } @@ -619,14 +621,15 @@ impl CheckFunc for PriorityClassNameCheck { let mut diagnostics = Vec::new(); if let Some(pod_spec) = extract::pod_spec::extract_pod_spec(&object.k8s_object) - && pod_spec.priority_class_name.is_none() { - diagnostics.push(Diagnostic { - message: "Pod does not have priorityClassName set".to_string(), - remediation: Some( - "Set priorityClassName to control pod scheduling priority.".to_string(), - ), - }); - } + && pod_spec.priority_class_name.is_none() + { + diagnostics.push(Diagnostic { + message: "Pod does not have priorityClassName set".to_string(), + remediation: Some( + "Set priorityClassName to control pod scheduling priority.".to_string(), + ), + }); + } diagnostics } @@ -689,15 +692,13 @@ impl CheckFunc for ServiceTypeCheck { if let K8sObject::Service(svc) = &object.k8s_object && let Some(svc_type) = &svc.type_ - && self.disallowed.contains(svc_type) { - diagnostics.push(Diagnostic { - message: format!("Service uses disallowed type '{}'", svc_type), - remediation: Some(format!( - "Consider using ClusterIP instead of {}.", - svc_type - )), - }); - } + && self.disallowed.contains(svc_type) + { + diagnostics.push(Diagnostic { + message: format!("Service uses disallowed type '{}'", svc_type), + remediation: Some(format!("Consider using ClusterIP instead of {}.", svc_type)), + }); + } diagnostics } @@ -752,18 +753,19 @@ impl CheckFunc for HpaMinReplicasCheck { if let K8sObject::HorizontalPodAutoscaler(hpa) = &object.k8s_object && let Some(min) = hpa.min_replicas - && min < self.min_replicas { - diagnostics.push(Diagnostic { - message: format!( - "HPA minReplicas is {} but should be at least {}", - min, self.min_replicas - ), - remediation: Some(format!( - "Set minReplicas to at least {} for better availability.", - self.min_replicas - )), - }); - } + && min < self.min_replicas + { + diagnostics.push(Diagnostic { + message: format!( + "HPA minReplicas is {} but should be at least {}", + min, self.min_replicas + ), + remediation: Some(format!( + "Set minReplicas to at least {} for better availability.", + self.min_replicas + )), + }); + } diagnostics } diff --git a/src/lib.rs b/src/lib.rs index 2a969481..38b15d6d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -124,7 +124,9 @@ pub async fn run_command(command: Commands) -> Result<()> { println!(" It's free and costs you nothing!\n"); println!(" Run: \x1b[1;36msync-ctl auth login\x1b[0m\n"); return Err(error::IaCGeneratorError::Config( - error::ConfigError::MissingConfig("Syncable authentication required".to_string()) + error::ConfigError::MissingConfig( + "Syncable authentication required".to_string(), + ), )); }