diff --git a/README.md b/README.md index 60e4d28..d5028a7 100644 --- a/README.md +++ b/README.md @@ -108,7 +108,8 @@ See [`docs/kag-finding-validation.md`](docs/kag-finding-validation.md) and [`tem - [`docs/kag-finding-validation.md`](docs/kag-finding-validation.md): pre-submission KAG gate for finding validation - [`docs/self-update-capability.md`](docs/self-update-capability.md): public export/sync capability - [`docs/github-pr-access.md`](docs/github-pr-access.md): minimal PAT and credential setup on the VPS -- [`miniciso-staff-service-catalog-v4-full.pdf`](miniciso-staff-service-catalog-v4-full.pdf): service catalog +- [`miniciso-staff-service-catalog-v5.md`](miniciso-staff-service-catalog-v5.md): service catalog source +- [`miniciso-staff-service-catalog-v5-full.pdf`](miniciso-staff-service-catalog-v5-full.pdf): service catalog PDF ## License diff --git a/meta/SUMMARY.json b/meta/SUMMARY.json index c87da58..7e5937a 100644 --- a/meta/SUMMARY.json +++ b/meta/SUMMARY.json @@ -16,13 +16,15 @@ "docs/dependencies-and-configuration.md", "docs/github-pr-access.md", "docs/headroom-kag-selective-retrieval.md", + "docs/kag-finding-validation.md", "docs/profile-setup.md", "docs/repo-architecture.md", "docs/repo-mapping.md", "docs/self-update-capability.md", "docs/staff-operating-model.md", "meta/MANIFEST.json", - "miniciso-staff-service-catalog-v4-full.pdf", + "miniciso-staff-service-catalog-v5.md", + "miniciso-staff-service-catalog-v5-full.pdf", "profiles/chief-of-staff/SOUL.md", "profiles/security-appsec-assessment/SOUL.md", "profiles/security-architecture/SOUL.md", @@ -35,11 +37,14 @@ "scripts/bootstrap.ps1", "scripts/bootstrap.sh", "scripts/export_safe_self_state.py", + "scripts/generate_service_catalog_pdf.py", "scripts/smoke-test.ps1", "scripts/smoke-test.sh", "scripts/sync_to_hermes.sh", "scripts/validate-repo.ps1", "scripts/validate-repo.sh", + "templates/adversarial-finding-validation-prompt.md", + "templates/finding-decision-template.md", "templates/intake-template.md", "templates/report-template.md", "tools/headroom_phase1/README.md", diff --git a/miniciso-staff-service-catalog-v5-full.pdf b/miniciso-staff-service-catalog-v5-full.pdf new file mode 100644 index 0000000..d136eb1 Binary files /dev/null and b/miniciso-staff-service-catalog-v5-full.pdf differ diff --git a/miniciso-staff-service-catalog-v5.md b/miniciso-staff-service-catalog-v5.md new file mode 100644 index 0000000..d0cf41c --- /dev/null +++ b/miniciso-staff-service-catalog-v5.md @@ -0,0 +1,611 @@ +# MiniCISO Staff Service Catalog v5 + +**Status:** Current consolidated catalog for the public `miniCISO` overlay +**Version:** v5 +**Date:** 2026-07-10 + +--- + +## 1. Executive Summary + +O catálogo v5 consolida o estado atual do MiniCISO Staff como um overlay público e reproduzível para Hermes Agent, com foco em: + +- coordenação central por `chief-of-staff`; +- SMEs especializados para análise técnica; +- `security-qa` como quality gate obrigatório; +- disciplina de evidência em três camadas; +- gate pré-submissão orientado por KAG para findings externos; +- decisão explícita `GO / RESEARCH / NO-GO` antes de qualquer report externo. + +O avanço mais importante desta versão é metodológico: o sistema deixa de aceitar como finding válido um comportamento que apenas demonstra uma primitiva técnica sem impacto qualificável provado. + +### Estado público refletido neste catálogo + +Este catálogo descreve o **estado público atual do repositório** `miniCISO`. + +Ele contém **9 perfis publicados**: + +1. `chief-of-staff` +2. `security-threat-modeling` +3. `security-architecture` +4. `security-code-review` +5. `security-appsec-assessment` +6. `security-offensive-security` +7. `security-compliance-mapper` +8. `security-recon-attack-surface-strategist` +9. `security-qa` + +### Guardrails universais + +- Sem remediação automática. +- Todo relatório final exige QA. +- Trabalho ofensivo exige autorização explícita, escopo, limites e critérios de parada. +- Findings externos não entram em drafting sem decisão `GO`. +- Evidência deve separar configuração declarada, runtime efetivo e comportamento validado quando aplicável. + +--- + +## 2. O que muda na v5 + +A v5 herda a base metodológica da v4 e adiciona uma camada de controle mais rígida para findings externos, especialmente em BBP/VDP. + +### Principais mudanças + +#### 2.1. Gate KAG de validação pré-submissão + +Nova regra oficial: + +- nenhum draft externo começa antes da decisão `GO`; +- `RESEARCH` gera plano de validação de impacto, não submissão; +- `NO-GO` bloqueia submissão e registra lição aprendida. + +#### 2.2. Separação formal entre primitiva e finding + +Passa a ser obrigatório tratar como relações não equivalentes: + +- primitiva técnica ≠ vulnerabilidade; +- comportamento observado ≠ impacto; +- capacidade potencial ≠ exploração demonstrada; +- acesso possível ≠ acesso comprovado; +- texto bom ≠ finding válido. + +#### 2.3. Adversarial QA + +O QA deixa de ser apenas revisão de forma e passa a executar uma tentativa explícita de rejeição metodológica do finding, buscando: + +- exclusão de programa; +- falta de impacto; +- ausência de quebra de trust boundary; +- hipótese apresentada como fato; +- severidade inflada; +- insuficiência probatória. + +#### 2.4. Impact Validator + +O fluxo operacional passa a reconhecer explicitamente uma função intermediária entre descoberta e report drafting: + +- transformar primitivas em hipóteses de impacto verificáveis e autorizadas; ou +- concluir que a primitiva não sustenta finding. + +#### 2.5. Artefato obrigatório de decisão + +Todo finding externo passa a exigir um artefato com: + +- regra/exclusão aplicável; +- impacto exigido; +- impacto demonstrado; +- força da evidência por claim; +- inferências proibidas; +- argumento mais forte de rejeição; +- checklist final para `GO`. + +--- + +## 3. Modelo operacional do Staff + +### Fluxo principal + +```text +Usuário → MiniCISO → SME Especializado → Security QA → MiniCISO → Usuário +``` + +### Função do coordenador + +O `chief-of-staff` é responsável por: + +- triagem inicial; +- decomposição do problema; +- escolha do SME correto; +- consolidação da resposta final; +- enforcement de QA; +- controle de escopo, risco e output. + +### Resultado esperado do sistema + +O MiniCISO não é apenas um catálogo de personas. Ele funciona como uma equipe com: + +- coordenação central; +- especialização por domínio; +- quality gate obrigatório; +- disciplina de evidência; +- formato consistente de saída. + +--- + +## 4. Evidência, qualidade e risco + +### Modelo de evidência em três camadas + +Toda conclusão deve, quando aplicável, separar: + +1. **Configuração declarada** + arquivos, IaC, dashboards, documentação, intenção declarada. +2. **Runtime / configuração efetiva** + estado realmente carregado/aplicado pelo sistema. +3. **Comportamento validado** + teste controlado ou observação direta do efeito. + +### Regra de decisão + +- Não concluir segurança apenas por configuração declarada. +- Quando houver conflito, runtime e comportamento prevalecem para avaliação de risco. +- Se runtime/comportamento não foram validados, reduzir `Confidence Level`. + +### Divergência de Configuração + +Quando declarado, runtime e comportamento não batem, isso vira um achado próprio: + +**Classificação obrigatória:** `Divergência de Configuração` + +Campos esperados: + +- controle afetado; +- estado declarado; +- estado runtime/efetivo; +- comportamento observado; +- impacto plausível; +- hipóteses de causa; +- validação adicional necessária; +- recomendação; +- owner sugerido; +- severidade e confidence. + +--- + +## 5. Gate KAG para findings externos + +### Objetivo + +Provar impacto qualificável ou bloquear submissão. + +### Pergunta correta + +O sistema não deve começar perguntando: + +> Como transformar esta evidência em um bom report? + +Deve começar perguntando: + +> Esta evidência sobreviveria à triagem como finding qualificável neste programa? + +### Estrutura mínima de decisão + +O gate trabalha com quatro conjuntos de conhecimento: + +1. **Scope Graph** +2. **Vulnerability Graph** +3. **Evidence Graph** +4. **Decision Graph** + +### Gates obrigatórios + +#### Gate 1 — Scope Qualification +- a classe está no escopo? +- existe exclusão explícita? +- o programa exige impacto adicional? +- a evidência atual satisfaz esse requisito? +- o teste necessário é autorizado? + +#### Gate 2 — Feature versus Vulnerability +- o comportamento viola uma trust boundary? +- ou só executa a função esperada do produto? + +#### Gate 3 — Impact Closure +A frase precisa fechar sem linguagem especulativa: + +> Por causa deste comportamento, um atacante pode [ação concreta], causando [consequência observável], algo que não seria possível por meio da funcionalidade normalmente autorizada. + +#### Gate 4 — Evidence Sufficiency +Cada claim deve ser classificado como: + +- `DIRECT` +- `SUPPORTED_INFERENCE` +- `SPECULATIVE` +- `UNSUPPORTED` + +#### Gate 5 — Adversarial QA +O QA tenta rejeitar o finding antes de lapidar o texto. + +### Decisões permitidas + +- `GO` +- `RESEARCH` +- `NO-GO` + +### Regra dura + +- Se `demonstrated = false`, não existe `GO`. +- Se a evidência cai em exclusão explícita sem exceção provada, a submissão deve ser bloqueada. +- Mais prova da mesma primitiva não substitui ausência de impacto. + +--- + +## 6. Perfis publicados e quando usar + +### 6.1. `chief-of-staff` + +**Quando acionar** +- coordenação geral; +- triagem de demanda; +- síntese final; +- roteamento entre SMEs; +- priorização e próximos passos. + +**Outcomes típicos** +- briefing executivo; +- plano de ação; +- consolidação inter-SME; +- decisão operacional. + +**Entrada ideal** +- objetivo; +- escopo; +- contexto; +- restrições; +- artefatos disponíveis. + +### 6.2. `security-threat-modeling` + +**Quando acionar** +- ativos, atores, trust boundaries, abuse cases, fluxos sensíveis. + +**Outcomes típicos** +- threat model; +- cenários priorizados; +- backlog defensivo. + +**Entrada ideal** +- diagrama/fluxo; +- ativos sensíveis; +- atores; +- objetivo da modelagem. + +### 6.3. `security-architecture` + +**Quando acionar** +- SSH, firewall, cloud, rede, systemd, gateways, exposição de serviços, segmentação, runtime divergente. + +**Outcomes típicos** +- architecture review; +- tradeoffs; +- recomendações priorizadas; +- plano seguro de correção. + +**Entrada ideal** +- desenho atual/alvo; +- configs declaradas; +- saídas runtime; +- testes observados; +- constraints. + +### 6.4. `security-code-review` + +**Quando acionar** +- revisão manual de repo, diff, scripts, auth, secrets, injection, lógica sensível. + +**Outcomes típicos** +- findings por arquivo/linha; +- correção sugerida; +- guidance de testes. + +**Entrada ideal** +- path ou diff; +- linguagem; +- foco de risco; +- permissão de execução de testes. + +### 6.5. `security-appsec-assessment` + +**Quando acionar** +- API, authn/authz, sessão, SAST/DAST/SCA, SBOM, manifests, containers, IaC. + +**Outcomes típicos** +- assessment AppSec; +- falso positivo vs finding real; +- priorização; +- backlog técnico. + +**Entrada ideal** +- scanner outputs; +- SARIF; +- SBOM; +- manifests; +- repositório; +- escopo da análise. + +### 6.6. `security-offensive-security` + +**Quando acionar** +- validação ofensiva autorizada; +- raciocínio adversarial seguro; +- PoCs controladas; +- interpretação de exploitabilidade. + +**Outcomes típicos** +- plano de teste; +- evidência segura; +- critérios de parada; +- avaliação de impacto técnico. + +**Entrada ideal** +- autorização explícita; +- alvo; +- limites; +- janela; +- regras de parada. + +### 6.7. `security-compliance-mapper` + +**Quando acionar** +- mapear achados para LGPD, GDPR, ISO 27001, SOC2, CIS, NIST e afins. + +**Outcomes típicos** +- gap matrix; +- mapping; +- readiness roadmap. + +**Entrada ideal** +- framework alvo; +- políticas/processos; +- achados técnicos; +- evidências; +- escopo. + +### 6.8. `security-recon-attack-surface-strategist` + +**Quando acionar** +- recon autorizado de baixo ruído; +- mapeamento e triagem de superfícies; +- priorização de hipóteses; +- ordem segura de validação. + +**Outcomes típicos** +- surface map; +- hipóteses priorizadas; +- próximos gates; +- backlog de investigação. + +**Entrada ideal** +- família de alvos; +- policy/restrições; +- orçamento de requests; +- sinais já observados. + +**Limite importante** +Recon não é finding. O output precisa passar por AppSec/OffSec/Architecture e depois por QA antes de virar conclusão final. + +### 6.9. `security-qa` + +**Quando acionar** +- toda entrega final; +- addendum; +- mudança de severidade; +- validação de evidência; +- revisão de overclaim. + +**Outcomes típicos** +- aprovado; +- aprovado com ressalvas; +- reprovado; +- correções obrigatórias. + +**Entrada ideal** +- draft; +- evidências; +- escopo; +- público-alvo; +- critérios MiniCISO. + +--- + +## 7. Notas sobre capacidades transversais + +Além dos 9 perfis públicos, o overlay atual já documenta algumas práticas transversais que influenciam a operação, mesmo sem aparecerem como perfis separados. + +### 7.1. Governança de findings externos + +O estado público atual já documenta, via operating model + KAG gate + templates, uma disciplina de governança para findings externos com foco em: + +- policy-first; +- scope qualification; +- authorization-aware validation; +- evidence minimization; +- bloqueio de submissão quando não há impacto qualificável demonstrado. + +### 7.2. Lições aprendidas como requisito de processo + +O overlay público já exige o registro de lição aprendida quando um candidate finding cai em `NO-GO`. + +Na prática, isso reforça principalmente: + +- falhas metodológicas; +- overclaim a partir de primitiva técnica; +- casos em que faltou prova de impacto; +- repetições de inferências proibidas. + +### 7.3. Supply-chain como área de análise, não como perfil público separado + +O estado público atual suporta análise de supply-chain principalmente dentro de `security-appsec-assessment` e da documentação de dependências/configuração. + +Este catálogo não assume, no overlay público atual, a existência de um perfil separado de supply-chain nem de um componente público chamado `Watchman`. + +--- + +## 8. Template universal de intake + +O input de alta qualidade para o MiniCISO deve conter, no mínimo: + +### Objective +- o que precisa ser decidido ou produzido + +### Scope +- sistemas, repositórios, assets +- in scope +- out of scope + +### Context +- background +- constraints +- business/security priority + +### Program and qualification context +- nome do programa +- plataforma +- URL da policy +- source das regras +- exclusões explícitas +- impacto exigido para a classe +- métodos permitidos +- métodos proibidos +- se callback/OOB é permitido +- estágio atual: intake | validation | report-drafting + +### Artifacts provided +- arquivos +- links +- logs / screenshots / reports + +### Restrictions +- read-only? +- external testing authorized? +- sensitive data constraints? + +### Desired output +- tipo de saída +- profundidade +- prazo + +### QA requirement +- saída final passa por `security-qa` + +--- + +## 9. Workflow recomendado + +### 9.1. Fluxo geral + +```text +Intake → Triagem → SME → Draft técnico → Security QA → Síntese final → Follow-up +``` + +### 9.2. Fluxo para findings externos + +```text +Primitive discovered + ↓ +Scope qualification + ↓ +Feature-versus-vulnerability analysis + ↓ +Impact hypotheses + ↓ +Safe and authorized validation + ↓ +Impact demonstrated? + ┌────┴────┐ + │ │ + No Yes + │ │ +RESEARCH Adversarial QA +or NO-GO │ + ↓ + Report +``` + +### 9.3. Critério de saída + +- **GO** → pode começar drafting externo +- **RESEARCH** → gera plano de validação de impacto +- **NO-GO** → bloqueia submissão e registra aprendizado + +--- + +## 10. Prompts copy-ready + +### 10.1. Threat modeling + +> Faça uma threat model de [sistema/fluxo]. Contexto: [objetivo]. Artefatos: [diagrama/fluxo]. Dados sensíveis: [lista]. Atores: [lista]. Quero riscos priorizados, recomendações, assumptions, confidence level, residual risk e next steps. Passe pelo Security QA. + +### 10.2. Architecture review + +> Revise esta arquitetura/controle. Stack: [stack]. Declarado: [configs]. Runtime: [outputs]. Comportamento: [testes]. Constraints: [prazo/custo/risco]. Quero recomendações priorizadas, divergências, assumptions, confidence, residual risk e next steps. Passe pelo Security QA. + +### 10.3. Secure code review + +> Revise segurança do código em [path/diff]. Foco: [categorias]. Pode executar testes locais: [sim/não]. Quero findings, falso positivo se aplicável, remediação proposta, assumptions, confidence, residual risk e next steps. Passe pelo Security QA. + +### 10.4. AppSec assessment + +> Avalie os findings de AppSec deste projeto. Artefatos: [scanner outputs], [SARIF], [SBOM], [repo]. Objetivo: [reduzir risco/validar findings/priorizar backlog]. Quero confirmados, falso positivos, priorização, remediação proposta, assumptions, confidence, residual risk e next steps. Passe pelo Security QA. + +### 10.5. Recon / attack surface + +> Faça recon autorizado e de baixo ruído para [família de alvos]. Constraints: [policy/rate limits]. Sinais já observados: [lista]. Quero mapa de superfícies, hipóteses priorizadas e próximos gates seguros. Não trate recon como finding final. Passe pelo fluxo correto até QA. + +### 10.6. QA adversarial para finding externo + +> Valide este candidate finding com gate KAG orientado por impacto. Primeiro tente rejeitar o finding pelos motivos mais fortes de triagem. Só aprove `GO` se houver trust boundary quebrada, impacto diretamente demonstrado e evidência suficiente. Caso contrário, devolva `RESEARCH` ou `NO-GO` com rationale e próximas ações seguras. + +--- + +## 11. Critérios de qualidade para a versão atual + +Um output MiniCISO de alta qualidade deve: + +- ser ancorado em evidência real; +- distinguir fato, hipótese e lacuna; +- explicitar assumptions; +- calibrar confidence; +- declarar residual risk; +- propor próximos passos concretos; +- passar por `security-qa` quando final. + +Para findings externos, adicionalmente deve: + +- respeitar escopo/autorização; +- não overclaimar a partir de primitiva técnica; +- não usar linguagem especulativa como base de submissão; +- provar impacto qualificável ou bloquear a submissão. + +--- + +## 12. Conclusão + +A v5 marca a maturidade do MiniCISO Staff como um sistema orientado a decisão, não apenas a produção de texto. + +Os pontos centrais desta versão são: + +- coordenação clara; +- QA obrigatório; +- disciplina de evidência; +- tratamento explícito de divergência; +- recon como insumo, não como finding; +- gate KAG para findings externos; +- decisão `GO / RESEARCH / NO-GO` antes de drafting. + +Em termos práticos, a v5 reduz a chance de: + +- aprovar relatório bem escrito sem impacto demonstrado; +- confundir comportamento esperado com vulnerabilidade; +- transformar primitiva em finding só por familiaridade com a classe; +- submeter casos que deveriam ter sido travados em QA. diff --git a/scripts/generate_service_catalog_pdf.py b/scripts/generate_service_catalog_pdf.py new file mode 100644 index 0000000..e6c62f6 --- /dev/null +++ b/scripts/generate_service_catalog_pdf.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import argparse +import math +from pathlib import Path +from typing import List, Tuple + +from reportlab.lib import colors +from reportlab.lib.enums import TA_CENTER, TA_JUSTIFY, TA_LEFT +from reportlab.lib.pagesizes import A4 +from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet +from reportlab.lib.units import mm +from reportlab.pdfbase.ttfonts import TTFont +from reportlab.pdfbase import pdfmetrics +from reportlab.platypus import ( + BaseDocTemplate, + Frame, + PageBreak, + PageTemplate, + Paragraph, + Preformatted, + Spacer, +) + + +def register_fonts() -> None: + candidates = [ + ("DejaVuSans", "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"), + ("DejaVuSans-Bold", "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"), + ("DejaVuSansMono", "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"), + ] + for name, path in candidates: + p = Path(path) + if p.exists(): + pdfmetrics.registerFont(TTFont(name, str(p))) + + +def build_styles(): + base = getSampleStyleSheet() + base["Normal"].fontName = "DejaVuSans" + base["Normal"].fontSize = 10.5 + base["Normal"].leading = 14 + base["Normal"].alignment = TA_LEFT + + styles = { + "title": ParagraphStyle( + "title", + parent=base["Title"], + fontName="DejaVuSans-Bold", + fontSize=24, + leading=30, + textColor=colors.HexColor("#0F172A"), + alignment=TA_CENTER, + spaceAfter=8, + ), + "subtitle": ParagraphStyle( + "subtitle", + parent=base["Heading2"], + fontName="DejaVuSans", + fontSize=12, + leading=16, + textColor=colors.HexColor("#334155"), + alignment=TA_CENTER, + spaceAfter=16, + ), + "h1": ParagraphStyle( + "h1", + parent=base["Heading1"], + fontName="DejaVuSans-Bold", + fontSize=18, + leading=22, + textColor=colors.HexColor("#0F172A"), + spaceBefore=8, + spaceAfter=8, + ), + "h2": ParagraphStyle( + "h2", + parent=base["Heading2"], + fontName="DejaVuSans-Bold", + fontSize=13, + leading=17, + textColor=colors.HexColor("#1D4ED8"), + spaceBefore=6, + spaceAfter=4, + ), + "body": ParagraphStyle( + "body", + parent=base["Normal"], + fontName="DejaVuSans", + fontSize=10.5, + leading=14, + alignment=TA_JUSTIFY, + textColor=colors.HexColor("#111827"), + spaceAfter=4, + ), + "bullet": ParagraphStyle( + "bullet", + parent=base["Normal"], + fontName="DejaVuSans", + fontSize=10.5, + leading=14, + leftIndent=10, + firstLineIndent=-8, + spaceAfter=2, + ), + "quote": ParagraphStyle( + "quote", + parent=base["Normal"], + fontName="DejaVuSans", + fontSize=10.5, + leading=14, + leftIndent=12, + borderPadding=6, + borderColor=colors.HexColor("#CBD5E1"), + borderWidth=0.8, + borderLeft=True, + textColor=colors.HexColor("#334155"), + spaceBefore=4, + spaceAfter=6, + ), + "meta": ParagraphStyle( + "meta", + parent=base["Normal"], + fontName="DejaVuSans", + fontSize=9, + leading=12, + textColor=colors.HexColor("#475569"), + alignment=TA_CENTER, + spaceAfter=2, + ), + "code": ParagraphStyle( + "code", + parent=base["Code"], + fontName="DejaVuSansMono", + fontSize=8.8, + leading=11, + backColor=colors.HexColor("#F8FAFC"), + borderColor=colors.HexColor("#CBD5E1"), + borderWidth=0.6, + borderPadding=6, + leftIndent=2, + rightIndent=2, + spaceBefore=4, + spaceAfter=6, + ), + } + return styles + + +def esc(text: str) -> str: + return ( + text.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + ) + + +def inline_md(text: str) -> str: + text = esc(text) + while "**" in text: + a = text.find("**") + b = text.find("**", a + 2) + if b == -1: + break + inner = text[a + 2 : b] + text = text[:a] + f"{inner}" + text[b + 2 :] + while "`" in text: + a = text.find("`") + b = text.find("`", a + 1) + if b == -1: + break + inner = text[a + 1 : b] + text = text[:a] + f"{esc(inner)}" + text[b + 1 :] + return text + + +def parse_markdown(md: str, styles) -> List: + lines = md.splitlines() + story: List = [] + in_code = False + code_lines: List[str] = [] + paragraph_lines: List[str] = [] + + def flush_paragraph(): + nonlocal paragraph_lines + if paragraph_lines: + text = " ".join(s.strip() for s in paragraph_lines).strip() + if text: + story.append(Paragraph(inline_md(text), styles["body"])) + paragraph_lines = [] + + def flush_code(): + nonlocal code_lines + if code_lines: + story.append(Preformatted("\n".join(code_lines), styles["code"])) + code_lines = [] + + title_done = False + for raw in lines: + line = raw.rstrip("\n") + stripped = line.strip() + + if stripped.startswith("```"): + flush_paragraph() + if in_code: + flush_code() + in_code = False + else: + in_code = True + continue + + if in_code: + code_lines.append(line) + continue + + if not stripped: + flush_paragraph() + story.append(Spacer(1, 3)) + continue + + if stripped == "---": + flush_paragraph() + story.append(Spacer(1, 6)) + continue + + if stripped.startswith("# "): + flush_paragraph() + text = stripped[2:].strip() + if not title_done: + story.append(Spacer(1, 28 * mm)) + story.append(Paragraph(inline_md(text), styles["title"])) + title_done = True + else: + story.append(PageBreak()) + story.append(Paragraph(inline_md(text), styles["h1"])) + continue + + if stripped.startswith("## "): + flush_paragraph() + story.append(Paragraph(inline_md(stripped[3:].strip()), styles["h1"])) + continue + + if stripped.startswith("### "): + flush_paragraph() + story.append(Paragraph(inline_md(stripped[4:].strip()), styles["h2"])) + continue + + if stripped.startswith("> "): + flush_paragraph() + story.append(Paragraph(inline_md(stripped[2:].strip()), styles["quote"])) + continue + + if stripped.startswith("- "): + flush_paragraph() + story.append(Paragraph("• " + inline_md(stripped[2:].strip()), styles["bullet"])) + continue + + if len(stripped) > 2 and stripped[0].isdigit() and stripped[1] == ".": + flush_paragraph() + story.append(Paragraph(inline_md(stripped), styles["bullet"])) + continue + + if title_done and stripped.startswith("**") and stripped.endswith("**"): + flush_paragraph() + story.append(Paragraph(inline_md(stripped), styles["h2"])) + continue + + if title_done and stripped.startswith("**Status:**"): + flush_paragraph() + story.append(Paragraph(inline_md(stripped), styles["meta"])) + continue + + paragraph_lines.append(line) + + flush_paragraph() + flush_code() + return story + + +def add_page_number(canvas, doc): + canvas.saveState() + canvas.setFont("DejaVuSans", 9) + canvas.setFillColor(colors.HexColor("#64748B")) + canvas.drawRightString(195 * mm, 10 * mm, f"MiniCISO Staff Service Catalog v5 — {canvas.getPageNumber()}") + canvas.restoreState() + + +def build_pdf(src: Path, out: Path) -> None: + register_fonts() + styles = build_styles() + text = src.read_text(encoding="utf-8") + story = parse_markdown(text, styles) + doc = BaseDocTemplate( + str(out), + pagesize=A4, + leftMargin=18 * mm, + rightMargin=18 * mm, + topMargin=16 * mm, + bottomMargin=16 * mm, + title="MiniCISO Staff Service Catalog v5", + author="Hermes Agent / Irlan Cidade", + ) + frame = Frame(doc.leftMargin, doc.bottomMargin, doc.width, doc.height, id="normal") + template = PageTemplate(id="main", frames=[frame], onPage=add_page_number) + doc.addPageTemplates([template]) + doc.build(story) + + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("source") + ap.add_argument("output") + args = ap.parse_args() + build_pdf(Path(args.source), Path(args.output)) + + +if __name__ == "__main__": + main()