Writing · 16 January 2026
Stop Treating Pentests Like Final Exams: Why Security Can’t Be Your Last Step
A pentest that finds critical vulnerabilities hasn't done its job; it has reported the failure of everything upstream of it. How to build it in instead.

ℹ️ Rewritten 18 September 2026
I first published this post on 16 January 2026, and I have since rewritten it. The cost comparison it originally contained has been replaced with published UK pentest pricing you can check for yourself. The continuous integration and deployment (CI/CD) examples referenced GitHub Actions that were unpinned, deprecated or, in one case, a version that was never released; they have been rebuilt and pinned to commit SHAs. A link that pointed at this post instead of the attestations post has been fixed.
“The pentest report just came back. We have 47 critical vulnerabilities. Release is tomorrow.”
I have heard variations of that sentence more than once. The details are composited across several engagements, so that no single team is identifiable. The shape of it is real enough: a team treats a penetration test, or pentest, as a final exam they can cram for, and discovers they have been building on quicksand for months.
If your pentest reveals critical vulnerabilities, it is not doing its job. It is reporting the failure of everything upstream of it.
The Pentest-as-Final-Exam Antipattern
The pattern is familiar enough to be a script. A team builds features for three to six months. A fortnight before release somebody remembers the security sign-off, an external pentest is booked, and the report arrives with a week to go. The choice narrows to delaying the release or shipping known vulnerabilities. Everybody promises to do better next time, and the next release runs the same script.
This treats security the way we treat spell-check: a pass you make just before publishing. Typos embarrass you. Vulnerabilities lose you customers, contracts and, if the data was sensitive enough, the business.
A properly positioned pentest is a verification tool rather than a discovery tool. It should confirm that your security practices work, because finding out whether you have any is a far more expensive question to ask a fortnight before release. Think of the difference between a metal detector sweeping for landmines and a final inspection confirming the ground is clear. In the second case, you have been clearing mines all along.
Building Security Into Your SDLC
As Tanya Janca, application security expert, author, and OWASP Lifetime Distinguished Member, explained in a conversation with me about secure coding:
If you add a security step to each phase, you’ll build way better software. And that is a whole bunch of activities, right? It’s not really one thing, but if your organization adds it to the way you build software, every single time you’ll release better software.
That is the whole idea, and it is less work than it sounds. Every phase of your software development life cycle (SDLC) already has activities in it; the question is whether any of them stop to ask what an attacker would do.
Design comes first. Before any code is written, ask how someone could abuse the feature. Document your trust boundaries, work out where the attack surface actually is, and design for least privilege from the start, because retrofitting it once somebody has found a way around it is a much larger job.
During development, run security linters such as Semgrep or SonarQube alongside the ones you already have, and validate input on the client and on the server. Client-side validation is a user-experience feature; it stops nobody who is using curl. OWASP publish four free standards that are considerably more specific than “write secure code”: the Application Security Verification Standard, the Top Ten, the Web Security Testing Guide and the Secure Headers Project. I maintain OwaspHeaders.Core, which implements that last one for ASP.NET Core, so I have spent more time with those particular headers than is entirely healthy.
Every build should run static analysis, scan your dependencies for known vulnerabilities, scan container images if you ship them, and look for secrets that have been committed by accident. None of that needs a human in the loop once it is wired up.
Testing means dynamic scanning against a running instance, API testing, infrastructure scanning, and security cases sitting in your normal test suite next to everything else. The point is the cadence. All of it runs continuously, so the fortnight before a release holds no surprises.
Deployment is not the end of it either. A web application firewall, runtime monitoring, and an incident response plan somebody has actually rehearsed all matter after the code is live, because that is when it is exposed to people who did not read your documentation.
ℹ️ Note
These lists are not exhaustive. Treat them as a minimum.
The Tools Pentesters Use Are Available To You
“But we don’t have a security team.” You do not need one to start. A good deal of what an external tester will run against you is free, and you can run it against yourself first.
Each of the examples below pins its GitHub Action to a commit SHA. A tag can be moved and a branch changes under you, so @master hands the maintainer, or anyone who compromises them, write access to your pipeline. That is the supply-chain problem this whole post is about, and it would be a strange thing to demonstrate carelessly.
ZAP (Zed Attack Proxy)
ZAP is free, open source, and built for automation:
# Add to your CI/CD pipeline
- name: ZAP Scan
uses: zaproxy/action-full-scan@3c58388149901b9a03b7718852c5ba889646c27c # v0.13.0
with:
target: 'https://staging.yourapp.com'
rules_file_name: '.zap/rules.tsv'
cmd_options: '-a'
More information about ZAP can be found at zaproxy.org.
Burp Suite
Burp Suite is the commercial option, and it is worth being clear about which edition does what, because the licences are not interchangeable. Burp Suite Professional is the manual testing tool: the proxy, the repeater and the scanner that a person drives while probing an application by hand. It does not run in a pipeline, and its documentation covers no continuous integration at all.
The edition that belongs in CI/CD is Burp Suite DAST, formerly Enterprise Edition. It runs Burp Scanner from a Docker container in your pipeline, configured from a YAML file and writing its results as JUnit or Burp XML, so findings arrive next to your test output.
That distinction is the one to settle before spending anything. ZAP already covers the automated scanning described in this post, at no cost. Professional buys depth for manual testing; DAST buys the pipeline integration.
More information about Burp Suite can be found at PortSwigger.
Semgrep
Semgrep does static analysis, catching vulnerabilities before anything runs. It is worth noting how this one has changed: the returntocorp/semgrep-action that most tutorials still reference is deprecated, and its own repository now says so. The current approach runs the Semgrep container directly, which is what Semgrep themselves document:
jobs:
semgrep:
runs-on: ubuntu-latest
container:
# Pinned to a release rather than `latest`, which moves
image: semgrep/semgrep:1.177.0
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: semgrep scan --config p/security-audit --config p/owasp-top-ten --error
More information about Semgrep can be found at semgrep.dev.
Integration Strategy
These are not one-time tools. The cadence matters as much as the coverage, and it follows from what each check costs: the fast, cheap ones run on everything, and the slow, expensive ones run where a delay is affordable. That gives you roughly this:
- Every Pull Request: Semgrep for code analysis
- Every Merge to Main: ZAP basic scan
- Every Release Candidate: Full security suite
- Weekly: Full infrastructure scan
- Before External Pentest: All of the above
A Complete Security Pipeline
Here is the whole thing as one workflow. Semgrep runs in its own job because a container applies to a job rather than a step, and the dynamic scan runs only against main, since there is no point scanning a staging environment that a feature branch has not reached yet.
name: Security Pipeline
on: [push, pull_request]
permissions:
contents: read
jobs:
semgrep:
runs-on: ubuntu-latest
container:
image: semgrep/semgrep:1.177.0
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- run: semgrep scan --config p/security-audit --config p/owasp-top-ten --error
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
# Flags vulnerable dependencies added by a pull request.
# Needs the dependency graph enabled on the repository.
- name: Dependency review
if: github.event_name == 'pull_request'
uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0
with:
fail-on-severity: high
# Filesystem scan; swap scan-type for 'image' to scan a built container
- name: Trivy scan
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
scan-type: fs
severity: CRITICAL,HIGH
exit-code: '1'
- name: SonarQube Analysis
uses: SonarSource/sonarqube-scan-action@ba9859eae8dd6bd29e412f25ddbbef3d032000f4 # v8.2.2
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
- name: Upload Security Reports
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: security-reports
path: |
*-report.html
*-report.json
dast:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: ZAP Scan
uses: zaproxy/action-full-scan@3c58388149901b9a03b7718852c5ba889646c27c # v0.13.0
with:
target: 'https://staging.yourapp.com'
ℹ️ Note
The above is provided as an example, please edit it to fit your purposes.
ℹ️ Warning
The above example was correct at the time of writing. However, we take no responsibility for any harm, misconfiguration, or other negative effects by your use of it.
As with all code you find on the internet, it is your responsibility to vet its contents before using it. That includes checking that the pinned SHAs above still point where you expect; a pin is a statement about a moment in time.
Beyond Tools: The Security Mindset
Tools catch vulnerabilities. Secure design prevents them, and the gap between those two things is most of the work.
Input validation is the obvious example. String concatenation into SQL is the vulnerability that every scanner on the list above will find, and the one that should never have been written:
// Bad: Trusting user input
public IActionResult Search(string query)
{
var sql = $"SELECT * FROM products WHERE name LIKE '%{query}%'";
// SQL injection vulnerability
}
// Good: Parameterised queries
public IActionResult Search(string query)
{
var results = _context.Products
.Where(p => p.Name.Contains(query))
.ToList();
}
Authentication and authorisation are where the subtler problems live. Session management, a password policy that reflects current guidance rather than 2009’s, multi-factor authentication wherever you can get away with it, least privilege applied to real accounts, and enough logging that you could reconstruct what happened. A scanner will tell you that a page is unauthenticated; it will not tell you that a user can read another tenant’s invoices by changing a number in the URL.
Data protection is the last of it. Encrypt sensitive data at rest, use TLS in transit, manage your keys somewhere other than the repository, and sanitise your logs and error messages. Stack traces returned to the browser are a reconnaissance gift.
Preparing for an External Pentest
You should still book one. The point of everything above is to change what it finds, and a little preparation changes what you get for the money.
Start roughly eight weeks out with an internal security sprint: run every automated tool you have, fix the critical and high findings, update your dependencies, and review your security configuration. Around six weeks out, stand up an environment that genuinely mirrors production, with the same infrastructure, OS versions, dependencies, configuration and realistic data patterns. A pentest against an environment that differs from production tests something you are not shipping.
At four weeks, have your own team try to break it, using the OWASP Top Ten as a checklist and paying particular attention to authentication, authorisation, input validation and information disclosure. That leaves the final fortnight to fix what you found, re-test the fixes, and write up your security measures so the external team can spend their days attacking your application instead of working out how it fits together.
Then, on the day, the external pentest should be finding advanced attack vectors, business logic flaws, complex chained attacks and genuine oversights. If it comes back with SQL injection in a login form, unpatched dependencies, default credentials or missing HTTPS, you have paid a specialist day rate for something a free scanner would have told you in an afternoon.
What A Pentest Actually Costs
The number is smaller than most people expect, and a few UK firms publish it. All the figures below were published as of September 2026; these pages are revised in place, so check them before quoting them back at anyone. Precursor Security, accredited by CREST, the body that certifies penetration testing providers, list a normal web application test at £3,750 to £6,250+ and price their work at approximately £1,200 per accredited consultant day. SECFORCE put a typical web application test at around six days and £6,000, and give £1,000 to £1,500 a day as the range for thorough manual testing. Aptive price a small static site with simple or no authentication from £2,000, and anything with real authentication, payment processing and API integrations from £3,250.
Two firms arriving independently at roughly £1,200 a day, and at roughly £6,000 for a six-day web application test, is about as close to a market rate as published pricing gets. Both also flag the same trap: SECFORCE call anything under £500 a day “likely not a real pen test”, and Precursor say day rates below that mark usually indicate automated scanning rather than manual testing. If a quote looks cheap, check how many days it buys and who is spending them.
So a serious test of one web application is a few thousand pounds and a week or two of somebody’s calendar. That is worth sitting with, because the pentest was never the expensive part of the story I opened with.
What costs money is when you find out. A vulnerability caught during the sprint that introduced it is a code change, reviewed and merged like any other. The same vulnerability caught by an external tester eight days before release is a code change, plus a re-test, plus the scheduling, plus the argument about whether to ship anyway, plus whatever the slipped date does to everything depending on it. Same defect, different week, considerably more expensive.
I am not going to put a figure on that second scenario. It depends entirely on your release, your contracts and your customers, and a number I invented would be worth less to you than the arithmetic you can do yourself. Take your own cost of a week’s delay, multiply it by the delay you would realistically accept, and set that against the cost of a scanner running on every pull request.
Rolling This Out
None of this arrives in a weekend. A realistic first month is basic scanning wired into CI/CD, the team walked through the OWASP Top Ten, security questions added to your code review checklist, and threat modelling on new features only. Do not go back and threat model the existing estate; you will lose interest by the third one.
The second month is automation: static analysis on pull requests, dependency scanning, dynamic scanning against staging, and a handful of security test cases written like any other test. By the third month you can run your first internal pentest, name someone on each team as the person who cares about this, and write the incident response plan you hope not to need.
From the fourth month onwards it stops being a project and becomes maintenance. Regular internal assessments, external pentests at whatever cadence you can afford, and some tracking of whether the numbers are moving. If you want a single thing to do this week, install ZAP, run it against staging, and share what comes back with your team. That conversation tends to do more than any amount of planning.
Where This Fits With SBOMs and Attestations
I have written before about SBOMs and attestations, and they answer different questions to this one. A software bill of materials (SBOM) tells you what is in your software. An attestation proves it was built the way you say it was and has not been tampered with since. Neither of them has anything to say about whether your login form is injectable.
You need all three, because they cover different failures: know your components, verify your supply chain, and validate your own implementation. A perfect supply chain delivering vulnerable code is still vulnerable code.
Start Before The Next Release, Not During It
Pentests are progress reports rather than final exams. If yours is discovering critical vulnerabilities, the failure happened months earlier, in all the places where nobody asked how this could be abused. Security is a practice, not a phase.
So start small. Run ZAP this week. Add Semgrep to your pull request checks. Build the habits while nothing is on fire, and when the external pentest arrives it will confirm your security rather than reveal its absence.
Your customers trust you with their data. Honour that trust by building security in rather than bolting it on. The best time to start was at project kickoff; the second best time is now.
If you’d like to shift your security posture from reactive to proactive, let’s talk.