| name | pentest-report-structure |
| description | Author or migrate a pentest report into the modular `report/` layout that the pentest-report-manager (Piersec) ingests — one `report.md` (executive summary + index), one shared `setup.md`, one file per finding under `findings/`, one file per appendix under `appendices/`. Use when the user says "structure the report", "split the findings into files", "make this report compatible with the report manager", "import this into the Piersec tool", "create the report skeleton for engagement X", "renomeie os findings no padrão", or otherwise asks for the canonical file layout / filename convention / metadata table format. Triggers on report scaffolding, finding splitting, migration from monolithic Word/PDF to the modular markdown layout, and validation that an existing tree is parser-compliant. |
Pentest Report — Canonical Modular Structure
This skill writes (or fixes) a pentest engagement so that every report lives at report/ with the same layout. The Piersec pentest-report-manager tool parses this layout deterministically; deviations cause silent metadata loss (severity, CVSS, status, dates).
When to invoke
- New engagement scaffolding (
report/ doesn't exist yet)
- Migrating a monolithic report (a single big
.md / .docx) into the modular layout
- The user already has
report/ but the importer raised warnings or some findings are missing fields
- Renaming finding files to the
sev-NNN-kebab-slug.md convention
- Adding a new finding or appendix to an existing engagement
If the user just wants to draft a single finding (without worrying about layout), defer to pentest-finding-writer. This skill is about the directory layout, filename rules, and the metadata-table format that the parser reads — not the prose inside a finding.
Anti-patterns that silently break the importer
Real failure observed (Flight Class engagement, 2026-05-26): 19 findings written
with the wrong conventions were uploaded; every single one was parsed as
"Informativa" with CVSS 0 because the parser silently defaulted when it couldn't
find what it expects. Avoid all of the following:
- ❌ YAML frontmatter (
---\nid: FIND-NNN\nseverity: Critical\n---) — the parser
reads a markdown table, not YAML, as the first non-heading block.
- ❌ Filenames like
find-NNN-slug.md or FIND-016.md — they don't match the
severity-prefix regex and the parser drops them from the severity bucket.
- ❌ English severity strings (
Critical/High/Medium/Low/Informational) — must be
PT (Crítica/Alta/Média/Baixa/Informativa).
- ❌ CVSS 4.0 (
CVSS:4.0/AV:N/AT:N/VC:H/...) — the metadata row is **CVSS 3.1**;
CVSS 4.0 is ignored. Convert to 3.1 even if 4.0 was scored first.
- ❌ Metadata expressed as a bullet list (
- **ID**: CRIT-003) — must be a real
two-column markdown table with no header row.
If the engagement already has findings in these wrong formats, migrate first:
rename files to <sev>-NNN-<slug>.md, replace frontmatter with the metadata
table, fix all wikilinks [[find-NNN-…]] → [[sev-NNN-…]] in report.md,
appendices, incidents, and PoCs. Then resume with Mode C for new findings.
Mandatory layout
report/
├── report.md ← exec summary, scope, methodology, index, recommendations
├── setup.md ← shared PoC reproduction setup (env vars, JWT / cookies, WAF bypass)
├── findings/ ← one markdown file per finding
│ ├── crit-001-kebab-slug.md
│ ├── crit-002-...md
│ ├── high-001-...md
│ ├── med-001-...md
│ ├── low-001-...md
│ └── info-001-...md
└── appendices/ ← long-form material referenced from report.md
├── timeline.md
├── injection-tests.md
├── subdomains-ct.md
└── config-matrix.md
Hard rules — these are parser invariants, not stylistic preferences:
- Directory name is literally
report/. The importer scans for this exact name when uploaded as a zip.
- Filenames are lowercase kebab-case, never CamelCase, never with spaces.
- Finding filenames match the regex
^(crit|high|med|low|info)-\d{3}-[a-z0-9-]+\.md$. The prefix determines severity ordering and is the fallback when the metadata table is missing the Severidade row. Sequence is zero-padded (001, 002, …) — crit-1-...md will not order correctly.
- One H1 per file, formatted
# {ID} — {Title} (em-dash, hyphen, or colon all parse, but prefer em-dash to match the existing corpus). Example: # CRIT-003 — IDOR cross-tenant em proponentes do s360.
- First content block under the H1 must be a 2-column metadata table (no header row, just
| key | value | pairs). The parser uses extractFirstMetadataTable — anything before the table that isn't # heading breaks ingestion.
- Setup file is referenced via relative path
../setup.md from every finding that needs auth. Do not duplicate setup material across findings.
- Apêndices are referenced from
report.md with relative paths ./appendices/<slug>.md.
Files & what they must contain
report.md
Top-level executive document. Reading order: header table → "Sumário" (TOC) → numbered sections.
Required sections (the parser looks for these slugs by H2 title — Portuguese or English both work):
| Slug (DB) | H2 title patterns recognised |
|---|
executive_summary | Sumário Executivo / Executive Summary |
scope | Escopo / Scope (works with Escopo e Metodologia combined too) |
methodology | Metodologia / Methodology |
risk_classification | Classificação de Risco / Risk Classification |
findings_summary | Resumo dos Achados / Findings Summary |
attack_chain | Cadeia de Ataque / Attack Chain |
positive_controls | Controles Positivos / Positive Controls |
recommendations | Recomendações / Recommendations |
Use templates/report.md as the starting skeleton. Copy it, then:
- Fill the header metadata table (cliente, contato, período, versão, classificação).
- Write the executive summary as prose.
- Build the Tabela Completa de Achados as a markdown table where every ID links to its file:
[CRIT-001](./findings/crit-001-...md).
- Build the Cadeia de Ataque Principal if a chain emerged — most reports have one.
- List Recomendações Priorizadas grouped by priority (P1 ≤ 7 days, P2 ≤ 30 days, P3 ≤ 90 days).
setup.md
Centralises everything a reproducer needs before running any finding PoC: proxy config, env vars, JWT issuance, cookie capture, WAF bypass. Every finding's "Prova de Conceito" section starts with > **Pré-requisitos**: ver [../setup.md](../setup.md). ….
Use templates/setup.md as the skeleton. The exact sub-sections will vary by engagement (web vs. internal vs. mobile vs. cloud), but the principle is identical: never repeat reproduction setup inside a finding. If something is needed by more than one finding, it goes in setup.md.
findings/<sev>-NNN-<slug>.md
One vulnerability per file. Use templates/findings/finding.md as the skeleton.
Required structure (in order):
# {ID} — {Title} — H1
- Metadata table — see template; the rows the parser cares about are:
ID (e.g. CRIT-003) — must match the H1 prefix
Severidade — Crítica / Alta / Média / Baixa / Informativa (or EN equivalents)
CVSS 3.1 — 8.6 (\AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:L/A:N`)` — score outside, vector in backticks
CWE — [CWE-639](https://cwe.mitre.org/data/definitions/639.html) — Authorization Bypass…
Alvo — URL or asset (inline code OK)
Aplicação — short app/component name
Status — Aberto (default for delivery), Mitigado, Corrigido, Aceito
Data de descoberta — ISO YYYY-MM-DD
## Descrição — what's wrong and why it matters
## Prova de Conceito — a > **Pré-requisitos** blockquote, then a single fenced bash/python/etc. block with the reproducer. Use the language fence (bash, python, http, json, sql, …) so Shiki can highlight it in the rendered PDF.
## Impacto — concrete business / regulatory impact (cite LGPD/GDPR/PCI articles when they apply)
## Recomendação — ordered list of concrete, actionable fixes
## Referências — OWASP / CWE / NIST / vendor links
Informative findings (info-*) keep the same structure but may omit Prova de Conceito if the observation is purely architectural — set CVSS 3.1 to 0.0 (N/A).
appendices/<slug>.md
Any material too long for report.md body. Typical examples (from real engagements):
timeline.md — daily activity log with findings discovered per day
injection-tests.md — full payload list for SQLi/SSTI/SSRF/XXE so the report doesn't get drowned by negatives
subdomains-ct.md — Certificate Transparency / DNS enumeration result table
config-matrix.md — security control matrix per application (HSTS, CSP, cookies, MFA, TLS …)
Each appendix file is a standalone H1 document — start with # Apêndice X — Title. The slug (filename without .md) is used as the URL fragment from report.md.
How to operate
Mode A — bootstrap a new engagement
report/
└── (empty)
- Ask the user for: client name, contact, executor, test type (white/grey/black box), period, version.
- Copy the four template files in this order:
cp $SKILL_DIR/templates/report.md report/report.md
cp $SKILL_DIR/templates/setup.md report/setup.md
- (leave
findings/ and appendices/ empty for now — create them as findings emerge)
- Replace the placeholder strings (
{{CLIENT}}, {{PERIOD}}, {{VERSION}}, …) in report.md.
- As each finding is discovered, run Mode C to add it.
Mode B — migrate a monolithic report
old-report.md (one huge file, multiple findings inline)
↓
report/ (split layout)
- Read the monolith. Identify each finding by its heading (typically
## CRIT-001 — … or ### Finding 1: …).
- For each finding:
- Decide the severity prefix and sequence from existing IDs (or assign new ones in order).
- Slug the title (lowercase, dashes, ASCII-only, < 60 chars).
- Filename:
crit-NNN-<slug>.md (or high-, med-, low-, info-).
- Extract the prose + PoC + impact + recommendation + references into the finding file using the template structure.
- Make sure the metadata table is the first non-heading element.
- Hoist anything that's "setup" (env vars, cookie capture, JWT issuance, WAF tricks) out of the findings into
setup.md. Each finding then just says > **Pré-requisitos**: ver \../setup.md` § 0.3` (or similar).
- Strip the per-finding content from the monolith. What remains becomes
report.md: header table, executive summary, scope, methodology, classification, findings summary, attack chain, positive controls, recommendations, links to appendices.
- Build the Tabela Completa de Achados in
report.md § 4.2 — every row links to ./findings/<file>.md.
Mode C — add a single finding to an existing report
Always use the generator script instead of manually creating files — it guarantees
correct filename (<sev>-NNN-<slug>.md), H1 (# {ID} — {Title}), metadata table
without a header row, PT severity (Crítica/Alta/Média/Baixa/Informativa), CVSS in the
canonical 8.7 (\AV:N/…`)` format, and CWE rendered as a clickable link.
~/.claude/skills/pentest-report-structure/scripts/new-finding.py \
--severity high \
--slug stored-xss-multivector \
--title "Stored XSS em múltiplos campos de texto livre" \
--cwe 79 \
--cwe-name "Improper Neutralization of Input During Web Page Generation" \
--cvss-score 8.7 \
--cvss-vector "AV:N/AC:L/PR:L/UI:R/S:C/C:H/I:H/A:N" \
--target "https://api.example.com/v1/clients" \
--app "Tandera Console API" \
--update-index
The script:
- Resolves
./report/ (override with --report-dir).
- Auto-computes the next NNN inside the severity bucket.
- Substitutes the metadata table; leaves prose sections as
{{placeholders}} for you to fill.
- With
--update-index, appends a row to the first | ID | Título | … | table it finds in report.md.
Fields you may still need to fill manually after running the script:
## Descrição — the actual finding narrative.
## Prova de Conceito — copy-paste reproducer with sanitised values.
## Impacto — business impact + LGPD/PCI mapping when applicable.
## Recomendação — actionable steps.
## Referências — OWASP / CWE / vendor links.
If you need to do it by hand (rare), the same rules apply:
- Filename:
report/findings/<sev>-NNN-<slug>.md, lowercase.
- H1:
# {ID} — {Title} (em-dash preferred).
- Metadata table: no header row; first non-heading block; PT severity; CWE link; CVSS as
score (\vector`)`.
- After creating the file, add a row to the index in
report.md and a bullet in the matching severity section. If the finding belongs to an attack chain, update § Cadeia de Ataque.
Do not create files like WEB-XSS-001.md (category-prefix, uppercase) — the importer
ignores them and will silently treat the engagement as having no findings in that bucket.
Mode D — validate an existing tree before handing off to the tool
Run the structural checks below. Any failure means the importer will silently drop information.
test -f report/report.md && test -f report/setup.md && test -d report/findings && test -d report/appendices
ls report/findings | grep -vE '^(crit|high|med|low|info)-[0-9]{3}-[a-z0-9-]+\.md$'
for f in report/findings/*.md; do
head -3 "$f" | grep -q '^# [A-Z]\+-[0-9]\+ [—\-:]' || echo "MISSING H1: $f"
head -20 "$f" | grep -q '^| \*\*ID\*\* ' || echo "MISSING METADATA TABLE: $f"
done
grep -oE '\./findings/[a-z0-9-]+\.md' report/report.md | sort -u | while read p; do
test -f "report/$p" || echo "BROKEN LINK: $p"
done
grep -L 'AV:[A-Z]/AC:[A-Z]/PR:[A-Z]/UI:[A-Z]/S:[A-Z]/C:[A-Z]/I:[A-Z]/A:[A-Z]' report/findings/*.md
Filename conventions — quick reference
crit-001-weak-applicant-link-auth.md ← critical, first in bucket
crit-002-idor-applicants-time-agility.md ← critical, second
high-001-sso-app-key-hardcoded-in-js.md ← high, first
med-013-dmarc-pnone-sample360.md ← medium, thirteenth
low-005-pws-applicants-add-returns-500.md ← low, fifth
info-002-companies-structure-in-filters.md ← informational
Slug guidance: short, descriptive, the first salient noun of the title. Avoid product names if generic (-login not -pws-login when it applies to every app). Avoid leaking sensitive identifiers (no real customer names in slugs).
Metadata-table format (the bit the parser is strict about)
This works:
| | |
|---|---|
| **ID** | CRIT-003 |
| **Severidade** | Crítica |
| **CVSS 3.1** | 9.4 (`AV:N/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:N`) |
| **CWE** | [CWE-639](https://cwe.mitre.org/data/definitions/639.html) — Authorization Bypass Through User-Controlled Key |
| **Alvo** | `https://example.com/api/v3.0/applicant/applicants/{uuid}/` |
| **Aplicação** | s360 (Django/DRF) |
| **Status** | Aberto |
| **Data de descoberta** | 2026-05-04 |
This does not work (and the parser will silently fill defaults):
- Using
### markers between rows
- Putting bullets
- **ID**: CRIT-003 instead of a real markdown table
- Wrapping the value in extra punctuation (
Severidade: **Crítica**.)
- Date in
DD/MM/YYYY format — must be ISO YYYY-MM-DD
- CVSS vector with extra spaces (
AV: N / AC: L / …) — keep it tight, no spaces
Templates & scripts
This skill ships starter templates and a generator script:
templates/report.md — top-level report skeleton with placeholders
templates/setup.md — PoC reproduction skeleton with placeholder sections
templates/findings/finding.md — single-finding skeleton
templates/appendices/appendix.md — generic appendix skeleton
templates/appendices/timeline.md — daily activity log skeleton (Apêndice A)
templates/appendices/injection-tests.md — full payload catalog skeleton (Apêndice B)
templates/appendices/subdomains-ct.md — CT/DNS enumeration table skeleton (Apêndice C)
templates/appendices/config-matrix.md — security-control matrix skeleton (Apêndice D)
scripts/new-finding.py — generator used by Mode C and by the pentest-finding-writer skill
Copy them into the engagement, never edit the originals. The placeholders use {{DOUBLE-CURLY}} notation so a quick grep -r '{{' report/ after filling reveals anything left blank.
Suggested bootstrap (Mode A)
SKILL_DIR=~/.claude/skills/pentest-report-structure/templates
mkdir -p report/findings report/appendices
cp "$SKILL_DIR/report.md" report/report.md
cp "$SKILL_DIR/setup.md" report/setup.md
cp "$SKILL_DIR/appendices/timeline.md" report/appendices/timeline.md
cp "$SKILL_DIR/appendices/injection-tests.md" report/appendices/injection-tests.md
cp "$SKILL_DIR/appendices/subdomains-ct.md" report/appendices/subdomains-ct.md
cp "$SKILL_DIR/appendices/config-matrix.md" report/appendices/config-matrix.md
Engagement-management hand-off
Once report/ is structurally complete and validated (Mode D passes cleanly), it can be zipped and uploaded directly to the pentest-report-manager. The Piersec tool will:
- Parse
report.md for the executive summary, scope, methodology, risk classification, findings summary, attack chain, positive controls, and recommendations sections.
- Parse every
findings/*.md into a structured finding (with severity, CVSS, CWE, target, application, status, discovery date).
- Parse every
appendices/*.md as a free-form appendix attached to the report.
- Render the whole thing into the Piersec-styled PDF/HTML/DOCX with proper page numbering, TOC, risk matrix, and per-finding cards.
Cross-reference: engagement-management, pentest-finding-writer.