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:

The report, section by section Management Engineering Auditor 1. Scope, as tested 2. Methodology and coverage map 3. Executive summary 4. Findings, in a fixed shape 5. Chains and business impact 6. What was tried and failed 7. Retest terms 8. Appendices, machine-readable export
One report, three readers. A section that serves none of them is padding.

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_id stays: 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.

Five findings F-04 Order lookup F-05 Email change F-06 Invoice download F-07 Address edit F-08 Return cancellation One root cause the API checks that the user is logged in, not that they own it One fix a server-side ownership check in the data layer, with a test per endpoint
Five findings, one cause, one fix. A report that says so saves the team four separate tickets.

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:

Start an ordinary customer account F-03 a referral link leaks the internal customer ID Low · 2.3 F-05 email change accepts any customer ID Medium · 6.0 Account takeover the password reset goes to the attacker’s email Chain: High · 8.5 Scores are CVSS v4 base scores; each finding carries its vector in the report
Two findings that look minor alone; together, any customer's 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

  1. Will the report state what could not be tested, and why?
  2. Which standard will the coverage be mapped to, and will I see that map?
  3. Does every finding carry a scored severity with its vector, a root cause, and a reproduction?
  4. Is there a retest, and how are statuses reported?
  5. 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.