feat(phase-37): timer pause/resume + professional reporting engine
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Pause/Resume timer:
- Add paused_at, red_paused_seconds, blue_paused_seconds fields to Test model
- Add pause_timer/resume_timer workflow functions with accumulated pause tracking
- Auto-resume on phase submit; subtract paused time from worklog duration
- Add POST /tests/{id}/pause-timer and resume-timer endpoints
- Update LiveTimer component with pause/resume button and paused visual state
- Wire pause/resume mutations through TestDetailPage and TestDetailHeader
Professional Reporting Engine - Fase 2:
- Add ReportEngine service with Jinja2 HTML rendering, WeasyPrint PDF, and docxtpl DOCX
- Add corporate CSS stylesheet with cover page, data tables, stats grid, findings
- Create purple_campaign, coverage_report, and executive_summary HTML templates
- Add report_generation_service collecting domain data for each report type
- Add professional_reports router: GET /reports/generate/purple-campaign/{id}, coverage-summary, executive-summary
- Add analytics router with flat JSON endpoints for PowerBI: /coverage, /tests, /trends, /operators
- Add advanced_metrics router: /coverage-by-tactic, /never-tested, /avg-validation-time, /detection-rate-trend
- Add weasyprint and docxtpl to requirements.txt
- Add REPORT_TEMPLATES_DIR, REPORT_OUTPUT_DIR, COMPANY_NAME, COMPANY_LOGO_PATH to config
This commit is contained in:
4
backend/app/templates/reports/assets/logo.svg
Normal file
4
backend/app/templates/reports/assets/logo.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 60" width="200" height="60">
|
||||
<rect width="200" height="60" rx="8" fill="#0e7490"/>
|
||||
<text x="100" y="38" fill="white" font-family="Arial,sans-serif" font-size="28" font-weight="bold" text-anchor="middle">AEGIS</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 284 B |
119
backend/app/templates/reports/coverage_report.html
Normal file
119
backend/app/templates/reports/coverage_report.html
Normal file
@@ -0,0 +1,119 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="styles/report.css">
|
||||
<title>Coverage Report — {{ company_name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<section class="cover-page">
|
||||
<img src="assets/logo.png" class="logo" alt="Logo">
|
||||
<h1>MITRE ATT&CK Coverage Report</h1>
|
||||
<h2>{{ company_name }}</h2>
|
||||
<p class="date">{{ generated_at }}</p>
|
||||
<p class="classification">{{ classification | default('INTERNAL') }}</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>1. Organization Score</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat">
|
||||
<span class="number">{{ org_score.overall | default(0) }}%</span>
|
||||
<span class="label">Overall Score</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="number">{{ org_score.coverage | default(0) }}%</span>
|
||||
<span class="label">Coverage</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="number">{{ org_score.detection_maturity | default(0) }}%</span>
|
||||
<span class="label">Detection Maturity</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Coverage Summary</h2>
|
||||
<div class="metric-cards">
|
||||
<div class="metric-card">
|
||||
<div class="value">{{ summary.total_techniques }}</div>
|
||||
<div class="label">Total Techniques</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="value">{{ summary.validated }}</div>
|
||||
<div class="label">Validated</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="value">{{ summary.partial }}</div>
|
||||
<div class="label">Partial</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="value">{{ summary.not_covered }}</div>
|
||||
<div class="label">Not Covered</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="value">{{ summary.in_progress }}</div>
|
||||
<div class="label">In Progress</div>
|
||||
</div>
|
||||
<div class="metric-card">
|
||||
<div class="value">{{ summary.not_evaluated }}</div>
|
||||
<div class="label">Not Evaluated</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Coverage by Tactic</h2>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tactic</th>
|
||||
<th>Total</th>
|
||||
<th>Validated</th>
|
||||
<th>Coverage %</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for tactic in tactics_coverage %}
|
||||
<tr>
|
||||
<td>{{ tactic.tactic }}</td>
|
||||
<td>{{ tactic.total }}</td>
|
||||
<td>{{ tactic.validated }}</td>
|
||||
<td>{{ tactic.coverage_pct }}%</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. Never-Tested Techniques</h2>
|
||||
{% if never_tested %}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MITRE ID</th>
|
||||
<th>Name</th>
|
||||
<th>Tactic</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for t in never_tested %}
|
||||
<tr>
|
||||
<td>{{ t.mitre_id }}</td>
|
||||
<td>{{ t.name }}</td>
|
||||
<td>{{ t.tactic }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>All techniques have been tested at least once.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>{{ company_name }} — Confidential</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
74
backend/app/templates/reports/executive_summary.html
Normal file
74
backend/app/templates/reports/executive_summary.html
Normal file
@@ -0,0 +1,74 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="styles/report.css">
|
||||
<title>Executive Summary — {{ company_name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<section class="cover-page">
|
||||
<img src="assets/logo.png" class="logo" alt="Logo">
|
||||
<h1>Executive Security Summary</h1>
|
||||
<h2>{{ company_name }}</h2>
|
||||
<p class="date">{{ generated_at }}</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Security Posture Overview</h2>
|
||||
<div class="stats-grid">
|
||||
<div class="stat">
|
||||
<span class="number">{{ org_score.overall | default(0) }}%</span>
|
||||
<span class="label">Overall Score</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="number">{{ total_tests }}</span>
|
||||
<span class="label">Tests Conducted</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="number">{{ active_campaigns }}</span>
|
||||
<span class="label">Active Campaigns</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Key Metrics</h2>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>Metric</th><th>Value</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr><td>Techniques validated</td><td>{{ summary.validated }} / {{ summary.total_techniques }}</td></tr>
|
||||
<tr><td>Detection rate</td><td>{{ detection_rate }}%</td></tr>
|
||||
<tr><td>Tests this quarter</td><td>{{ tests_this_quarter }}</td></tr>
|
||||
<tr><td>Open remediations</td><td>{{ open_remediations }}</td></tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Top Gaps</h2>
|
||||
{% if top_gaps %}
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>Tactic</th><th>Coverage</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for gap in top_gaps %}
|
||||
<tr>
|
||||
<td>{{ gap.tactic }}</td>
|
||||
<td>{{ gap.coverage_pct }}%</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% else %}
|
||||
<p>No significant gaps identified.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>{{ company_name }} — Confidential</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
130
backend/app/templates/reports/purple_campaign.html
Normal file
130
backend/app/templates/reports/purple_campaign.html
Normal file
@@ -0,0 +1,130 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<link rel="stylesheet" href="styles/report.css">
|
||||
<title>Purple Team Assessment Report — {{ campaign.name }}</title>
|
||||
</head>
|
||||
<body>
|
||||
<section class="cover-page">
|
||||
<img src="assets/logo.png" class="logo" alt="Logo">
|
||||
<h1>Purple Team Assessment Report</h1>
|
||||
<h2>{{ campaign.name }}</h2>
|
||||
<p class="date">{{ generated_at }}</p>
|
||||
<p class="classification">{{ classification | default('INTERNAL') }}</p>
|
||||
</section>
|
||||
|
||||
<section class="toc">
|
||||
<h2>Table of Contents</h2>
|
||||
<ul>
|
||||
<li>1. Executive Summary</li>
|
||||
<li>2. Scope & Methodology</li>
|
||||
<li>3. Techniques Tested</li>
|
||||
<li>4. Critical Findings</li>
|
||||
<li>5. Coverage Evolution</li>
|
||||
<li>6. Recommendations</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>1. Executive Summary</h2>
|
||||
<p>Campaign <strong>{{ campaign.name }}</strong> tested
|
||||
{{ tests | length }} techniques across {{ tactics | length }} tactics.
|
||||
Overall organization coverage score: <strong>{{ org_score }}%</strong>.</p>
|
||||
<div class="stats-grid">
|
||||
<div class="stat">
|
||||
<span class="number">{{ tests_validated }}</span>
|
||||
<span class="label">Validated</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="number">{{ tests_detected }}</span>
|
||||
<span class="label">Detected</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<span class="number">{{ tests_not_detected }}</span>
|
||||
<span class="label">Not Detected</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>2. Scope & Methodology</h2>
|
||||
<p>{{ campaign.description or 'No description provided.' }}</p>
|
||||
{% if campaign.scheduled_at and campaign.completed_at %}
|
||||
<p>Period: {{ campaign.scheduled_at }} — {{ campaign.completed_at }}</p>
|
||||
{% endif %}
|
||||
{% if threat_actors %}
|
||||
<p>Threat actors modeled:
|
||||
{% for actor in threat_actors %}{{ actor.name }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||
</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>3. Techniques Tested</h2>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>MITRE ID</th>
|
||||
<th>Name</th>
|
||||
<th>Tactic</th>
|
||||
<th>State</th>
|
||||
<th>Detection</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for test in tests %}
|
||||
<tr class="result-{{ test.detection_result }}">
|
||||
<td>{{ test.technique_mitre_id }}</td>
|
||||
<td>{{ test.name }}</td>
|
||||
<td>{{ test.tactic }}</td>
|
||||
<td>{{ test.state }}</td>
|
||||
<td>{{ test.detection_result }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>4. Critical Findings</h2>
|
||||
{% if critical_findings %}
|
||||
{% for finding in critical_findings %}
|
||||
<div class="finding {{ finding.severity }}">
|
||||
<h3>{{ finding.technique_id }}: {{ finding.name }}</h3>
|
||||
<p>{{ finding.description }}</p>
|
||||
<p><strong>Recommendation:</strong> {{ finding.recommendation }}</p>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p>No critical findings — all tested techniques were detected.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>5. Coverage Evolution</h2>
|
||||
{% if previous_campaign %}
|
||||
<p>Compared to previous campaign (<em>{{ previous_campaign.name }}</em>):
|
||||
Coverage changed from {{ previous_score }}% to {{ org_score }}%.</p>
|
||||
{% else %}
|
||||
<p>This is the first campaign run — no historical comparison available.</p>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>6. Recommendations</h2>
|
||||
<ul>
|
||||
{% for finding in critical_findings %}
|
||||
<li><strong>{{ finding.technique_id }}</strong>: {{ finding.recommendation }}</li>
|
||||
{% endfor %}
|
||||
{% if not critical_findings %}
|
||||
<li>Continue periodic purple team exercises to maintain coverage.</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<footer>
|
||||
<p>{{ company_name }} — Confidential</p>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
238
backend/app/templates/reports/styles/report.css
Normal file
238
backend/app/templates/reports/styles/report.css
Normal file
@@ -0,0 +1,238 @@
|
||||
/* ── Aegis Professional Report CSS ─────────────────────────────── */
|
||||
|
||||
@page {
|
||||
size: A4;
|
||||
margin: 2cm 2.5cm;
|
||||
@bottom-center {
|
||||
content: "Page " counter(page) " of " counter(pages);
|
||||
font-size: 9px;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", -apple-system, "Helvetica Neue", Arial, sans-serif;
|
||||
font-size: 11pt;
|
||||
color: #1f2937;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* ── Cover Page ─────────────────────────────────────────────────── */
|
||||
|
||||
.cover-page {
|
||||
page-break-after: always;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 80vh;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cover-page .logo {
|
||||
max-width: 180px;
|
||||
margin-bottom: 3rem;
|
||||
}
|
||||
|
||||
.cover-page h1 {
|
||||
font-size: 28pt;
|
||||
color: #0e7490;
|
||||
font-weight: 700;
|
||||
margin: 0 0 0.5rem;
|
||||
}
|
||||
|
||||
.cover-page h2 {
|
||||
font-size: 18pt;
|
||||
color: #374151;
|
||||
font-weight: 400;
|
||||
margin: 0 0 2rem;
|
||||
}
|
||||
|
||||
.cover-page .date {
|
||||
font-size: 12pt;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.cover-page .classification {
|
||||
margin-top: 2rem;
|
||||
padding: 0.4rem 1.5rem;
|
||||
border: 2px solid #ef4444;
|
||||
color: #ef4444;
|
||||
font-weight: 700;
|
||||
font-size: 10pt;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* ── Section headings ───────────────────────────────────────────── */
|
||||
|
||||
section {
|
||||
page-break-inside: avoid;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 16pt;
|
||||
color: #0e7490;
|
||||
border-bottom: 2px solid #0e7490;
|
||||
padding-bottom: 0.3rem;
|
||||
margin-top: 2rem;
|
||||
page-break-after: avoid;
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-size: 13pt;
|
||||
color: #1f2937;
|
||||
margin-top: 1.2rem;
|
||||
}
|
||||
|
||||
/* ── Stats grid ─────────────────────────────────────────────────── */
|
||||
|
||||
.stats-grid {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.stat {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 8px;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
.stat .number {
|
||||
display: block;
|
||||
font-size: 28pt;
|
||||
font-weight: 700;
|
||||
color: #0e7490;
|
||||
}
|
||||
|
||||
.stat .label {
|
||||
display: block;
|
||||
font-size: 10pt;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* ── Data table ─────────────────────────────────────────────────── */
|
||||
|
||||
.data-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 1rem 0;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
.data-table th {
|
||||
background: #0e7490;
|
||||
color: white;
|
||||
padding: 0.5rem 0.75rem;
|
||||
text-align: left;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.data-table td {
|
||||
padding: 0.4rem 0.75rem;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.data-table tbody tr:nth-child(even) {
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
/* ── Detection result row colors ───────────────────────────────── */
|
||||
|
||||
tr.result-detected td:last-child { color: #059669; font-weight: 600; }
|
||||
tr.result-not_detected td:last-child { color: #dc2626; font-weight: 600; }
|
||||
tr.result-partially_detected td:last-child { color: #d97706; font-weight: 600; }
|
||||
tr.result-pending td:last-child { color: #6b7280; }
|
||||
|
||||
/* ── Findings ───────────────────────────────────────────────────── */
|
||||
|
||||
.finding {
|
||||
padding: 1rem;
|
||||
border-left: 4px solid #d1d5db;
|
||||
margin-bottom: 1rem;
|
||||
background: #f9fafb;
|
||||
border-radius: 0 6px 6px 0;
|
||||
}
|
||||
|
||||
.finding.critical { border-left-color: #dc2626; }
|
||||
.finding.high { border-left-color: #ea580c; }
|
||||
.finding.medium { border-left-color: #d97706; }
|
||||
.finding.low { border-left-color: #059669; }
|
||||
|
||||
.finding h3 {
|
||||
margin-top: 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
/* ── Footer ─────────────────────────────────────────────────────── */
|
||||
|
||||
footer {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
text-align: center;
|
||||
font-size: 9pt;
|
||||
color: #9ca3af;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
/* ── Table of Contents ──────────────────────────────────────────── */
|
||||
|
||||
.toc {
|
||||
page-break-after: always;
|
||||
}
|
||||
|
||||
.toc h2 {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.toc ul {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.toc li {
|
||||
padding: 0.3rem 0;
|
||||
border-bottom: 1px dotted #d1d5db;
|
||||
}
|
||||
|
||||
/* ── Metric cards (for coverage reports) ────────────────────────── */
|
||||
|
||||
.metric-cards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.metric-card {
|
||||
flex: 1 1 calc(33% - 1rem);
|
||||
min-width: 140px;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metric-card .value {
|
||||
font-size: 24pt;
|
||||
font-weight: 700;
|
||||
color: #0e7490;
|
||||
}
|
||||
|
||||
.metric-card .label {
|
||||
font-size: 9pt;
|
||||
color: #6b7280;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
Reference in New Issue
Block a user