Este tutorial descreve como configurar uma solução na AWS para processar relatórios do AWS Inspector, armazená-los em um bucket S3, processá-los com uma função Lambda e gerar um dashboard HTML interativo.
Dashboard ScreenShot:
1. Resumo da Solução
A solução automatiza o processamento de relatórios de vulnerabilidades gerados pelo AWS Inspector. Aqui está uma visão geral:
- AWS Inspector: Gera relatórios de vulnerabilidades em formato JSON e os armazena em um bucket S3 privado.
-
S3: Armazena os relatórios JSON no caminho
/report
e os relatórios HTML processados no caminho/final
. - AWS KMS: Criptografa os dados no bucket S3 para segurança.
- AWS Lambda: Processa os relatórios JSON usando Python 3.13, com a biblioteca AWS Lambda Powertools, e gera um dashboard HTML interativo.
- Dashboard HTML: Exibe estatísticas e detalhes das vulnerabilidades encontradas em instâncias EC2 e imagens ECR.
A Lambda é acionada por eventos S3, processa apenas arquivos .json
no caminho /report
, e salva o resultado no caminho /final
.
2. Diagrama da Solução
-
Fluxo:
- AWS Inspector → Gera relatório JSON → S3 (Bucket:
seu-bucket
, Path:/report
). - S3 → Evento notifica a Lambda.
- Lambda → Lê o JSON, processa com KMS para descriptografia, gera HTML.
- Lambda → Salva o HTML no S3 (Path:
/final
).
- AWS Inspector → Gera relatório JSON → S3 (Bucket:
-
Componentes:
- AWS Inspector, S3, KMS, Lambda.
-
Conexões:
- Inspector usa KMS para criptografia.
- Lambda acessa S3 e KMS.
3. Configuração do AWS KMS para o AWS Inspector
O AWS Key Management Service (KMS) é usado para criptografar os relatórios do Inspector no S3. Abaixo está o passo a passo para criar e configurar uma chave KMS (Customer Managed Key - CMK).
Passo a Passo
-
Acesse o Console KMS:
- No console AWS, vá para Key Management Service (KMS).
- Clique em Create key (Criar chave) no canto superior direito.
-
Escolha o Tipo de Chave:
- Selecione Symmetric (Simétrica) como tipo de chave.
- Clique em Next (Próximo).
-
Configure as Opções da Chave:
- Key type: Mantenha como Symmetric.
- Key usage: Escolha Encrypt and decrypt (Criptografar e descriptografar).
- Key material origin: Mantenha como KMS.
- Clique em Next.
-
Adicione Descrição e Tags:
-
Alias: Insira um nome, por exemplo,
inspector-report-key
. - Description: "Chave para criptografia de relatórios do AWS Inspector".
-
Tags: Adicione tags opcionais (ex.:
Project=Security
). - Clique em Next.
-
Alias: Insira um nome, por exemplo,
-
Definir Administradores da Chave:
- Na seção Key administrators, adicione os usuários/roles que gerenciarão a chave.
- Exemplo: Selecione uma role IAM como
[ROLE_ADMINISTRATOR_MASKED]
. - Esses administradores terão permissões para criar, excluir e gerenciar a chave.
-
Definir Usuários da Chave:
- Na seção Key usage permissions, adicione quem pode usar a chave para criptografia/descriptografia.
- Exemplo: Adicione
[ROLE_ADMINISTRATOR_MASKED]
para uso geral.
-
Revisar e Criar:
- Revise as configurações e clique em Create key.
- Anote o ARN da chave gerada (ex.:
arn:aws:kms:us-east-1:[ACCOUNT_ID_MASKED]:key/[KEY_ID]
).
Política da Chave (Key Policy)
A política abaixo define quem tem acesso à chave e como ela pode ser usada. Substitua [ACCOUNT_ID_MASKED]
pelo seu ID de conta e [ROLE_ADMINISTRATOR_MASKED]
pela sua role de administrador.
{
"Version": "2012-10-17",
"Id": "key-consolepolicy-3",
"Statement": [
{
"Sid": "Enable IAM User Permissions",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::[ACCOUNT_ID_MASKED]:root"
},
"Action": "kms:*",
"Resource": "*"
},
{
"Sid": "Allow access for Key Administrators",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::[ACCOUNT_ID_MASKED]:role/[ROLE_ADMINISTRATOR_MASKED]"
},
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion",
"kms:RotateKeyOnDemand"
],
"Resource": "*"
},
{
"Sid": "Allow use of the key",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::[ACCOUNT_ID_MASKED]:role/[ROLE_ADMINISTRATOR_MASKED]"
},
"Action": [
"kms:Encrypt",
"kms:Decrypt",
"kms:ReEncrypt*",
"kms:GenerateDataKey*",
"kms:DescribeKey"
],
"Resource": "*"
},
{
"Sid": "Allow attachment of persistent resources",
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::[ACCOUNT_ID_MASKED]:role/[ROLE_ADMINISTRATOR_MASKED]"
},
"Action": [
"kms:CreateGrant",
"kms:ListGrants",
"kms:RevokeGrant"
],
"Resource": "*",
"Condition": {
"Bool": {
"kms:GrantIsForAWSResource": "true"
}
}
},
{
"Sid": "Allow Amazon Inspector to use the key",
"Effect": "Allow",
"Principal": {
"Service": "inspector2.amazonaws.com"
},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey*"
],
"Resource": "*",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "[ACCOUNT_ID_MASKED]"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:inspector2:us-east-1:[ACCOUNT_ID_MASKED]:report/*"
}
}
}
]
}
-
Explicação:
- Root: Permite que a conta raiz gerencie a chave.
-
Administradores: A role
[ROLE_ADMINISTRATOR_MASKED]
pode gerenciar a chave (ex.: rotacionar, excluir). - Uso da Chave: A mesma role pode usá-la para criptografia/descriptografia.
- Recursos Persistentes: Permite grants para recursos AWS.
-
Inspector: O serviço
inspector2.amazonaws.com
pode gerar chaves de dados e descriptografar, restrito à sua conta e relatórios.
-
Aplicar a Política:
- Após criar a chave, edite a Key Policy no console KMS e cole a política acima (com a adição do SID do AWS Inspector com seus ajustes).
4. Configuração do Bucket S3 e Lambda
Configuração do Bucket S3
-
Criar o Bucket:
- No console AWS, vá para S3 e clique em Create bucket.
- Nome:
seu-bucket-inspector-report
(substitua por um nome único). - Região: Escolha sua região (ex.:
us-east-1
). - Desative ACLs e mantenha o bucket privado (padrão).
-
Estrutura de Pastas:
- Crie manualmente os caminhos:
-
/report
: Para relatórios JSON do Inspector. -
/final
: Para relatórios HTML gerados pela Lambda.
-
- Crie manualmente os caminhos:
-
Bucket Policy:
- Na aba Permissions, edite a Bucket Policy e aplique:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "allow-inspector",
"Effect": "Allow",
"Principal": {
"Service": "inspector2.amazonaws.com"
},
"Action": [
"s3:PutObject",
"s3:PutObjectAcl",
"s3:AbortMultipartUpload"
],
"Resource": "arn:aws:s3:::seu-bucket-inspector-report/*",
"Condition": {
"StringEquals": {
"aws:SourceAccount": "[ACCOUNT_ID_MASKED]"
},
"ArnLike": {
"aws:SourceArn": "arn:aws:inspector2:us-east-1:[ACCOUNT_ID_MASKED]:report/*"
}
}
}
]
}
- Explicação: Permite que o Inspector grave objetos no bucket, restrito à sua conta e relatórios.
Configuração da Lambda
-
Criar a Função Lambda:
- No console AWS, vá para Lambda e clique em Create function.
- Nome:
process-inspector-report
. - Runtime: Python 3.13.
- Clique em Create.
- Clique em Configuration > Edit > Altere o Timeout para 1 min.
-
Adicionar o Código:
- Coloque referências à empresa "SUA-EMPRESA":
- No código, troque para seu bucket
"seu-bucket-inspector-report"
. - No HTML, substitua o logo
<img src="https://www.xpto.com.br/images/xpto-logo.svg"
por<img src="https://seusite.com/logo.svg"
. - Troque "SUA-EMPRESA" por "Seu Projeto" no título e rodapé.
- No código, troque para seu bucket
- Copie e cole o código fornecido (com essas alterações) na aba Code.
- Código Lambda para referência:
- Coloque referências à empresa "SUA-EMPRESA":
import json
import boto3
import logging
import os
from datetime import datetime
from collections import Counter, defaultdict
from typing import Dict, List, Any, Tuple
from aws_lambda_powertools import Logger, Tracer
from aws_lambda_powertools.utilities.data_classes import S3Event, event_source
logger = Logger()
tracer = Tracer()
s3_client = boto3.client('s3')
@logger.inject_lambda_context
@tracer.capture_lambda_handler
@event_source(data_class=S3Event)
def lambda_handler(event: S3Event, context):
"""
Lambda function to process AWS Inspector reports stored in S3.
The function:
1. Gets the report JSON file from the source bucket
2. Processes the findings to extract relevant information
3. Generates an HTML dashboard report
4. Uploads the HTML report to the destination bucket
"""
try:
# Get the source bucket and key from the event
source_bucket = event.bucket_name
source_key = event.object_key
if not source_key.endswith('.json'):
logger.info(f"Skipping non-JSON file: {source_key}")
return {
'statusCode': 200,
'body': 'Skipped non-JSON file'
}
# Get the object from S3
logger.info(f"Processing file {source_key} from bucket {source_bucket}")
response = s3_client.get_object(Bucket=source_bucket, Key=source_key)
file_content = response['Body'].read().decode('utf-8')
findings = json.loads(file_content).get('findings', [])
if not findings:
logger.warning("No findings in the report")
return {
'statusCode': 200,
'body': 'No findings to process'
}
# Process the findings
processed_data = process_findings(findings)
# Generate HTML report
html_content = generate_html_report(processed_data)
# Define the destination key
timestamp = datetime.now().strftime("%d%m%Y-%H%M%S")
destination_bucket = "seu-bucket-inspector-report"
destination_key = f"final/aws-inspector-report-{timestamp}.html"
# Upload the HTML file to S3
s3_client.put_object(
Bucket=destination_bucket,
Key=destination_key,
Body=html_content,
ContentType='text/html'
)
logger.info(f"Successfully uploaded report to s3://{destination_bucket}/{destination_key}")
return {
'statusCode': 200,
'body': f'Successfully processed {len(findings)} findings and uploaded report to {destination_bucket}/{destination_key}'
}
except Exception as e:
logger.exception("Error processing report")
raise e
def process_findings(findings: List[Dict]) -> Dict:
"""
Process the findings to extract relevant information for the report.
Args:
findings: List of finding dictionaries from the AWS Inspector report
Returns:
Dictionary with processed data for the HTML report
"""
severity_counts = Counter()
service_vulnerability_counts = defaultdict(int)
ecr_findings = []
ec2_findings = []
# Process each finding
for finding in findings:
severity = finding.get('severity', 'UNKNOWN')
severity_counts[severity] += 1
# Determine the resource type and collect relevant findings
resources = finding.get('resources', [])
for resource in resources:
resource_type = resource.get('type', '')
if resource_type == 'AWS_ECR_CONTAINER_IMAGE':
service_vulnerability_counts['ECR'] += 1
details = resource.get('details', {}).get('awsEcrContainerImage', {})
ecr_finding = {
'description': finding.get('description', ''),
'score': finding.get('epss', {}).get('score', 'N/A'),
'exploitAvailable': finding.get('exploitAvailable', 'N/A'),
'fixAvailable': finding.get('fixAvailable', 'N/A'),
'inspectorScore': finding.get('inspectorScore', 'N/A'),
'referenceUrls': finding.get('packageVulnerabilityDetails', {}).get('referenceUrls', []),
'sourceUrl': finding.get('packageVulnerabilityDetails', {}).get('sourceUrl', ''),
'vendorSeverity': finding.get('packageVulnerabilityDetails', {}).get('vendorSeverity', ''),
'vulnerabilityId': finding.get('packageVulnerabilityDetails', {}).get('vulnerabilityId', ''),
'imageHash': details.get('imageHash', ''),
'repositoryName': details.get('repositoryName', ''),
'title': finding.get('title', ''),
'severity': severity,
'vulnerability_details': []
}
# Extract vulnerable package details
vuln_packages = finding.get('packageVulnerabilityDetails', {}).get('vulnerablePackages', [])
for package in vuln_packages:
package_detail = {
'filePath': package.get('filePath', ''),
'type': package.get('packageManager', ''),
'fixedInVersion': package.get('fixedInVersion', ''),
'name': package.get('name', ''),
'packageManager': package.get('packageManager', ''),
'version': package.get('version', '')
}
ecr_finding['vulnerability_details'].append(package_detail)
ecr_findings.append(ecr_finding)
elif resource_type == 'AWS_EC2_INSTANCE':
service_vulnerability_counts['EC2'] += 1
details = resource.get('details', {}).get('awsEc2Instance', {})
ec2_finding = {
'description': finding.get('description', ''),
'score': finding.get('epss', {}).get('score', 'N/A'),
'exploitAvailable': finding.get('exploitAvailable', 'N/A'),
'fixAvailable': finding.get('fixAvailable', 'N/A'),
'inspectorScore': finding.get('inspectorScore', 'N/A'),
'referenceUrls': finding.get('packageVulnerabilityDetails', {}).get('referenceUrls', []),
'sourceUrl': finding.get('packageVulnerabilityDetails', {}).get('sourceUrl', ''),
'vendorSeverity': finding.get('packageVulnerabilityDetails', {}).get('vendorSeverity', ''),
'vulnerabilityId': finding.get('packageVulnerabilityDetails', {}).get('vulnerabilityId', ''),
'id': resource.get('id', ''),
'title': finding.get('title', ''),
'name': resource.get('tags', {}).get('Name', ''),
'severity': severity,
'vulnerability_details': []
}
# Extract vulnerable package details
vuln_packages = finding.get('packageVulnerabilityDetails', {}).get('vulnerablePackages', [])
for package in vuln_packages:
package_detail = {
'filePath': package.get('filePath', ''),
'type': package.get('packageManager', ''),
'fixedInVersion': package.get('fixedInVersion', ''),
'name': package.get('name', ''),
'packageManager': package.get('packageManager', ''),
'version': package.get('version', '')
}
ec2_finding['vulnerability_details'].append(package_detail)
ec2_findings.append(ec2_finding)
else:
service_vulnerability_counts['Other'] += 1
# Sort findings by severity
severity_order = {
'CRITICAL': 0,
'HIGH': 1,
'MEDIUM': 2,
'LOW': 3,
'INFORMATIONAL': 4,
'UNTRIAGED': 5,
'UNKNOWN': 6
}
ecr_findings.sort(key=lambda x: severity_order.get(x['severity'], 999))
ec2_findings.sort(key=lambda x: severity_order.get(x['severity'], 999))
# Get top 5 services with most vulnerabilities
top_services = dict(sorted(service_vulnerability_counts.items(), key=lambda item: item[1], reverse=True)[:5])
# Get top 5 vulnerabilities
vuln_counts = Counter(finding['packageVulnerabilityDetails'].get('vulnerabilityId', 'UNKNOWN')
for finding in findings if 'packageVulnerabilityDetails' in finding)
top_vulns = vuln_counts.most_common(5)
top_vulns_data = [
{'vuln': vuln, 'count': count}
for vuln, count in top_vulns
]
# Get top 5 servers (ECR repos + EC2 instances)
server_counts = Counter()
for finding in ecr_findings:
server_counts[finding['repositoryName']] += 1
for finding in ec2_findings:
server_counts[finding['name'] or finding['id']] += 1
top_servers = server_counts.most_common(5)
top_servers_data = [
{'server': server, 'count': count}
for server, count in top_servers
]
return {
'timestamp': datetime.now().strftime("%d-%m-%Y"),
'total_findings': len(findings),
'severity_counts': severity_counts,
'service_vulnerability_counts': service_vulnerability_counts,
'service_data': [
{'service': k, 'count': v}
for k, v in top_services.items()
],
'ecr_findings': ecr_findings,
'ec2_findings': ec2_findings,
'top_vulns_data': top_vulns_data,
'top_servers_data': top_servers_data
}
def generate_html_report(data: Dict) -> str:
"""
Generate a modern HTML report for AWS Inspector findings with updated design.
Args:
data: Dictionary with processed finding data
Returns:
HTML content as a string
"""
service_js_data = json.dumps(data['service_data'])
top_vulns_js_data = json.dumps(data['top_vulns_data'])
top_servers_js_data = json.dumps(data['top_servers_data'])
# Organize ECR findings by repository
ecr_by_repo = defaultdict(list)
for finding in data['ecr_findings']:
ecr_by_repo[finding['repositoryName']].append(finding)
# Organize EC2 findings by instance
ec2_by_instance = defaultdict(list)
for finding in data['ec2_findings']:
instance_key = finding['name'] or finding['id']
ec2_by_instance[instance_key].append(finding)
# Define the JavaScript section with escaped curly braces for objects
script_content = """
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels"></script>
<script>
function showSection(sectionId) {{
document.querySelectorAll('.resource-section').forEach(function(section) {{
section.style.display = 'none';
section.style.overflow = 'visible';
section.style.maxHeight = 'none';
section.style.height = 'auto';
section.style.flexShrink = '0'; // Ensure no flex shrinking
}});
// If sectionId is 'none', don't show any section (Clear Selection functionality)
if (sectionId === 'none') {{
return;
}}
const selectedSection = document.getElementById(sectionId);
if (selectedSection) {{
selectedSection.style.display = 'block';
selectedSection.style.overflow = 'visible';
selectedSection.style.maxHeight = 'none';
selectedSection.style.height = 'auto';
selectedSection.style.flexShrink = '0'; // Ensure no flex shrinking
}} else {{
console.error('Section not found: ' + sectionId);
}}
}}
document.addEventListener('DOMContentLoaded', function() {{
Chart.register(ChartDataLabels);
const serviceData = JSON.parse('{0}');
const servicesCtx = document.getElementById('servicesChart') ? document.getElementById('servicesChart').getContext('2d') : null;
if (servicesCtx) {{
new Chart(servicesCtx, {{
type: 'doughnut',
data: {{
labels: serviceData.map(function(d) {{ return d.service; }}),
datasets: [{{
data: serviceData.map(function(d) {{ return d.count || 0; }}),
backgroundColor: ['#6366f1', '#8b5cf6', '#22c55e', '#0ea5e9', '#475569'],
borderWidth: 2,
borderColor: '#fff',
hoverOffset: 10
}}]
}},
options: {{
cutout: '65%',
plugins: {{
legend: {{ position: 'bottom', labels: {{ padding: 20, font: {{ size: 12 }} }} }},
datalabels: {{
color: '#fff',
font: {{ weight: '600', size: 12 }},
formatter: function(value, ctx) {{
const total = ctx.dataset.data.reduce(function(a, b) {{ return a + b; }}, 0);
return total === 0 ? '0%' : ((value / total) * 100).toFixed(1) + '%';
}}
}}
}}
}},
plugins: [ChartDataLabels]
}});
}} else {{
console.error('Canvas for servicesChart not found');
}}
const vulnsData = JSON.parse('{1}');
const vulnsCtx = document.getElementById('vulnsChart') ? document.getElementById('vulnsChart').getContext('2d') : null;
if (vulnsCtx) {{
new Chart(vulnsCtx, {{
type: 'bar',
data: {{
labels: vulnsData.map(function(d) {{ return d.vuln.substring(0, 20) + (d.vuln.length > 20 ? '...' : ''); }}),
datasets: [{{
label: 'Count',
data: vulnsData.map(function(d) {{ return d.count || 0; }}),
backgroundColor: '#6366f1',
borderRadius: 8,
barThickness: 18
}}]
}},
options: {{
scales: {{
y: {{ beginAtZero: true, display: false }},
x: {{ grid: {{ display: false }} }}
}},
plugins: {{
legend: {{ display: false }},
datalabels: {{
anchor: 'end',
align: 'top',
color: '#0f172a',
font: {{ weight: '600', size: 12 }}
}}
}}
}},
plugins: [ChartDataLabels]
}});
}} else {{
console.error('Canvas for vulnsChart not found');
}}
const serversData = JSON.parse('{2}');
const serversCtx = document.getElementById('serversChart') ? document.getElementById('serversChart').getContext('2d') : null;
if (serversCtx) {{
new Chart(serversCtx, {{
type: 'bar',
data: {{
labels: serversData.map(function(d) {{ return d.server.substring(0, 20) + (d.server.length > 20 ? '...' : ''); }}),
datasets: [{{
label: 'Count',
data: serversData.map(function(d) {{ return d.count || 0; }}),
backgroundColor: '#22c55e',
borderRadius: 8,
barThickness: 18
}}]
}},
options: {{
scales: {{
y: {{ beginAtZero: true, display: false }},
x: {{ grid: {{ display: false }} }}
}},
plugins: {{
legend: {{ display: false }},
datalabels: {{
anchor: 'end',
align: 'top',
color: '#0f172a',
font: {{ weight: '600', size: 12 }}
}}
}}
}},
plugins: [ChartDataLabels]
}});
}} else {{
console.error('Canvas for serversChart not found');
}}
}});
</script>
""".format(service_js_data, top_vulns_js_data, top_servers_js_data)
# Main HTML content with updated CSS and Bootstrap override
html = f'''<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>AWS Inspector Dashboard - SUA-EMPRESA</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/css/all.min.css">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-datalabels@2.2.0/dist/chartjs-plugin-datalabels.min.js"></script>
<style>
:root {{
--bg-color: linear-gradient(135deg, #eef2ff 0%, #dbeafe 100%);
--card-bg: rgba(255, 255, 255, 0.95);
--primary: #6366f1;
--secondary: #475569;
--text: #0f172a;
--critical: #f43f5e;
--high: #fb923c;
--medium: #facc15;
--low: #22c55e;
--info: #0ea5e9;
--shadow: 0 8px 32px rgba(31, 41, 55, 0.1);
--transition: all 0.3s ease;
}}
body {{
font-family: 'Inter', sans-serif;
background: var(--bg-color);
color: var(--text);
margin: 0;
padding: 0;
min-height: 100vh;
}}
.header {{
background: var(--primary);
color: white;
padding: 2rem 0;
text-align: center;
box-shadow: var(--shadow);
position: relative;
overflow: hidden;
}}
.header::before {{
content: '';
position: absolute;
top: -50%;
left: -50%;
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255, 255, 255, 0.2) 0%, transparent 70%);
transform: rotate(30deg);
}}
.header-content {{
position: relative;
z-index: 1;
}}
.header img {{
height: 50px;
margin-bottom: 1rem;
}}
.header h1 {{
font-size: 2.25rem;
font-weight: 700;
margin: 0;
letter-spacing: -0.025em;
}}
.header p {{
font-size: 1rem;
opacity: 0.9;
margin: 0.5rem 0 0;
}}
.container {{
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
position: relative;
z-index: 1;
}}
.stats-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 1.5rem;
margin-top: -2.5rem;
margin-bottom: 3rem;
}}
.stat-card {{
background: var(--card-bg);
border-radius: 1.25rem;
padding: 1.5rem;
box-shadow: var(--shadow);
text-align: center;
backdrop-filter: blur(10px);
transition: var(--transition);
}}
.stat-card:hover {{
transform: scale(1.03);
box-shadow: 0 12px 40px rgba(31, 41, 55, 0.15);
}}
.stat-card h3 {{
font-size: 2rem;
font-weight: 700;
margin: 0;
}}
.stat-card p {{
font-size: 0.9rem;
color: var(--secondary);
margin: 0;
text-transform: uppercase;
letter-spacing: 0.05em;
}}
.charts-grid {{
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2rem;
margin-bottom: 3rem;
}}
.chart-card {{
background: var(--card-bg);
border-radius: 1.25rem;
padding: 1.5rem;
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}}
.chart-card h3 {{
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
color: var(--primary);
}}
.section-card {{
background: var(--card-bg);
border-radius: 1.25rem;
padding: 2rem;
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
margin-bottom: 2rem;
overflow: visible; /* Ensure section-card allows overflow */
max-height: none; /* Remove any max-height constraint */
height: auto; /* Allow natural height */
min-height: 0; /* Ensure no minimum height restricts expansion */
}}
.section-card h2 {{
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1.5rem;
color: var(--text);
}}
.dropdown {{
margin-bottom: 1.5rem;
position: relative;
z-index: 1000;
}}
.dropdown-toggle {{
background: var(--primary);
border: none;
border-radius: 0.75rem;
padding: 0.75rem 1.5rem;
font-weight: 600;
box-shadow: 0 4px 14px rgba(99, 102, 241, 0.3);
transition: var(--transition);
}}
.dropdown-toggle:hover {{
background: #4f46e5;
box-shadow: 0 6px 20px rgba(99, 102, 241, 0.4);
transform: translateY(-2px);
}}
.dropdown-menu {{
border: none;
border-radius: 0.75rem;
box-shadow: var(--shadow);
background: var(--card-bg);
backdrop-filter: blur(10px);
padding: 0.5rem;
z-index: 1050;
}}
.dropdown-item {{
border-radius: 0.5rem;
padding: 0.5rem 1rem;
transition: var(--transition);
}}
.dropdown-item:hover {{
background: #eef2ff;
color: var(--primary);
}}
.finding-card {{
background: var(--card-bg);
border-radius: 0.75rem;
padding: 1.25rem;
margin-bottom: 1rem;
border-left: 5px solid;
transition: var(--transition);
overflow: visible; /* Ensure finding-card allows overflow */
max-height: none; /* Remove any max-height constraint */
height: auto; /* Allow natural height */
min-height: 0; /* Ensure no minimum height restricts expansion */
/* Override Bootstrap .d-flex to ensure child expansion */
flex-direction: column !important; /* Stack children vertically */
flex-wrap: wrap !important; /* Allow wrapping if needed */
align-items: flex-start !important; /* Align children to start */
flex-shrink: 0 !important; /* Prevent flex shrinking */
flex-basis: auto !important; /* Allow natural size */
flex-grow: 1 !important; /* Allow growth if needed */
}}
.finding-card:hover {{
transform: translateY(-3px);
box-shadow: 0 6px 20px rgba(31, 41, 55, 0.12);
}}
.resource-section {{
overflow: visible; /* Ensure resource-section allows overflow */
max-height: none; /* Remove any max-height constraint */
height: auto; /* Allow natural height */
min-height: 0; /* Ensure no minimum height restricts expansion */
flex-shrink: 0; /* Prevent flex shrinking */
flex-basis: auto; /* Allow natural size */
flex-grow: 1; /* Allow growth if needed */
}}
.severity-badge {{
padding: 0.35rem 0.8rem;
font-size: 0.85rem;
font-weight: 600;
border-radius: 2rem;
color: white;
margin-right: 0.75rem;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}}
.critical {{ border-color: var(--critical); }}
.high {{ border-color: var(--high); }}
.medium {{ border-color: var(--medium); }}
.low {{ border-color: var(--low); }}
.informational {{ border-color: var(--info); }}
.untriaged, .unknown {{ border-color: var(--secondary); }}
.badge-critical {{ background: var(--critical); }}
.badge-high {{ background: var(--high); }}
.badge-medium {{ background: var(--medium); color: var(--text); }}
.badge-low {{ background: var(--low); }}
.badge-info {{ background: var(--info); }}
.badge-untriaged, .badge-unknown {{ background: var(--secondary); }}
.details {{
margin-top: 1rem;
background: rgba(248, 250, 252, 0.9);
padding: 1rem;
border-radius: 0.5rem;
border-left: 3px solid var(--primary);
/* Ensure no constraints interfere */
height: auto;
max-height: none;
min-height: 0;
flex-shrink: 0; /* Prevent flex shrinking */
flex-basis: auto; /* Allow natural size */
flex-grow: 1; /* Allow growth if needed */
}}
.footer {{
text-align: center;
padding: 2rem;
color: var(--secondary);
font-size: 0.9rem;
background: var(--card-bg);
border-top-left-radius: 2rem;
border-top-right-radius: 2rem;
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}}
@media (max-width: 768px) {{
.header h1 {{ font-size: 1.75rem; }}
.stat-card h3 {{ font-size: 1.5rem; }}
.charts-grid {{ grid-template-columns: 1fr; }}
}}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<img src="https://www.xpto.com.br/images/xpto-logo.svg" alt="SUA-EMPRESA">
<h1>AWS Inspector Dashboard</h1>
<p>Data do Relatório: {data['timestamp']}</p>
</div>
</header>
<div class="container">
<div class="stats-grid">
<div class="stat-card">
<h3 style="color: var(--primary)">{data['total_findings']}</h3>
<p>Total Findings</p>
</div>
<div class="stat-card">
<h3 style="color: var(--critical)">{data['severity_counts'].get('CRITICAL', 0) + data['severity_counts'].get('HIGH', 0)}</h3>
<p>Critical/High</p>
</div>
<div class="stat-card">
<h3 style="color: var(--medium)">{data['severity_counts'].get('MEDIUM', 0)}</h3>
<p>Medium</p>
</div>
<div class="stat-card">
<h3 style="color: var(--low)">{data['severity_counts'].get('LOW', 0) + data['severity_counts'].get('INFORMATIONAL', 0)}</h3>
<p>Low/Info</p>
</div>
</div>
<div class="charts-grid">
<div class="chart-card">
<h3>Top Services</h3>
<canvas id="servicesChart" height="200"></canvas>
</div>
<div class="chart-card">
<h3>Top 5 Vulnerabilities</h3>
<canvas id="vulnsChart" height="200"></canvas>
</div>
<div class="chart-card">
<h3>Top 5 Servers</h3>
<canvas id="serversChart" height="200"></canvas>
</div>
</div>
<!-- ECR Section -->
<div class="section-card">
<h2><i class="fab fa-docker me-2"></i> ECR Findings ({len(data['ecr_findings'])})</h2>
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" id="ecrDropdown" data-bs-toggle="dropdown" aria-expanded="false">
Select Repository
</button>
<ul class="dropdown-menu" aria-labelledby="ecrDropdown">
<li><a class="dropdown-item" href="#" onclick="showSection('none')">Clear Selection</a></li>
<li><a class="dropdown-item" href="#ecr-all" onclick="showSection('ecr-all')">All Repositories</a></li>
'''
for repo in ecr_by_repo.keys():
html += f''' <li><a class="dropdown-item" href="#ecr-{repo}" onclick="showSection('ecr-{repo}')">{repo}</a></li>'''
html += f''' </ul>
</div>
<div id="ecr-all" class="resource-section" style="display:none;">
'''
for repo, findings in ecr_by_repo.items():
for i, finding in enumerate(findings):
severity_class = finding['severity'].lower() if finding['severity'] else 'unknown'
html += f'''
<div class="finding-card {severity_class}">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="severity-badge badge-{severity_class}">{finding['severity']}</span>
<a href="{finding['sourceUrl']}" target="_blank">{finding['vulnerabilityId']}</a> - <strong>{finding['title']}</strong>
<p class="mb-0 text-muted">Repo: {repo}</p>
</div>
</div>
<div class="details">
<p>{finding['description']}</p>
<p><strong>ID:</strong> <a href="{finding['sourceUrl']}" target="_blank">{finding['vulnerabilityId']}</a></p>
<p><strong>Fix:</strong> {finding['fixAvailable']}</p>
</div>
</div>
'''
html += f''' </div>
'''
for repo in ecr_by_repo.keys():
html += f''' <div id="ecr-{repo}" class="resource-section" style="display:none;">'''
for i, finding in enumerate(ecr_by_repo[repo]):
severity_class = finding['severity'].lower() if finding['severity'] else 'unknown'
html += f'''
<div class="finding-card {severity_class}">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="severity-badge badge-{severity_class}">{finding['severity']}</span>
<a href="{finding['sourceUrl']}" target="_blank">{finding['vulnerabilityId']}</a> - <strong>{finding['title']}</strong>
</div>
</div>
<div class="details">
<p>{finding['description']}</p>
<p><strong>ID:</strong> <a href="{finding['sourceUrl']}" target="_blank">{finding['vulnerabilityId']}</a></p>
<p><strong>Fix:</strong> {finding['fixAvailable']}</p>
</div>
</div>
'''
html += f''' </div>'''
html += f''' </div>
<!-- EC2 Section -->
<div class="section-card">
<h2><i class="fas fa-server me-2"></i> EC2 Findings ({len(data['ec2_findings'])})</h2>
<div class="dropdown">
<button class="btn dropdown-toggle" type="button" id="ec2Dropdown" data-bs-toggle="dropdown" aria-expanded="false">
Select Instance
</button>
<ul class="dropdown-menu" aria-labelledby="ec2Dropdown">
<li><a class="dropdown-item" href="#" onclick="showSection('none')">Clear Selection</a></li>
<li><a class="dropdown-item" href="#ec2-all" onclick="showSection('ec2-all')">All Instances</a></li>
'''
for instance in ec2_by_instance.keys():
html += f''' <li><a class="dropdown-item" href="#ec2-{instance}" onclick="showSection('ec2-{instance}')">{instance}</a></li>'''
html += f''' </ul>
</div>
<div id="ec2-all" class="resource-section" style="display:none;">
'''
for instance, findings in ec2_by_instance.items():
for i, finding in enumerate(findings):
severity_class = finding['severity'].lower() if finding['severity'] else 'unknown'
html += f'''
<div class="finding-card {severity_class}">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="severity-badge badge-{severity_class}">{finding['severity']}</span>
<a href="{finding['sourceUrl']}" target="_blank">{finding['vulnerabilityId']}</a> - <strong>{finding['title']}</strong>
<p class="mb-0 text-muted">Instance: {instance}</p>
</div>
</div>
<div class="details">
<p>{finding['description']}</p>
<p><strong>ID:</strong> <a href="{finding['sourceUrl']}" target="_blank">{finding['vulnerabilityId']}</a></p>
<p><strong>Fix:</strong> {finding['fixAvailable']}</p>
</div>
</div>
'''
html += f''' </div>
'''
for instance in ec2_by_instance.keys():
html += f''' <div id="ec2-{instance}" class="resource-section" style="display:none;">'''
for i, finding in enumerate(ec2_by_instance[instance]):
severity_class = finding['severity'].lower() if finding['severity'] else 'unknown'
html += f'''
<div class="finding-card {severity_class}">
<div class="d-flex justify-content-between align-items-center">
<div>
<span class="severity-badge badge-{severity_class}">{finding['severity']}</span>
<a href="{finding['sourceUrl']}" target="_blank">{finding['vulnerabilityId']}</a> - <strong>{finding['title']}</strong>
</div>
</div>
<div class="details">
<p>{finding['description']}</p>
<p><strong>ID:</strong> <a href="{finding['sourceUrl']}" target="_blank">{finding['vulnerabilityId']}</a></p>
<p><strong>Fix:</strong> {finding['fixAvailable']}</p>
</div>
</div>
'''
html += f''' </div>'''
html += f''' </div>
</div>
<footer class="footer">
<p>© {datetime.now().year} SUA-EMPRESA - AWS Inspector Vulnerability Report</p>
</footer>
'''
# Concatenate the script content at the end
html += script_content + '''
</body>
</html>
'''
return html
Configurar Layers:
- Clique em Layers → Add a layer.
- Escolha AWS provided e procure por
AWSLambdaPowertools
. - Selecione a versão mais recente para Python 3.13 e adicione.
Permissões IAM:
- Crie uma IAM policy para a Lambda:
- No console IAM, crie uma policy com:
- Política S3 + KMS:
- No console IAM, crie uma policy com:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::seu-bucket-inspector-report/*"
},
{
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey"
],
"Resource": "arn:aws:kms:us-east-1:[ACCOUNT_ID_MASKED]:key/[KEY_ID]"
}
]
}
- Anexe essa permissão criada à Role da Lambda (alterando o ARN da para sua KMS criada).
Configurar o Gatilho S3:
- Na Lambda, clique em Add trigger.
- Escolha S3.
- Bucket:
seu-bucket-inspector-report
. - Event type: All object create events.
- Prefix:
report/
. - Suffix:
.json
. - Clique em Add.
Pronto! Agora você deve gerar o relatório do KMS, em .json, para o s3://seu-bucket-inspector-report/report/
e buscar o HTML gerado no path seu-bucket-inspector-report/final/
.
Top comments (0)