Security Findings Management and Remediation Tracking Questions
Managing the lifecycle of security and compliance findings from identification through closure. Covers triaging and prioritizing findings and large backlogs by business impact, assigning ownership, designing tracker states and remediation ticket workflows with SLAs, escalation and time-bound risk-accepted exceptions, tracking remediation to completion, verifying fixes and choosing a retest approach, ingesting, deduplicating and normalizing findings across scanners and manual tests, and turning post-incident and vendor-blocked issues into tracked, prioritized remediation plans. Also covers reporting on remediation status, aging and program metrics (and how those metrics get misread), building auditor-ready evidence and mapping findings to compliance controls, scripting and querying remediation data, and writing the findings report itself: structure for developer and executive audiences, reproducible evidence, remediation guidance and safe handling of exploit proof. Discovering vulnerabilities and scoring them are out of scope.
A program reports that 95% of critical findings are fixed within 30 days. What could make that number misleading, and what would you change or add to give an honest picture?
Sample Answer
Direct answer
"95% of critical findings fixed within 30 days" can be true and still hide most of the problem. The usual tricks are a denominator that only counts closed items, a clock that starts late, "fixed" meaning "ticket closed" rather than "verified fixed", and closures that were not fixes at all (risk accepted, duplicate, downgraded). An honest picture measures from discovery, over everything whose 30-day window has ended, counts only verified fixes, and reports the overdue open items alongside.
What can make it misleading
- Denominator counts only closed items. Open, overdue findings never enter the ratio.
- Clock starts at ticket creation, not discovery, so any delay before a ticket exists is free.
- Closed is not fixed. Tickets closed with no verification, as risk accepted, as duplicate or as won't fix, count as success.
- Severity changes. Downgrading a critical to high removes it from the population.
- Coverage. Findings never discovered (unscanned assets) are absent from the numerator and denominator alike.
- Only a percentage is reported, which hides the tail (the few very old items): the oldest critical may be 300 days old.
- Gaming: splitting a finding into pieces closes many small tickets fast, and reopening later does not undo the earlier win. Example: one critical flaw affecting four parameters is split into four tickets. Three easy ones close in 10 days and the hard one takes 90. The report now shows 3 fast closures and 75% within 30 days, where the honest single finding would show 0 of 1.
Worked example (executed)
40 critical findings discovered in a quarter; day 100 is "today". 20 are closed, 20 are open. Of the closed ones, 4 are fixed late measured from discovery, 3 were closed without verification (2 unverified, 1 risk accepted), and 13 were verified in time. The other 20 are open. Here is the whole set of 40 as a table (day numbers count from the start of the quarter):
| Rows (id) | Count | What they are |
|---|---|---|
| 0 to 12 | 13 | Closed with a verified fix, done within 30 days of discovery |
| 13 to 16 | 4 | Closed with a verified fix, but late measured from discovery (id 13 took 37 days; ids 14 to 16 had their ticket opened 15 days after discovery and closed 35 days after discovery) |
| 17, 18 | 2 | Closed as "fixed" with no verification |
| 19 | 1 | Closed as risk accepted (a named owner chose to live with the flaw), so not a fix |
| 20 to 27 | 8 | Open, discovered on day 41 to 69, so already more than 30 days old on day 100 |
| 28 to 39 | 12 | Open, discovered on day 72 to 94, so their 30-day window has not ended yet |
The honest denominator is the 20 closed plus the 8 overdue open = 28. The 12 open findings whose window has not ended are left out on purpose: they could still be fixed on time, so counting them either way would be a guess.
import sqlite3
db = sqlite3.connect(":memory:") # a throwaway in-memory SQL database, just to run the queries
db.execute("""CREATE TABLE findings (id INTEGER, discovered INTEGER, ticket INTEGER,
closed INTEGER, reason TEXT)""")
rows = [] # columns: id, day discovered, day ticket opened, day closed, close reason
for i in range(20): # the 20 closed findings, discovered on days 1, 3, 5, ...
d = 1 + 2 * i
if i <= 12:
rows.append((i, d, d + 2, d + 2 + 10 + i % 10, "fixed_verified"))
elif i == 13:
rows.append((i, d, d + 2, d + 2 + 35, "fixed_verified"))
elif i <= 16:
rows.append((i, d, d + 15, d + 35, "fixed_verified"))
elif i <= 18:
rows.append((i, d, d + 2, d + 12, "fixed_unverified"))
else:
rows.append((i, d, d + 2, d + 7, "risk_accepted"))
for k, d in enumerate(range(41, 70, 4)): # 8 open findings, discovered on days 41, 45, ..., 69
rows.append((20 + k, d, d + 2, None, "open"))
for k, d in enumerate(range(72, 95, 2)): # 12 open findings, discovered on days 72, 74, ..., 94
rows.append((28 + k, d, d + 2, None, "open"))
db.executemany("INSERT INTO findings VALUES (?,?,?,?,?)", rows)
ASOF = 100
q = lambda s, *a: db.execute(s, a).fetchone()[0]
print("total critical findings:", q("SELECT COUNT(*) FROM findings"))
closed = q("SELECT COUNT(*) FROM findings WHERE closed IS NOT NULL")
fast = q("SELECT COUNT(*) FROM findings WHERE closed IS NOT NULL AND closed - ticket <= 30")
print("headline: closed within 30 days of ticket / closed =", fast, "/", closed, "=", round(100 * fast / closed, 1), "%")
eligible = q("SELECT COUNT(*) FROM findings WHERE discovered <= ?", ASOF - 30)
good = q("""SELECT COUNT(*) FROM findings WHERE reason='fixed_verified'
AND closed - discovered <= 30""")
print("honest: verified fix within 30 days of discovery / findings whose window has ended =", good, "/", eligible, "=", round(100 * good / eligible, 1), "%")
print("open and already past 30 days:", q("SELECT COUNT(*) FROM findings WHERE closed IS NULL AND discovered <= ?", ASOF - 30))
print("closed but not verified fixed:", q("SELECT COUNT(*) FROM findings WHERE closed IS NOT NULL AND reason != 'fixed_verified'"))
print("verified fixed, late from discovery:", q("SELECT COUNT(*) FROM findings WHERE reason='fixed_verified' AND closed - discovered > 30"))
Output:
total critical findings: 40
headline: closed within 30 days of ticket / closed = 19 / 20 = 95.0 %
honest: verified fix within 30 days of discovery / findings whose window has ended = 13 / 28 = 46.4 %
open and already past 30 days: 8
closed but not verified fixed: 3
verified fixed, late from discovery: 4
The headline 95% is 19 of the 20 closed items, using the ticket clock. The honest figure is 13 of the 28 findings whose 30-day window has ended, about 46%, because 8 overdue findings are still open, 3 closures were not verified fixes, and 4 fixes were late when the clock starts at discovery. Same data, very different story.
What I would change or add
- Measure from discovery, over all findings whose window has ended (not only closed ones).
- Count verified fixes only (closure reasons like "risk accepted" or "won't fix" mean nobody fixed anything); report risk-accepted items as a separate number with expiry dates.
- Report the tail: open overdue count and age of the oldest item, or the 90th percentile of age (the age that 90% of open items are younger than, so it shows how bad the old end is), not just an average.
- Track reopen rate (closed findings that reproduce again, e.g. 5 reopened out of 50 closed = 10%) and coverage (share of assets scanned, e.g. 90 of 100 = 90%).
- Freeze the severity at discovery for this metric and show reclassifications separately.
- Publish the definition next to the number, so nobody has to guess.
Pitfalls
- Fixing the metric definition without fixing behaviour: expect people to game the new number too, so audit a sample of closures each quarter.
- Replacing one number with twenty. Keep a headline (verified fixed within window) plus two or three supporting numbers.
What does remediation validation mean after a penetration test, and how do you choose between a full retest, a targeted retest and sampling?
Sample Answer
Direct answer
After a penetration test (an authorized, simulated attack on a system), remediation validation is independently confirming, with evidence, that a reported vulnerability is really fixed and that the fix did not break something else, before the finding is closed. A developer saying "fixed" is a claim; a retest (repeating the original attack against the fixed system) is proof. I choose the scope by risk: full retest, targeted retest or sampling.
What "validated" should mean
- The original attack steps no longer work.
- Variants of the attack (other parameters, other encodings, neighbouring endpoints) no longer work, so the fix addressed the root cause and not just one payload.
- No new issue appeared from the change.
- The result is recorded with evidence: date, tester, requests and responses, and a pass, fail or partial verdict.
Choosing the scope
| Option | What it is | Use when |
|---|---|---|
| Full retest | Re-run the whole test scope | Big code or architecture changes, many critical findings, a contract or compliance requirement, or a fix in a shared component |
| Targeted retest | Re-test each reported finding plus close variants | The default for most engagements |
| Sampling | Retest a representative subset of a large group of similar findings, plus confirm the root-cause fix | Many low-severity findings of one class, for example 40 missing security headers |
Never sample critical or high findings: each one is retested individually.
Worked example (illustrative)
A pentest reported 1 critical SQL injection, 4 mediums and 40 low findings for missing security headers.
- The SQL injection (attacker-supplied text altering a database query) gets a targeted retest: the original payload, other payload styles, and the other parameters on the same endpoint. The fix should use parameterized queries (the database receives the query and the user input separately), not a blocklist of bad characters.
- The 4 mediums each get a targeted retest.
- For the 40 lows, I verify that the shared header configuration changed, then test 5 pages across different services.
- If any sampled item fails, I widen to all 40.
Pitfalls
- Closing on the developer's word.
- Retesting only the exact original payload.
- Sampling across findings of different classes, which is not representative.
Every action item from a security review is supposed to end up as an owned, tracked ticket with a deadline and a verification step, but in practice many vanish. Design the process and tooling so none get lost, and say how you would know it works.
Sample Answer
Direct answer
Make the ticket the only place an action item is allowed to live. Every item from a review becomes a ticket in the tracker the owning team already uses, before the review meeting ends, with four mandatory fields (in the team's ticket tracker, such as Jira, a widely used tool for tracking work items): a named owner, a due date derived from severity, a written acceptance check, and a link back to the review. Then run a closed loop: automatic reminders, escalation when overdue, verification by someone other than the fixer, and a weekly reconciliation that catches items that never became tickets. You know it works when three leakage numbers stay near zero: items with no ticket, tickets overdue with no update, and tickets closed without evidence.
The process
- Capture at the source. A review ends with a structured action list (a form or template), not free text in meeting notes. Rule: "no ticket, no action item". Tickets are created in the meeting or imported from the report the same day.
- Ownership. The owner is a team resolved from a service catalog (a registry mapping each service to the team that runs it), plus a named individual assignee. Anything that cannot be resolved goes to a triage queue (a holding list where someone decides who owns the item) with a two-business-day limit, so "unowned" is a visible state with a clock, not a hiding place.
- Deadline. The due date comes from a severity policy (for example critical 7 days, high 30, medium 90; these numbers are an illustrative policy, set your own). Extending a date needs a recorded reason and an approver.
- Verification. The acceptance check is written when the ticket is created (see the worked example). States: Open, In progress, Fixed pending verification, Verified closed, Risk accepted (a senior owner has signed a written decision to live with the item unfixed until an expiry date). Only the verifier, not the assignee, can move a ticket to Verified closed.
- Chasing. The tracker's automation rules send reminders at roughly half and 80% of the time budget, escalate an overdue ticket to the owner's manager and then to their director, and post a weekly digest of overdue items to team leads.
Tooling
- The team's existing tracker (Jira or equivalent) holds the tickets, with a label carrying the review ID.
- A small automation (tracker rules or a script on a schedule) creates tickets, sends reminders and escalations.
- A weekly reconciliation job compares the review's list of item IDs against tickets carrying that review's label and flags any item without a ticket. This is the piece that catches items that vanished before ever being tracked, which no dashboard of existing tickets can show.
- A dashboard of overdue, unowned and unverified items per team, reviewed in a standing meeting.
How you know it works
| Signal | Definition | Healthy |
|---|---|---|
| Ticket coverage | items with a ticket / items listed in the review | 100% within one day |
| Orphan rate | tickets with no owner or no due date / all open tickets | near 0 |
| Silent overdue | overdue tickets with no update in 14 days / overdue tickets | falling |
| Unverified closures | tickets closed without evidence / tickets closed | near 0 |
Also plant a canary: a harmless dummy action item in a review, and check it reaches a ticket, gets an owner and is chased. Audit a random sample of closed tickets each quarter against their evidence.
Worked example
A design review of a payments service ends with 7 action items, numbered R-41-1 to R-41-7. Six tickets carry the label review-R-41. The Monday reconciliation reports 6 / 7 = 86% coverage and names R-41-5 ("restrict the admin endpoint to the VPN (virtual private network)") as missing. A ticket is created that day. Its acceptance check (a written test for what "done" means) reads: "From outside the VPN, a request to the admin endpoint returns 403; the network rule change is linked; the security engineer reruns the request and attaches the result."
Tracing the chase on a ticket with a 30-day window created Monday 1 June: the half-way reminder goes out on 16 June (day 15), the 80% reminder on 25 June (day 24), and if it is still open at the 1 July deadline the manager is alerted, with the director notified a week later on 8 July. The ticket also shows, in one line each, its owner (the platform team resolved from the service catalog), that it sat in triage for one day before an owner was found, and that if it were formally risk accepted it would show that status with an expiry date such as 30 September.
If the tickets are created by automation
Some teams have a small program create the tickets when a review finishes. The review system publishes an event (a message saying "review R-41 completed") onto a queue (a waiting line of messages), and a consumer (the program reading that line) creates the tickets. Two ideas matter. Idempotent means running the same step twice has the same effect as once: the consumer first looks for an existing ticket with the item's ID and only creates one if none exists, so a retry or two consumers reading the same message cannot create duplicates. A poison message is one that fails every time it is processed; after a few retries it is moved to a separate dead-letter list and an alert is raised, so it cannot silently produce no ticket. The weekly reconciliation job is the safety net for both.
Trade-offs and pitfalls
- Ticket sprawl. Not every remark deserves a ticket, but every commitment does. If an item is a decision rather than a fix, it still gets a ticket whose done condition is "decision recorded".
- Auto-closing stale tickets to tidy the dashboard is how items vanish again. Escalate instead.
- Reminder fatigue. Escalate on a ladder rather than repeating the same nag, or people mute it.
- Gaming. Closing without a fix is prevented by the separate verifier and the evidence rule, not by exhortation.
- What would change the design: a five-person company can run this as a shared board reviewed weekly. The automation earns its keep when items arrive from many reviews, across many teams.
You are setting up the lifecycle for security findings in a new tracker. Which states and transitions would you define, where would you put automated gates, and how would you catch findings that get stuck between states?
Sample Answer
Direct answer
I define a small number of states that each mean one verifiable thing, allow only the transitions that reflect real progress, put automated gates where a state change needs proof, and run a scheduled check that flags any finding whose age in a state exceeds a limit or whose tracker state disagrees with the scanner.
States and transitions
stateDiagram-v2
[*] --> New
New --> Triaged: validated
New --> FalsePositive: not real
New --> Duplicate: merged
Triaged --> Assigned: owner set
Assigned --> InProgress: work starts
InProgress --> FixDeployed: change released
FixDeployed --> Verified: rescan or retest clean
FixDeployed --> Assigned: still vulnerable
Verified --> Reopened: regression seen
Reopened --> Assigned
Triaged --> RiskAccepted: approved with expiry
RiskAccepted --> Assigned: expiry reached
Verified --> [*]
FalsePositive --> [*]
Reading the diagram (it is Mermaid, a text format that renders as a state diagram): follow one finding. It starts New, is validated (Triaged, meaning someone confirmed it is real and rated it), gets an owner (Assigned), work begins (InProgress), the fix ships (FixDeployed), and a clean rescan makes it Verified, the normal end of the path (Verified can still be left through Reopened if a regression appears). If the rescan still finds it, it goes back to Assigned. A regression is a fixed flaw coming back in a later scan, which sends it from Verified to Reopened. The state names in the diagram (InProgress, FixDeployed, RiskAccepted) are the same states as fix_in_progress, fix_deployed and accepted_risk in the code below, written in the database style. A gate is an automatic check that blocks a state change until a condition is met.
Where automated gates go
- Intake gate: no move to Assigned until an owner is mapped from asset ownership data and required fields (severity, asset, evidence) are present. Duplicates are merged automatically by matching asset and finding type.
- Verification gate: a finding cannot reach Verified without attached evidence: a clean rescan or a retest record (a tester's confirmation the fix works). Developers can move it to FixDeployed, but only the scanner or security can verify.
- Risk acceptance gate: (a risk acceptance is a signed decision to live with a risk for a limited time) requires an approver, a compensating control (another safeguard that reduces the risk while it stays open) and an expiry, and expiry returns the finding to Assigned automatically.
- Release gate: the deployment pipeline blocks new critical findings in code being released, with a time-boxed exception path.
Catching findings stuck between states
Two checks run daily. First, a per-state age limit. Second, drift detection: (drift means the tracker and the scanner disagree) tracker says Verified but the scanner still sees the finding, so reopen it; tracker says open but the scanner no longer sees it, so prompt verification.
Runnable example of the age-limit check (Python 3, built-in sqlite3). The limits are illustrative and follow the urgency of each state: triaged is 5 days because assigning an owner is quick, fix_in_progress 60 days because real engineering takes time, fix_deployed 14 days because a rescan is quick, and accepted_risk 180 days because acceptances are periodically re-reviewed:
import sqlite3
db = sqlite3.connect(":memory:")
db.execute("""CREATE TABLE findings(
id INTEGER PRIMARY KEY, state TEXT, state_changed DATE)""")
db.executemany("INSERT INTO findings VALUES (?,?,?)", [
(101, "triaged", "2026-08-28"),
(102, "triaged", "2026-08-05"),
(103, "fix_in_progress", "2026-06-20"),
(104, "fix_deployed", "2026-08-01"),
(105, "fix_deployed", "2026-08-29"),
(106, "accepted_risk", "2026-03-01"),
])
# max days allowed in each state before a finding counts as stuck
LIMITS = {"triaged": 5, "fix_in_progress": 60, "fix_deployed": 14, "accepted_risk": 180}
AS_OF = "2026-09-01"
for fid, state, changed in db.execute("SELECT * FROM findings ORDER BY id"):
age = int(db.execute("SELECT julianday(?)-julianday(?)", (AS_OF, changed)).fetchone()[0])
if age > LIMITS[state]:
print(f"STUCK #{fid} {state} for {age}d (limit {LIMITS[state]}d)")
Output:
STUCK #102 triaged for 27d (limit 5d)
STUCK #103 fix_in_progress for 73d (limit 60d)
STUCK #104 fix_deployed for 31d (limit 14d)
STUCK #106 accepted_risk for 184d (limit 180d)
Finding 106 has sat in accepted_risk for 184 days against a 180-day review limit, so its acceptance is overdue for re-review or expiry (the table stores no expiry date, so the age limit stands in for it), and 104 was deployed but never verified. Those are the typical stuck shapes.
False positive and reopened branches
A false positive needs a reason and a reviewer, and its signature (a fingerprint such as asset plus finding type plus location) is remembered so the scanner stops re-raising it. A false positive is a scanner alert that is not a real vulnerability. Reopened increments a counter, and a high count flags a weak fix.
Pitfalls
Too many states (people stop updating them), no verification gate (closure means nothing), and no owner on Reopened.
You must show auditors, over three consecutive quarters, that remediation is effective for a regulated environment. What metrics, sampling, evidence and retention would you set up?
Sample Answer
Direct answer
A regulated environment is one where law or contract (for example payment-card or healthcare rules) requires demonstrable security controls, so audits are formal. Treat it as a controlled experiment auditors can repeat: fix the metric definitions before quarter one, collect evidence as work happens, and let auditors sample a stratified population each quarter. "Effective" must mean more than "tickets closed": findings are fixed on time, the fix holds (verified by rescan, low reopen rate), and exceptions are deliberate and time-limited. Three consecutive quarters matters because auditors are looking for a consistent operating pattern, not one good week (an operating pattern is the same behaviour repeating over time). The table in section 3 uses "due in the quarter" as its denominator.
1. Metrics (define numerator, denominator and window; freeze the definitions)
| Metric | Definition | Guard against gaming |
|---|---|---|
| SLA compliance | Findings closed within their severity deadline / findings due in the quarter (SLA = service-level agreement, the fix-by deadline per severity) | Count risk-accepted items separately, never as compliant |
| P90 time to remediate | 90th percentile of days from discovery to verified fix, per severity | Report the count beside it; never average away outliers |
| Reopen rate | Findings reopened after closure / findings closed | Rises if teams close without rescan |
| Aged backlog | Open findings past due, in buckets (1-30, 31-90, 90+ days late) | Reclassifying severity to dodge deadlines; audit severity changes |
| Exception health | Active risk acceptances (signed decisions to live with a risk for a stated time), each with approver and expiry; number expired without renewal | Blanket, open-ended acceptances |
Any change to a definition mid-way is documented with the date and the restated prior quarters. Auditors forgive an imperfect metric; they do not forgive a moving one.
Worked numbers for the other four metrics (illustrative, Q2):
- P90 time to remediate: P90 means 90% of findings were fixed in this many days or fewer. For 10 critical findings fixed in 3, 4, 5, 5, 6, 7, 8, 9, 12 and 30 days, sort them and take the 9th of 10 (0.9 x 10 = 9): P90 = 12 days. The single 30-day outlier does not move it, which is why the count is reported beside it.
- Reopen rate: 3 of the 60 findings closed in Q2 were reopened, so 3 / 60 = 5%.
- Aged backlog: 12 open findings are past due, 7 by 1-30 days, 4 by 31-90, 1 by more than 90.
- Exception health: 8 active risk acceptances, 1 expired without renewal, so 1 of 8 needs action now.
2. Sampling
- Population = all findings closed in the quarter, exported from the system of record (the one database treated as the official source) with a timestamp and hash before sampling. This is a different set from the SLA table's "due in the quarter" below: the 60 closed in Q2 include findings that were due earlier or later, and some findings due in Q2 were still open, which is why closed (60) can exceed due (50).
- Stratify (split the population into groups by severity) so rare, important items are not lost. Illustrative Q2 population of 60 closed findings: 6 critical, 18 high, 36 medium. Sample all 6 critical, 6 of 18 high, 6 of 36 medium = 18 items (30% of 60). The 6 per group is an example rule, not a standard: take every item in the rarest, riskiest group, and a fixed number from each other group, with the size agreed with the auditor in advance (that gives 100% of critical, 33% of high, about 17% of medium).
- Select randomly with a recorded seed (the starting number that makes a random draw repeatable) and script so the auditor can reproduce the draw. Agree the method and sample sizes with the auditor up front; do not pick your own "representative" items.
- For each sampled item, walk the chain: finding, ticket, fix change record, verification rescan, approvals.
3. Evidence and worked trend (illustrative)
| Quarter | Due | Closed within SLA | SLA compliance |
|---|---|---|---|
| Q1 | 40 | 30 | 75% |
| Q2 | 50 | 40 | 80% |
| Q3 | 40 | 34 | 85% |
The trend (75%, 80%, 85%) shows the process improving, and each quarter's package includes: the frozen metric definitions, the raw export and hash, the sample list with the seed, per-item evidence, the aged-backlog list with owners and plans, and the exception register. If a quarter dips, present the cause and corrective action; a documented dip handled well beats a suspiciously smooth line.
4. Retention
- Keep evidence at least as long as the strictest applicable requirement (the regulation, the framework and the contract), and long enough to cover the full three-quarter observation period plus the auditor's look-back (how far into the past they may ask to see evidence). Confirm the exact period with compliance or legal rather than assuming a number.
- Store immutably, with access logs, and keep the sampling scripts and metric queries with the data; a number without its query is not evidence.
- Delete personal data and secrets from screenshots before retention, or restrict access.
Trade-offs
- Continuous automatic collection is more work upfront but avoids a scramble of reconstructed evidence at quarter end.
- Tighter SLAs look better to auditors only if you can meet them; set them from your realistic P90, then improve.
- If a finding cannot be fixed, a documented, approved, expiring risk acceptance with a compensating control (an alternative safeguard reducing the risk) is a stronger audit story than a quietly aging ticket.
Unlock Full Question Bank
Get access to all 38 Security Findings Management and Remediation Tracking interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.