from datetime import datetime from collections import Counter from typing import List from threat_kb import get_threats_for_component from detector import Detection SEVERIDADE_ORDEM = {"Crítica": 0, "Alta": 1, "Média": 2, "Baixa": 3} def _dedup_components(detections: List[Detection]) -> Counter: """Conta ocorrências únicas de cada tipo de componente (ignora bounding boxes duplicadas).""" return Counter(d.label for d in detections) def build_markdown_report(detections: List[Detection], demo_mode: bool = False, sistema_nome: str = "Sistema Analisado") -> str: counts = _dedup_components(detections) data_geracao = datetime.now().strftime("%d/%m/%Y %H:%M") linhas = [] linhas.append(f"# Relatório de Modelagem de Ameaças (STRIDE)") linhas.append(f"**Sistema:** {sistema_nome} ") linhas.append(f"**Data de geração:** {data_geracao} ") linhas.append(f"**Gerado por:** pipeline de detecção de componentes + base de conhecimento STRIDE ") if demo_mode: linhas.append("\n> ⚠️ **Modo demonstração**: nenhum modelo treinado foi encontrado em " "`models/best.pt`. Os componentes abaixo foram estimados por uma " "heurística de cor/contorno apenas para fins de demonstração da interface. " "Execute `train.py` para obter detecções reais do modelo supervisionado.\n") linhas.append("\n---\n") if not counts: linhas.append("Nenhum componente foi identificado no diagrama enviado. " "Verifique a qualidade/resolução da imagem e tente novamente.") return "\n".join(linhas) linhas.append("## 1. Componentes Identificados\n") linhas.append("| Componente | Ocorrências |") linhas.append("|---|---|") for label, qtd in sorted(counts.items(), key=lambda x: -x[1]): linhas.append(f"| {label.replace('_', ' ').title()} | {qtd} |") linhas.append("\n## 2. Ameaças por Componente (STRIDE)\n") total_ameacas = 0 for label in counts: threats = get_threats_for_component(label) if not threats: continue linhas.append(f"### {label.replace('_', ' ').title()}\n") threats_ordenadas = sorted(threats, key=lambda t: SEVERIDADE_ORDEM.get(t.severidade, 9)) for t in threats_ordenadas: total_ameacas += 1 linhas.append(f"**[{t.categoria}] {t.nome_categoria} — Severidade: {t.severidade}**") linhas.append(f"- *Ameaça:* {t.descricao}") linhas.append(f"- *Contramedida recomendada:* {t.contramedida}\n") linhas.append("## 3. Resumo Executivo\n") n_criticas = sum(1 for label in counts for t in get_threats_for_component(label) if t.severidade == "Crítica") n_altas = sum(1 for label in counts for t in get_threats_for_component(label) if t.severidade == "Alta") linhas.append(f"- Componentes identificados: **{len(counts)}**") linhas.append(f"- Ameaças mapeadas: **{total_ameacas}**") linhas.append(f"- Ameaças de severidade **Crítica**: **{n_criticas}**") linhas.append(f"- Ameaças de severidade **Alta**: **{n_altas}**") linhas.append("\n> Este relatório é gerado automaticamente por um MVP do fluxo e deve ser " "revisado por um especialista em segurança antes de decisões de mitigação.") return "\n".join(linhas) def save_pdf_report(markdown_text: str, output_path: str = "relatorio_stride.pdf"): """Converte o relatório (texto simples, sem renderização rica de markdown) para PDF.""" from fpdf import FPDF pdf = FPDF() pdf.set_auto_page_break(auto=True, margin=15) pdf.add_page() pdf.set_font("Helvetica", size=11) for raw_line in markdown_text.split("\n"): line = raw_line.replace("**", "").replace("### ", "").replace("## ", "").replace("# ", "") line = line.replace("*Ameaça:*", "Ameaca:").replace("*Contramedida recomendada:*", "Contramedida:") # A fonte core (Helvetica) do fpdf2 só suporta latin-1; removemos # caracteres fora dessa faixa (ex.: emojis) para evitar falhas de render. line = line.encode("latin-1", errors="ignore").decode("latin-1") if line.strip() == "": pdf.ln(4) continue pdf.set_x(pdf.l_margin) pdf.multi_cell(0, 6, line) pdf.output(output_path) return output_path