From 2a293dcea6f793fa83dd57310d666f2d3c073b39 Mon Sep 17 00:00:00 2001 From: Natan Date: Fri, 15 May 2026 20:00:23 +0200 Subject: [PATCH 01/10] Update grade report card rendering logic Refactor grade report rendering to conditionally display grade cards based on violations. --- static/app.js | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/static/app.js b/static/app.js index 98c85e3..492f803 100644 --- a/static/app.js +++ b/static/app.js @@ -1417,11 +1417,28 @@ function initApp() {

📊 Infrastructure Report Card

- ${renderGradeCard('Overall Grade', gradeReport.overall, '🎯')} - ${renderGradeCard('Cost Optimization', gradeReport.cost, '💰')} - ${renderGradeCard('IaC Security', gradeReport.security, '🔒')} - ${renderGradeCard('Container Security', gradeReport.container, '🐳')} -
+ ${gradeReport.overall && + [gradeReport.cost, gradeReport.security, gradeReport.container] + .filter(Boolean).length > 1 + ? renderGradeCard('Overall Grade', gradeReport.overall, '🎯') + : '' + } + + ${gradeReport.cost?.violations > 0 + ? renderGradeCard('Cost Optimization', gradeReport.cost, '💰') + : '' + } + + ${gradeReport.security?.violations > 0 + ? renderGradeCard('IaC Security', gradeReport.security, '🔒') + : '' + } + + ${gradeReport.container?.violations > 0 + ? renderGradeCard('Container Security', gradeReport.container, '🐳') + : '' + } +
${recommendations.length > 0 ? `

💡 Recommendations

From 4e80e1d4c6dd63a701c3480d5aec440c61362779 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 18 May 2026 16:30:03 +0200 Subject: [PATCH 02/10] Fix scanner card rendering Fixed the scanner card rendering logic. Previously, cards were hidden when the number of violations was 0, which also hid scanners that actually ran successfully with a perfect score. Now the cards are rendered based on whether the scanner report object exists, so: * scanners with 0 violations are still visible, * scanners that did not run stay hidden, * Overall Grade is only shown when more than one scanner result exists. Closes #53 --- static/app.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/static/app.js b/static/app.js index 492f803..3211357 100644 --- a/static/app.js +++ b/static/app.js @@ -1424,19 +1424,19 @@ function initApp() { : '' } - ${gradeReport.cost?.violations > 0 - ? renderGradeCard('Cost Optimization', gradeReport.cost, '💰') - : '' + ${gradeReport.cost + ? renderGradeCard('Cost Optimization', gradeReport.cost, '💰') + : '' } - ${gradeReport.security?.violations > 0 - ? renderGradeCard('IaC Security', gradeReport.security, '🔒') - : '' + ${gradeReport.security + ? renderGradeCard('IaC Security', gradeReport.security, '🔒') + : '' } - ${gradeReport.container?.violations > 0 - ? renderGradeCard('Container Security', gradeReport.container, '🐳') - : '' + ${gradeReport.container + ? renderGradeCard('Container Security', gradeReport.container, '🐳') + : '' }
${recommendations.length > 0 ? ` From d85c332457870c90e8dc3e9b761454e0dba88f0b Mon Sep 17 00:00:00 2001 From: Natan Date: Wed, 20 May 2026 09:29:27 +0200 Subject: [PATCH 03/10] Fix: Only generate grade objects for scanners that actually ran with findings --- reporter/grading.py | 341 ++++++++++++++++++++++++-------------------- 1 file changed, 183 insertions(+), 158 deletions(-) diff --git a/reporter/grading.py b/reporter/grading.py index 93c92c8..001baf2 100644 --- a/reporter/grading.py +++ b/reporter/grading.py @@ -301,37 +301,52 @@ def __init__(self, calculator: GradeCalculator = None): self.calculator = calculator or GradeCalculator() def generate_report(self, - findings: List[Dict[str, Any]], - resource_count: int = 0, - scanner_type: str = 'comprehensive', - extra_recommendations: List[str] = None) -> ScanReport: - """ - Generate complete scan report. - - Args: - findings: All scan findings - resource_count: Number of resources scanned - scanner_type: Type of scanner used - extra_recommendations: Additional recommendations to include - - Returns: - Complete ScanReport object - """ - # Separate findings by scanner type - cost_findings = [f for f in findings if f.get('scanner') == 'regex'] - security_findings = [f for f in findings if f.get('scanner') == 'checkov'] - container_findings = [f for f in findings if f.get('scanner') in ['docker-scout', 'grype']] - - # For security and container, score only the most severe finding per resource - security_scoring_findings = self._most_severe_per_resource(security_findings) - - # For containers, group by image (not by file) for better aggregation - container_scoring_findings = self._most_severe_per_container_image(container_findings) - - # Calculate individual grades + findings: List[Dict[str, Any]], + resource_count: int = 0, + scanner_type: str = 'comprehensive', + extra_recommendations: List[str] = None) -> ScanReport: + """ + Generate complete scan report. + + Args: + findings: All scan findings + resource_count: Number of resources scanned + scanner_type: Type of scanner used + extra_recommendations: Additional recommendations to include + + Returns: + Complete ScanReport object + """ + # Separate findings by scanner type + cost_findings = [f for f in findings if f.get('scanner') == 'regex'] + security_findings = [f for f in findings if f.get('scanner') == 'checkov'] + container_findings = [f for f in findings if f.get('scanner') in ['docker-scout', 'grype']] + + # For security and container, score only the most severe finding per resource + security_scoring_findings = self._most_severe_per_resource(security_findings) + + # For containers, group by image (not by file) for better aggregation + container_scoring_findings = self._most_severe_per_container_image(container_findings) + + # ========== CRITICAL FIX ========== + # Only generate grades for scanners that were ACTUALLY USED (have findings) + # Determine which scanners ran based on scanner_type parameter + scanner_types = [s.strip() for s in scanner_type.split(',')] + is_comprehensive = scanner_type in ['comprehensive', 'both', 'all'] + + # Determine which scanners should run based on scanner_type + should_calc_cost = is_comprehensive or 'regex' in scanner_types + should_calc_security = is_comprehensive or 'checkov' in scanner_types + should_calc_container = is_comprehensive or 'containers' in scanner_types or 'docker-scout' in scanner_types or 'grype' in scanner_types + + # Calculate individual grades ONLY if scanner was requested AND has findings + if should_calc_cost and cost_findings: cost_grade = self.calculator.calculate_grade(cost_findings, resource_count) - - # Security uses resource-based max score to avoid overweighting many checks per resource + else: + cost_grade = None # Mark as skipped scanner + + # Security uses resource-based max score to avoid overweighting many checks per resource + if should_calc_security and security_findings: max_severity_weight = max(self.calculator.severity_weights.values()) base_resource_count = resource_count if resource_count and resource_count > 0 else 0 security_resource_count = max(base_resource_count, len(security_scoring_findings), 1) @@ -340,51 +355,57 @@ def generate_report(self, security_scoring_findings, security_max_score, violations=len(security_findings), all_findings=security_findings ) - - # Container security grading (aggregated by image) - # Use scoring_findings for severity breakdown to show container counts, not total vulnerabilities + else: + security_grade = None # Mark as skipped scanner + + # Container security grading (aggregated by image) + if should_calc_container and container_findings: + max_severity_weight = max(self.calculator.severity_weights.values()) + base_resource_count = resource_count if resource_count and resource_count > 0 else 0 container_resource_count = max(base_resource_count, len(container_scoring_findings), 1) container_max_score = container_resource_count * max_severity_weight container_grade = self.calculator.calculate_grade_with_max( container_scoring_findings, container_max_score, violations=len(container_scoring_findings), all_findings=container_scoring_findings ) - - # Calculate overall grade - overall_grade = self._calculate_overall_grade( - cost_grade, security_grade, container_grade, cost_findings, security_findings, container_findings - ) - - # Generate analysis - recommendations = self._generate_recommendations( - cost_grade, security_grade, container_grade, cost_findings, security_findings, container_findings - ) - - # Add extra recommendations if provided - if extra_recommendations: - recommendations.extend(extra_recommendations) - - top_issues = self._identify_top_issues(findings) - - # Additional metrics (extensible) - metrics = self._calculate_additional_metrics(findings, resource_count) - - return ScanReport( - overall_grade=overall_grade, - cost_grade=cost_grade, - security_grade=security_grade, - container_grade=container_grade, - cost_findings=cost_findings, - security_findings=security_findings, - container_findings=container_findings, - all_findings=findings, - resource_count=resource_count, - scanner_type=scanner_type, - total_violations=len(findings), - recommendations=recommendations, - top_issues=top_issues, - metrics=metrics - ) + else: + container_grade = None # Mark as skipped scanner + + # Calculate overall grade (using only grades that exist) + overall_grade = self._calculate_overall_grade( + cost_grade, security_grade, container_grade, cost_findings, security_findings, container_findings + ) + + # Generate analysis + recommendations = self._generate_recommendations( + cost_grade, security_grade, container_grade, cost_findings, security_findings, container_findings + ) + + # Add extra recommendations if provided + if extra_recommendations: + recommendations.extend(extra_recommendations) + + top_issues = self._identify_top_issues(findings) + + # Additional metrics (extensible) + metrics = self._calculate_additional_metrics(findings, resource_count) + + return ScanReport( + overall_grade=overall_grade, + cost_grade=cost_grade, + security_grade=security_grade, + container_grade=container_grade, + cost_findings=cost_findings, + security_findings=security_findings, + container_findings=container_findings, + all_findings=findings, + resource_count=resource_count, + scanner_type=scanner_type, + total_violations=len(findings), + recommendations=recommendations, + top_issues=top_issues, + metrics=metrics + ) def _most_severe_per_resource(self, findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Return only the most severe finding per resource.""" @@ -416,83 +437,84 @@ def _most_severe_per_container_image(self, findings: List[Dict[str, Any]]) -> Li return list(by_image.values()) - def _calculate_overall_grade(self, cost_grade: GradeInfo, - security_grade: GradeInfo, - container_grade: GradeInfo, - cost_findings: List, - security_findings: List, - container_findings: List) -> GradeInfo: - """Calculate overall grade from cost, security, and container grades.""" - # Determine which scanners were used - scanners_used = [] - if cost_findings: - scanners_used.append('cost') - if security_findings: - scanners_used.append('security') - if container_findings: - scanners_used.append('container') - - if not scanners_used: - overall_percentage = 100.0 - combined_score = 0 - max_combined = 0 - elif len(scanners_used) == 1: - # Single scanner - use its grade directly - if 'cost' in scanners_used: - overall_percentage = cost_grade.percentage - combined_score = cost_grade.score - max_combined = cost_grade.max_score - elif 'security' in scanners_used: - overall_percentage = security_grade.percentage - combined_score = security_grade.score - max_combined = security_grade.max_score - else: - overall_percentage = container_grade.percentage - combined_score = container_grade.score - max_combined = container_grade.max_score + def _calculate_overall_grade(self, cost_grade: GradeInfo, + security_grade: GradeInfo, + container_grade: GradeInfo, + cost_findings: List, + security_findings: List, + container_findings: List) -> GradeInfo: + """Calculate overall grade from cost, security, and container grades.""" + # Determine which scanners actually produced grades + scanners_used = [] + if cost_grade is not None: + scanners_used.append('cost') + if security_grade is not None: + scanners_used.append('security') + if container_grade is not None: + scanners_used.append('container') + + if not scanners_used: + overall_percentage = 100.0 + combined_score = 0 + max_combined = 0 + elif len(scanners_used) == 1: + # Single scanner - use its grade directly + if 'cost' in scanners_used: + overall_percentage = cost_grade.percentage + combined_score = cost_grade.score + max_combined = cost_grade.max_score + elif 'security' in scanners_used: + overall_percentage = security_grade.percentage + combined_score = security_grade.score + max_combined = security_grade.max_score else: - # Multiple scanners - weighted average - total_weight = sum(SCORE_WEIGHTS[s] for s in scanners_used) - overall_percentage = sum( - (cost_grade.percentage if s == 'cost' else - security_grade.percentage if s == 'security' else - container_grade.percentage) * SCORE_WEIGHTS[s] - for s in scanners_used - ) / total_weight - combined_score = cost_grade.score + security_grade.score + container_grade.score - max_combined = cost_grade.max_score + security_grade.max_score + container_grade.max_score - - letter = self.calculator.get_letter_grade(overall_percentage) - - # Merge severity breakdowns - combined_breakdown = { - 'critical': cost_grade.severity_breakdown['critical'] + security_grade.severity_breakdown['critical'] + container_grade.severity_breakdown['critical'], - 'high': cost_grade.severity_breakdown['high'] + security_grade.severity_breakdown['high'] + container_grade.severity_breakdown['high'], - 'medium': cost_grade.severity_breakdown['medium'] + security_grade.severity_breakdown['medium'] + container_grade.severity_breakdown['medium'], - 'low': cost_grade.severity_breakdown['low'] + security_grade.severity_breakdown['low'] + container_grade.severity_breakdown['low'], - 'info': cost_grade.severity_breakdown['info'] + security_grade.severity_breakdown['info'] + container_grade.severity_breakdown['info'] - } - - return GradeInfo( - letter=letter, - percentage=round(overall_percentage, 1), - score=combined_score, - max_score=max_combined, - risk_level=RISK_LEVELS.get(letter, 'Unknown'), - violations=cost_grade.violations + security_grade.violations + container_grade.violations, - severity_breakdown=combined_breakdown - ) + overall_percentage = container_grade.percentage + combined_score = container_grade.score + max_combined = container_grade.max_score + else: + # Multiple scanners - weighted average + total_weight = sum(SCORE_WEIGHTS[s] for s in scanners_used) + overall_percentage = sum( + (cost_grade.percentage if s == 'cost' and cost_grade else + security_grade.percentage if s == 'security' and security_grade else + container_grade.percentage if s == 'container' and container_grade else 0) * SCORE_WEIGHTS[s] + for s in scanners_used + ) / total_weight + combined_score = (cost_grade.score if cost_grade else 0) + (security_grade.score if security_grade else 0) + (container_grade.score if container_grade else 0) + max_combined = (cost_grade.max_score if cost_grade else 0) + (security_grade.max_score if security_grade else 0) + (container_grade.max_score if container_grade else 0) + + letter = self.calculator.get_letter_grade(overall_percentage) + + # Merge severity breakdowns (only from grades that exist) + combined_breakdown = { + 'critical': (cost_grade.severity_breakdown['critical'] if cost_grade else 0) + (security_grade.severity_breakdown['critical'] if security_grade else 0) + (container_grade.severity_breakdown['critical'] if container_grade else 0), + 'high': (cost_grade.severity_breakdown['high'] if cost_grade else 0) + (security_grade.severity_breakdown['high'] if security_grade else 0) + (container_grade.severity_breakdown['high'] if container_grade else 0), + 'medium': (cost_grade.severity_breakdown['medium'] if cost_grade else 0) + (security_grade.severity_breakdown['medium'] if security_grade else 0) + (container_grade.severity_breakdown['medium'] if container_grade else 0), + 'low': (cost_grade.severity_breakdown['low'] if cost_grade else 0) + (security_grade.severity_breakdown['low'] if security_grade else 0) + (container_grade.severity_breakdown['low'] if container_grade else 0), + 'info': (cost_grade.severity_breakdown['info'] if cost_grade else 0) + (security_grade.severity_breakdown['info'] if security_grade else 0) + (container_grade.severity_breakdown['info'] if container_grade else 0) + } + + return GradeInfo( + letter=letter, + percentage=round(overall_percentage, 1), + score=combined_score, + max_score=max_combined, + risk_level=RISK_LEVELS.get(letter, 'Unknown'), + violations=(cost_grade.violations if cost_grade else 0) + (security_grade.violations if security_grade else 0) + (container_grade.violations if container_grade else 0), + severity_breakdown=combined_breakdown + ) def _generate_recommendations(self, cost_grade: GradeInfo, - security_grade: GradeInfo, - container_grade: GradeInfo, - cost_findings: List, - security_findings: List, - container_findings: List) -> List[str]: - """Generate actionable recommendations - max 1 per category.""" - recommendations = [] - - # IaC Security - show most critical issue only + security_grade: GradeInfo, + container_grade: GradeInfo, + cost_findings: List, + security_findings: List, + container_findings: List) -> List[str]: + """Generate actionable recommendations - max 1 per category.""" + recommendations = [] + + # IaC Security - show most critical issue only + if security_grade is not None: iac_critical = security_grade.severity_breakdown.get('critical', 0) iac_high = security_grade.severity_breakdown['high'] if iac_critical > 0: @@ -505,8 +527,9 @@ def _generate_recommendations(self, cost_grade: GradeInfo, f"🔒 Priority: Fix {iac_high} high-severity " f"IaC security {'issue' if iac_high == 1 else 'issues'} before deployment" ) - - # Container Security - show most critical issue only + + # Container Security - show most critical issue only + if container_grade is not None: container_critical = container_grade.severity_breakdown.get('critical', 0) container_high = container_grade.severity_breakdown['high'] if container_critical > 0: @@ -519,28 +542,30 @@ def _generate_recommendations(self, cost_grade: GradeInfo, f"🐳 Priority: Address {container_high} {'image with' if container_high == 1 else 'images with'} high-severity " f"vulnerabilities - update container images or patch affected packages" ) - - # Cost - show only if high priority - if cost_grade.severity_breakdown['high'] > 0: - recommendations.append( - f"💰 Optimize {cost_grade.severity_breakdown['high']} high-cost " - f"{'issue' if cost_grade.severity_breakdown['high'] == 1 else 'issues'} for significant savings" - ) - - # Overall assessment - max 1 - worst_grade = min([cost_grade.letter, security_grade.letter, container_grade.letter]) + + # Cost - show only if high priority + if cost_grade is not None and cost_grade.severity_breakdown['high'] > 0: + recommendations.append( + f"💰 Optimize {cost_grade.severity_breakdown['high']} high-cost " + f"{'issue' if cost_grade.severity_breakdown['high'] == 1 else 'issues'} for significant savings" + ) + + # Overall assessment - max 1 + grades = [g for g in [cost_grade, security_grade, container_grade] if g is not None] + if grades: + worst_grade = min([g.letter for g in grades]) total_findings = len(cost_findings) + len(security_findings) + len(container_findings) if worst_grade in ['D', 'F']: recommendations.append( "⚠️ Infrastructure needs improvement - consider professional review" ) - elif cost_grade.letter == 'A' and security_grade.letter == 'A' and container_grade.letter == 'A' and total_findings > 0: + elif all(g.letter == 'A' for g in grades) and total_findings > 0: recommendations.append("✅ Excellent infrastructure health - maintain current practices") elif worst_grade in ['B', 'C']: recommendations.append("👍 Good foundation - address remaining issues for optimal results") - - return recommendations or ["✅ No significant issues found"] + + return recommendations or ["✅ No significant issues found"] def _identify_top_issues(self, findings: List[Dict[str, Any]], top_n: int = 5) -> List[Dict[str, Any]]: From dd5c431424f2021f7df2d17ef0134db2ad2c4f9e Mon Sep 17 00:00:00 2001 From: Natan Date: Wed, 20 May 2026 09:29:39 +0200 Subject: [PATCH 04/10] Fix: Add null checks for skipped scanner grade cards in frontend --- static/app.js | 30 ++++++++---------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/static/app.js b/static/app.js index 3211357..6d97283 100644 --- a/static/app.js +++ b/static/app.js @@ -1417,28 +1417,14 @@ function initApp() {

📊 Infrastructure Report Card

- ${gradeReport.overall && - [gradeReport.cost, gradeReport.security, gradeReport.container] - .filter(Boolean).length > 1 - ? renderGradeCard('Overall Grade', gradeReport.overall, '🎯') - : '' - } - - ${gradeReport.cost - ? renderGradeCard('Cost Optimization', gradeReport.cost, '💰') - : '' - } - - ${gradeReport.security - ? renderGradeCard('IaC Security', gradeReport.security, '🔒') - : '' - } - - ${gradeReport.container - ? renderGradeCard('Container Security', gradeReport.container, '🐳') - : '' - } -
+ ${gradeReport.overall && [gradeReport.cost, gradeReport.security, gradeReport.container].filter(Boolean).length > 1 + ? renderGradeCard('Overall Grade', gradeReport.overall, '🎯') + : '' + } + ${gradeReport.cost ? renderGradeCard('Cost Optimization', gradeReport.cost, '💰') : ''} + ${gradeReport.security ? renderGradeCard('IaC Security', gradeReport.security, '🔒') : ''} + ${gradeReport.container ? renderGradeCard('Container Security', gradeReport.container, '🐳') : ''} +
${recommendations.length > 0 ? `

💡 Recommendations

From 0a77f92864fa7523c87fc4ee31c9a3921df891a3 Mon Sep 17 00:00:00 2001 From: Natan Date: Fri, 22 May 2026 16:39:40 +0200 Subject: [PATCH 05/10] Fix frontend rendering for optional scanner grades --- static/app.js | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/static/app.js b/static/app.js index 6d97283..7be5c71 100644 --- a/static/app.js +++ b/static/app.js @@ -1410,20 +1410,18 @@ function initApp() {
`; }; - + const singleScannerMode = gradeReport.metrics?.single_scanner_mode; const recommendations = gradeReport.analysis?.recommendations || []; return ` -
-

📊 Infrastructure Report Card

-
- ${gradeReport.overall && [gradeReport.cost, gradeReport.security, gradeReport.container].filter(Boolean).length > 1 - ? renderGradeCard('Overall Grade', gradeReport.overall, '🎯') - : '' - } - ${gradeReport.cost ? renderGradeCard('Cost Optimization', gradeReport.cost, '💰') : ''} - ${gradeReport.security ? renderGradeCard('IaC Security', gradeReport.security, '🔒') : ''} - ${gradeReport.container ? renderGradeCard('Container Security', gradeReport.container, '🐳') : ''} +
+ ${!singleScannerMode && gradeReport.overall + ? renderGradeCard('Overall Grade', gradeReport.overall, '🎯') + : ''} + + ${renderGradeCard('Cost Optimization', gradeReport.cost, '💰')} + ${renderGradeCard('IaC Security', gradeReport.security, '🔒')} + ${renderGradeCard('Container Security', gradeReport.container, '🐳')}
${recommendations.length > 0 ? `
@@ -1625,5 +1623,4 @@ function toggleCVE(cveId) { } else { details.style.display = 'none'; if (icon) icon.textContent = '▼'; - } -} + }} From cc86cd49bf93ee19dfdbd32e68d96bdbb82bec8f Mon Sep 17 00:00:00 2001 From: Natan Date: Fri, 22 May 2026 16:40:36 +0200 Subject: [PATCH 06/10] Fix single scanner grade calculation and NoneType handling --- reporter/grading.py | 438 ++++++++++++++++++++++++-------------------- 1 file changed, 238 insertions(+), 200 deletions(-) diff --git a/reporter/grading.py b/reporter/grading.py index 001baf2..6aa8fb9 100644 --- a/reporter/grading.py +++ b/reporter/grading.py @@ -134,10 +134,10 @@ class ScanReport: def to_dict(self) -> Dict: """Convert to dictionary for JSON serialization.""" return { - 'overall': self.overall_grade.to_dict(), - 'cost': self.cost_grade.to_dict(), - 'security': self.security_grade.to_dict(), - 'container': self.container_grade.to_dict(), + 'overall': self.overall_grade.to_dict() if self.overall_grade else None, + 'cost': self.cost_grade.to_dict() if self.cost_grade else None, + 'security': self.security_grade.to_dict() if self.security_grade else None, + 'container': self.container_grade.to_dict() if self.container_grade else None, 'findings': { 'cost': self.cost_findings, 'security': self.security_findings, @@ -301,111 +301,123 @@ def __init__(self, calculator: GradeCalculator = None): self.calculator = calculator or GradeCalculator() def generate_report(self, - findings: List[Dict[str, Any]], - resource_count: int = 0, - scanner_type: str = 'comprehensive', - extra_recommendations: List[str] = None) -> ScanReport: - """ - Generate complete scan report. - - Args: - findings: All scan findings - resource_count: Number of resources scanned - scanner_type: Type of scanner used - extra_recommendations: Additional recommendations to include - - Returns: - Complete ScanReport object - """ - # Separate findings by scanner type - cost_findings = [f for f in findings if f.get('scanner') == 'regex'] - security_findings = [f for f in findings if f.get('scanner') == 'checkov'] - container_findings = [f for f in findings if f.get('scanner') in ['docker-scout', 'grype']] - - # For security and container, score only the most severe finding per resource - security_scoring_findings = self._most_severe_per_resource(security_findings) - - # For containers, group by image (not by file) for better aggregation - container_scoring_findings = self._most_severe_per_container_image(container_findings) - - # ========== CRITICAL FIX ========== - # Only generate grades for scanners that were ACTUALLY USED (have findings) - # Determine which scanners ran based on scanner_type parameter - scanner_types = [s.strip() for s in scanner_type.split(',')] - is_comprehensive = scanner_type in ['comprehensive', 'both', 'all'] - - # Determine which scanners should run based on scanner_type - should_calc_cost = is_comprehensive or 'regex' in scanner_types - should_calc_security = is_comprehensive or 'checkov' in scanner_types - should_calc_container = is_comprehensive or 'containers' in scanner_types or 'docker-scout' in scanner_types or 'grype' in scanner_types - - # Calculate individual grades ONLY if scanner was requested AND has findings - if should_calc_cost and cost_findings: - cost_grade = self.calculator.calculate_grade(cost_findings, resource_count) - else: - cost_grade = None # Mark as skipped scanner - - # Security uses resource-based max score to avoid overweighting many checks per resource - if should_calc_security and security_findings: + findings: List[Dict[str, Any]], + resource_count: int = 0, + scanner_type: str = 'comprehensive', + extra_recommendations: List[str] = None) -> ScanReport: + """ + Generate complete scan report. + + Args: + findings: All scan findings + resource_count: Number of resources scanned + scanner_type: Type of scanner used + extra_recommendations: Additional recommendations to include + + Returns: + Complete ScanReport object + """ + scanner_set = set(s.strip() for s in scanner_type.split(',')) + + enabled_scanners = [] + + if scanner_set & {'regex', 'comprehensive'}: + enabled_scanners.append('cost') + + if scanner_set & {'checkov', 'comprehensive'}: + enabled_scanners.append('security') + + if scanner_set & {'containers', 'comprehensive'}: + enabled_scanners.append('container') + # Separate findings by scanner type + cost_findings = [f for f in findings if f.get('scanner') == 'regex'] + security_findings = [f for f in findings if f.get('scanner') == 'checkov'] + container_findings = [f for f in findings if f.get('scanner') in ['docker-scout', 'grype']] + + # For security and container, score only the most severe finding per resource + security_scoring_findings = self._most_severe_per_resource(security_findings) + + # For containers, group by image (not by file) for better aggregation + container_scoring_findings = self._most_severe_per_container_image(container_findings) + + # Calculate individual grades + cost_grade = ( + self.calculator.calculate_grade(cost_findings, resource_count) + if 'cost' in enabled_scanners + else None + ) + + # Security uses resource-based max score to avoid overweighting many checks per resource max_severity_weight = max(self.calculator.severity_weights.values()) base_resource_count = resource_count if resource_count and resource_count > 0 else 0 security_resource_count = max(base_resource_count, len(security_scoring_findings), 1) security_max_score = security_resource_count * max_severity_weight - security_grade = self.calculator.calculate_grade_with_max( - security_scoring_findings, security_max_score, - violations=len(security_findings), all_findings=security_findings + security_grade = ( + self.calculator.calculate_grade_with_max( + security_scoring_findings, + security_max_score, + violations=len(security_findings), + all_findings=security_findings + ) + if 'security' in enabled_scanners + else None ) - else: - security_grade = None # Mark as skipped scanner - - # Container security grading (aggregated by image) - if should_calc_container and container_findings: - max_severity_weight = max(self.calculator.severity_weights.values()) - base_resource_count = resource_count if resource_count and resource_count > 0 else 0 + + # Container security grading (aggregated by image) + # Use scoring_findings for severity breakdown to show container counts, not total vulnerabilities container_resource_count = max(base_resource_count, len(container_scoring_findings), 1) container_max_score = container_resource_count * max_severity_weight - container_grade = self.calculator.calculate_grade_with_max( - container_scoring_findings, container_max_score, - violations=len(container_scoring_findings), all_findings=container_scoring_findings + container_grade = ( + self.calculator.calculate_grade_with_max( + container_scoring_findings, + container_max_score, + violations=len(container_scoring_findings), + all_findings=container_scoring_findings + ) + if 'container' in enabled_scanners + else None + ) + + # Calculate overall grade + overall_grade = self._calculate_overall_grade( + cost_grade, security_grade, container_grade, cost_findings, security_findings, container_findings + ) + + # Generate analysis + recommendations = self._generate_recommendations( + cost_grade, security_grade, container_grade, cost_findings, security_findings, container_findings + ) + + # Add extra recommendations if provided + if extra_recommendations: + recommendations.extend(extra_recommendations) + + top_issues = self._identify_top_issues(findings) + + # Additional metrics (extensible) + metrics = self._calculate_additional_metrics(findings, resource_count) + + single_scanner_mode = len(enabled_scanners) == 1 + + return ScanReport( + overall_grade=overall_grade, + cost_grade=cost_grade, + security_grade=security_grade, + container_grade=container_grade, + cost_findings=cost_findings, + security_findings=security_findings, + container_findings=container_findings, + all_findings=findings, + resource_count=resource_count, + scanner_type=scanner_type, + total_violations=len(findings), + recommendations=recommendations, + top_issues=top_issues, + metrics={ + **(metrics or {}), + 'single_scanner_mode': single_scanner_mode + } ) - else: - container_grade = None # Mark as skipped scanner - - # Calculate overall grade (using only grades that exist) - overall_grade = self._calculate_overall_grade( - cost_grade, security_grade, container_grade, cost_findings, security_findings, container_findings - ) - - # Generate analysis - recommendations = self._generate_recommendations( - cost_grade, security_grade, container_grade, cost_findings, security_findings, container_findings - ) - - # Add extra recommendations if provided - if extra_recommendations: - recommendations.extend(extra_recommendations) - - top_issues = self._identify_top_issues(findings) - - # Additional metrics (extensible) - metrics = self._calculate_additional_metrics(findings, resource_count) - - return ScanReport( - overall_grade=overall_grade, - cost_grade=cost_grade, - security_grade=security_grade, - container_grade=container_grade, - cost_findings=cost_findings, - security_findings=security_findings, - container_findings=container_findings, - all_findings=findings, - resource_count=resource_count, - scanner_type=scanner_type, - total_violations=len(findings), - recommendations=recommendations, - top_issues=top_issues, - metrics=metrics - ) def _most_severe_per_resource(self, findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]: """Return only the most severe finding per resource.""" @@ -437,86 +449,101 @@ def _most_severe_per_container_image(self, findings: List[Dict[str, Any]]) -> Li return list(by_image.values()) - def _calculate_overall_grade(self, cost_grade: GradeInfo, - security_grade: GradeInfo, - container_grade: GradeInfo, - cost_findings: List, - security_findings: List, - container_findings: List) -> GradeInfo: - """Calculate overall grade from cost, security, and container grades.""" - # Determine which scanners actually produced grades - scanners_used = [] - if cost_grade is not None: - scanners_used.append('cost') - if security_grade is not None: - scanners_used.append('security') - if container_grade is not None: - scanners_used.append('container') - - if not scanners_used: - overall_percentage = 100.0 - combined_score = 0 - max_combined = 0 - elif len(scanners_used) == 1: - # Single scanner - use its grade directly - if 'cost' in scanners_used: - overall_percentage = cost_grade.percentage - combined_score = cost_grade.score - max_combined = cost_grade.max_score - elif 'security' in scanners_used: - overall_percentage = security_grade.percentage - combined_score = security_grade.score - max_combined = security_grade.max_score + def _calculate_overall_grade(self, cost_grade: GradeInfo, + security_grade: GradeInfo, + container_grade: GradeInfo, + cost_findings: List, + security_findings: List, + container_findings: List) -> GradeInfo: + """Calculate overall grade from cost, security, and container grades.""" + # Determine which scanners were used + scanners_used = [] + + if cost_grade: + scanners_used.append('cost') + + if security_grade: + scanners_used.append('security') + + if container_grade: + scanners_used.append('container') + + if not scanners_used: + return None + elif len(scanners_used) == 1: + # Single scanner - use its grade directly + if 'cost' in scanners_used: + overall_percentage = cost_grade.percentage + combined_score = cost_grade.score + max_combined = cost_grade.max_score + elif 'security' in scanners_used: + overall_percentage = security_grade.percentage + combined_score = security_grade.score + max_combined = security_grade.max_score + else: + overall_percentage = container_grade.percentage + combined_score = container_grade.score + max_combined = container_grade.max_score else: - overall_percentage = container_grade.percentage - combined_score = container_grade.score - max_combined = container_grade.max_score - else: - # Multiple scanners - weighted average - total_weight = sum(SCORE_WEIGHTS[s] for s in scanners_used) - overall_percentage = sum( - (cost_grade.percentage if s == 'cost' and cost_grade else - security_grade.percentage if s == 'security' and security_grade else - container_grade.percentage if s == 'container' and container_grade else 0) * SCORE_WEIGHTS[s] - for s in scanners_used - ) / total_weight - combined_score = (cost_grade.score if cost_grade else 0) + (security_grade.score if security_grade else 0) + (container_grade.score if container_grade else 0) - max_combined = (cost_grade.max_score if cost_grade else 0) + (security_grade.max_score if security_grade else 0) + (container_grade.max_score if container_grade else 0) - - letter = self.calculator.get_letter_grade(overall_percentage) - - # Merge severity breakdowns (only from grades that exist) - combined_breakdown = { - 'critical': (cost_grade.severity_breakdown['critical'] if cost_grade else 0) + (security_grade.severity_breakdown['critical'] if security_grade else 0) + (container_grade.severity_breakdown['critical'] if container_grade else 0), - 'high': (cost_grade.severity_breakdown['high'] if cost_grade else 0) + (security_grade.severity_breakdown['high'] if security_grade else 0) + (container_grade.severity_breakdown['high'] if container_grade else 0), - 'medium': (cost_grade.severity_breakdown['medium'] if cost_grade else 0) + (security_grade.severity_breakdown['medium'] if security_grade else 0) + (container_grade.severity_breakdown['medium'] if container_grade else 0), - 'low': (cost_grade.severity_breakdown['low'] if cost_grade else 0) + (security_grade.severity_breakdown['low'] if security_grade else 0) + (container_grade.severity_breakdown['low'] if container_grade else 0), - 'info': (cost_grade.severity_breakdown['info'] if cost_grade else 0) + (security_grade.severity_breakdown['info'] if security_grade else 0) + (container_grade.severity_breakdown['info'] if container_grade else 0) - } - - return GradeInfo( - letter=letter, - percentage=round(overall_percentage, 1), - score=combined_score, - max_score=max_combined, - risk_level=RISK_LEVELS.get(letter, 'Unknown'), - violations=(cost_grade.violations if cost_grade else 0) + (security_grade.violations if security_grade else 0) + (container_grade.violations if container_grade else 0), - severity_breakdown=combined_breakdown - ) + # Multiple scanners - weighted average + total_weight = sum(SCORE_WEIGHTS[s] for s in scanners_used) + overall_percentage = sum( + (cost_grade.percentage if s == 'cost' else + security_grade.percentage if s == 'security' else + container_grade.percentage) * SCORE_WEIGHTS[s] + for s in scanners_used + ) / total_weight + combined_score = sum( + g.score for g in [cost_grade, security_grade, container_grade] + if g + ) + + max_combined = sum( + g.max_score for g in [cost_grade, security_grade, container_grade] + if g + ) + + letter = self.calculator.get_letter_grade(overall_percentage) + + # Merge severity breakdowns + grades = [g for g in [cost_grade, security_grade, container_grade] if g] + + combined_breakdown = { + 'critical': sum(g.severity_breakdown['critical'] for g in grades), + 'high': sum(g.severity_breakdown['high'] for g in grades), + 'medium': sum(g.severity_breakdown['medium'] for g in grades), + 'low': sum(g.severity_breakdown['low'] for g in grades), + 'info': sum(g.severity_breakdown['info'] for g in grades) + } + + return GradeInfo( + letter=letter, + percentage=round(overall_percentage, 1), + score=combined_score, + max_score=max_combined, + risk_level=RISK_LEVELS.get(letter, 'Unknown'), + violations=sum(g.violations for g in grades), + severity_breakdown=combined_breakdown + ) def _generate_recommendations(self, cost_grade: GradeInfo, - security_grade: GradeInfo, - container_grade: GradeInfo, - cost_findings: List, - security_findings: List, - container_findings: List) -> List[str]: - """Generate actionable recommendations - max 1 per category.""" - recommendations = [] - - # IaC Security - show most critical issue only - if security_grade is not None: - iac_critical = security_grade.severity_breakdown.get('critical', 0) - iac_high = security_grade.severity_breakdown['high'] + security_grade: GradeInfo, + container_grade: GradeInfo, + cost_findings: List, + security_findings: List, + container_findings: List) -> List[str]: + """Generate actionable recommendations - max 1 per category.""" + recommendations = [] + + # IaC Security - show most critical issue only + iac_critical = ( + security_grade.severity_breakdown.get('critical', 0) + if security_grade else 0 + ) + iac_high = ( + security_grade.severity_breakdown.get('high', 0) + if security_grade else 0 + ) if iac_critical > 0: recommendations.append( f"🔥 URGENT: Fix {iac_critical} critical-severity " @@ -527,11 +554,16 @@ def _generate_recommendations(self, cost_grade: GradeInfo, f"🔒 Priority: Fix {iac_high} high-severity " f"IaC security {'issue' if iac_high == 1 else 'issues'} before deployment" ) - - # Container Security - show most critical issue only - if container_grade is not None: - container_critical = container_grade.severity_breakdown.get('critical', 0) - container_high = container_grade.severity_breakdown['high'] + + # Container Security - show most critical issue only + container_critical = ( + container_grade.severity_breakdown.get('critical', 0) + if container_grade else 0 + ) + container_high = ( + container_grade.severity_breakdown.get('high', 0) + if container_grade else 0 + ) if container_critical > 0: recommendations.append( f"🔥 URGENT: Address {container_critical} {'image with' if container_critical == 1 else 'images with'} critical " @@ -542,30 +574,37 @@ def _generate_recommendations(self, cost_grade: GradeInfo, f"🐳 Priority: Address {container_high} {'image with' if container_high == 1 else 'images with'} high-severity " f"vulnerabilities - update container images or patch affected packages" ) - - # Cost - show only if high priority - if cost_grade is not None and cost_grade.severity_breakdown['high'] > 0: - recommendations.append( - f"💰 Optimize {cost_grade.severity_breakdown['high']} high-cost " - f"{'issue' if cost_grade.severity_breakdown['high'] == 1 else 'issues'} for significant savings" - ) - - # Overall assessment - max 1 - grades = [g for g in [cost_grade, security_grade, container_grade] if g is not None] - if grades: - worst_grade = min([g.letter for g in grades]) + + # Cost - show only if high priority + if cost_grade and cost_grade.severity_breakdown.get('high', 0) > 0: + recommendations.append( + f"💰 Optimize {cost_grade.severity_breakdown['high']} high-cost " + f"{'issue' if cost_grade.severity_breakdown['high'] == 1 else 'issues'} for significant savings" + ) + + # Overall assessment - max 1 + available_letters = [ + g.letter for g in [cost_grade, security_grade, container_grade] + if g + ] + + worst_grade = min(available_letters) if available_letters else 'A' total_findings = len(cost_findings) + len(security_findings) + len(container_findings) if worst_grade in ['D', 'F']: recommendations.append( "⚠️ Infrastructure needs improvement - consider professional review" ) - elif all(g.letter == 'A' for g in grades) and total_findings > 0: + elif all( + g and g.letter == 'A' + for g in [cost_grade, security_grade, container_grade] + if g + ) and total_findings > 0: recommendations.append("✅ Excellent infrastructure health - maintain current practices") elif worst_grade in ['B', 'C']: recommendations.append("👍 Good foundation - address remaining issues for optimal results") - - return recommendations or ["✅ No significant issues found"] + + return recommendations or ["✅ No significant issues found"] def _identify_top_issues(self, findings: List[Dict[str, Any]], top_n: int = 5) -> List[Dict[str, Any]]: @@ -600,5 +639,4 @@ def _calculate_additional_metrics(self, findings: List[Dict[str, Any]], # Calculate estimated potential savings (for cost findings) # This is extensible - add more calculations as needed - - return metrics + From 7052794b33ca52b7033322f63dc0b3bbb4b277be Mon Sep 17 00:00:00 2001 From: Natan Date: Fri, 22 May 2026 16:42:59 +0200 Subject: [PATCH 07/10] Fix scanner-specific grade rendering and Slack summary crashes Added traceback import and improved error handling with detailed error messages. --- app.py | 54 ++++++++++++++++++++++++++++++++---------------------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/app.py b/app.py index e9e9927..c0700c7 100644 --- a/app.py +++ b/app.py @@ -15,6 +15,7 @@ from scanner.docker_scout_scanner import is_docker_scout_available from scanner.grype_scanner import is_grype_available from reporter.grading import ReportGenerator +import traceback load_dotenv() @@ -148,10 +149,11 @@ def get_branches(): return jsonify({'branches': branches}) except Exception as e: - error_msg = str(e).lower() - if 'could not read' in error_msg or 'not found' in error_msg or 'does not exist' in error_msg: - return jsonify({'error': 'Unable to access repository. Please verify the URL.'}), 400 - return jsonify({'error': f'Failed to fetch branches: {str(e)}'}), 500 + print(f"Error: {e}") + + return jsonify({ + "error": str(e) + }), 500 @app.route('/api/scan/github', methods=['POST']) def scan_github(): @@ -284,11 +286,27 @@ def clone_repo(): findings_summary = ", ".join(findings_parts) # Build grades summary - grades_parts = [f"Overall {overall_grade.letter} ({overall_grade.percentage}%)"] - grades_parts.append(f"Cost {cost_grade.letter} ({cost_grade.percentage}%)") - grades_parts.append(f"Security {security_grade.letter} ({security_grade.percentage}%)") - if container_findings > 0: - grades_parts.append(f"Containers {container_grade.letter} ({container_grade.percentage}%)") + grades_parts = [] + + if overall_grade: + grades_parts.append( + f"Overall {overall_grade.letter} ({overall_grade.percentage}%)" + ) + + if cost_grade: + grades_parts.append( + f"Cost {cost_grade.letter} ({cost_grade.percentage}%)" + ) + + if security_grade: + grades_parts.append( + f"Security {security_grade.letter} ({security_grade.percentage}%)" + ) + + if container_grade: + grades_parts.append( + f"Containers {container_grade.letter} ({container_grade.percentage}%)" + ) grades_summary = " ".join(grades_parts) slack_message = ( @@ -320,19 +338,11 @@ def clone_repo(): return jsonify(report_dict) except Exception as e: - # User-friendly error message without exposing technical details - error_msg = str(e).lower() - if 'could not read' in error_msg or 'not found' in error_msg or 'does not exist' in error_msg: - return jsonify({ - 'error': 'Unable to access repository. Please verify the URL format (https://github.com/username/repo) and ensure the repository is public.' - }), 400 - else: - return jsonify({ - 'error': 'Unable to process repository. Please check the URL and try again.' - }), 500 - finally: - # Clean up - shutil.rmtree(temp_dir, ignore_errors=True) + print(f"Error: {e}") + + return jsonify({ + "error": str(e) + }), 500 @app.route('/api/results/save', methods=['POST']) From f7710787f436d2790234e940975b323013ad0949 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 25 May 2026 17:32:26 +0200 Subject: [PATCH 08/10] restore grade report wrapper and section header * restored missing `grade-report-section` wrapper * restored Infrastructure Health Report section title * fixed orphaned closing
breaking DOM layout * preserved single-scanner rendering logic * ensured recommendations section renders correctly --- reporter/grading.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/reporter/grading.py b/reporter/grading.py index 6aa8fb9..edf0333 100644 --- a/reporter/grading.py +++ b/reporter/grading.py @@ -588,7 +588,7 @@ def _generate_recommendations(self, cost_grade: GradeInfo, if g ] - worst_grade = min(available_letters) if available_letters else 'A' + worst_grade = max(available_letters) if available_letters else 'A' total_findings = len(cost_findings) + len(security_findings) + len(container_findings) if worst_grade in ['D', 'F']: @@ -596,10 +596,10 @@ def _generate_recommendations(self, cost_grade: GradeInfo, "⚠️ Infrastructure needs improvement - consider professional review" ) elif all( - g and g.letter == 'A' + g.letter == 'A' for g in [cost_grade, security_grade, container_grade] if g - ) and total_findings > 0: + ) and total_findings > 0: recommendations.append("✅ Excellent infrastructure health - maintain current practices") elif worst_grade in ['B', 'C']: recommendations.append("👍 Good foundation - address remaining issues for optimal results") From f4432386cf31b6ce7b97e20f388b4693cda03690 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 25 May 2026 17:39:56 +0200 Subject: [PATCH 09/10] restore grade report wrapper and section header * restored missing `grade-report-section` wrapper * restored Infrastructure Health Report section title * fixed orphaned closing
breaking DOM layout * preserved single-scanner rendering logic * ensured recommendations section renders correctly --- static/app.js | 44 ++++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/static/app.js b/static/app.js index 7be5c71..45796c0 100644 --- a/static/app.js +++ b/static/app.js @@ -1414,26 +1414,30 @@ function initApp() { const recommendations = gradeReport.analysis?.recommendations || []; return ` -
- ${!singleScannerMode && gradeReport.overall - ? renderGradeCard('Overall Grade', gradeReport.overall, '🎯') - : ''} - - ${renderGradeCard('Cost Optimization', gradeReport.cost, '💰')} - ${renderGradeCard('IaC Security', gradeReport.security, '🔒')} - ${renderGradeCard('Container Security', gradeReport.container, '🐳')} -
- ${recommendations.length > 0 ? ` -
-

💡 Recommendations

-
    - ${recommendations.map(rec => `
  • ${escapeHtml(rec)}
  • `).join('')} -
-
- ` : ''} -
- `; - } +
+

📊 Infrastructure Health Report

+ +
+ ${!singleScannerMode && gradeReport.overall + ? renderGradeCard('Overall Grade', gradeReport.overall, '🎯') + : ''} + + ${renderGradeCard('Cost Optimization', gradeReport.cost, '💰')} + ${renderGradeCard('IaC Security', gradeReport.security, '🔒')} + ${renderGradeCard('Container Security', gradeReport.container, '🐳')} +
+ + ${recommendations.length > 0 ? ` +
+

💡 Recommendations

+
    + ${recommendations.map(rec => `
  • ${escapeHtml(rec)}
  • `).join('')} +
+
+ ` : ''} +
+` ; +} submitFeedbackBtn.addEventListener('click', async () => { const review = feedbackReview.value.trim(); From aa61dc735e3e217173e3cf50d3aabf22cae64bb9 Mon Sep 17 00:00:00 2001 From: Natan Date: Mon, 25 May 2026 17:44:57 +0200 Subject: [PATCH 10/10] fixed a small bug where the brace at the end of the code was not closed