Skip to content
Draft
54 changes: 32 additions & 22 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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():
Expand Down Expand Up @@ -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 = (
Expand Down Expand Up @@ -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'])
Expand Down
133 changes: 98 additions & 35 deletions reporter/grading.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -317,6 +317,18 @@ def generate_report(self,
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']
Expand All @@ -329,25 +341,41 @@ def generate_report(self,
container_scoring_findings = self._most_severe_per_container_image(container_findings)

# Calculate individual grades
cost_grade = self.calculator.calculate_grade(cost_findings, resource_count)
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
)

# 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
Expand All @@ -368,6 +396,8 @@ def generate_report(self,

# Additional metrics (extensible)
metrics = self._calculate_additional_metrics(findings, resource_count)

single_scanner_mode = len(enabled_scanners) == 1

return ScanReport(
overall_grade=overall_grade,
Expand All @@ -383,7 +413,10 @@ def generate_report(self,
total_violations=len(findings),
recommendations=recommendations,
top_issues=top_issues,
metrics=metrics
metrics={
**(metrics or {}),
'single_scanner_mode': single_scanner_mode
}
)

def _most_severe_per_resource(self, findings: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
Expand Down Expand Up @@ -425,17 +458,18 @@ def _calculate_overall_grade(self, cost_grade: GradeInfo,
"""Calculate overall grade from cost, security, and container grades."""
# Determine which scanners were used
scanners_used = []
if cost_findings:

if cost_grade:
scanners_used.append('cost')
if security_findings:

if security_grade:
scanners_used.append('security')
if container_findings:

if container_grade:
scanners_used.append('container')

if not scanners_used:
overall_percentage = 100.0
combined_score = 0
max_combined = 0
return None
elif len(scanners_used) == 1:
# Single scanner - use its grade directly
if 'cost' in scanners_used:
Expand All @@ -459,18 +493,27 @@ def _calculate_overall_grade(self, cost_grade: GradeInfo,
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
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': 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']
'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(
Expand All @@ -479,7 +522,7 @@ def _calculate_overall_grade(self, cost_grade: GradeInfo,
score=combined_score,
max_score=max_combined,
risk_level=RISK_LEVELS.get(letter, 'Unknown'),
violations=cost_grade.violations + security_grade.violations + container_grade.violations,
violations=sum(g.violations for g in grades),
severity_breakdown=combined_breakdown
)

Expand All @@ -493,8 +536,14 @@ def _generate_recommendations(self, cost_grade: GradeInfo,
recommendations = []

# IaC Security - show most critical issue only
iac_critical = security_grade.severity_breakdown.get('critical', 0)
iac_high = security_grade.severity_breakdown['high']
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 "
Expand All @@ -507,8 +556,14 @@ def _generate_recommendations(self, cost_grade: GradeInfo,
)

# Container Security - show most critical issue only
container_critical = container_grade.severity_breakdown.get('critical', 0)
container_high = container_grade.severity_breakdown['high']
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 "
Expand All @@ -521,21 +576,30 @@ def _generate_recommendations(self, cost_grade: GradeInfo,
)

# Cost - show only if high priority
if cost_grade.severity_breakdown['high'] > 0:
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
worst_grade = min([cost_grade.letter, security_grade.letter, container_grade.letter])
available_letters = [
g.letter for g in [cost_grade, security_grade, container_grade]
if g
]

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']:
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 [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")
Expand Down Expand Up @@ -575,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

48 changes: 26 additions & 22 deletions static/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -1410,29 +1410,34 @@ function initApp() {
</div>
`;
};

const singleScannerMode = gradeReport.metrics?.single_scanner_mode;
const recommendations = gradeReport.analysis?.recommendations || [];

return `
<div class="grade-report-section">
<h2 class="section-title">📊 Infrastructure Report Card</h2>
<div class="grade-cards-container">
${renderGradeCard('Overall Grade', gradeReport.overall, '🎯')}
${renderGradeCard('Cost Optimization', gradeReport.cost, '💰')}
${renderGradeCard('IaC Security', gradeReport.security, '🔒')}
${renderGradeCard('Container Security', gradeReport.container, '🐳')}
</div>
${recommendations.length > 0 ? `
<div class="recommendations-section">
<h3 class="recommendations-title">💡 Recommendations</h3>
<ul class="recommendations-list">
${recommendations.map(rec => `<li>${escapeHtml(rec)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
`;
}
<div class="grade-report-section">
<h2 class="section-title">📊 Infrastructure Health Report</h2>

<div class="grade-cards-container">
${!singleScannerMode && gradeReport.overall
? renderGradeCard('Overall Grade', gradeReport.overall, '🎯')
: ''}

${renderGradeCard('Cost Optimization', gradeReport.cost, '💰')}
${renderGradeCard('IaC Security', gradeReport.security, '🔒')}
${renderGradeCard('Container Security', gradeReport.container, '🐳')}
</div>

${recommendations.length > 0 ? `
<div class="recommendations-section">
<h3 class="recommendations-title">💡 Recommendations</h3>
<ul class="recommendations-list">
${recommendations.map(rec => `<li>${escapeHtml(rec)}</li>`).join('')}
</ul>
</div>
` : ''}
</div>
` ;
}

submitFeedbackBtn.addEventListener('click', async () => {
const review = feedbackReview.value.trim();
Expand Down Expand Up @@ -1622,5 +1627,4 @@ function toggleCVE(cveId) {
} else {
details.style.display = 'none';
if (icon) icon.textContent = '▼';
}
}
}}
Loading