The report arrives as a PDF with a cover page, an executive summary and thirty findings coloured red, orange and yellow. It is easy to treat it as a compliance artefact: file it, raise some tickets, wait for next year. That wastes most of what you paid for.
Read it the way you would read a bug tracker export. Every finding is a bug with a reproduction. Your job is to understand it, fix the cause, and stop it coming back.
Triage: severity is a starting point
Testers rate findings with a scheme such as CVSS plus their judgement. That is a useful first sort, but they do not know your context as well as you do. Re-rate each finding against three questions:
- Who can reach it? Anonymous internet user, any logged-in customer, or an internal admin?
- What does it give them? Data from one account, data from all accounts, code execution, or a slightly misleading error page?
- What does it chain with? A low-rated information leak can be the first step that makes a medium-rated bug exploitable.
Group findings into three buckets: fix now, fix this sprint, and accept or schedule with a written reason. Accepted risks should have an owner and a review date, not just a comment in a spreadsheet.
Reproduce before you fix
Do not fix what you have not seen fail. Use the tester's evidence, usually HTTP requests and responses, to reproduce each finding against a non-production environment.
# Example: IDOR finding, reproduced with two test accounts
# Account A's session, requesting Account B's invoice
curl -s -H "Authorization: Bearer $TOKEN_USER_A" \
https://staging.example.test/api/invoices/10432 | jq .owner_id
If you cannot reproduce it, do not close it. Ask the testers. The finding may depend on a role, a feature flag or data you do not have in staging. Closing an unreproduced finding as "cannot reproduce" is how real bugs survive.
Reproduction also tells you where the bug really lives. The report names an endpoint; your debugger tells you which shared function failed.
Fix the root cause, not the line
A tester has limited time. If they found missing authorisation on one endpoint, there are probably others they did not get to. Ask: what pattern let this happen?
| Finding as reported | Narrow fix | Root-cause fix |
|---|---|---|
IDOR on /invoices/{id} | Add an owner check to that handler | Scope all object lookups by tenant in the data layer |
| Reflected XSS in search | Escape that parameter | Turn on template auto-escaping everywhere; add a CSP |
| Verbose stack trace | Catch that exception | Global error handler that never returns internals in production |
| Weak password policy | Raise minimum length | Follow OWASP ASVS authentication requirements; add rate limiting |
Once you know the pattern, search for it. A Semgrep rule or a simple grep for the unsafe call often finds the siblings the tester never saw.
Ask for a retest that means something
Most engagements include a retest window. Use it well:
- Send the testers a list of finding IDs, the fix, and the commit or release that contains it.
- Note where you fixed the class rather than the instance, so they can probe the siblings too.
- Ask them to confirm with the same technique they used originally, plus one variation.
- Get the retest result in writing and attach it to the ticket.
A retest that only checks the exact original request is a weak signal. A one-character change in a payload often walks around a narrow fix.
Turn every finding into a regression test
This is the step most teams skip, and it is the one that compounds. A pentest finding is a perfect test case: a known-bad input and a known-correct outcome. Encode it so the line rejects the bug automatically next time.
# tests/security/test_invoice_access.py
import pytest
@pytest.mark.security
def test_user_cannot_read_another_users_invoice(client, user_a, user_b):
invoice = user_b.create_invoice()
resp = client.get(
f"/api/invoices/{invoice.id}",
headers=user_a.auth_headers(),
)
# Pentest finding PT-2026-07: was 200 with user B's data
assert resp.status_code in (403, 404)
Tag these tests so you can run them as a suite, and keep the finding ID in a comment so anyone who sees a failure knows why the test exists. Over a few engagements you build a security regression suite shaped by real attacks on your own product.
What to do on Monday
- Re-rate the findings with the reach, impact and chain questions.
- Reproduce the top five in staging and note the root cause for each.
- Search the codebase for siblings of each root cause.
- Write one regression test per finding as part of the fix, not after.
- Book the retest with a clear list of what changed.
Next year's report should be shorter, and it should contain new bugs, not old ones in new places. More on how we run tests on the pentest page.