Pentest report sample: a proven finding, section by section, and a template to reuse

A good pentest report has six parts: an executive summary, the scope, the method, the findings, the fixes and the retest status. Each finding needs a severity, the proof (the exact request and the answer it got), the impact on your data, the fix and a dated retest. A list of scanner alerts with no proof is not a pentest report.

The team behind Keybrum's attacksUpdated 14 min read

In short

  • A credible finding says who can do it, what they get and how much, then proves it with the exact request and response. Without that, it is a guess.
  • CVSS rates a flaw in general. Your report should also rate it in your app: the same policy mistake is critical on a table of door codes and not a finding at all on a table of public reviews.
  • The scope, and the list of what was not tested, decide what a clean result is worth.
  • A finding is fixed when the same attack fails on the live app, on a date the report records. A commit message is not a retest.
  • Scanner dumps, CVEs claimed from a version number and findings with no proof are padding. PCI's guidance says a list of vulnerabilities alone does not meet the intent of a pentest.
On this page

What a pentest report should contain

A pentest report is the written record of an authorized attack on your app: what was in scope, how it was attacked, what worked, and how to fix it. No single format is mandatory, but the references agree on the core. The PCI Security Standards Council's penetration testing guidance suggests an executive summary, a statement of scope, the methodology, the limitations, a narrative of the testing and the findings. The OWASP Web Security Testing Guide adds a timeline, a findings summary table and appendices that explain the severity scale. PTES, the Penetration Testing Execution Standard, splits the whole into an executive summary and a technical report.

SectionWhat it answersWithout it
Executive summaryWhat was tested, what a stranger can do today, what to fix firstNobody outside the engineering team can act on the report
Scope and coverageWhich hosts, identities and features were attacked, and which were notA clean result tells you nothing, because you do not know what it covers
Method and limitsBlack box or with credentials, from where, with which accounts, under which constraintsYou cannot repeat the test or compare two reports
FindingsFor each flaw: severity, proof, impact, fixYou get opinions instead of evidence
RemediationWhat to change, at which layer, in which orderYour team, or your coding agent, guesses the fix
Retest statusWhich findings were attacked again after the fix, when, and with what resultFixed means someone said so
The six parts every serious pentest report has, whatever the headings are called.

The sample: Keybrum's report on Pawpal, a demo app

The examples below come from a report Keybrum produced on Pawpal, a demo app we built to be attacked. Pawpal is a small pet-sitting marketplace written to look like a Lovable export on Supabase: a Vite and React frontend, a Supabase backend, one Edge Function, and the mistakes such apps ship with, planted on purpose. Its 1,240 profiles and 380 bookings are generated test data, and every key in it is fake or a local default. No real company or customer appears in this report.

It is a black-box report: Keybrum attacks the live app from the internet with what any visitor receives (the page, its JavaScript, its public keys), then as a throwaway account when sign-up is open. Its outline:

  1. Header: the target, the time of the attack in UTC, how ownership was proven, and how many tests worked, were refused or were unclear.
  2. Summary: attacks run, attacks that worked, open findings by severity.
  3. Findings at a glance: one line per finding: reference, severity, who can do it.
  4. What a stranger gets, and how: your data, your bill or your users' accounts, and the likely chain of an attack.
  5. Open findings, with evidence and fix: shown in the next section.
  6. Closed findings: fixed ones with the date the attack failed, and ones you marked as intended, with your reason.
  7. Scope and coverage: what was tested, what was not, and the pages, scripts, tables, functions, buckets and API routes found to attack.
  8. Method, then an appendix with every test and the app's answer.

Findings at a glance

RefSeverityFindingWho can do it
F1CriticalAnyone can read, add and change rows in profiles: 1,240 rows, with email, phone and nameNo account needed
F2CriticalAny signed-up user can read other people's bookings: 380 rows, with access codes, address and private textNeeds only a free sign-up
F3HighYour OpenAI key is public and it works: anyone can call OpenAI models on your billNo account needed
Three rows from the Pawpal demo report, renumbered for this page.

Each title says who can do what, to how much data, so a founder reading only this table knows where the leak is and why it matters. Broken access control on /bookings sends a developer to the right file and leaves everyone else guessing.

Anatomy of one finding, with a redacted example

Here is F2 as the demo report shows it. Every credible finding, whoever writes it, has these parts.

PartIn the Pawpal reportWhy it is there
Reference and severityF2, Critical, Databases, proven 4 Nov 2026You can discuss one finding by its id and sort the work
Who can do itNeeds only a free sign-upAnyone on the internet and one of your admins are very different risks
What happenedA brand-new account that owns nothing read 380 rows of bookings that belong to other people, including access codes, address and private text.States the attack as a fact, with the identity used and the count
ImpactA personal-data breach under GDPR and most privacy laws: the data can be scraped in one request and reused for spam, phishing or fraud.Turns the flaw into what you stand to lose
EvidenceThe request, the HTTP status, the row count, one redacted row and a command to replay itAnyone on your team can confirm it in a minute, and the retest repeats exactly this
FixScope the read policies on bookings to the row's owner or participants, as a SQL migration and as a prompt for your coding agentNames the layer to change: a database policy, not a hidden button
Retest statusOpen until the same request, sent by a new account, gets none of the other people's rowsClosing a finding takes proof too: the same attack, failing on the live app
http
# Exhibit A: read bookings as a brand-new account that owns nothing
GET /rest/v1/bookings?select=*&limit=10
HTTP 206, content-range 0-9/380

[
  {
    "pet_name": "Oscar",
    "pet_type": "cat",
    "address": "56 ••• (redacted)",
    "door_code": "••••",
    "start_date": "2025-06-07",
    "end_date": "2025-06-20",
    "price_cents": 48100,
    ...
  }
]
The evidence for F2. The content-range header gives the total: 380 rows. Keybrum keeps the request, the status, the count and one row, with personal values masked.

This is what makes a finding hard to argue with: you see which identity asked, what it asked for and what came back, without trusting anyone's judgement. Our IDOR guide shows how to run the same two-account check on your own project.

sql
-- Row Level Security must be on for public.bookings.
alter table public.bookings enable row level security;

-- List the current policies on the table.
select policyname, cmd, roles, qual
from pg_policies
where schemaname = 'public' and tablename = 'bookings';

-- Drop the select policy that lets every signed-in user read
-- every row (qual = true). This is Pawpal's name for it: use
-- the name the query above returns on your project.
drop policy "Authenticated users can view bookings" on public.bookings;

-- Each booking is readable by its owner and its sitter only.
create policy "Owners read their bookings" on public.bookings
  for select to authenticated
  using ((select auth.uid()) in (owner_id, sitter_id));
The fix that comes with F2: one migration, so each booking is visible only to the people in it. Policies that allow access add up, so the old one must go: a new policy next to it changes nothing.

The report also hands the fix to your coding agent as a prompt: the proof, the migration, what must keep working (each user still sees the bookings they are part of), and the constraints, such as never disabling row level security or deleting data to make a test pass. It ends by asking for a retest, because the finding only closes when the attack fails on the live app. The Supabase RLS guide explains the policy patterns behind this fix.

Severity ratings: CVSS or impact-based?

Most reports rate findings from low to critical, sometimes with an informational tier below. What matters is where the level comes from.

CVSS, the Common Vulnerability Scoring System maintained by FIRST, scores a flaw from 0 to 10 from its technical traits: how it is reached, what privileges it needs, what it does to confidentiality, integrity and availability. The specification maps scores to words: 0.1 to 3.9 is low, 4.0 to 6.9 medium, 7.0 to 8.9 high and 9.0 to 10.0 critical. The base score assumes the reasonable worst case across every deployment, and FIRST states that factors like the number of customers affected or regulatory requirements are outside the scope of CVSS. Public databases such as NVD usually publish only that base score.

Impact-based ratings start from your app instead: who can do it, what they reach, what it costs you. The OWASP Risk Rating Methodology multiplies likelihood by impact, and says the business impact should win over the technical one when the tester knows it.

CVSS base scoreImpact-based rating
What it measuresThe flaw's technical traits, worst caseWhat this flaw exposes in this app
Same flaw, two appsSame scoreCan be two different levels
Best forKnown CVEs, forms that require a score, comparing across vendorsDeciding what to fix first in your own app
Weak spotCannot tell a table of blog posts from a table of passport scansOnly as good as the rules written next to it

A concrete case. NVD lists CVE-2025-48757, insufficient row level security in sites generated with Lovable (a CVE Lovable disputes), at 9.3, critical, in a CVSS 3.1 score from MITRE. That one number covers every affected app. Inside Pawpal, the same family of mistakes produces F2, critical because of the door codes. On the reviews table, Keybrum treats public reading as intended, since reviews hold no personal data, and reports only that anyone can add rows to it.

Keybrum's ratings are impact-based and follow written rules. Three facts set the level: who can do it (anyone, or only someone who signed up), what the data is (emails and phones rank below bank details, ID numbers, API keys, health data and access codes), and whether the attacker can read, add or change rows. Anyone reading emails and phone numbers is critical; the same read behind a free sign-up is high; access codes are critical either way. Whatever method a report uses, PCI's guidance asks it to document how each rating was derived, and OWASP suggests explaining the scale in an appendix.

What to avoid: scanner dumps and unproven findings

Merely reporting lists of vulnerabilities is not helpful in this endeavor and does not meet the intent of the penetration test.
PCI Security Standards Council, Penetration Testing Guidance (2017)

Four patterns fill pages without telling you anything:

  • Findings without proof. A sentence like the application may be vulnerable to with no request and no response is a guess. Ask for the request, the identity that sent it and the answer. The cost of guesses is real: curl ended its bug bounty in January 2026 after a flood of AI-generated reports, and its maintainer wrote that in 2025 fewer than one submission in twenty was a real vulnerability, against more than 15% in earlier years.
  • Scanner dumps. Pages of raw tool output pasted in as findings. OWASP's guide tells testers to clean tool output and not just dump it. Twenty lines about missing headers hide the one finding that matters.
  • CVEs claimed from a version number. A Next.js version inside the range of CVE-2025-29927 is not a finding on its own: the advisory says the bypass matters when the authorization check happens in middleware, and that deployments hosted on Vercel are protected automatically. A credible report shows the protected page answering when it should not.
  • No scope, no list of what was not tested. Zero critical findings on half your app reads like a clean result for all of it.

Proof is also the line between a pentest and a scan. Our page on pentest vs vulnerability scan covers when each one is the right tool.

Retest status: when a finding counts as fixed

A report is a snapshot, and the retest turns it into a result. PCI's guidance suggests a retest report with an executive summary, the dates of the original test and of the retest, the original findings and the results of the retest. OWASP's guide suggests a section with the updated status of every finding from the previous test.

In the Pawpal report, a finding closes only when the same attack fails on the live app. Closed findings are listed in two groups:

  • Fixed: the finding, and the date the attack failed.
  • Intended: a finding you marked as expected behaviour, with your reason, for example a bucket of public photos.

The appendix lists every test with one of four outcomes: worked (a finding holds the proof), refused (the app resisted), unclear (an error or an unexpected answer, never reported as a finding) and skipped (not run, with the reason). The refused lines matter as much as the findings. In Pawpal, the messages and payouts tables are protected correctly, and the attacks on them are listed as refused: that is how you know they were tested and not forgotten. For the variants worth retesting after a fix, such as sibling routes and other methods on the same data, see the web app pentest guide.

A pentest report template you can reuse

Use this outline for a report you write, or as a checklist for one you receive. It follows the PCI and OWASP outlines and adds the parts founders most often have to chase: who can do it, the evidence and a dated retest.

markdown
# Pentest report: <app name>
Target: <URL, environments>        Tested: <dates and times, UTC>
Tester: <person or tool, contact>   Report: <date, version>
Authorization: <who approved, how ownership was proven>

## 1. Summary
What was tested, what a stranger can do today,
open findings by severity, what to fix first.

## 2. Findings at a glance
| Ref | Severity | Finding: who can do what, to how much data | Who can do it | Status |

## 3. Scope and coverage
In scope: hosts, identities (no account, user A, user B, admin), features.
Not tested, and why.

## 4. Method and limits
Black, grey or white box. Accounts used. Tools and versions.
Time window. Anything that blocked or slowed testing.

## 5. Findings (one block per root cause)
Ref:            F2
Severity:       Critical (rule used: see appendix)
Who can do it:  Needs only a free sign-up
Proven on:      <date>
Title:          <who> can <do what> to <which data>: <how much>
What happened:  <the attack, stated as a fact, with the identity used>
Impact:         <what you lose: data, money, accounts, trust>
Evidence:       <request, response status, redacted excerpt, replay command>
Fix:            <the change, at the right layer, with code or SQL>
Retest:         <open | fixed: attack failed on <date> | accepted: <reason>>

## 6. Retest results
Date of the original test, date of the retest, each finding and its new result.

## Appendix
How severity is decided. Every test and the app's answer. Tool output, cleaned.
Copy it into a doc. Write one block per root cause: if three requests share one fix, they are one finding.

Before you accept a report, from us or anyone else, run through it once:

  • Every finding has the request, the identity that sent it and the response.
  • Each severity says who can do it and what they get, and the appendix explains the scale.
  • The scope lists what was not tested, and why.
  • Each fix names the layer to change: a policy, a server check, a header, an upgrade.
  • Personal data in the evidence is masked.
  • Every closed finding has a retest date and the result of that retest.
  • The report says it is a point-in-time result, not a guarantee that nothing else is wrong.

Which report to send a customer

Do not send the full report to a prospect who asks for your latest pentest. It holds working evidence of your weaknesses, and Keybrum's full report says so in its footer: share it only with people who need it. For customers and security questionnaires, send a summary: who tested, when, the scope, the method, and how many findings are open by severity.

On a plan, Keybrum publishes that second document for you: a public trust page and report that show what was tested and when, the scope and the method, with counts and never details. Our guide to security questionnaire answers covers the pentest questions that come up in vendor reviews.

Get a report like this on your own app

Many samples that rank for this search sit behind a request form and describe a fictional company. The fastest way to see what a report on your app would say is to run one on it.

Keybrum's one-time report costs €49 for one app. You prove you own the app, then Keybrum attacks it from the internet with a library of more than 100 attacks, as a stranger and as a freshly signed-up account, and you get every finding in the format above: proof, impact, fix, and a prompt for your coding agent, which can also read the findings through Keybrum's MCP server. Plans start at €29 a month, attack every deploy and keep retests open (prices exclude VAT). For what a human-led test costs, see how much a pentest costs; for what software can and cannot prove, see automated pentests.

Questions

Where can I find a sample penetration test report?

Security firms publish samples, often behind a request form, and TCM Security shares a free barebones demo report on GitHub with a single finding. This page walks through a real finding from Keybrum's report on Pawpal, a demo app built to be attacked. To see the same format on your own app, run a one-time report.

What should a pentest report template include?

A summary, the scope with what was not tested, the method and its limits, a findings table, and one detailed block per finding: severity, who can do it, what happened, impact, evidence, fix and retest status. Add retest results and an appendix that explains the severity scale. The template on this page has all of it.

Is a vulnerability scan report the same as a pentest report?

No. A scan report lists what a tool thinks might be wrong, often from version numbers and banners. A pentest report shows what worked, with the request and the response. See pentest vs vulnerability scan.

Does a pentest report need CVSS scores?

Only if an auditor or a customer requires them. OWASP's testing guide notes that a CVSS score is required on some engagements and can just add complexity on others. What every report needs is a written explanation of how each severity was decided, which PCI's guidance asks for too.

Can I share my pentest report with customers?

Share a summary rather than the full findings: who tested, when, what was in scope, and open findings by severity. The full report holds working evidence of your weaknesses. On a plan, Keybrum's public trust page shows counts and dates, never details.

What does retest status mean in a pentest report?

It says whether each finding was attacked again after the fix, when, and with what result. A finding marked fixed without a retest date is only a claim. In Keybrum's reports, a finding closes only when the same attack fails on the live app.

Sources

  1. 1.Information Supplement: Penetration Testing Guidance, version 1.1, PCI Security Standards Council
  2. 2.Web Security Testing Guide v4.2: Reporting, OWASP
  3. 3.PTES Technical Guidelines: Reporting, Penetration Testing Execution Standard
  4. 4.CVSS v4.0 Specification Document, FIRST
  5. 5.OWASP Risk Rating Methodology, OWASP
  6. 6.CVE-2025-48757, NIST National Vulnerability Database
  7. 7.Authorization Bypass in Next.js Middleware (GHSA-f82v-jwr5-mffw, CVE-2025-29927), GitHub Advisory Database
  8. 8.The end of the curl bug-bounty, Daniel Stenberg
  9. 9.TCM Security Sample Pentest Report, TCM Security

Your app is public. So are its mistakes.

Check what any attacker can already see in it, free, in ten seconds. The full attack, with proof and fixes: €49 once, or €29 a month on a plan.

or continue with Google or GitHub