Guide
How to structure a pentest report that survives an audit
A pentest report is written for someone who was not on the test and will read it two years later. This is what it has to contain to hold up.
Most pentest reports are written for the technical contact who commissioned the test. They work well for the three weeks after delivery and become useless afterwards, when the people who will really squeeze them show up: an external auditor, a buyer reviewing your security questionnaire, or the other side’s lawyer after an incident.
That reader was not on the test, does not know your infrastructure, has nobody to ask and has only the PDF. If the report does not let them reconstruct what was tested, when, with what permissions and what was left out, it does not work as evidence. It works as an opinion.
Scope is defined by what was left out
Almost every report describes scope as a list of targets: so many IP ranges, so many applications. That is half the job. The missing half — and the one an auditor always asks about — is negative scope.
Write down explicitly what was not tested and why. The real reasons are usually mundane and all of them are legitimate as long as they are recorded: the payments environment was frozen for quarter-end; the SSO provider would not authorise testing against its tenant; denial of service was excluded by contract; the billing module shipped three days after the window closed.
A report that says “the platform was audited” without bounding it leaves open
the reading that everything unmentioned is clean. A report that says “the
/billing/* endpoints were not tested because they fell outside the agreed
window of 4–15 March” is shorter, more honest and far harder to attack.
Test conditions are part of the result
A finding without its boundary conditions is not reproducible, and what is not reproducible is not evidence. Each test needs to record:
- The exact window, with dates, times and time zone. Not “March 2026”.
- Traffic origin: the IP addresses tested from. It is the first thing the detection team asks for when they want to know why nothing fired.
- Accounts and roles used, identified by username, with the exact role.
“A privileged user” says nothing;
qa_auditorwith roleproject:viewerdoes. - Controls active during the test: whether the WAF was in blocking mode, whether rate limiting applied, whether the test origin was allow-listed. A pentest run from an allow-listed IP measures a different surface from the one an attacker sees, and that must be said.
- Differences between the tested environment and production, if production was not the target. Version, data, disabled integrations.
This block costs half a page and it is what spares you the most uncomfortable conversation there is: explaining two years later why an exploited vulnerability does not appear in the report.
The unit is the finding, and it needs a stable identity
A finding must be citable, trackable and closable on its own. That needs an
identifier that does not change between report versions or between campaigns.
If the search-box SQL injection is FND-014 in the March report and FND-003
in the September one, you have lost the ability to show it is the same problem
and that it stayed open for six months. Which is exactly what the auditor wants
to know.
Each finding needs, at minimum:
- A stable identifier and a title describing the problem, not the symptom.
- The affected asset with surgical precision: FQDN, IP, port, path and parameter.
- The target’s version or build at the time of testing.
- Date and time of discovery.
- Reproduction steps someone else can run without phoning you.
- The full request and response, as selectable text, not as a screenshot.
- Impact described in terms of what the attacker gains, not the OWASP category it belongs to.
- A risk rating with the method declared and the vectors visible.
Screenshots are not evidence on their own
A screenshot proves that at some point someone saw that screen. It does not prove when, against which system, or with which account. If you are going to use screenshots, make sure they include the full address bar, a visible clock and the session identifier or authenticated user.
Better still: pair every screenshot with the text trace — raw HTTP request, console output with a timestamp, hash of the file retrieved. Text can be verified; an image can only be believed.
The retest has to link back to the original finding
The most common closing-phase mistake is delivering the retest as a standalone document saying “14 of 17 findings have been remediated”. That is not useful.
The retest needs, finding by finding: the original identifier, the retest date, the same reproduction steps executed again, the observed result and an explicit verdict — fixed, mitigated, risk accepted or still present. If it is “mitigated”, say what is mitigated and what remains exposed. If it is “accepted”, name the person who accepted it and the date.
That chain — finding, fix, retest, signed verdict — is the only thing that turns a report into proof that the security programme works. Without it you have a list of problems and a promise.
Version the report and protect it from silent edits
A report corrected over email and resent as “v2 final good one” has no traceability. Number the versions, record what changed in each and who approved it, and compute a hash of the delivered PDF you can cite later. If your organisation uses electronic signatures, use them: it turns an argument about authenticity into a thirty-second check.
How an auditor actually reads it
As a checklist, the typical path of a reasonable auditor is this: look first at the date and the scope, check whether the scope covers the system they are auditing, look for the exclusions, go to the summary of open findings, pick two at random and ask for the closure evidence for those two. If both are traced end to end, they accept the sample. If one of the two has no retest date, they start opening all the others.
Write the report for that path and you have solved ninety per cent of the problem. The rest is discipline: same identifiers, same templates, same mandatory fields across every campaign. That is precisely the kind of consistency that is hard to hold by hand and that, in Violet Security, we solve by making every finding arrive with its identifier, its asset and its retest trail already attached.