You can be the most talented tester in the room, chain three vulnerabilities into full domain compromise, and still deliver a report that gets ignored. The finding is only half the job. The report is the product the client actually pays for, and it is the only artifact that outlives the engagement.
New testers consistently underinvest here. They spend three weeks on the assessment and three hours on the write up, then wonder why the same findings reappear at the next annual test. A good report is not a data dump of tool output. It is a decision making document that tells a busy reader what is broken, how badly, how you proved it, and exactly what to do about it.
This guide walks through everything a beginner should include in a penetration testing report, with real commands for collecting defensible evidence, a repeatable finding structure, accurate severity scoring, and the tooling that turns scattered notes into a professional deliverable.
A report has two audiences that want different things from the same document. Executives want risk in business terms and a clear answer to "how exposed are we?" Engineers want reproducible steps and a precise fix. If you write for only one of them, half your readers stop reading.
The report is also your legal and professional record. It defines what you were authorized to do, what you actually did, and what you found. When a client disputes a finding six months later, or an auditor asks for proof of testing, that document is what stands. Treat it as evidence, not as a summary.
Beginners who understand this early tend to grow faster, because the discipline of clean documentation forces clean thinking about impact and root cause. Our consultants at Redfox Cybersecurity review the report structure with every new tester before they review their exploitation skills, precisely because the report is where careers are made or stalled.
A complete report has a predictable skeleton. Consistency helps the reader, and it helps you, because a template means you never forget the scope statement or the retest plan under deadline pressure.
This is the one page a CISO or board member will read. Write it last, write it in plain language, and never put a raw CVE number or a payload in it. State what was tested, the overall risk posture, the two or three themes that matter, and the headline count of findings by severity.
A weak executive summary says "we found 14 vulnerabilities." A strong one says "an unauthenticated attacker on the guest network could reach domain administrator within one day, primarily due to weak service account passwords and missing network segmentation." One of those sentences triggers budget. The other triggers a shrug.
Record exactly what was in scope, what was excluded, the testing window, the source IP addresses you tested from, and any constraints such as no denial of service or business hours only. This section protects both parties. It is the first thing a lawyer reads if anything goes sideways.
Capture your testing source addresses at the start so the client's blue team can correlate your activity against real attacks later.
# Record the engagement's source identity at kickoff
echo "Engagement: ACME-EXT-2026-014" > engagement_meta.txt
curl -s https://api.ipify.org >> engagement_meta.txt # external egress IP
ip -o addr show | awk '{print $2, $4}' >> engagement_meta.txt
date -u '+Start (UTC): %Y-%m-%dT%H:%M:%SZ' >> engagement_meta.txt
# Hash the scope document so its integrity is provable later
sha256sum scope_authorization.pdf >> engagement_meta.txt
cat engagement_meta.txt
[cta]
Explain how you tested so the results are credible and repeatable. Reference the framework you followed, such as PTES, the OWASP Testing Guide, or the OWASP Web Security Testing Guide, and describe the phases you moved through: reconnaissance, enumeration, exploitation, post exploitation, and cleanup. This tells the reader your findings came from a structured process, not from running one scanner and pasting the output.
This is the heart of the report and gets its own structure below. Every finding must stand alone as a complete unit.
Consolidate the fixes into a prioritized plan. Engineers do not want to hunt through 20 findings to build a work queue. Give them one ordered list: fix these three things this week, these five this quarter.
Full port scans, tool versions, wordlists used, and a complete list of tested endpoints. Raw data lives here so the body of the report stays readable.
Inconsistency in findings is the fastest way to look like a beginner. Use the same field order for every single finding, from the critical to the informational. A reliable structure looks like this:
Here is what the reproduction and evidence portion of a well documented injection finding actually contains. Notice that it is precise enough for a developer to run without asking you a single follow up question.
POST /api/v2/invoices/search HTTP/1.1
Host: billing.acme.example
Content-Type: application/json
Cookie: session=guest-anon-77f1
{"ref":"INV-2026' UNION SELECT table_name,2,3 FROM information_schema.tables-- -"}
[cta]
Then show the confirming behavior and, where authorized, controlled extraction. Prefer a tool that produces a clean, auditable log over manual guessing.
# Confirm and characterize with sqlmap, capturing a full session log
sqlmap -r invoice_search.req \
--batch --risk=2 --level=3 \
--technique=BEUST \
--dbms=postgresql \
--dbs \
--flush-session \
--output-dir=./evidence/sqli \
-v 3 | tee evidence/sqli/sqlmap_run.log
# Enumerate only what proves impact, then stop. Do not exfiltrate real PII.
sqlmap -r invoice_search.req --batch \
-D billing -T users --columns \
--output-dir=./evidence/sqli
[cta]
The discipline of extracting only enough to prove impact, rather than dumping an entire customer table, is a professional and often contractual boundary. State in the finding what you accessed and confirm that you did not remove sensitive data.
Beginners tend to rate everything critical, which trains clients to distrust every rating you produce. Severity has to be defensible. Use CVSS as the anchor, show the full vector string, and adjust with environmental context where you have it.
Do not eyeball the score. Compute it, ideally programmatically, so it is reproducible.
# pip install cvss
from cvss import CVSS3
# Unauthenticated network SQLi, no privileges, no user interaction,
# high confidentiality impact, low integrity/availability
vector = "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:L/A:N"
c = CVSS3(vector)
print("Base score :", c.base_score) # 8.2
print("Severity :", c.severities()[0]) # High
print("Vector :", c.clean_vector())
# Apply environmental context: this DB holds regulated PII, raise conf. requirement
env_vector = vector + "/CR:H"
ce = CVSS3(env_vector)
print("Environmental :", ce.environmental_score, ce.severities()[2])
[cta]
Explain in one sentence why the score is what it is. "Rated High rather than Critical because exploitation requires network access to an internal only endpoint" builds far more trust than a naked number. When a client sees that you actively argued a score down, they believe the ones you argue up.
For teams standardizing their rating approach across many engagements, the methodology guidance from Redfox Cybersecurity helps keep severity consistent between different testers on the same account.
Evidence is what separates a finding from an opinion. A screenshot with a timestamp, a captured request and response pair, and a hash of any extracted file make a finding indisputable. Sloppy evidence gets findings downgraded or dismissed during the client's internal review.
Build an evidence directory per finding and hash everything so integrity is provable.
# One directory per finding keeps evidence traceable
FIND="F-07_sqli_invoice_search"
mkdir -p evidence/$FIND
# Capture the full HTTP exchange, not just a screenshot
curl -sik -X POST "https://billing.acme.example/api/v2/invoices/search" \
-H "Content-Type: application/json" \
--data '{"ref":"INV-2026'\'' AND 1=1-- -"}' \
> evidence/$FIND/request_response_true.txt
curl -sik -X POST "https://billing.acme.example/api/v2/invoices/search" \
-H "Content-Type: application/json" \
--data '{"ref":"INV-2026'\'' AND 1=2-- -"}' \
> evidence/$FIND/request_response_false.txt
# Timestamped terminal capture for reproducibility
script -q -c "nmap -sV -p 443 billing.acme.example" \
evidence/$FIND/nmap_service.log
# Integrity manifest across all collected artifacts
find evidence/$FIND -type f -exec sha256sum {} \; \
> evidence/$FIND/MANIFEST.sha256
cat evidence/$FIND/MANIFEST.sha256
[cta]
For screenshots, capture the full browser chrome including the URL bar and the system clock, then annotate the exact element that proves the point. An unannotated screenshot forces the reader to hunt for what you saw, and readers who have to hunt tend to disbelieve.
When you redact, redact irreversibly. Blurring is reversible and has burned testers before. Draw solid black boxes and flatten the image.
# Irreversible redaction: draw an opaque box, then strip metadata and re-encode
convert screenshot_raw.png -fill black \
-draw "rectangle 340,210 900,260" redacted.png
exiftool -all= redacted.png
convert redacted.png -flatten evidence/$FIND/screenshot_redacted.png
[cta]
The impact statement answers "so what?" Translate the technical flaw into consequence. "SQL injection" means nothing to a finance director. "An anonymous attacker on the internet can read every customer's billing history and payment references" triggers action. Tie the impact to data, money, availability, or compliance obligations the client already cares about.
Remediation must be specific and actionable. "Sanitize user input" is not remediation, it is a platitude. Give the exact fix and, where you can, the code.
// Vulnerable: string concatenation into the query
String q = "SELECT * FROM invoices WHERE ref = '" + ref + "'";
// Remediated: parameterized query binds input as data, never as SQL
String sql = "SELECT * FROM invoices WHERE ref = ?";
try (PreparedStatement ps = conn.prepareStatement(sql)) {
ps.setString(1, ref); // input can never alter query structure
ResultSet rs = ps.executeQuery();
}
[cta]
Layer the defenses in the remediation text: parameterized queries as the primary fix, least privilege on the database account as defense in depth, and a web application firewall rule as a stopgap while code ships. Ordering these by effectiveness tells the engineering team what actually closes the hole versus what merely slows an attacker.
Nobody wants 4,000 lines of scanner output pasted into a Word file. Part of reporting maturity is converting machine output into curated, human readable findings, while keeping the raw data in an appendix for verification.
Export scans in structured formats so they feed cleanly into a reporting pipeline rather than being retyped by hand.
# Nmap: emit all formats at once, including XML for parsing
nmap -sV -sC -oA appendix/full_portscan --open acme.example
# Convert the XML into a readable HTML appendix
xsltproc appendix/full_portscan.xml -o appendix/full_portscan.html
# Nuclei with structured JSON output for programmatic import
nuclei -l live_hosts.txt \
-severity medium,high,critical \
-jsonl -o appendix/nuclei_results.jsonl \
-stats
# Reduce the JSONL to a triage table you can review before writing findings
jq -r '[.info.severity, .info.name, .host] | @tsv' \
appendix/nuclei_results.jsonl | sort | column -t
[cta]
For assembling the report itself, learn a dedicated collaboration and reporting platform early rather than fighting a word processor. Ghostwriter, Dradis, Faraday, and PlexTrac are all built for this. They let you maintain a reusable finding library, so your standard remediation text for a given vulnerability is written once and reused with consistent quality.
# Ghostwriter: self-hosted collaborative reporting via its official compose setup
git clone https://github.com/GhostManager/Ghostwriter.git
cd Ghostwriter
./ghostwriter-cli-linux install
# Faraday: multi-tool workspace that ingests scanner output directly
faraday-cli workspace create acme-ext-2026
faraday-cli tool report appendix/full_portscan.xml --workspace acme-ext-2026
faraday-cli tool report appendix/nuclei_results.jsonl --workspace acme-ext-2026
[cta]
A finding library also enforces the consistency that clients notice. When two testers on the same account describe the same TLS misconfiguration in identical, correct language, the client sees a mature practice rather than individual freelancers. The team at Redfox Cybersecurity maintains a vetted finding library for exactly this reason.
A few patterns show up again and again in early reports, and every one of them is avoidable.
Rating everything critical. If nine of your ten findings are critical, none of them are. Calibrate honestly and the client trusts your one real critical.
Pasting scanner output as findings. A Nuclei hit is a lead, not a finding. Verify it manually, confirm exploitability, and rewrite it in your own words with proof. Unverified automated output belongs in the appendix labeled as such.
Missing reproduction steps. If a developer cannot reproduce the issue from your report alone, they will close it as "cannot reproduce" and you will argue about it for a week.
Vague remediation. "Implement security best practices" tells no one to do anything.
No positive observations. Note the controls that worked. A report that only lists failures reads as adversarial. Acknowledging that MFA was enforced or that egress filtering blocked exfiltration attempts makes the whole document more credible and more balanced.
Forgetting the retest plan. State how findings will be verified as fixed, and by when. A finding with no closure path tends to stay open forever.
Once you have written a handful of reports, extract the structure into a template so you never start from a blank page under deadline. A Markdown template that renders to PDF keeps the content in plain text and version controllable.
# Render a Markdown report to a branded PDF with a title page and TOC
pandoc report.md \
--from markdown \
--template=corp_template.tex \
--toc --toc-depth=3 \
--number-sections \
-V geometry:margin=1in \
-V colorlinks=true \
--metadata title="External Penetration Test: ACME Corp" \
--metadata date="$(date -u +%Y-%m-%d)" \
-o ACME-EXT-2026-014_Report.pdf
# Verify the deliverable's integrity before sending
sha256sum ACME-EXT-2026-014_Report.pdf | tee report.sha256
[cta]
Version control the template, not the client data. Keeping engagement content in an access controlled reporting platform while the reusable template lives in a repository gives you both consistency and confidentiality.
Every beginner's penetration testing report should include a plain language executive summary, a precise scope and rules of engagement section, a stated methodology, consistently structured findings, defensible CVSS scoring, reproducible evidence, business focused impact, specific remediation, and a retest plan. Those elements turn a list of vulnerabilities into a document that drives real change.
The finding is where you prove skill, but the report is where you prove value. Collect evidence as if it will be challenged, because it might be. Score severity as if a peer will audit it, because one will. Write remediation as if the engineer has never met you, because usually they have not.
Learn a proper reporting platform early, build a finding library so your best writing is reused, and template everything you can so deadline pressure never degrades quality. Do that consistently and your reports will get read, acted on, and remembered, which is the entire point of the work.