Rapportage

Charles Darwin keerde in 1836 terug van zijn vijfjarige reis met de HMS Beagle. Hij had twintig notitieboeken vol observaties, honderden specimens, en een hoofd boordevol inzichten die de wetenschap voorgoed zouden veranderen. Maar eerst moest hij gaan zitten en het allemaal opschrijven. Het kostte hem meer dan twintig jaar om On the Origin of Species te publiceren. Niet omdat hij twijfelde aan zijn theorie, maar omdat hij wist dat de manier waarop hij het vertelde net zo belangrijk was als wat hij vertelde. Als niemand het begreep, had het geen zin.

Een penetratietest is, in zekere zin, net zo’n expeditie. Je trekt weken of maanden door onbekend digitaal terrein. Je vindt dingen die niemand eerder heeft gezien. Je maakt aantekeningen, verzamelt bewijsmateriaal, en bouwt langzaam een beeld op van hoe het systeem er werkelijk uitziet – niet zoals het in de architectuurtekeningen staat, maar zoals het echt is, met al zijn scheuren en lekkages. En dan moet je het opschrijven. Voor mensen die er niet bij waren. Voor mensen die misschien niet eens begrijpen wat een SQL injection is.

Dat is het moment waarop de echte uitdaging begint.


Waarom rapportage ertoe doet

Laten we eerlijk zijn: de meeste pentesters zijn niet in dit vak gestapt omdat ze dol zijn op schrijven. Ze zijn erin gestapt omdat ze dol zijn op breken. Op puzzels oplossen. Op dat moment waarop je na uren proberen eindelijk die shell krijgt en even heel stil wordt, gevolgd door een onderdrukt “yes” dat door een stille werkkamer galmt.

Maar hier is het ongemakkelijke feit: je rapport is het enige wat overblijft. Als je vertrokken bent, als je laptop is opgeruimd en je VPN-verbinding is verbroken, is dat rapport het enige bewijs dat je er bent geweest. Het is tegelijkertijd je visitekaartje, je factuur, en je nalatenschap.

En toch – en hier begint het pijn te doen – behandelen de meeste pentesters hun rapport alsof het een verplicht bijproduct is. Een vervelend formulier dat moet worden ingevuld voordat je aan de volgende klus kunt beginnen. Ze schrijven het in de laatste twee uur van het project, met een kop koude koffie en een vage herinnering aan wat ze drie weken geleden gevonden hebben.

Het resultaat? Rapporten die lezen alsof ze zijn geschreven door iemand die liever ergens anders was. Wat ook zo is.

De pentester als vertaler

Hier is iets wat niemand je vertelt op je OSCP-cursus: de belangrijkste vaardigheid van een pentester is niet technisch. Het is vertalen. Je bent een vertaler tussen twee werelden die elkaars taal niet spreken.

Aan de ene kant heb je de technische realiteit: CVE-nummers, CVSS vectors, stack traces, en hex dumps. Aan de andere kant heb je het management: mensen die beslissingen nemen over budgetten, prioriteiten, en risico’s, maar die niet weten wat een buffer overflow is en dat ook niet hoeven te weten.

Jouw rapport moet beide werelden bedienen. Het moet technisch genoeg zijn voor de developers om het probleem te vinden en te fixen. En het moet begrijpelijk genoeg zijn voor het management om te beslissen hoeveel geld en tijd ze eraan willen besteden.

Dat is geen gemakkelijke evenwichtsoefening. Het is eigenlijk twee rapporten schrijven in een. En de meeste pentesters kunnen precies een van de twee.

Je kunt de hele dag hacken, maar als je rapport waardeloos is, dan is het alsof je de mooiste foto ter wereld hebt gemaakt met de lensdop erop. Het bewijs bestaat, ergens in je hoofd, maar niemand anders zal het ooit zien. En als niemand het ziet, word het ook niet gefixt. En als het niet gefixt wordt, wat was dan het hele punt?

Precies.


IB Findings Management

Incompetent Bastard heeft een volledig findings management systeem ingebouwd. Niet omdat het leuk was om te bouwen, maar omdat het alternatief – bevindingen bijhouden in een spreadsheet, of erger nog, in je hoofd – het soort professionele nalatigheid is waarvoor dit boek is vernoemd.

De findings management blueprint (findings_bp) in IB is het centrale zenuwstelsel van je rapportage. Hier komen alle bevindingen samen, krijgen ze structuur, worden ze geclassificeerd, en worden ze uiteindelijk omgezet in een rapport.

Het findings overzicht

Navigeer naar /dashboard/findings en je ziet het overzicht. Bovenaan staan vier kaarten met statistieken: het totaal aantal findings, het aantal templates, het aantal scoped items, en een knop om het rapport te genereren. Simpel, overzichtelijk, en precies wat je nodig hebt.

/dashboard/findings

Het scherm is opgedeeld in twee kolommen. Links: de beschikbare templates waarmee je nieuwe findings kunt aanmaken. Rechts: de findings die je al hebt vastgelegd, met hun ID, naam, host, en acties om te bewerken of te verwijderen.

Finding aanmaken: de formuliervelden

Elke finding in IB wordt vastgelegd met een formulier dat precies de juiste balans vindt tussen volledigheid en bruikbaarheid. Laten we de velden doorlopen:

Naam (naam): De titel van je finding. Dit is wat in het rapport verschijnt als kop. Maak het beschrijvend maar beknopt. “SQL Injection in login form” is goed. “Bug gevonden” is dat niet.

Host (locatie): Het IP-adres of de hostname waar de kwetsbaarheid is gevonden. Dit is cruciaal voor scoping – je wilt weten welke systemen geraakt zijn.

CVSS 4.0 Vector (cvss): De CVSS 4.0 vector string die de ernst van de kwetsbaarheid beschrijft. Hierover later meer.

CVSS Basescore (basescore): De numerieke score die uit de vector wordt berekend. IB’s ingebouwde calculator vult dit automatisch in.

User flag (gebruikersvlag): Voor CTF-achtige engagements of als bewijs dat je user-level access hebt bereikt.

Root flag (rootvlag): Idem, maar dan voor root/admin-level access. Het bewijs dat je helemaal bovenaan de berg stond.

Hoe kwam de bevinding tot stand? (invoegen): Hier beschrijf je de aanvalsketen. Hoe ben je van punt A naar punt B gekomen? Welke stappen heb je genomen? Dit is het narratief van je aanval.

Werk de bevinding uit (uitwerken): Het veld voor je gedetailleerde technische uitwerking. Dit is waar je LaTeX-opmaak kunt gebruiken voor het rapport. IB biedt een toolbar met LaTeX-shortcuts boven dit veld.

Het formulier is bewust compact gehouden. Geen vijftig velden waarvan je er dertig nooit invult. Geen verplichte dropdown-menu’s voor zaken die je niet weet. Gewoon de essentie.

IB Tip: Het uitwerken-veld ondersteunt LaTeX-opmaak. Gebruik de toolbar boven het veld voor veelgebruikte commando’s zoals \begin{lstlisting} voor codeblokken en \plaatje{bestandsnaam}{caption} voor screenshots.

De workflow: van template naar finding

De workflow in IB werkt als volgt:

  1. Je selecteert een template uit de lijst links op het findings-overzicht
  2. Je klikt op “Add” naast de template
  3. Het formulier opent, met de template-referentie al ingevuld
  4. Je vult de specifieke details in: naam, host, evidence, flags
  5. Je slaat op en de finding verschijnt in het overzicht rechts

Dit template-systeem is de kern van efficiënte rapportage. In plaats van elke finding helemaal from scratch te beschrijven – de beschrijving, de impact, de aanbeveling – gebruik je een template die al het generieke werk bevat. Jij voegt alleen de specifieke context toe: waar vond je het, hoe vond je het, en wat is het bewijs.

# De route voor het toevoegen van een finding op basis van een template
@findings_bp.route('/dashboard/findings/add/<bevinding_id>', methods=['GET', 'POST'])
def bevinding_toevoegen(bevinding_id):
    form = BevindingForm(ref=bevinding_id)
    # ...

Het ref-veld koppelt je finding aan de template. Wanneer het rapport wordt gegenereerd, haalt IB de beschrijving, impact, aanbeveling, en referenties uit de template, en combineert die met jouw specifieke uitwerking.

Standaard finding templates

IB wordt geleverd met een set standaard finding templates. Deze worden geladen vanuit standard_findings.json en bevatten veertien veelvoorkomende kwetsbaarheidscategorieen:

# Template OWASP CWE
1 OS Command Injection A03 - Injection CWE-78
2 Cross-Site Scripting (XSS)
3 XML External Entity (XXE)
4 SQL Injection (SQLi)
5 Path Traversal
6 Server-Side Template Injection (SSTI)
7 CORS Misconfiguration
8 Server-Side Request Forgery (SSRF)
9 Insecure Direct Object References (IDOR)
10 Security Headers
11 Vulnerable and Outdated Components
12 Broken Access Control
13 Cryptographic Failures
14 Insecure Design

Elke template bevat:

Dit zijn niet zomaar skeletjes. De templates bevatten complete, professioneel geschreven beschrijvingen met gestructureerde aanbevelingen in LaTeX-formaat. Neem bijvoorbeeld de OS Command Injection template: die bevat niet alleen een uitleg van wat command injection is, maar ook concrete aanbevelingen voor inputvalidatie, parameterized API’s, het least privilege principle, en meer.

IB Tip: Gebruik de “Laad standaard bevindingen” knop op het findings-overzicht om alle 227 standaard templates te laden vanuit het hacksec-patched project. Let op: dit overschrijft bestaande templates.

Het template datamodel

Onder de motorkap slaat IB templates op in de db_bevindingen_templates tabel:

class db_bevindingen_templates(db.Model):
    __tablename__ = 'db_bevindingen_templates'
    id = db.Column(db.Integer, primary_key=True)
    titel = db.Column(db.String(255))
    bevtype = db.Column(db.String(255))
    cwe = db.Column(db.String(5))
    owasp = db.Column(db.String(255))
    mitre = db.Column(db.String(10))
    cvss = db.Column(db.String(255))
    basescore = db.Column(db.String(10))
    nlbeschrijving = db.Column(db.Text())    # Nederlandse beschrijving
    enbeschrijving = db.Column(db.Text())    # Engelse beschrijving
    nlimpact = db.Column(db.Text())          # Impact (NL)
    enimpact = db.Column(db.Text())          # Impact (EN)
    nlaanbeveling = db.Column(db.Text())     # Aanbeveling (NL)
    enaanbeveling = db.Column(db.Text())     # Aanbeveling (EN)
    referenties = db.Column(db.Text())       # Referentielinks

Merk op dat de templates tweetalig zijn. Elk template heeft velden voor zowel Nederlands als Engels. Het rapport wordt momenteel in het Nederlands gegenereerd, maar de structuur is er al voor meertalige ondersteuning.

OWASP classificatie

Elke finding in IB kan worden geclassificeerd volgens de OWASP Top 10 (2021 editie):

Code Categorie
A1 Broken Access Control
A2 Cryptographic Failures
A3 Injection
A4 Insecure Design
A5 Security Misconfiguration
A6 Vulnerable and Outdated Components
A7 Identification and Authentication Failures
A8 Software and Data Integrity Failures
A9 Security Logging and Monitoring Failures
A10 Server Side Request Forgery

De OWASP classificatie wordt opgeslagen als een nummer (1-10) in de template en vertaald via een template filter naar de volledige beschrijving:

owasptop10 = [
    'A1 - Broken Access Control',
    'A2 - Crypthographic Failures',
    'A3 - Injection',
    # ... enzovoort
]

@app.template_filter('owaspcategorie')
def owaspcategorie(num):
    if num:
        return owasptop10[int(num)-1]
    else:
        return 'A5 - Security Misconfiguration'

Dit is een elegant mechanisme. De template slaat alleen het nummer op, maar overal in de interface en het rapport verschijnt de volledige categorie met code.

Overigens: als je geen OWASP classificatie opgeeft, defaultt IB naar A5 – Security Misconfiguration. Dat is een aardige filosofische keuze. Als je niet eens de moeite neemt om een kwetsbaarheid te classificeren, dan is het per definitie een configuratie- probleem. Van jouw kant.

Status tracking

Het finding-model in IB is bewust minimalistisch gehouden. Er is geen apart status- veld in de database – de status van een finding wordt impliciet bepaald door het bestaan ervan. Een finding is “open” zodra die is aangemaakt, en “verwijderd” zodra je op delete klikt. Voor de tussenliggende statussen (verified, fixed, accepted) kun je de naam of het uitwerkveld gebruiken.

Dit is een bewuste keuze. In een pentest-tool die bedoeld is voor de tester zelf is een complex status-systeem overkill. Je bent geen Jira aan het bouwen. Je bent een rapport aan het schrijven.

Wil je toch statussen bijhouden, dan kan dat via de export/import functionaliteit. Bij export naar JSON wordt elk finding object voorzien van een status-veld:

{
    "title": "SQL Injection in login form",
    "status": "open",
    "cvss_v4_vector": "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N",
    "cvss_v4_score": 9.3,
    "standard_code": "4",
    "location": "192.168.1.100"
}

CVSS 4.0 scoring

Als je ooit hebt geprobeerd om de ernst van een kwetsbaarheid uit te leggen aan een manager, dan weet je dat woorden tekort schieten. “Het is erg” is niet overtuigend genoeg. “Het is heel erg” is dat evenmin. Je hebt een getal nodig. Mensen houden van getallen. Getallen zijn objectief, meetbaar, en passen in een spreadsheet.

Dat getal is de CVSS score.

CVSS – het Common Vulnerability Scoring System – is een open raamwerk voor het communiceren van de ernst van softwarekwetsbaarheden. Versie 4.0, uitgebracht door FIRST in 2023, is de nieuwste incarnatie en brengt significante verbeteringen ten opzichte van versie 3.1.

De CVSS 4.0 vector string

Een CVSS 4.0 score wordt uitgedrukt als een vector string. Dit is een compacte textrepresentatie van alle metrics die samen de score bepalen. Hij ziet er zo uit:

CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N

Laten we dat ontleden. De vector begint altijd met CVSS:4.0/ als prefix, gevolgd door elf base metrics gescheiden door slashes. Elke metric bestaat uit een afkorting en een waarde, gescheiden door een dubbele punt.

De elf base metrics

CVSS 4.0 definieert elf verplichte base metrics, verdeeld over drie groepen:

Exploitability Metrics – Hoe makkelijk is de aanval?

Metric Naam Waarden Beschrijving
AV Attack Vector N/A/L/P Vanwaar kan de aanval worden uitgevoerd?
AC Attack Complexity L/H Hoe complex is de aanval?
AT Attack Requirements N/P Zijn er voorwaarden nodig?
PR Privileges Required N/L/H Welk toegangsniveau is nodig?
UI User Interaction N/P/A Is gebruikersinteractie nodig?

Vulnerable System Impact – Wat is de impact op het kwetsbare systeem?

Metric Naam Waarden Beschrijving
VC Confidentiality N/L/H Impact op vertrouwelijkheid
VI Integrity N/L/H Impact op integriteit
VA Availability N/L/H Impact op beschikbaarheid

Subsequent System Impact – Wat is de impact op andere systemen?

Metric Naam Waarden Beschrijving
SC Confidentiality N/L/H Impact op vertrouwelijkheid van andere systemen
SI Integrity N/L/H Impact op integriteit van andere systemen
SA Availability N/L/H Impact op beschikbaarheid van andere systemen

Dit onderscheid tussen “Vulnerable System Impact” en “Subsequent System Impact” is nieuw in CVSS 4.0 en lost een van de grootste frustraties van versie 3.1 op. In CVSS 3.1 had je een “Scope” metric die probeerde om in een enkel veld vast te leggen of een kwetsbaarheid impact heeft op andere systemen. Het was verwarrend, inconsistent, en de bron van eindeloze discussies.

CVSS 4.0 vervangt dat met zes aparte impact-metrics: drie voor het kwetsbare systeem zelf (VC, VI, VA) en drie voor downstream systemen (SC, SI, SA). Dat is veel preciezer.

Attack Vector in detail

De Attack Vector (AV) metric verdient speciale aandacht, want hij heeft de grootste invloed op de score:

Attack Requirements: nieuw in 4.0

Een andere belangrijke toevoeging in CVSS 4.0 is Attack Requirements (AT). Deze metric vangt voorwaarden die buiten de controle van de aanvaller liggen:

IB’s ingebouwde CVSS 4.0 calculator

IB heeft een volledig functionele CVSS 4.0 calculator ingebouwd in het finding- formulier. Je hoeft geen externe website te openen of handmatig een vector te construeren.

De calculator verschijnt als een inklapbaar paneel in het finding-formulier. Klik op “CVSS 4.0 Calculator” om het paneel te openen. Je ziet drie secties met dropdown- menu’s voor elke metric:

+-----------------+  +--------------------+  +---------------------+
| Exploitability  |  | Vulnerable System  |  | Subsequent System   |
|                 |  | Impact             |  | Impact              |
| Attack Vector   |  |                    |  |                     |
| [N - Network ]  |  | Confidentiality    |  | Confidentiality     |
|                 |  | [H - High    ]     |  | [N - None    ]      |
| Attack Complex. |  |                    |  |                     |
| [L - Low     ]  |  | Integrity          |  | Integrity           |
|                 |  | [H - High    ]     |  | [N - None    ]      |
| Attack Require. |  |                    |  |                     |
| [N - None    ]  |  | Availability       |  | Availability        |
|                 |  | [H - High    ]     |  | [N - None    ]      |
| Privileges Req. |  |                    |  |                     |
| [N - None    ]  |  +--------------------+  +---------------------+
|                 |
| User Interact.  |
| [N - None    ]  |
+-----------------+

Zodra je een metric wijzigt, stuurt de calculator een request naar de server-side API (/api/cvss4/calculate) die de Python cvss library gebruikt voor de berekening:

@findings_bp.route('/api/cvss4/calculate', methods=['GET'])
def cvss4_calculate():
    CVSS4 = _get_cvss4_class()
    vector = request.args.get('vector', '').strip()
    # Validatie van prefix en verplichte metrics...
    c = CVSS4(vector)
    score = c.base_score
    # Severity mapping
    if score == 0:      severity = 'none'
    elif score < 4.0:   severity = 'low'
    elif score < 7.0:   severity = 'medium'
    elif score < 9.0:   severity = 'high'
    else:               severity = 'critical'
    return jsonify({'ok': True, 'score': score, 'severity': severity})

De score en severity worden realtime teruggegeven en zichtbaar gemaakt in het formulier met een kleurgecodeerde badge.

Severity mapping

IB gebruikt de standaard FIRST severity schaal:

Score Severity Badge kleur
0.0 None Grijs
0.1 - 3.9 Low Blauw
4.0 - 6.9 Medium Oranje
7.0 - 8.9 High Donker oranje
9.0 - 10.0 Critical Rood

Practical scoring: wanneer is iets Critical vs High?

De theorie van CVSS is helder. De praktijk is dat niet.

Hier is het probleem: CVSS is een technische score. Het meet de technische ernst van een kwetsbaarheid. Het meet niet de business impact. Een SQL injection in een testomgeving die geen echte data bevat, scoort technisch hetzelfde als een SQL injection in de productiedatabase van een bank. Maar het risico is uiteraard compleet anders.

Enkele richtlijnen voor het scoren in de praktijk:

Critical (9.0-10.0): Reserveer dit voor kwetsbaarheden die een aanvaller in staat stellen om zonder authenticatie, over het netwerk, volledige controle over het systeem te krijgen. Unauthenticated remote code execution. Pre-auth SQL injection die de hele database blootstelt. Dit zijn de dingen waarvoor je midden in de nacht belt.

High (7.0-8.9): Kwetsbaarheden die serieuze impact hebben maar enige beperking kennen. Misschien is authenticatie nodig, of werkt de aanval alleen onder bepaalde omstandigheden. Authenticated RCE. SSRF die toegang geeft tot cloud metadata.

Medium (4.0-6.9): Kwetsbaarheden die op zichzelf beperkte impact hebben, maar in combinatie met andere bevindingen gevaarlijk kunnen worden. Stored XSS. CSRF. Open redirects.

Low (0.1-3.9): Informatielekken, ontbrekende headers, verbose error messages. Dingen die een aanvaller helpen maar niet direct schade veroorzaken.

Het is verleidelijk om alles als Critical te scoren – het maakt je rapport indrukwekkender en je opdrachtgever nerveuzer. Maar als alles Critical is, is niets Critical. Het is de beveiligingsversie van het jongetje dat “wolf” riep. Uiteindelijk stopt iedereen met luisteren.


Evidence verzamelen

Een finding zonder bewijs is een mening. En meningen zijn niet wat je opdrachtgever betaalt. Ze betalen voor feiten, gedocumenteerd, reproduceerbaar, en zo onweerlegbaar dat niemand kan zeggen “dat kan bij ons niet, want wij hebben een firewall”.

Screenshots

Screenshots zijn het meest directe bewijs, maar ze zijn ook het makkelijkst om slecht te doen. Een paar regels:

Timing: Maak screenshots op het moment dat je de kwetsbaarheid vindt. Niet achteraf, wanneer je het rapport schrijft en de omgeving misschien al gewijzigd is.

Context: Een screenshot van een shell prompt zonder context bewijst niets. Zorg dat de screenshot laat zien waar je bent (welk systeem), hoe je daar gekomen bent (het commando dat je uitvoerde), en wat het resultaat is.

Annotatie: Markeer relevante delen van je screenshot. Een screenshot van een HTTP response van tweehonderd regels is waardeloos als je niet aangeeft welke drie regels ertoe doen.

Bestandsnamen: Gebruik beschrijvende namen. screenshot_2024_01_15_sqli_login.png is bruikbaar. img_234897.png is dat niet.

Request/response logs

Voor web-kwetsbaarheden zijn request/response logs vaak overtuigender dan screenshots. Ze laten exact zien wat je gestuurd hebt en wat je terugkreeg.

Bewaar altijd:

In IB kun je deze logs opnemen in het uitwerken-veld van je finding, met LaTeX- opmaak voor codeblokken:

\begin{lstlisting}
POST /login HTTP/1.1
Host: target.example.com
Content-Type: application/x-www-form-urlencoded

username=admin' OR '1'='1&password=anything
\end{lstlisting}

Proof of concept code

Voor complexere kwetsbaarheden is een proof-of-concept script het sterkste bewijs. Het stelt je opdrachtgever in staat om de kwetsbaarheid zelf te reproduceren en te verifieren dat een fix werkt.

Houd je PoC-code:

IB Evidence upload

IB biedt een evidence-systeem waarmee je bestanden direct aan findings kunt koppelen. De evidence API ondersteunt uploaden, downloaden, en verwijderen:

POST   /api/findings/<finding_id>/evidence    # Upload een bestand
GET    /api/findings/<finding_id>/evidence    # Lijst alle evidence
GET    /api/findings/evidence/<id>/download   # Download evidence
DELETE /api/findings/evidence/<id>            # Verwijder evidence

Ondersteunde bestandstypen zijn bewust breed gehouden:

De maximale bestandsgrootte is 10 MB per upload. Bestanden worden opgeslagen in meuk/flask/db/evidence/<finding_id>/ met een UUID als bestandsnaam om conflicten te voorkomen:

_EVIDENCE_DIR = os.path.join(os.path.dirname(__file__), 'db', 'evidence')
_MAX_EVIDENCE_SIZE = 10 * 1024 * 1024  # 10 MB

stored_name = uuid.uuid4().hex + ext
dest_dir = _evidence_path(finding_id)
f.save(os.path.join(dest_dir, stored_name))

De originele bestandsnaam wordt bewaard in de database zodat je bij het downloaden een herkenbaar bestand terugkrijgt, niet een string van 32 hexadecimale karakters.

Je kunt ook alle evidence in een keer exporteren als ZIP-archief via /api/findings/evidence/export. Handig voor archivering of als je de evidence wilt overdragen aan een collega.

IB Tip: Upload je evidence zo vroeg mogelijk. Doe het niet pas bij het schrijven van het rapport. Op dat moment ben je vergeten welke screenshot bij welke finding hoorde, en dan begin je te gokken. En gokken in een pentest-rapport is zoiets als gokken met je belastingaangifte: het kan goed gaan, maar als het misgaat is het echt vervelend.


Notes

Naast findings heeft IB een apart notitiesysteem. Notes zijn bedoeld voor lopende observaties, aantekeningen, en informatie die niet direct een kwetsbaarheid is maar wel relevant voor het rapport.

Denk aan:

De Notes interface

De notes-pagina (/dashboard/notes) is opgedeeld in twee secties:

Links: Een formulier om nieuwe notes toe te voegen. Elke note heeft een naam (titel) en een inhoudsveld met LaTeX-toolbar ondersteuning.

Rechts: Een lijst van bestaande notes met drag-and-drop functionaliteit om de volgorde te bepalen.

@notes_bp.route("/dashboard/notes", methods=["GET"])
def notes_page():
    notes = db_notes.query.order_by(
        db_notes.volgorde.asc(),
        db_notes.id.desc()
    ).all()
    return render_template("notes_overview.html", notes=notes)

Notes in het rapport

Hier is het slimme deel: elke note heeft een “Rapport” toggle. Als je deze aanzet, wordt de note opgenomen in het gegenereerde rapport onder de sectie “Verkenning & ontdekking”.

Dit is een checkbox naast elke note in de lijst. De status wordt bewaard via een API-endpoint:

@notes_bp.route("/api/notes/<int:note_id>/toggle-rapport", methods=["POST"])
def notes_toggle_rapport(note_id):
    note = db_notes.query.get_or_404(note_id)
    note.rapport = not (note.rapport or False)
    db.session.commit()
    return jsonify({"ok": True, "rapport": note.rapport})

Notes die in het rapport staan worden visueel gemarkeerd met een groene rand en een andere achtergrondkleur, zodat je in een oogopslag kunt zien welke notes mee worden genomen.

Volgorde bepalen

De volgorde van notes in het rapport wordt bepaald door drag-and-drop. Sleep een note omhoog of omlaag in de lijst, en de volgorde wordt automatisch opgeslagen:

@notes_bp.route("/api/notes/reorder", methods=["POST"])
def notes_reorder():
    data = request.get_json(silent=True)
    order = data["order"]
    for idx, note_id in enumerate(order):
        note = db_notes.query.get(note_id)
        if note:
            note.volgorde = idx
    db.session.commit()
    return jsonify({"ok": True})

Dit is een kleine functie die een groot verschil maakt. In plaats van je rapport handmatig te herschikken nadat het is gegenereerd, bepaal je de volgorde vooraf.

IB Tip: Gebruik notes om de “verkenning”-sectie van je rapport op te bouwen terwijl je test. Maak een note voor elke fase: scoping, reconnaissance, enumeration, exploitation. Op het moment dat je het rapport genereert, hoef je alleen nog de “Rapport” toggles aan te zetten en de volgorde te bepalen.


Rapport generatie

Nu komen we bij het deel waar het allemaal samenkomt. Je hebt je findings vastgelegd, je evidence geupload, je notes geschreven. Het is tijd om er een rapport van te maken.

IB’s pandoc/LaTeX pipeline

IB gebruikt een pipeline die er in theorie eenvoudig uitziet maar in de praktijk behoorlijk ingenieus is:

Findings + Notes + Templates
        |
        v
    LaTeX output (findings_nl.tex)
        |
        v
    Regex transformatie (LaTeX -> HTML)
        |
        v
    HTML tussenbestand (tex.html)
        |
        v
    Pandoc conversie (HTML -> Markdown)
        |
        v
    Markdown rapport (tex.md)

De route /dashboard/findings/rapport triggert het hele proces. Laten we stap voor stap doorlopen wat er gebeurt.

Stap 1: Data verzamelen

bevindingen = db_bevindingen.query.group_by(db_bevindingen.ref).all()
notities = db_notes.query.filter_by(rapport=True).order_by(db_notes.volgorde.asc()).all()

IB haalt alle findings op, gegroepeerd per template-referentie. Als je drie SQL injection findings hebt die allemaal naar dezelfde template verwijzen, worden ze samengevoegd onder een enkele sectie. Daarnaast worden alle notes opgehaald die als “rapport” zijn gemarkeerd, gesorteerd op volgorde.

Stap 2: Notes als LaTeX subsecties

De notes worden als eerste verwerkt. Elke note wordt een \subsection in het rapport:

for notitie in notities:
    notes = notes + '\\subsection{' + (notitie.naam or '') + '}\n' \
          + (notitie.uitwerken or '') + '\n\n'

Stap 3: Findings renderen via Jinja templates

Elke groep findings wordt gerenderd via de findings_nl.html template. Dit is een Jinja2 template die LaTeX output genereert:

\subsection{SQL Injection in login form}
\label{bev001}
\bevindingkop{001}{A3 - Injection}{78}

[beschrijving uit template]

[uitwerking van de specifieke finding]

\subsubsection{Risico}
Risico inschatting

\subsubsection{impact}
[impact uit template]

\subsubsection{aanbeveling}
[aanbeveling uit template]

\subsubsection{Referenties}
[referenties uit template]

\newpage

Dit is de kern van het template-systeem. De generieke informatie (beschrijving, impact, aanbeveling) komt uit de template. De specifieke informatie (naam, evidence, uitwerking) komt uit de finding. Samen vormen ze een complete finding-sectie.

Stap 4: LaTeX naar HTML via regex

Nu komt het interessante deel. IB heeft een eigen LaTeX-naar-HTML converter geschreven met regex. Geen externe LaTeX compiler nodig. De code doorloopt het LaTeX-document en vervangt LaTeX-commando’s door HTML-equivalenten:

# Secties
tex = tex.replace('\\section{'+str(x)+'}', '<h1>'+str(x)+'</h1>')
tex = tex.replace('\\subsection{'+str(x)+'}', '<h2>'+str(x)+'</h2>')
tex = tex.replace('\\subsubsection{'+str(x)+'}', '<h3>'+str(x)+'</h3>')

# Lijsten
tex = tex.replace('\\begin{description}', '<ul>')
tex = tex.replace('\\end{description}', '</ul>')
tex = tex.replace('\\item', '<li>')

# Code
tex = tex.replace('\\begin{lstlisting}', '<pre>')
tex = tex.replace('\\end{lstlisting}', '</pre>')

# Afbeeldingen
tex = tex.replace('\\plaatje{'+x[0]+'}{'+x[1]+'}',
    '<img src="../raw/screenshots/'+str(x[0])+'" alt="'+str(x[1])+'" />')

De converter handelt ook cross-referenties af. Het \uitwerking{bevN} commando wordt vertaald naar een HTML-link die verwijst naar de bijbehorende finding, en het \bevinding{bevN} commando doet hetzelfde. Dit betekent dat je in je notes kunt verwijzen naar specifieke findings, en die verwijzingen worden automatisch werkende links in het rapport.

Voetnoten worden eveneens geconverteerd. Elk \footnote{tekst} wordt een genummerde referentie met de voettekst onderaan het document.

Stap 5: HTML naar Markdown via pandoc

Het HTML-tussenbestand wordt opgeslagen als rapport/tex.html. Vervolgens gebruikt IB pandoc om dit om te zetten naar Markdown:

md = pandoc('rapport/tex.html', '-o', 'rapport/tex.md')

Het Markdown-bestand krijgt een YAML front matter header:

---
title: "Pentest Rapport"
subtitle: "Rapportage"
author: Incompetent Bastard
date: today
...

Stap 6: Het eindresultaat

Het proces levert drie bestanden op:

  1. rapport/findings_nl.tex – De ruwe LaTeX output
  2. rapport/tex.html – De HTML-versie (ook direct zichtbaar in de browser)
  3. rapport/tex.md – De Markdown-versie met YAML front matter

De HTML-versie wordt direct in de browser getoond na het genereren. De Markdown- versie kan vervolgens met pandoc worden omgezet naar PDF, DOCX, of welk formaat je maar wilt:

pandoc rapport/tex.md -o rapport/rapport.pdf \
  --pdf-engine=xelatex \
  --template=rapport/template.tex

Walkthrough: rapport genereren in IB

De complete flow in de praktijk:

  1. Ga naar /dashboard/findings
  2. Controleer of al je findings er staan en correct zijn
  3. Ga naar /dashboard/notes
  4. Zet de “Rapport” toggle aan voor alle relevante notes
  5. Bepaal de volgorde met drag-and-drop
  6. Ga terug naar /dashboard/findings
  7. Klik op “Generate report”
  8. Het rapport opent in een nieuw tabblad als HTML
  9. De bestanden staan klaar in de rapport/ directory

IB Tip: Het rapport wordt elke keer opnieuw gegenereerd. Er is geen caching. Dit betekent dat je gerust wijzigingen kunt aanbrengen aan je findings en notes, en simpelweg opnieuw op “Generate report” kunt klikken om de nieuwste versie te krijgen.

Import en export

IB ondersteunt het importeren en exporteren van findings als JSON. Dit is handig voor:

De export-route (/api/findings/export) genereert een JSON-bestand met de volledige structuur:

{
    "schema_version": "1.0",
    "exported_at": "2026-02-23T14:30:00Z",
    "project": { "name": "Incompetent Bastard" },
    "catalogs": {
        "ref_owasp_top10": [...],
        "ref_cwe": [...],
        "ref_mitre_attack": [...],
        "standard_findings": [...]
    },
    "project_findings": [...]
}

Dit formaat bevat niet alleen de findings zelf, maar ook de bijbehorende catalogi (OWASP, CWE, MITRE ATT&CK) en de standaard finding-definities. Dat maakt het een op zichzelf staand document dat je kunt importeren in een andere IB-instantie zonder dat er informatie verloren gaat.


Best practices rapportage

Nu we weten hoe IB’s rapportage werkt, laten we het hebben over hoe je een rapport schrijft dat daadwerkelijk gelezen wordt. Want dat is het doel, uiteindelijk. Niet het genereren van een PDF. Het schrijven van iets dat iemand leest, begrijpt, en naar handelt.

De executive summary

De executive summary is het belangrijkste onderdeel van je rapport. Het is ook het enige onderdeel dat gegarandeerd wordt gelezen. De rest van je rapport is voor de technische mensen. De executive summary is voor de mensen die beslissingen nemen.

Regels voor een goede executive summary:

Risico communicatie

Het communiceren van risico is een kunst die de meeste pentesters nooit leren. Ze beschrijven technische details en verwachten dat de lezer zelf de vertaling maakt naar business impact. Dat doet de lezer niet. Die heeft daar noch de tijd noch de kennis voor.

Goede risico-communicatie vertaalt techniek naar consequenties:

Niet: “De applicatie is kwetsbaar voor SQL injection via de parameter id in het endpoint /api/users.”

Wel: “Een aanvaller kan de volledige klantenbase downloaden, inclusief namen, emailadressen, en wachtwoorden. Gezien de 50.000 geregistreerde gebruikers, zou dit leiden tot een meldplicht datalekken bij de Autoriteit Persoonsgegevens.”

Het eerste is technisch correct. Het tweede is bruikbaar.

Remediation advies formuleren

Het verschil tussen een goed en een slecht rapport zit vaak in de aanbevelingen. Slechte aanbevelingen zeggen wat er mis is. Goede aanbevelingen zeggen wat er gedaan moet worden.

Slecht: “Fix de SQL injection.”

Beter: “Gebruik parameterized queries in plaats van string concatenation voor alle database-queries.”

Best: “Vervang de string concatenation op regel 47 van UserController.java door een PreparedStatement. Voorbeeld: PreparedStatement ps = conn.prepareStatement("SELECT * FROM users WHERE id = ?"); ps.setInt(1, userId);

Hoe specifieker je aanbeveling, hoe groter de kans dat die wordt uitgevoerd. Een developer die precies weet welke regel code hij moet wijzigen, doet dat eerder dan een developer die eerst moet uitzoeken waar het probleem zit.

Tijdlijnen en prioritering

Niet alle kwetsbaarheden zijn gelijk, en dat moet je rapport weerspiegelen. Geef elke finding een aanbevolen tijdlijn:

Severity Aanbevolen termijn
Critical Binnen 24-48 uur
High Binnen 1-2 weken
Medium Binnen 1-3 maanden
Low Bij volgende release

Wees realistisch. Een organisatie met honderd developers kan drie critical findings in een week fixen. Een startup met twee developers kan dat niet. Pas je tijdlijnen aan aan de context.

De structuur van een goed rapport

Een compleet penetratietest-rapport bevat typisch de volgende secties:

  1. Executive Summary – Voor management. Maximaal een pagina.
  2. Scope en methodologie – Wat is er getest, hoe, en wanneer.
  3. Samenvatting bevindingen – Overzichtstabel met alle findings.
  4. Verkenning en ontdekking – Hoe je te werk bent gegaan (IB Notes).
  5. Gedetailleerde bevindingen – Elke finding met beschrijving, evidence, impact, en aanbeveling (IB Findings).
  6. Bijlagen – Screenshots, PoC-code, ruwe data.

IB’s rapportgeneratie dekt secties 4 en 5 automatisch. De executive summary en scope-beschrijving voeg je toe via notes.


De rapporten die niemand leest

We moeten het erover hebben. Want het is de olifant in de kamer van elke pentest-praktijk.

Je besteedt dagen, soms weken aan een rapport. Je formuleert elke zin zorgvuldig. Je maakt screenshots, schrijft PoC-code, berekent CVSS scores tot op een decimaal. Je levert een document af van vijftig pagina’s dat een volledig, reproduceerbaar beeld geeft van de beveiligingsstatus van de applicatie.

En dan leest niemand het.

Oh, ze zeggen dat ze het gelezen hebben. In de meeting knikken ze en zeggen dingen als “ja, we nemen dit zeer serieus”. Maar als je drie maanden later terugkomt voor een hertest, zijn dezelfde kwetsbaarheden er nog. Allemaal. Als trouwe huisdieren die geduldig op je terugkeer hebben gewacht.

De reden is simpel: rapporten die lezen als technische documentatie worden behandeld als technische documentatie. Ze worden opgeslagen in een map genaamd “Security Reports 2026” op een SharePoint die niemand ooit opent. Ze zijn het digitale equivalent van die stapel papieren op je bureau die je elke week van links naar rechts verplaatst en dan weer terug.

De oplossing is niet om betere rapporten te schrijven. Niet alleen, althans. De oplossing is om het rapport niet als eindproduct te zien, maar als het begin van een gesprek. Presenteer je bevindingen. Loop er doorheen. Laat het zien. Demonstreer de SQL injection live, terwijl de developers meekijken. Dat vergeten ze niet.

Een rapport is een geheugensteun voor een gesprek dat je al hebt gehad. Als je het gesprek overslaat, is het rapport slechts papier.

Of, in de context van IB, een hoop HTML in een browsertabblad dat iemand sluit tussen de vijftien andere tabbladen die hij toch al niet aan het gebruiken was.


Referentietabel

Topic IB Feature Route/Bestand
Findings Findings Management blueprint /dashboard/findings
Templates Standaard finding templates (14 stuks) standard_findings.json
CVSS CVSS 4.0 calculator /api/cvss4/calculate
Evidence Evidence upload/download/export /api/findings/<id>/evidence
Notes Notes blueprint met rapport-toggle /dashboard/notes
Rapport pandoc/LaTeX pipeline /dashboard/findings/rapport
Export JSON export van alle findings /api/findings/export
Import JSON import van findings /api/findings/import
Evidence ZIP Export alle evidence als archief /api/findings/evidence/export

Samenvatting

Rapportage is niet het saaie sluitstuk van een pentest. Het is het enige deel dat voortleeft nadat je bent vertrokken. Het is het verschil tussen een kwetsbaarheid die wordt gevonden en een kwetsbaarheid die wordt verholpen.

IB geeft je de gereedschappen om die rapportage efficient en gestructureerd te doen: templates die het generieke werk uit handen nemen, een CVSS 4.0 calculator die scoring objectief maakt, evidence management dat je bewijsmateriaal koppelt aan je findings, notes voor je verkenningsverhaal, en een rapport-pipeline die alles samenvoegt tot een document.

Maar het gereedschap is slechts het begin. Het verschil zit in hoe je het gebruikt. In de zorgvuldigheid waarmee je je findings formuleert. In de helderheid waarmee je risico’s communiceert. In het empathische vermogen om je te verplaatsen in de lezer die niet weet wat jij weet, en toch moet begrijpen waarom het belangrijk is.

Darwin had dat begrepen. Hij schreef niet voor biologen. Hij schreef voor iedereen. En daarom veranderde hij de wereld.

Jouw rapport hoeft de wereld niet te veranderen. Maar het moet wel een applicatie veiliger maken. En daarvoor moet iemand het lezen. Dus zorg dat het de moeite waard is.