A penetration test is only as useful as its report. The test itself takes days; the report is what the engineering team works from for months, what the auditor reads, and what management uses to decide whether the money was well spent. Yet most reports we are asked to review fall into one of two failures: a raw scanner export with a cover page, or a narrative so hedged that nobody can tell what to fix first.
This is the structure we hold our own reports to, and the one we suggest you write into your next request for proposal.
The same document is read three ways, and each reader starts somewhere else:
1. Scope, as tested — not as planned
The first page should state exactly what was in scope and what was actually exercised: hostnames, IP ranges, application versions, user roles, environments (production, staging, a replica), and the dates. If a system was in scope but could not be tested — an endpoint was down, credentials never arrived, a WAF blocked the tester and was not adjusted — the report must say so. Otherwise a clean result for that system is a false assurance.
Ask for: the list of tested assets with the date each was touched, and a list of exclusions with reasons.
2. Methodology and standard
Name the methodology and stick to it. For web applications that is usually the OWASP Web Security Testing Guide, with the Application Security Verification Standard for depth; for APIs, the OWASP API Security Top 10 and the same ASVS chapters; for infrastructure, PTES or the NIST 800-115 phases. The value is not the acronym — it is that a named checklist lets a reader see what was not found, not only what was.
A short table mapping the standard's sections to "tested / not applicable / out of scope" is the single most useful page many reports lack.
Here is that page for a web application, mapped to the twelve WSTG categories. The engagement is fictional; the same one runs through the rest of this article.
| WSTG category | Status | Findings |
|---|---|---|
| INFO, information gathering | Tested | F-03 |
| CONF, configuration and deployment | Out of scope: the hosting provider's platform | – |
| IDNT, identity management | Tested, nothing found | – |
| ATHN, authentication | Tested, nothing found (see section 6) | – |
| ATHZ, authorisation | Tested | F-04 to F-08 |
| SESS, session management | Tested, nothing found | – |
| INPV, input validation | Tested | F-01 |
| ERRH, error handling | Tested | F-02 |
| CRYP, cryptography | Tested, nothing found | – |
| BUSL, business logic | Tested | F-09 |
| CLNT, client side | Tested, nothing found | – |
| APIT, API testing | Tested | F-04, F-05 |
The rows that say "nothing found" are worth as much as the rows with findings: they are what tells an auditor the area was looked at.
3. Executive summary that an executive can act on
One page. What was tested, the overall risk in one sentence, the count of findings by severity, and the two or three things that must happen this quarter. No screenshots, no payloads. If the reader stops here, they should know whether to worry and who to call.
4. Findings, one per page, in a fixed shape
Every finding needs the same fields, in the same order, so that a hundred findings can be triaged in an afternoon:
| Field | What it must contain |
|---|---|
| Title | The weakness, not the tool alert ("Order lookup exposes other customers' orders", not "IDOR found") |
| Severity | A rating and the rationale, using a published scheme (CVSS v4 base score with the vector string, or the OWASP risk rating) |
| Affected assets | Exact URLs, parameters, hosts, versions — enough to reproduce without asking |
| Reproduction | Step-by-step, with the request and response, redacted only where necessary |
| Impact | What an attacker actually gets: which data, which accounts, which systems — in the customer's terms |
| Root cause | The missing control, not the symptom ("authorisation is checked by the client only") |
| Remediation | A specific fix and, where relevant, an interim mitigation; a link to a standard, not a paragraph copied from it |
| Evidence | Screenshots or captures that prove the finding on the tested system |
A sample finding
Here is one finding in that shape, from the fictional engagement against a shop's API:
| Field | Content |
|---|---|
| ID and title | F-04 · Order lookup returns other customers' orders |
| Severity | High, CVSS v4 base score 7.1: CVSS:4.0/AV:N/AC:L/AT:N/PR:L/UI:N/VC:H/VI:N/VA:N/SC:N/SI:N/SA:N. Any signed-in customer can read any other customer's order (name, address, items); integrity and availability are not affected |
| Affected assets | GET https://shop.example/api/v2/orders/{order_id}, parameter order_id, API version 4.2.1, staging replica, tested on day 3 |
| Reproduction | Sign in as test customer A (customer 5531) and request an order ID that belongs to another customer; the whole order comes back. Order IDs are sequential, so every order can be enumerated |
| Impact | Personal data of every customer who has ever ordered: names, delivery addresses and purchase history |
| Root cause | The endpoint checks that the request carries a valid session, not that the order belongs to the session's customer |
| Remediation | Check ownership on the server for every object the API returns (see the root cause below). Interim: rate-limit order lookups and alert when a session reads orders that are not its own |
| Evidence | The request and response below, captured on the tested system |
| Retest | Fixed: the same request now returns 404 |
GET /api/v2/orders/100482 HTTP/1.1
Host: shop.example
Authorization: Bearer eyJ… (test customer A, customer_id 5531)
HTTP/1.1 200 OK
Content-Type: application/json
{"order_id": 100482, "customer_id": 5537, "name": "[redacted]",
"address": "[redacted]", "items": 3, "total": 1840000}
Note: Redact personal data in evidence, but never the part that proves the finding. Here the name and the address are hidden while
customer_idstays: 5537 is not the caller's 5531, and that difference is the finding.
Two things separate a report you can act on from one you cannot. First, severity must be argued, not asserted: "High" means nothing without the vector. Second, root cause must be named: five findings that share one cause are one fix, and the report should say so.
5. Chains and business impact
Individual findings understate risk when they combine. A low-severity information disclosure plus a medium-severity access-control flaw can equal full account takeover. A good report includes at least one attack chain written as a story: where the tester started, what each step yielded, and where it ended. That narrative is what makes a board understand the number in the summary.
An example from the same engagement. The tester started with an ordinary customer account:
Alone, F-03 is Low (CVSS v4 2.3): a referral link that a customer shares carries their internal customer ID. Alone, F-05 is Medium (6.0): the endpoint that changes a customer's email accepts any customer ID, but the IDs cannot be guessed. Together, the leaked ID feeds the email change, the password reset then goes to the attacker's address, and the account is taken over: 8.5, High, scored as one chain. Neither score on its own would have reached the executive summary; the chain does.
6. What was tried and did not work
A tester who spent a day on the authentication flow and found nothing has produced evidence. It belongs in the report as a short section of negative results: which attack classes were attempted against which components. Without it, the customer cannot distinguish "secure" from "not looked at".
7. Retest terms
State whether a retest is included, its window, and what it covers. A report without a retest path leaves findings in limbo. The retest report should reference the original finding IDs and give each a status: fixed, partially fixed, not fixed, or accepted as a risk with a named owner.
For the fictional engagement, the retest page looks like this:
| Finding | Status at retest | Note |
|---|---|---|
| F-04 Order lookup | Fixed | The same request returns 404 |
| F-05 Email change | Fixed | Ownership is checked, so the chain with F-03 is broken |
| F-06 Invoice download | Partially fixed | Ownership is checked, but download links issued before the fix stay valid for 24 hours |
| F-03 Referral link | Accepted as a risk | Owner: head of e-commerce; reviewed again in six months, now that F-05 is fixed |
8. Appendices that save time
Scanner outputs, full request logs, tooling and versions used, and a machine-readable export of the findings (CSV or JSON) so the engineering team can import them into their tracker rather than retype them. The machine-readable version is not a nicety; it is what makes the report get worked.
The questions to ask before you sign
- Will the report state what could not be tested, and why?
- Which standard will the coverage be mapped to, and will I see that map?
- Does every finding carry a scored severity with its vector, a root cause, and a reproduction?
- Is there a retest, and how are statuses reported?
- Will I receive the findings in a form my tracker can import?
A vendor who answers all five without hesitation is describing a report you can act on. If the answer to any of them is "we can add that", ask to see a redacted sample first.