Black Shard

Insights6 February 2026

Turning penetration test findings into work your engineers will finish

A report is ordered by severity because that is how a tester writes it, and a codebase changes by root cause. The re-sort, the rule that keeps a class shut once you close it, and the closure record a retester can check without asking you.

Sorted machine screws grouped in the compartments of a steel parts tray on dark slate, lit cold with a cyan highlight

Why a severity-ordered findings list is not a remediation plan

A report arrives sorted by severity because severity is the only ordering a tester can produce without knowing your codebase, and each finding marks a place where something observable went wrong from outside. Engineers do not change a system one observation at a time. They change a function, a template, a middleware layer or a configuration value, and one of those changes can move ten rows on the report at once.

The first act of remediation is a re-sort. Trace each finding back to the decision that produced it, then group the findings that share one. The count drops, sometimes sharply, because one templating choice or one missing check point can generate a page of report rows. Each of the resulting items can carry an owner, a design and a test strategy. A single row carries a severity label and an argument about whether the label is right.

Verify each collapse before you merge tickets, because two findings that look identical can have different causes. One cross-site scripting flaw comes from a server-side templating bypass, the other from a client-side sink that never touches a template. Merged into one ticket, one gets fixed and the other is closed by association. Keep the original finding identifiers attached to the work, because a retester will ask about them individually.

  • Cross-site scripting findings across several pages usually trace to one templating decision: escaping is on by default, somebody added a raw-output helper for one legitimate case, and it is now called in places nobody has audited.
  • A run of broken access control findings on different endpoints is one missing check point, because the check lives in each handler and any handler written next month can omit it.
  • Hardcoded credential findings are one secret handling decision, and deleting the string does not close them. The credential is burned, it has to be rotated, and version control history still holds the old value.

The difference between fixing an instance and closing a class

An instance fix stops the request printed in the report from working. A class fix removes the conditions that made it possible. On a run of authorisation findings, the instance fix adds a check to each handler named in the report, ships, retests clean and looks finished. The next handler gets written six weeks later by an engineer who never read the report, and the finding comes back in the following test under a different URL.

A class fix moves the control to a point every request passes through, and makes the absence of a decision an error, so a handler that declares nothing fails closed. The routing layer refuses to serve a handler that has not stated what authorisation it requires. The data access layer applies the tenant predicate itself, so a query that forgets it returns nothing at all.

Enumerate the call sites before you write the fix. The report lists what the tester reached with the account they held, and the codebase holds the rest, so the number of places needing the change is normally larger than the number of findings. Then write what holds the class shut: a regression test per instance, confirmed failing against the unpatched build, plus a rule in the build that fails on the banned pattern or on a route with no authorisation declaration. Where the pattern already has hundreds of hits, land the rule with a recorded baseline of existing violations that is only ever allowed to shrink.

What a remediation record has to carry

The register is the artefact everybody else reads: the retester, an auditor, a customer's security reviewer, and your own team a year from now. A row saying fixed with a date beside it satisfies none of them, because it states a conclusion and carries none of the working. Each closed finding needs enough attached that a stranger can repeat the check without asking you a question.

An owner recorded as a team name means nobody is accountable. A change reference reading fixed in sprint 14 points at nothing, while a commit identifier points at the change itself. A common error is treating merged as deployed, because a fix in the main branch is not a fix in the environment the retester will point their tools at. Evidence recorded as a screenshot of a passing pipeline proves the build ran and nothing else.

The field almost nobody keeps is the scope of the class fix. Record how many call sites the enumeration found, the search that found them so it can be run again, and how many were changed. That row is what lets a retester sample an instance the original engagement never touched. Without it, nobody outside your team can tell a closed class from a set of patched examples.

FieldWhat it has to sayWhat it answers for a retester
Finding referenceThe report's identifier, your ticket, and the class identifier where findings were mergedWhether the item they must verify maps to the change you made
OwnerA named individual, never a team or a queueWho to ask when the evidence does not hold
Change referenceCommit or pull request, and the release that carried itWhich build to test, and what the change did
Environments and datesMerge date, deployment date per environment, and which environment the retest usesWhether the fix is live where they are testing
EvidenceThe original reproduction steps re-run, plus the test that fails without the fixWhether the attack path is closed or only the symptom
Scope of the class fixCall sites found, the search that found them, and how many changedWhich untested instances they can sample
VerificationWho confirmed it, when, and whether that person wrote the fixWhether closure was checked independently

What a closed finding needs before someone else can rely on it

What does an honest risk acceptance register look like?

Accepting a risk is legitimate and a normal outcome for part of any findings list. It stops being legitimate when it appears as a spreadsheet cell reading accepted with no name beside it. That entry removes the finding from everyone's attention and leaves you holding a record of a weakness with no record of the decision.

The accepter has to be the person who carries the consequence and controls the budget that would fix it. An engineer who marks an item accepted because the fix is awkward has recorded an opinion, and nobody with authority has agreed to anything. Set the thresholds so authority scales with severity: a system owner for a low, the executive accountable for that business function for a high, a board-level risk forum for a critical. If every acceptance carries the same engineering manager's name, that pattern is a finding in its own right.

The reason has to be built from clauses somebody can recheck at review. Reachable only from the corporate network, requires a role held by four named staff, and the data behind it is already published is a reason, because each clause can be tested again in six months. Low likelihood is not, because nobody can say what would falsify it. Name the compensating control and write its failure mode beside it, since an edge filtering rule stops helping the moment traffic reaches the origin by another path.

Give the acceptance an expiry with no automatic renewal. The triggers that force an early review should be events: the compensating control is removed or changes owner, the asset becomes reachable from the internet, or exploit code is published. If the accepted finding is the one that gets exploited, that record is what gets read out afterwards, and it has to show who decided, what they knew, and what they were relying on.

How long should it take to fix penetration test findings?

The usual answer is a remediation SLA, a table of days per severity band copied out of a framework, and the usual result is missing it. A deadline you set yourself and miss is worse evidence than a longer one you meet, because the register then records both the exposure and the fact that your process does not hold. Sort by two variables. Exposure is reachability combined with what sits behind the flaw, and the shape of the change runs from a configuration value at the edge, through a code change with tests, to a schema migration or a backfill across live records.

A critical authorisation flaw that needs a new tenant column, a backfill over production data and a dual-write period is weeks of careful work in a system that cannot go down, while a medium missing-header finding ships this afternoon. Ordering strictly by severity queues the header change behind the migration and reports both as open. Put everything with no data implication into the first week whatever its severity, because clearing it leaves only the items that need thought. Then run the structural items as small projects whose milestones carry the dates, so the first slipped milestone is visible while there is still room to move.

Where the work runs for weeks, put an interim control in front of the flaw, record it as a mitigation with its own expiry, and leave the finding open. Some windows are not yours to set at all: where a finding is an unpatched internet-facing component, the Essential Eight maturity model already fixes the window at two weeks from release of the patch, update or other vendor mitigation, and at 48 hours where an exploit exists. Watch also for the way this usually goes wrong: the cheap mediums get closed, the status report says most of the list is done, and the two findings carrying nearly all the exposure are still open on the morning of the retest.

What do you tell a customer or an insurer while the work is in flight?

They want a date and a name. Progress described with adjectives, remediation is underway, we are actively addressing the items, reads as evasion. What travels well is a short status by severity band: how many findings the report raised, how many are closed and when verification happened, what remains with a target date and the role that owns it, what has been accepted and at what level of authority, and when the retest window opens.

Give a date you are confident of meeting, because once you miss one the correspondence is about the date and no longer about the risk. If a structural fix will run past what the customer expected, say what the interim control does in the meantime and when it comes off. Where they have asked for the report itself, an attestation letter is the document to send.

An insurer asks a different question. At renewal and at claim the questions are about which controls are operating and whether what you said at underwriting was accurate, so where testing found a control failing that you had described as in place, raise it deliberately and in writing at the time. One class of finding also stops being remediation work entirely. A finding describes something an attacker could do, and if the evidence shows unauthorised access already happened, you are in breach assessment: an entity with reasonable grounds to suspect an eligible data breach has to assess it, and the Privacy Act gives 30 days to complete that assessment.

Bundle the work before you book the retest

A retest verifies the state of the system on the day, so book it once the system is in one state. Feeding fixes to a retester in a trickle produces several partial documents with different dates, and the customer who later asks for evidence either reconciles them or pays for a reissue. Batch the fixes into a release, deploy it to the environment the retester will use, then book the window.

Hand over the build identifier, the environment and its address, the accounts and roles they need recreated, the map from report finding identifiers to the changes that closed them, and the call-site enumeration from every class fix. That last item is what turns a retest into a real check, because it lets the tester pick instances the original engagement never touched. Send the acceptance records too, so the window is not spent rediscovering decisions you took deliberately.

Re-run the original attack yourself first, from the report's own steps, in the environment they will be given. The two most common retest failures have nothing to do with the quality of the fix: the change never left the main branch, or a configuration value differs between environments. The third is that the fix works and has broken a legitimate flow, so run the affected user journeys as well as the unit tests. A retest is also not a new test. If closing a class meant rewriting an authorisation layer, that code has not been examined by anybody outside your team, and the retest scope will not cover it unless you widen it.

How we run remediation alongside build work

Black Shard sits on both sides of this. We run penetration testing and secure code review, and we build and operate production systems, so remediation here is ordinary engineering work in the same repositories, pipeline and review process as feature delivery. A class fix lands as a change to a choke point plus the rule that keeps it closed, and it goes through review like anything else, because a security fix rushed around the normal process is a reliable way to write the next finding.

Inside vCISO engagements we work registers built from other firms' reports, and the method does not change. Re-sort by root cause, put a named person on each piece of work, get the acceptances signed at the right level, and hold the retest until the release is coherent. Where we run a client's system ourselves, as with the operations and compliance portal we built and run for GRM LAW, a Brisbane law firm, the same discipline applies to our own changes.

If your most recent report is six months old and the register still shows a column of mediums with no owner and no date, the report has stopped describing your system. Start with the re-sort, because the number of things to schedule is smaller than the number of rows. Our approach and trust pages set out how we work.

Know what your code would give away.

Australia-wide, from our Brisbane head office. Someone will contact you as soon as possible.

Open a briefinfo@blackshard.com.au