Initial commit

This commit is contained in:
Tanguy Duhamel
2025-09-29 21:26:41 +02:00
parent f0fd367ed8
commit 323a434c73
208 changed files with 72069 additions and 53 deletions
@@ -0,0 +1,43 @@
"""
Infrastructure Security Modules
This package contains modules for Infrastructure as Code (IaC) security testing.
Available modules:
- Checkov: Terraform/CloudFormation/Kubernetes IaC security
- Hadolint: Dockerfile security linting and best practices
- Kubesec: Kubernetes security risk analysis
- Polaris: Kubernetes configuration validation
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
from typing import List, Type
from ..base import BaseModule
# Module registry for automatic discovery
INFRASTRUCTURE_MODULES: List[Type[BaseModule]] = []
def register_module(module_class: Type[BaseModule]):
"""Register an infrastructure security module"""
INFRASTRUCTURE_MODULES.append(module_class)
return module_class
def get_available_modules() -> List[Type[BaseModule]]:
"""Get all available infrastructure security modules"""
return INFRASTRUCTURE_MODULES.copy()
# Import modules to trigger registration
from .checkov import CheckovModule
from .hadolint import HadolintModule
from .kubesec import KubesecModule
from .polaris import PolarisModule
@@ -0,0 +1,411 @@
"""
Checkov Infrastructure Security Module
This module uses Checkov to scan Infrastructure as Code (IaC) files for
security misconfigurations and compliance violations.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class CheckovModule(BaseModule):
"""Checkov Infrastructure as Code security scanning module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="checkov",
version="3.1.34",
description="Infrastructure as Code security scanning for Terraform, CloudFormation, Kubernetes, and more",
author="FuzzForge Team",
category="infrastructure",
tags=["iac", "terraform", "cloudformation", "kubernetes", "security", "compliance"],
input_schema={
"type": "object",
"properties": {
"frameworks": {
"type": "array",
"items": {"type": "string"},
"default": ["terraform", "cloudformation", "kubernetes"],
"description": "IaC frameworks to scan"
},
"checks": {
"type": "array",
"items": {"type": "string"},
"description": "Specific checks to run"
},
"skip_checks": {
"type": "array",
"items": {"type": "string"},
"description": "Checks to skip"
},
"severity": {
"type": "array",
"items": {"type": "string", "enum": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]},
"default": ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"],
"description": "Minimum severity levels to report"
},
"compact": {
"type": "boolean",
"default": False,
"description": "Use compact output format"
},
"quiet": {
"type": "boolean",
"default": False,
"description": "Suppress verbose output"
},
"soft_fail": {
"type": "boolean",
"default": True,
"description": "Return exit code 0 even when issues are found"
},
"include_patterns": {
"type": "array",
"items": {"type": "string"},
"description": "File patterns to include"
},
"exclude_patterns": {
"type": "array",
"items": {"type": "string"},
"description": "File patterns to exclude"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"check_id": {"type": "string"},
"check_name": {"type": "string"},
"severity": {"type": "string"},
"file_path": {"type": "string"},
"line_range": {"type": "array"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
frameworks = config.get("frameworks", [])
supported_frameworks = [
"terraform", "cloudformation", "kubernetes", "dockerfile",
"ansible", "helm", "serverless", "bicep", "github_actions"
]
for framework in frameworks:
if framework not in supported_frameworks:
raise ValueError(f"Unsupported framework: {framework}. Supported: {supported_frameworks}")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute Checkov IaC security scanning"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info(f"Running Checkov IaC scan on {workspace}")
# Check if there are any IaC files
iac_files = self._find_iac_files(workspace, config.get("frameworks", []))
if not iac_files:
logger.info("No Infrastructure as Code files found")
return self.create_result(
findings=[],
status="success",
summary={"total_findings": 0, "files_scanned": 0}
)
# Build checkov command
cmd = ["checkov", "-d", str(workspace)]
# Add output format
cmd.extend(["--output", "json"])
# Add frameworks
frameworks = config.get("frameworks", ["terraform", "cloudformation", "kubernetes"])
cmd.extend(["--framework"] + frameworks)
# Add specific checks
if config.get("checks"):
cmd.extend(["--check", ",".join(config["checks"])])
# Add skip checks
if config.get("skip_checks"):
cmd.extend(["--skip-check", ",".join(config["skip_checks"])])
# Add compact flag
if config.get("compact", False):
cmd.append("--compact")
# Add quiet flag
if config.get("quiet", False):
cmd.append("--quiet")
# Add soft fail
if config.get("soft_fail", True):
cmd.append("--soft-fail")
# Add include patterns
if config.get("include_patterns"):
for pattern in config["include_patterns"]:
cmd.extend(["--include", pattern])
# Add exclude patterns
if config.get("exclude_patterns"):
for pattern in config["exclude_patterns"]:
cmd.extend(["--exclude", pattern])
# Disable update checks and telemetry
cmd.extend(["--no-guide", "--skip-download"])
logger.debug(f"Running command: {' '.join(cmd)}")
# Run Checkov
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace
)
stdout, stderr = await process.communicate()
# Parse results
findings = []
if process.returncode == 0 or config.get("soft_fail", True):
findings = self._parse_checkov_output(stdout.decode(), workspace, config)
else:
error_msg = stderr.decode()
logger.error(f"Checkov failed: {error_msg}")
return self.create_result(
findings=[],
status="failed",
error=f"Checkov execution failed: {error_msg}"
)
# Create summary
summary = self._create_summary(findings, len(iac_files))
logger.info(f"Checkov found {len(findings)} security issues")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"Checkov module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
def _find_iac_files(self, workspace: Path, frameworks: List[str]) -> List[Path]:
"""Find Infrastructure as Code files in workspace"""
iac_patterns = {
"terraform": ["*.tf", "*.tfvars"],
"cloudformation": ["*.yaml", "*.yml", "*.json", "*template*"],
"kubernetes": ["*.yaml", "*.yml"],
"dockerfile": ["Dockerfile", "*.dockerfile"],
"ansible": ["*.yaml", "*.yml", "playbook*"],
"helm": ["Chart.yaml", "values.yaml", "*.yaml"],
"bicep": ["*.bicep"],
"github_actions": [".github/workflows/*.yaml", ".github/workflows/*.yml"]
}
found_files = []
for framework in frameworks:
patterns = iac_patterns.get(framework, [])
for pattern in patterns:
found_files.extend(workspace.rglob(pattern))
return list(set(found_files)) # Remove duplicates
def _parse_checkov_output(self, output: str, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]:
"""Parse Checkov JSON output into findings"""
findings = []
if not output.strip():
return findings
try:
data = json.loads(output)
# Get severity filter
allowed_severities = set(s.upper() for s in config.get("severity", ["CRITICAL", "HIGH", "MEDIUM", "LOW", "INFO"]))
# Process failed checks
failed_checks = data.get("results", {}).get("failed_checks", [])
for check in failed_checks:
# Extract information
check_id = check.get("check_id", "unknown")
check_name = check.get("check_name", "")
severity = check.get("severity", "MEDIUM").upper()
file_path = check.get("file_path", "")
file_line_range = check.get("file_line_range", [])
resource = check.get("resource", "")
description = check.get("description", "")
guideline = check.get("guideline", "")
# Apply severity filter
if severity not in allowed_severities:
continue
# Make file path relative to workspace
if file_path:
try:
rel_path = Path(file_path).relative_to(workspace)
file_path = str(rel_path)
except ValueError:
pass
# Map severity to our standard levels
finding_severity = self._map_severity(severity)
# Create finding
finding = self.create_finding(
title=f"IaC Security Issue: {check_name}",
description=description or f"Checkov check {check_id} failed for resource {resource}",
severity=finding_severity,
category=self._get_category(check_id, check_name),
file_path=file_path if file_path else None,
line_start=file_line_range[0] if file_line_range and len(file_line_range) > 0 else None,
line_end=file_line_range[1] if file_line_range and len(file_line_range) > 1 else None,
recommendation=self._get_recommendation(check_id, check_name, guideline),
metadata={
"check_id": check_id,
"check_name": check_name,
"checkov_severity": severity,
"resource": resource,
"guideline": guideline,
"bc_category": check.get("bc_category", ""),
"benchmarks": check.get("benchmarks", {}),
"fixed_definition": check.get("fixed_definition", "")
}
)
findings.append(finding)
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse Checkov output: {e}")
except Exception as e:
logger.warning(f"Error processing Checkov results: {e}")
return findings
def _map_severity(self, checkov_severity: str) -> str:
"""Map Checkov severity to our standard severity levels"""
severity_map = {
"CRITICAL": "critical",
"HIGH": "high",
"MEDIUM": "medium",
"LOW": "low",
"INFO": "info"
}
return severity_map.get(checkov_severity.upper(), "medium")
def _get_category(self, check_id: str, check_name: str) -> str:
"""Determine finding category based on check"""
check_lower = f"{check_id} {check_name}".lower()
if any(term in check_lower for term in ["encryption", "encrypt", "kms", "ssl", "tls"]):
return "encryption"
elif any(term in check_lower for term in ["access", "iam", "rbac", "permission"]):
return "access_control"
elif any(term in check_lower for term in ["network", "security group", "firewall", "vpc"]):
return "network_security"
elif any(term in check_lower for term in ["logging", "monitor", "audit"]):
return "logging_monitoring"
elif any(term in check_lower for term in ["storage", "s3", "bucket", "database"]):
return "data_protection"
elif any(term in check_lower for term in ["secret", "password", "key", "credential"]):
return "secrets_management"
elif any(term in check_lower for term in ["backup", "snapshot", "versioning"]):
return "backup_recovery"
else:
return "infrastructure_security"
def _get_recommendation(self, check_id: str, check_name: str, guideline: str) -> str:
"""Generate recommendation based on check"""
if guideline:
return f"Follow the guideline: {guideline}"
# Generic recommendations based on common patterns
check_lower = f"{check_id} {check_name}".lower()
if "encryption" in check_lower:
return "Enable encryption for sensitive data at rest and in transit using appropriate encryption algorithms."
elif "access" in check_lower or "iam" in check_lower:
return "Review and tighten access controls. Follow the principle of least privilege."
elif "network" in check_lower or "security group" in check_lower:
return "Restrict network access to only necessary ports and IP ranges."
elif "logging" in check_lower:
return "Enable comprehensive logging and monitoring for security events."
elif "backup" in check_lower:
return "Implement proper backup and disaster recovery procedures."
else:
return f"Review and fix the security configuration issue identified by check {check_id}."
def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
check_counts = {}
for finding in findings:
# Count by severity
severity_counts[finding.severity] += 1
# Count by category
category = finding.category
category_counts[category] = category_counts.get(category, 0) + 1
# Count by check
check_id = finding.metadata.get("check_id", "unknown")
check_counts[check_id] = check_counts.get(check_id, 0) + 1
return {
"total_findings": len(findings),
"files_scanned": total_files,
"severity_counts": severity_counts,
"category_counts": category_counts,
"top_checks": dict(sorted(check_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
"files_with_issues": len(set(f.file_path for f in findings if f.file_path))
}
@@ -0,0 +1,406 @@
"""
Hadolint Infrastructure Security Module
This module uses Hadolint to scan Dockerfiles for security best practices
and potential vulnerabilities.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class HadolintModule(BaseModule):
"""Hadolint Dockerfile security scanning module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="hadolint",
version="2.12.0",
description="Dockerfile security linting and best practices validation",
author="FuzzForge Team",
category="infrastructure",
tags=["dockerfile", "docker", "security", "best-practices", "linting"],
input_schema={
"type": "object",
"properties": {
"severity": {
"type": "array",
"items": {"type": "string", "enum": ["error", "warning", "info", "style"]},
"default": ["error", "warning", "info", "style"],
"description": "Minimum severity levels to report"
},
"ignored_rules": {
"type": "array",
"items": {"type": "string"},
"description": "Hadolint rules to ignore"
},
"trusted_registries": {
"type": "array",
"items": {"type": "string"},
"description": "List of trusted Docker registries"
},
"allowed_maintainers": {
"type": "array",
"items": {"type": "string"},
"description": "List of allowed maintainer emails"
},
"dockerfile_patterns": {
"type": "array",
"items": {"type": "string"},
"default": ["**/Dockerfile", "**/*.dockerfile", "**/Containerfile"],
"description": "Patterns to find Dockerfile-like files"
},
"strict": {
"type": "boolean",
"default": False,
"description": "Enable strict mode (fail on any issue)"
},
"no_fail": {
"type": "boolean",
"default": True,
"description": "Don't fail on lint errors (useful for reporting)"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"rule": {"type": "string"},
"severity": {"type": "string"},
"message": {"type": "string"},
"file_path": {"type": "string"},
"line": {"type": "integer"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
severity_levels = config.get("severity", ["error", "warning", "info", "style"])
valid_severities = ["error", "warning", "info", "style"]
for severity in severity_levels:
if severity not in valid_severities:
raise ValueError(f"Invalid severity level: {severity}. Valid: {valid_severities}")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute Hadolint Dockerfile security scanning"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info(f"Running Hadolint Dockerfile scan on {workspace}")
# Find all Dockerfiles
dockerfiles = self._find_dockerfiles(workspace, config)
if not dockerfiles:
logger.info("No Dockerfiles found for Hadolint analysis")
return self.create_result(
findings=[],
status="success",
summary={"total_findings": 0, "files_scanned": 0}
)
logger.info(f"Found {len(dockerfiles)} Dockerfile(s) to analyze")
# Process each Dockerfile
all_findings = []
for dockerfile in dockerfiles:
findings = await self._scan_dockerfile(dockerfile, workspace, config)
all_findings.extend(findings)
# Create summary
summary = self._create_summary(all_findings, len(dockerfiles))
logger.info(f"Hadolint found {len(all_findings)} issues across {len(dockerfiles)} Dockerfiles")
return self.create_result(
findings=all_findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"Hadolint module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
def _find_dockerfiles(self, workspace: Path, config: Dict[str, Any]) -> List[Path]:
"""Find Dockerfile-like files in workspace"""
patterns = config.get("dockerfile_patterns", [
"**/Dockerfile", "**/*.dockerfile", "**/Containerfile"
])
# Debug logging
logger.info(f"Hadolint searching in workspace: {workspace}")
logger.info(f"Workspace exists: {workspace.exists()}")
if workspace.exists():
all_files = list(workspace.rglob("*"))
logger.info(f"All files in workspace: {all_files}")
dockerfiles = []
for pattern in patterns:
matches = list(workspace.glob(pattern))
logger.info(f"Pattern '{pattern}' found: {matches}")
dockerfiles.extend(matches)
logger.info(f"Final dockerfiles list: {dockerfiles}")
return list(set(dockerfiles)) # Remove duplicates
async def _scan_dockerfile(self, dockerfile: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]:
"""Scan a single Dockerfile with Hadolint"""
findings = []
try:
# Build hadolint command
cmd = ["hadolint", "--format", "json"]
# Add severity levels
severity_levels = config.get("severity", ["error", "warning", "info", "style"])
if "error" not in severity_levels:
cmd.append("--no-error")
if "warning" not in severity_levels:
cmd.append("--no-warning")
if "info" not in severity_levels:
cmd.append("--no-info")
if "style" not in severity_levels:
cmd.append("--no-style")
# Add ignored rules
ignored_rules = config.get("ignored_rules", [])
for rule in ignored_rules:
cmd.extend(["--ignore", rule])
# Add trusted registries
trusted_registries = config.get("trusted_registries", [])
for registry in trusted_registries:
cmd.extend(["--trusted-registry", registry])
# Add strict mode
if config.get("strict", False):
cmd.append("--strict-labels")
# Add the dockerfile
cmd.append(str(dockerfile))
logger.debug(f"Running command: {' '.join(cmd)}")
# Run hadolint
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace
)
stdout, stderr = await process.communicate()
# Parse results
if process.returncode == 0 or config.get("no_fail", True):
findings = self._parse_hadolint_output(
stdout.decode(), dockerfile, workspace
)
else:
error_msg = stderr.decode()
logger.warning(f"Hadolint failed for {dockerfile}: {error_msg}")
# Continue with other files even if one fails
except Exception as e:
logger.warning(f"Error scanning {dockerfile}: {e}")
return findings
def _parse_hadolint_output(self, output: str, dockerfile: Path, workspace: Path) -> List[ModuleFinding]:
"""Parse Hadolint JSON output into findings"""
findings = []
if not output.strip():
return findings
try:
# Hadolint outputs JSON array
issues = json.loads(output)
for issue in issues:
# Extract information
rule = issue.get("code", "unknown")
message = issue.get("message", "")
level = issue.get("level", "warning").lower()
line = issue.get("line", 0)
column = issue.get("column", 0)
# Make file path relative to workspace
try:
rel_path = dockerfile.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(dockerfile)
# Map Hadolint level to our severity
severity = self._map_severity(level)
# Get category based on rule
category = self._get_category(rule, message)
# Create finding
finding = self.create_finding(
title=f"Dockerfile issue: {rule}",
description=message or f"Hadolint rule {rule} violation",
severity=severity,
category=category,
file_path=file_path,
line_start=line if line > 0 else None,
recommendation=self._get_recommendation(rule, message),
metadata={
"rule": rule,
"hadolint_level": level,
"column": column,
"file": str(dockerfile)
}
)
findings.append(finding)
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse Hadolint output: {e}")
except Exception as e:
logger.warning(f"Error processing Hadolint results: {e}")
return findings
def _map_severity(self, hadolint_level: str) -> str:
"""Map Hadolint severity to our standard severity levels"""
severity_map = {
"error": "high",
"warning": "medium",
"info": "low",
"style": "info"
}
return severity_map.get(hadolint_level.lower(), "medium")
def _get_category(self, rule: str, message: str) -> str:
"""Determine finding category based on rule and message"""
rule_lower = rule.lower()
message_lower = message.lower()
# Security-related categories
if any(term in rule_lower for term in ["dl3", "dl4"]):
if "user" in message_lower or "root" in message_lower:
return "privilege_escalation"
elif "secret" in message_lower or "password" in message_lower:
return "secrets_management"
elif "version" in message_lower or "pin" in message_lower:
return "dependency_management"
elif "add" in message_lower or "copy" in message_lower:
return "file_operations"
else:
return "security_best_practices"
elif any(term in rule_lower for term in ["dl1", "dl2"]):
return "syntax_errors"
elif "3001" in rule or "3002" in rule:
return "user_management"
elif "3008" in rule or "3009" in rule:
return "privilege_escalation"
elif "3014" in rule or "3015" in rule:
return "port_management"
elif "3020" in rule or "3021" in rule:
return "copy_operations"
else:
return "dockerfile_best_practices"
def _get_recommendation(self, rule: str, message: str) -> str:
"""Generate recommendation based on Hadolint rule"""
recommendations = {
# Security-focused recommendations
"DL3002": "Create a non-root user and switch to it before running the application.",
"DL3008": "Pin package versions to ensure reproducible builds and avoid supply chain attacks.",
"DL3009": "Clean up package manager cache after installation to reduce image size and attack surface.",
"DL3020": "Use COPY instead of ADD for local files to avoid unexpected behavior.",
"DL3025": "Use JSON format for CMD and ENTRYPOINT to avoid shell injection vulnerabilities.",
"DL3059": "Use multi-stage builds to reduce final image size and attack surface.",
"DL4001": "Don't use sudo in Dockerfiles as it's unnecessary and can introduce vulnerabilities.",
"DL4003": "Use a package manager instead of downloading and installing manually.",
"DL4004": "Don't use SSH in Dockerfiles as it's a security risk.",
"DL4005": "Use SHELL instruction to specify shell for RUN commands instead of hardcoding paths.",
}
if rule in recommendations:
return recommendations[rule]
# Generic recommendations based on patterns
message_lower = message.lower()
if "user" in message_lower and "root" in message_lower:
return "Avoid running containers as root user. Create and use a non-privileged user."
elif "version" in message_lower or "pin" in message_lower:
return "Pin package versions to specific versions to ensure reproducible builds."
elif "cache" in message_lower or "clean" in message_lower:
return "Clean up package manager caches to reduce image size and potential security issues."
elif "secret" in message_lower or "password" in message_lower:
return "Don't include secrets in Dockerfiles. Use build arguments or runtime secrets instead."
else:
return f"Follow Dockerfile best practices to address rule {rule}."
def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
rule_counts = {}
for finding in findings:
# Count by severity
severity_counts[finding.severity] += 1
# Count by category
category = finding.category
category_counts[category] = category_counts.get(category, 0) + 1
# Count by rule
rule = finding.metadata.get("rule", "unknown")
rule_counts[rule] = rule_counts.get(rule, 0) + 1
return {
"total_findings": len(findings),
"files_scanned": total_files,
"severity_counts": severity_counts,
"category_counts": category_counts,
"top_rules": dict(sorted(rule_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
"files_with_issues": len(set(f.file_path for f in findings if f.file_path))
}
@@ -0,0 +1,447 @@
"""
Kubesec Infrastructure Security Module
This module uses Kubesec to scan Kubernetes manifests for security
misconfigurations and best practices violations.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class KubesecModule(BaseModule):
"""Kubesec Kubernetes security scanning module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="kubesec",
version="2.14.0",
description="Kubernetes security scanning for YAML/JSON manifests with security best practices validation",
author="FuzzForge Team",
category="infrastructure",
tags=["kubernetes", "k8s", "security", "best-practices", "manifests"],
input_schema={
"type": "object",
"properties": {
"scan_mode": {
"type": "string",
"enum": ["scan", "http"],
"default": "scan",
"description": "Kubesec scan mode (local scan or HTTP API)"
},
"threshold": {
"type": "integer",
"default": 15,
"description": "Minimum security score threshold"
},
"exit_code": {
"type": "integer",
"default": 0,
"description": "Exit code to return on failure"
},
"format": {
"type": "string",
"enum": ["json", "template"],
"default": "json",
"description": "Output format"
},
"kubernetes_patterns": {
"type": "array",
"items": {"type": "string"},
"default": ["**/*.yaml", "**/*.yml", "**/k8s/*.yaml", "**/kubernetes/*.yaml"],
"description": "Patterns to find Kubernetes manifest files"
},
"exclude_patterns": {
"type": "array",
"items": {"type": "string"},
"description": "Patterns to exclude from scanning"
},
"strict": {
"type": "boolean",
"default": False,
"description": "Enable strict mode (fail on any security issue)"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"score": {"type": "integer"},
"security_issues": {"type": "array"},
"file_path": {"type": "string"},
"manifest_kind": {"type": "string"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
scan_mode = config.get("scan_mode", "scan")
if scan_mode not in ["scan", "http"]:
raise ValueError(f"Invalid scan mode: {scan_mode}. Valid: ['scan', 'http']")
threshold = config.get("threshold", 0)
if not isinstance(threshold, int):
raise ValueError(f"Threshold must be an integer, got: {type(threshold)}")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute Kubesec Kubernetes security scanning"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info(f"Running Kubesec Kubernetes scan on {workspace}")
# Find all Kubernetes manifests
k8s_files = self._find_kubernetes_files(workspace, config)
if not k8s_files:
logger.info("No Kubernetes manifest files found")
return self.create_result(
findings=[],
status="success",
summary={"total_findings": 0, "files_scanned": 0}
)
logger.info(f"Found {len(k8s_files)} Kubernetes manifest file(s) to analyze")
# Process each manifest file
all_findings = []
for k8s_file in k8s_files:
findings = await self._scan_manifest(k8s_file, workspace, config)
all_findings.extend(findings)
# Create summary
summary = self._create_summary(all_findings, len(k8s_files))
logger.info(f"Kubesec found {len(all_findings)} security issues across {len(k8s_files)} manifests")
return self.create_result(
findings=all_findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"Kubesec module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
def _find_kubernetes_files(self, workspace: Path, config: Dict[str, Any]) -> List[Path]:
"""Find Kubernetes manifest files in workspace"""
patterns = config.get("kubernetes_patterns", [
"**/*.yaml", "**/*.yml", "**/k8s/*.yaml", "**/kubernetes/*.yaml"
])
exclude_patterns = config.get("exclude_patterns", [])
k8s_files = []
for pattern in patterns:
files = workspace.glob(pattern)
for file in files:
# Check if file contains Kubernetes resources
if self._is_kubernetes_manifest(file):
# Check if file should be excluded
should_exclude = False
for exclude_pattern in exclude_patterns:
if file.match(exclude_pattern):
should_exclude = True
break
if not should_exclude:
k8s_files.append(file)
return list(set(k8s_files)) # Remove duplicates
def _is_kubernetes_manifest(self, file: Path) -> bool:
"""Check if a file is a Kubernetes manifest"""
try:
content = file.read_text(encoding='utf-8')
# Simple heuristic: check for common Kubernetes fields
k8s_indicators = [
"apiVersion:", "kind:", "metadata:", "spec:",
"Deployment", "Service", "Pod", "ConfigMap",
"Secret", "Ingress", "PersistentVolume"
]
return any(indicator in content for indicator in k8s_indicators)
except Exception:
return False
async def _scan_manifest(self, manifest_file: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]:
"""Scan a single Kubernetes manifest with Kubesec"""
findings = []
try:
# Build kubesec command
cmd = ["kubesec", "scan"]
# Add format
format_type = config.get("format", "json")
if format_type == "json":
cmd.append("-f")
cmd.append("json")
# Add the manifest file
cmd.append(str(manifest_file))
logger.debug(f"Running command: {' '.join(cmd)}")
# Run kubesec
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace
)
stdout, stderr = await process.communicate()
# Parse results
if process.returncode == 0:
findings = self._parse_kubesec_output(
stdout.decode(), manifest_file, workspace, config
)
else:
error_msg = stderr.decode()
logger.warning(f"Kubesec failed for {manifest_file}: {error_msg}")
except Exception as e:
logger.warning(f"Error scanning {manifest_file}: {e}")
return findings
def _parse_kubesec_output(self, output: str, manifest_file: Path, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]:
"""Parse Kubesec JSON output into findings"""
findings = []
if not output.strip():
return findings
try:
# Kubesec outputs JSON array
results = json.loads(output)
if not isinstance(results, list):
results = [results]
threshold = config.get("threshold", 0)
for result in results:
score = result.get("score", 0)
object_name = result.get("object", "Unknown")
valid = result.get("valid", True)
message = result.get("message", "")
# Make file path relative to workspace
try:
rel_path = manifest_file.relative_to(workspace)
file_path = str(rel_path)
except ValueError:
file_path = str(manifest_file)
# Process scoring and advise sections
advise = result.get("advise", [])
scoring = result.get("scoring", {})
# Create findings for low scores
if score < threshold or not valid:
severity = "high" if score < 0 else "medium" if score < 5 else "low"
finding = self.create_finding(
title=f"Kubernetes Security Score Low: {object_name}",
description=message or f"Security score {score} below threshold {threshold}",
severity=severity,
category="kubernetes_security",
file_path=file_path,
recommendation=self._get_score_recommendation(score, advise),
metadata={
"score": score,
"threshold": threshold,
"object": object_name,
"valid": valid,
"advise_count": len(advise),
"scoring_details": scoring
}
)
findings.append(finding)
# Create findings for each advisory
for advisory in advise:
selector = advisory.get("selector", "")
reason = advisory.get("reason", "")
href = advisory.get("href", "")
# Determine severity based on advisory type
severity = self._get_advisory_severity(reason, selector)
category = self._get_advisory_category(reason, selector)
finding = self.create_finding(
title=f"Kubernetes Security Advisory: {selector}",
description=reason,
severity=severity,
category=category,
file_path=file_path,
recommendation=self._get_advisory_recommendation(reason, href),
metadata={
"selector": selector,
"href": href,
"object": object_name,
"advisory_type": "kubesec_advise"
}
)
findings.append(finding)
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse Kubesec output: {e}")
except Exception as e:
logger.warning(f"Error processing Kubesec results: {e}")
return findings
def _get_advisory_severity(self, reason: str, selector: str) -> str:
"""Determine severity based on advisory reason and selector"""
reason_lower = reason.lower()
selector_lower = selector.lower()
# High severity issues
if any(term in reason_lower for term in [
"privileged", "root", "hostnetwork", "hostpid", "hostipc",
"allowprivilegeescalation", "runasroot", "security", "capabilities"
]):
return "high"
# Medium severity issues
elif any(term in reason_lower for term in [
"resources", "limits", "requests", "readonly", "securitycontext"
]):
return "medium"
# Low severity issues
elif any(term in reason_lower for term in [
"labels", "annotations", "probe", "liveness", "readiness"
]):
return "low"
else:
return "medium"
def _get_advisory_category(self, reason: str, selector: str) -> str:
"""Determine category based on advisory"""
reason_lower = reason.lower()
if any(term in reason_lower for term in ["privilege", "root", "security", "capabilities"]):
return "privilege_escalation"
elif any(term in reason_lower for term in ["network", "host"]):
return "network_security"
elif any(term in reason_lower for term in ["resources", "limits"]):
return "resource_management"
elif any(term in reason_lower for term in ["probe", "health"]):
return "health_monitoring"
else:
return "kubernetes_best_practices"
def _get_score_recommendation(self, score: int, advise: List[Dict]) -> str:
"""Generate recommendation based on score and advisories"""
if score < 0:
return "Critical security issues detected. Address all security advisories immediately."
elif score < 5:
return "Low security score detected. Review and implement security best practices."
elif len(advise) > 0:
return f"Security score is {score}. Review {len(advise)} advisory recommendations for improvement."
else:
return "Review Kubernetes security configuration and apply security hardening measures."
def _get_advisory_recommendation(self, reason: str, href: str) -> str:
"""Generate recommendation for advisory"""
if href:
return f"{reason} For more details, see: {href}"
reason_lower = reason.lower()
# Specific recommendations based on common patterns
if "privileged" in reason_lower:
return "Remove privileged: true from security context. Run containers with minimal privileges."
elif "root" in reason_lower or "runasroot" in reason_lower:
return "Configure runAsNonRoot: true and set runAsUser to a non-root user ID."
elif "allowprivilegeescalation" in reason_lower:
return "Set allowPrivilegeEscalation: false to prevent privilege escalation."
elif "resources" in reason_lower:
return "Define resource requests and limits to prevent resource exhaustion."
elif "readonly" in reason_lower:
return "Set readOnlyRootFilesystem: true to prevent filesystem modifications."
elif "capabilities" in reason_lower:
return "Drop unnecessary capabilities and add only required ones."
elif "probe" in reason_lower:
return "Add liveness and readiness probes for better health monitoring."
else:
return f"Address the security concern: {reason}"
def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
object_counts = {}
scores = []
for finding in findings:
# Count by severity
severity_counts[finding.severity] += 1
# Count by category
category = finding.category
category_counts[category] = category_counts.get(category, 0) + 1
# Count by object
obj = finding.metadata.get("object", "unknown")
object_counts[obj] = object_counts.get(obj, 0) + 1
# Collect scores
score = finding.metadata.get("score")
if score is not None:
scores.append(score)
return {
"total_findings": len(findings),
"files_scanned": total_files,
"severity_counts": severity_counts,
"category_counts": category_counts,
"object_counts": object_counts,
"average_score": sum(scores) / len(scores) if scores else 0,
"min_score": min(scores) if scores else 0,
"max_score": max(scores) if scores else 0,
"files_with_issues": len(set(f.file_path for f in findings if f.file_path))
}
@@ -0,0 +1,519 @@
"""
Polaris Infrastructure Security Module
This module uses Polaris to validate Kubernetes resources against security
and best practice policies.
"""
# Copyright (c) 2025 FuzzingLabs
#
# Licensed under the Business Source License 1.1 (BSL). See the LICENSE file
# at the root of this repository for details.
#
# After the Change Date (four years from publication), this version of the
# Licensed Work will be made available under the Apache License, Version 2.0.
# See the LICENSE-APACHE file or http://www.apache.org/licenses/LICENSE-2.0
#
# Additional attribution and requirements are provided in the NOTICE file.
import asyncio
import json
from pathlib import Path
from typing import Dict, Any, List
import subprocess
import logging
from ..base import BaseModule, ModuleMetadata, ModuleFinding, ModuleResult
from . import register_module
logger = logging.getLogger(__name__)
@register_module
class PolarisModule(BaseModule):
"""Polaris Kubernetes best practices validation module"""
def get_metadata(self) -> ModuleMetadata:
"""Get module metadata"""
return ModuleMetadata(
name="polaris",
version="8.5.0",
description="Kubernetes best practices validation and policy enforcement using Polaris",
author="FuzzForge Team",
category="infrastructure",
tags=["kubernetes", "k8s", "policy", "best-practices", "validation"],
input_schema={
"type": "object",
"properties": {
"audit_path": {
"type": "string",
"description": "Path to audit (defaults to workspace)"
},
"config_file": {
"type": "string",
"description": "Path to Polaris config file"
},
"only_show_failed_tests": {
"type": "boolean",
"default": True,
"description": "Show only failed validation tests"
},
"severity_threshold": {
"type": "string",
"enum": ["error", "warning", "info"],
"default": "info",
"description": "Minimum severity level to report"
},
"format": {
"type": "string",
"enum": ["json", "yaml", "pretty"],
"default": "json",
"description": "Output format"
},
"kubernetes_patterns": {
"type": "array",
"items": {"type": "string"},
"default": ["**/*.yaml", "**/*.yml", "**/k8s/*.yaml", "**/kubernetes/*.yaml"],
"description": "Patterns to find Kubernetes manifest files"
},
"exclude_patterns": {
"type": "array",
"items": {"type": "string"},
"description": "File patterns to exclude"
},
"disable_checks": {
"type": "array",
"items": {"type": "string"},
"description": "List of check names to disable"
},
"enable_checks": {
"type": "array",
"items": {"type": "string"},
"description": "List of check names to enable (if using custom config)"
}
}
},
output_schema={
"type": "object",
"properties": {
"findings": {
"type": "array",
"items": {
"type": "object",
"properties": {
"check_name": {"type": "string"},
"severity": {"type": "string"},
"category": {"type": "string"},
"file_path": {"type": "string"},
"resource_name": {"type": "string"}
}
}
}
}
}
)
def validate_config(self, config: Dict[str, Any]) -> bool:
"""Validate configuration"""
severity_threshold = config.get("severity_threshold", "warning")
valid_severities = ["error", "warning", "info"]
if severity_threshold not in valid_severities:
raise ValueError(f"Invalid severity threshold: {severity_threshold}. Valid: {valid_severities}")
format_type = config.get("format", "json")
valid_formats = ["json", "yaml", "pretty"]
if format_type not in valid_formats:
raise ValueError(f"Invalid format: {format_type}. Valid: {valid_formats}")
return True
async def execute(self, config: Dict[str, Any], workspace: Path) -> ModuleResult:
"""Execute Polaris Kubernetes validation"""
self.start_timer()
try:
# Validate inputs
self.validate_config(config)
self.validate_workspace(workspace)
logger.info(f"Running Polaris Kubernetes validation on {workspace}")
# Find all Kubernetes manifests
k8s_files = self._find_kubernetes_files(workspace, config)
if not k8s_files:
logger.info("No Kubernetes manifest files found")
return self.create_result(
findings=[],
status="success",
summary={"total_findings": 0, "files_scanned": 0}
)
logger.info(f"Found {len(k8s_files)} Kubernetes manifest file(s) to validate")
# Run Polaris audit
findings = await self._run_polaris_audit(workspace, config, k8s_files)
# Create summary
summary = self._create_summary(findings, len(k8s_files))
logger.info(f"Polaris found {len(findings)} policy violations across {len(k8s_files)} manifests")
return self.create_result(
findings=findings,
status="success",
summary=summary
)
except Exception as e:
logger.error(f"Polaris module failed: {e}")
return self.create_result(
findings=[],
status="failed",
error=str(e)
)
def _find_kubernetes_files(self, workspace: Path, config: Dict[str, Any]) -> List[Path]:
"""Find Kubernetes manifest files in workspace"""
patterns = config.get("kubernetes_patterns", [
"**/*.yaml", "**/*.yml", "**/k8s/*.yaml", "**/kubernetes/*.yaml"
])
exclude_patterns = config.get("exclude_patterns", [])
k8s_files = []
for pattern in patterns:
files = workspace.glob(pattern)
for file in files:
# Check if file contains Kubernetes resources
if self._is_kubernetes_manifest(file):
# Check if file should be excluded
should_exclude = False
for exclude_pattern in exclude_patterns:
if file.match(exclude_pattern):
should_exclude = True
break
if not should_exclude:
k8s_files.append(file)
return list(set(k8s_files)) # Remove duplicates
def _is_kubernetes_manifest(self, file: Path) -> bool:
"""Check if a file is a Kubernetes manifest"""
try:
content = file.read_text(encoding='utf-8')
# Simple heuristic: check for common Kubernetes fields
k8s_indicators = [
"apiVersion:", "kind:", "metadata:", "spec:",
"Deployment", "Service", "Pod", "ConfigMap",
"Secret", "Ingress", "PersistentVolume"
]
return any(indicator in content for indicator in k8s_indicators)
except Exception:
return False
async def _run_polaris_audit(self, workspace: Path, config: Dict[str, Any], k8s_files: List[Path]) -> List[ModuleFinding]:
"""Run Polaris audit on workspace"""
findings = []
try:
# Build polaris command
cmd = ["polaris", "audit"]
# Add audit path
audit_path = config.get("audit_path", str(workspace))
cmd.extend(["--audit-path", audit_path])
# Add config file if specified
config_file = config.get("config_file")
if config_file:
cmd.extend(["--config", config_file])
# Add format
format_type = config.get("format", "json")
cmd.extend(["--format", format_type])
# Add only failed tests flag
if config.get("only_show_failed_tests", True):
cmd.append("--only-show-failed-tests")
# Add severity threshold
severity_threshold = config.get("severity_threshold", "warning")
cmd.extend(["--severity", severity_threshold])
# Add disable checks
disable_checks = config.get("disable_checks", [])
for check in disable_checks:
cmd.extend(["--disable-check", check])
logger.debug(f"Running command: {' '.join(cmd)}")
# Run polaris
process = await asyncio.create_subprocess_exec(
*cmd,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
cwd=workspace
)
stdout, stderr = await process.communicate()
# Parse results
if process.returncode == 0 or format_type == "json":
findings = self._parse_polaris_output(stdout.decode(), workspace, config)
else:
error_msg = stderr.decode()
logger.warning(f"Polaris audit failed: {error_msg}")
except Exception as e:
logger.warning(f"Error running Polaris audit: {e}")
return findings
def _parse_polaris_output(self, output: str, workspace: Path, config: Dict[str, Any]) -> List[ModuleFinding]:
"""Parse Polaris JSON output into findings"""
findings = []
if not output.strip():
return findings
try:
data = json.loads(output)
# Get severity threshold for filtering
severity_threshold = config.get("severity_threshold", "warning")
severity_levels = {"error": 3, "warning": 2, "info": 1}
min_severity_level = severity_levels.get(severity_threshold, 2)
# Process audit results
audit_results = data.get("AuditResults", [])
for result in audit_results:
namespace = result.get("Namespace", "default")
results_by_kind = result.get("Results", {})
for kind, kind_results in results_by_kind.items():
for resource_name, resource_data in kind_results.items():
# Get container results
container_results = resource_data.get("ContainerResults", {})
pod_result = resource_data.get("PodResult", {})
# Process container results
for container_name, container_data in container_results.items():
self._process_container_results(
findings, container_data, kind, resource_name,
container_name, namespace, workspace, min_severity_level
)
# Process pod-level results
if pod_result:
self._process_pod_results(
findings, pod_result, kind, resource_name,
namespace, workspace, min_severity_level
)
except json.JSONDecodeError as e:
logger.warning(f"Failed to parse Polaris output: {e}")
except Exception as e:
logger.warning(f"Error processing Polaris results: {e}")
return findings
def _process_container_results(self, findings: List[ModuleFinding], container_data: Dict,
kind: str, resource_name: str, container_name: str,
namespace: str, workspace: Path, min_severity_level: int):
"""Process container-level validation results"""
results = container_data.get("Results", {})
for check_name, check_result in results.items():
severity = check_result.get("Severity", "warning")
success = check_result.get("Success", True)
message = check_result.get("Message", "")
category_name = check_result.get("Category", "")
# Skip if check passed or severity too low
if success:
continue
severity_levels = {"error": 3, "warning": 2, "info": 1}
if severity_levels.get(severity, 1) < min_severity_level:
continue
# Map severity to our standard levels
finding_severity = self._map_severity(severity)
category = self._get_category(check_name, category_name)
finding = self.create_finding(
title=f"Polaris Policy Violation: {check_name}",
description=message or f"Container {container_name} in {kind} {resource_name} failed check {check_name}",
severity=finding_severity,
category=category,
file_path=None, # Polaris doesn't provide file paths in audit mode
recommendation=self._get_recommendation(check_name, message),
metadata={
"check_name": check_name,
"polaris_severity": severity,
"polaris_category": category_name,
"resource_kind": kind,
"resource_name": resource_name,
"container_name": container_name,
"namespace": namespace,
"context": "container"
}
)
findings.append(finding)
def _process_pod_results(self, findings: List[ModuleFinding], pod_result: Dict,
kind: str, resource_name: str, namespace: str,
workspace: Path, min_severity_level: int):
"""Process pod-level validation results"""
results = pod_result.get("Results", {})
for check_name, check_result in results.items():
severity = check_result.get("Severity", "warning")
success = check_result.get("Success", True)
message = check_result.get("Message", "")
category_name = check_result.get("Category", "")
# Skip if check passed or severity too low
if success:
continue
severity_levels = {"error": 3, "warning": 2, "info": 1}
if severity_levels.get(severity, 1) < min_severity_level:
continue
# Map severity to our standard levels
finding_severity = self._map_severity(severity)
category = self._get_category(check_name, category_name)
finding = self.create_finding(
title=f"Polaris Policy Violation: {check_name}",
description=message or f"{kind} {resource_name} failed check {check_name}",
severity=finding_severity,
category=category,
file_path=None, # Polaris doesn't provide file paths in audit mode
recommendation=self._get_recommendation(check_name, message),
metadata={
"check_name": check_name,
"polaris_severity": severity,
"polaris_category": category_name,
"resource_kind": kind,
"resource_name": resource_name,
"namespace": namespace,
"context": "pod"
}
)
findings.append(finding)
def _map_severity(self, polaris_severity: str) -> str:
"""Map Polaris severity to our standard severity levels"""
severity_map = {
"error": "high",
"warning": "medium",
"info": "low"
}
return severity_map.get(polaris_severity.lower(), "medium")
def _get_category(self, check_name: str, category_name: str) -> str:
"""Determine finding category based on check name and category"""
check_lower = check_name.lower()
category_lower = category_name.lower()
# Use Polaris category if available
if "security" in category_lower:
return "security_configuration"
elif "efficiency" in category_lower:
return "resource_efficiency"
elif "reliability" in category_lower:
return "reliability"
# Fallback to check name analysis
if any(term in check_lower for term in ["security", "privilege", "root", "capabilities"]):
return "security_configuration"
elif any(term in check_lower for term in ["resources", "limits", "requests"]):
return "resource_management"
elif any(term in check_lower for term in ["probe", "health", "liveness", "readiness"]):
return "health_monitoring"
elif any(term in check_lower for term in ["image", "tag", "pull"]):
return "image_management"
elif any(term in check_lower for term in ["network", "host"]):
return "network_security"
else:
return "kubernetes_best_practices"
def _get_recommendation(self, check_name: str, message: str) -> str:
"""Generate recommendation based on check name and message"""
check_lower = check_name.lower()
# Security-related recommendations
if "privileged" in check_lower:
return "Remove privileged: true from container security context to reduce security risks."
elif "runasroot" in check_lower:
return "Configure runAsNonRoot: true and specify a non-root user ID."
elif "allowprivilegeescalation" in check_lower:
return "Set allowPrivilegeEscalation: false to prevent privilege escalation attacks."
elif "capabilities" in check_lower:
return "Remove unnecessary capabilities and add only required ones using drop/add lists."
elif "readonly" in check_lower:
return "Set readOnlyRootFilesystem: true to prevent filesystem modifications."
# Resource management recommendations
elif "memory" in check_lower and "requests" in check_lower:
return "Set memory requests to ensure proper resource allocation and scheduling."
elif "memory" in check_lower and "limits" in check_lower:
return "Set memory limits to prevent containers from using excessive memory."
elif "cpu" in check_lower and "requests" in check_lower:
return "Set CPU requests for proper resource allocation and quality of service."
elif "cpu" in check_lower and "limits" in check_lower:
return "Set CPU limits to prevent CPU starvation of other containers."
# Health monitoring recommendations
elif "liveness" in check_lower:
return "Add liveness probes to detect and recover from container failures."
elif "readiness" in check_lower:
return "Add readiness probes to ensure containers are ready before receiving traffic."
# Image management recommendations
elif "tag" in check_lower:
return "Use specific image tags instead of 'latest' for reproducible deployments."
elif "pullpolicy" in check_lower:
return "Set imagePullPolicy appropriately based on your deployment requirements."
# Generic recommendation
elif message:
return f"Address the policy violation: {message}"
else:
return f"Review and fix the configuration issue identified by check: {check_name}"
def _create_summary(self, findings: List[ModuleFinding], total_files: int) -> Dict[str, Any]:
"""Create analysis summary"""
severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0}
category_counts = {}
check_counts = {}
resource_counts = {}
for finding in findings:
# Count by severity
severity_counts[finding.severity] += 1
# Count by category
category = finding.category
category_counts[category] = category_counts.get(category, 0) + 1
# Count by check
check_name = finding.metadata.get("check_name", "unknown")
check_counts[check_name] = check_counts.get(check_name, 0) + 1
# Count by resource
resource_kind = finding.metadata.get("resource_kind", "unknown")
resource_counts[resource_kind] = resource_counts.get(resource_kind, 0) + 1
return {
"total_findings": len(findings),
"files_scanned": total_files,
"severity_counts": severity_counts,
"category_counts": category_counts,
"top_checks": dict(sorted(check_counts.items(), key=lambda x: x[1], reverse=True)[:10]),
"resource_type_counts": resource_counts,
"unique_resources": len(set(f"{f.metadata.get('resource_kind')}:{f.metadata.get('resource_name')}" for f in findings)),
"namespaces": len(set(f.metadata.get("namespace", "default") for f in findings))
}