The report arrives as a PDF with a severity column and a recommendation column, and the temptation is to sort by severity and start at the top. Do that second.

This is the mechanical follow-on to who closes penetration test findings. That piece argues about ownership. This one assumes you own it.

First, get it out of the PDF and into something you can work in. A row per finding, with columns for the application it belongs to, the severity, the CVSS score, the recommended fix, a disposition, and a free-text field for what you actually did. That last column is the one that matters later, and nobody creates it up front.

Sort by severity, then regroup by application

Severity tells you what to care about. Application tells you how to schedule the work.

A platform assessment comes back with findings spread across several components, each with its own codebase and release cycle. Sorted by severity alone, that is a to-do list that bounces you between all of them.

Regrouped by application, it is a handful of focused pieces of work. You open the API project once and close everything in it: the injection issue, the cipher suite policy, the CORS configuration, the error-handling settings, the version headers. One branch, one deploy, one round of verification.

Every finding gets a verdict, not just the ones you fix

Four dispositions, and everything lands in exactly one:

Remediate. You changed something.

False positive. The finding is not true of your system, and you can show why.

No remediation needed. The finding is accurate but does not warrant action, and you can say why. A certificate flagged as nearing expiry when the platform renews it automatically. An informational disclosure that is a normal consequence of running behind a CDN.

Deferred. Real, not fixed now, with a stated reason and a cycle it belongs to.

That last one needs discipline. Deferring is legitimate. Deferring without writing down the reason is how a finding shows up again on next year's test with nobody able to explain the intervening twelve months.

The proportions are more consistent than people expect. A little over half the report gets remediated. A fifth or so turns out not to be true. A handful are accurate and warrant no action. A few are real, deferred, and written down as deferred. If your tracker shows ninety percent remediated, you have not triaged — you have complied.

A fifth of the report is usually not real

That is not a bad testing firm, it is what happens when tools do a lot of the work, and it is the part of the process nobody budgets for. Hold any such count loosely, though - as the next section shows, a finding can be half a false positive.

The pattern is almost always the same: a scanner reads an artifact it does not fully understand and reports the shape of a problem rather than a problem.

  • A configuration flag reported as disabling transport security, where the tool had misread the value and both the source and the shipped build were set the safe way.
  • A weak cipher mode flagged accurately, inside a widely used third-party library rather than in any code the team wrote - which makes it a supply chain item with a different owner and a different fix, not the application defect the report described.
  • Missing exploit mitigations reported against an installer whose executables all carried them.
  • Memory-unsafe functions attributed to a compiled binary produced by a toolchain that does not emit them.

Disproving a finding takes about as long as fixing an easy one, and it produces no visible change, which is exactly why teams skip it and just "fix" things that were never broken. Do the work. Write the disproof down. In a regulated context - an FDA submission especially, where a deficiency letter will ask for exactly this - you will be asked to justify every disposition, and "we looked and it was not there" is not a justification.

A finding can be half true

The most dangerous disposition is the one applied to a whole finding when only part of it was checked.

A finding reports hard-coded secrets across several artifacts. Somebody reviews the two that are easy to check, finds nothing

  • and marks the whole finding a false positive. They were right about the part they checked. The artifact nobody opened did have credentials compiled into it.

That is worth sitting with, because it is a different class of problem to the scanner artifacts above. In the version of this I have seen, the pipeline was already configured to inject secrets properly. The infrastructure to do it correctly existed and was bypassed once, and nothing in the process caught it. That is exactly the failure mode you get when nobody senior is reading what goes into the repository.

Two lessons. Add secret scanning to CI so the next one fails the build rather than waiting for an annual test. And when a finding names several components, disposition each component separately. A single verdict on a multi-component finding is how a real problem gets filed under "not real."

The report's own false-positive calls can be wrong too

The most useful things I have found in a report are findings the testers already dismissed. Deprecated transport versions get flagged, someone checks manually, the check comes back clean, and both get marked false positive - because the manual verification was run against a different endpoint than the scan hit.

The findings were accurate. They were labeled as noise, and they would have shipped as closed. Read the findings the testers dismissed with the same attention as the ones they raised. Nobody else is going to.

Arguing with a severity rating

Two shapes are worth describing because they run in opposite directions.

A critical that was not. An interface reachable without authentication gets rated critical on the assumption that reaching it means arbitrary code execution. Then you account for the control one layer down that the tester had no visibility into - a signature requirement on anything the interface accepts, say - and the real exposure is a denial of service rather than code execution. Show the control, get the rating corrected, document the mitigation.

That shape recurs everywhere - an interface that looks wide open until you account for a control one layer down that the tester had no visibility into.

That conversation is worth having. Severity ratings are the assessor's view of generic risk in the absence of full knowledge of your system. When you have knowledge they lacked, give it to them and get the report corrected. What you must not do is quietly downgrade a finding in your own tracker and leave the report saying critical.

A control that cannot be implemented as written. A report recommends certificate pinning in a mobile app, pinned to the API's own certificate. Where that certificate is managed by the cloud provider and rotates on a schedule, pinning the leaf ships an app that breaks itself several times a year.

The answer is usually to pin to the issuing root instead. It is a weaker pin than a leaf pin in theory. In practice, that root is the one issuing certificates for the load balancers the service runs on, an attacker cannot obtain a certificate under it for that hostname without compromising the provider, and it survives rotation.

"We cannot do this" would have been an accepted risk. It was not the only option. When a recommendation does not fit your architecture, the useful question is what the recommendation was protecting against and what else gets you there.

Fixing is not the only way to close a finding

A remediation column invites a binary reading: either the code changed or the finding is open. Most real programs are not binary. A finding can be closed by eliminating it, by mitigating it with a control that makes it not matter, or by accepting it with a stated rationale.

Regulators expect this. FDA's deficiency language asks for risk controls and, where you are not eliminating something, for compensating controls "described in an appropriate level of detail." Mitigation is an anticipated outcome, not an excuse you are sneaking past someone.

Three closure shapes are mitigations rather than fixes:

A control one layer down did the work. The unauthenticated update interface above was not re-engineered. The signature requirement on the loader already reduced the impact from code execution to a denial of service, so the closure was to document that control, restate the residual risk at its real level, and get the severity corrected in the report.

A different control met the same objective. Certificate pinning to the service's own leaf certificate was not implementable against a rotating managed certificate. Pinning to the provider's root achieved the objective the recommendation existed to serve. That is a mitigation by substitution, and it is the most useful kind: the recommendation failed, the goal did not.

The recommended control cost more than the risk. A header recommendation that would break legitimate cross-origin resources the product needs is a recommendation to decline - in writing, with what it would have protected against, why the exposure is acceptable, and what else is already in place.

The discipline that separates a mitigation from an evasion is whether you can state three things in writing: what the residual risk actually is, why it is acceptable, and what has to change for that answer to change. If you cannot state the residual risk, you have not mitigated anything - you have decided not to look.

The third one matters most and gets skipped most. A mitigation depends on something: a signature check, a network boundary, a provider's certificate hierarchy, a feature nobody uses yet. When that thing changes, the mitigation silently stops being one. Write down what it rests on, so the next person can tell.

Do more than the minimum while you are in there

Critical and high get fixed. That is not a decision.

But when you already have the API project open and the pipeline warm, adding a strict transport security header, removing version banners, turning off remote stack traces, and tightening a content security policy costs minutes each. Left alone they become a separate initiative with its own ticket, its own review, and its own deploy, six months from now, at ten times the cost.

A good share of what gets remediated on any engagement is mediums, lows, and informationals that got fixed simply because the file was already open.

Verify it yourself before the retest

A clean retest is not luck. Before one is scheduled, go through every remediated finding and confirm the fix from the outside: requested the endpoint, checked the response headers, negotiated the TLS session, read the shipped manifest rather than the source.

A developer saying a fix is deployed and the fix being live in the environment the tester will hit are different claims. The gap between them is usually a build that did not promote, a configuration that applies to one of three environments, or a change that works in the branch and was never merged.

If a provider runs the environment rather than your own team, budget extra time here, because an MSP is not a security program and "it was deployed" from a ticket queue is a claim, not evidence.

You want to find that. The alternative is that the testing firm finds it, at which point you are paying for a third round and explaining the gap to whoever is waiting on the clean report.