mirror of
https://github.com/FuzzingLabs/fuzzforge_ai.git
synced 2026-07-09 18:18:35 +02:00
Initial commit
This commit is contained in:
@@ -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))
|
||||
}
|
||||
Reference in New Issue
Block a user