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.
Design the dashboard for remediation backlog health across many services. What would it show, where does the data come from, and how often is it refreshed?
Sample Answer
Direct answer
The dashboard answers one question for each audience: is our backlog getting healthier, and where is it not? It shows open volume by severity, how old items are against their deadlines, how fast they get fixed, whether closures were verified, and which teams and services hold the overdue items. Data comes from the ticket tracker (the source of truth), the scanners (for what was found), and the service catalog (a list that records which team owns each service), loaded daily into a warehouse table (a database table built for reporting, so heavy dashboard queries do not slow the live ticket tool). Trends use daily snapshots; the overdue-critical list refreshes hourly.
What it shows
| Panel | Definition |
|---|---|
| Open backlog by severity | Count of open findings per severity, per week, so direction is visible |
| Aging buckets | Open items by age (0-15, 16-30, over 30 days), where remediation aging means days since the finding was opened |
| SLA breach rate (overdue %) | Overdue open items / open items, where SLA (service-level agreement) here means the internal deadline for the severity. This is a breach rate, so lower is better; SLA compliance is 100% minus it |
| Opened versus closed | Findings created and closed per week (are we keeping up?) |
| MTTR | Mean time to remediate (average fix time): average days from discovery to verified closure for items closed in the period. It is measured to verified closure, not to the developer clicking Close, and from discovery, not from ticket creation |
| Verification rate | Closed items with evidence / closed items |
| Risk-accepted items | Count, and how many expire within 30 days |
| Unowned items | Open items with no assigned team |
| Team and service table | The above per service, sortable, with drill-down (click a number to see the underlying tickets) |
Data sources and refresh
- Tracker API for status, dates, owner and evidence flags. Ingest via scheduled export or webhook events (the tracker sends an automatic message each time a ticket changes).
- Scanner feeds (through the central ingestion pipeline) for discovery date and coverage.
- Service catalog for the owner and criticality of each service.
- Refresh: a nightly snapshot table (a copy of every finding's status saved once per day, so one row per finding per day) makes trends and historical numbers reproducible. An hourly refresh of only critical overdue items feeds the on-call view. Executives see a weekly rollup (the daily numbers summed or averaged into one weekly figure).
Worked example
Open high-severity findings, with a 30-day deadline:
| Service | Open | 0-15 days | 16-30 days | Over 30 days | Overdue % |
|---|---|---|---|---|---|
| Payments | 12 | 6 | 3 | 3 | 3 / 12 = 25% |
| Search | 5 | 4 | 1 | 0 | 0 / 5 = 0% |
| Checkout | 8 | 2 | 2 | 4 | 4 / 8 = 50% |
| Total | 25 | 12 | 6 | 7 | 7 / 25 = 28% |
In the table, the 28% is 7 overdue of 25 open (7 / 25 = 0.28), so SLA compliance is the other 72%.
The table points straight at Checkout: 4 of its 8 items are overdue, so the first conversation is with that team, not a company-wide reminder.
The other panels, with numbers for one week
- Opened versus closed: 14 findings opened and 9 closed, so the backlog grew by 5. Keeping up would need closed to be at least opened.
- Verification rate: of those 9 closed items, 7 carry retest evidence, so 7 / 9 = about 78%.
- MTTR: the same 9 closed items were fixed after 10, 15, 20, 20, 25, 25, 30, 35 and 45 days from discovery. The total is 225 days, and 225 / 9 = 25 days. The single 45-day item is why the oldest-item figure sits beside the average.
Design choices and pitfalls
- Every ratio shows its numerator and denominator, or people quote percentages that hide a tiny base.
- Show the oldest open item and the overdue count, not only averages, since averages hide the worst case.
- Do not rank teams on raw counts: a team owning a large estate always looks worse. Normalise or show both.
- A dashboard nobody reviews is decoration. Attach it to a standing weekly meeting with named owners.
- Stale data is worse than none; show the "last updated" time on the page.
During an incident you shipped a temporary mitigation that restored service but left a security gap. How do you document it, turn it into tracked remediation work, communicate the risk, and make sure the stopgap is really replaced?
Sample Answer
Direct answer
A stopgap that is not tracked becomes permanent. So the moment service is restored I turn it into a recorded, owned, time-boxed risk: written down in the incident timeline, converted to a ticket with a named owner and a due date, communicated to the people who own the risk, and closed only when the stopgap is removed and someone independent confirms it.
Worked scenario (illustrative)
During an outage, to let a failover database replica connect, an engineer changed a firewall rule (the network filter that decides who may connect) so the database port (5432, the door clients use to talk to it) accepted connections from every address on the internet (written 0.0.0.0/0) instead of only the replica's own address (10.0.4.17). Service returned in 10 minutes. The gap: the database is now reachable from outside, by anyone who can guess or find it, instead of by one server.
Step by step, in order
- Document during the incident. In the incident timeline: exact change, who made it, time, and the gap it created. A one-line note beats memory later.
- Create a temporary-mitigation record the same day. Fields: what changed and where, why, what risk it adds (who could reach what), compensating controls now in place (a compensating control is a temporary safeguard that reduces the risk while the real fix is pending, such as extra logging or an alert on connections from unexpected addresses), owner, expiry date, and the permanent-fix ticket link. Filled in for this scenario:
Record: TM-2026-041
Change: Firewall rule db-failover-allow, port 5432, source 0.0.0.0/0 (was 10.0.4.17/32)
Why: Failover replica could not connect during the 2026-09-28 outage
Risk added: Anyone on the internet can attempt to log in to the production database
Compensating: Password-only logins disabled for non-replica users; alert on any 5432
connection from outside 10.0.0.0/8; connection log reviewed daily
Owner: Payments platform team lead
Expires: 2026-10-05
Permanent fix: SEC-2210 (restrict source to 10.0.4.17/32), due 2026-10-05
- Turn it into tracked work. One ticket for the permanent fix (restrict to the specific hosts), owned by the service team, with a due date (for example 5 business days, since this is a live exposure). Also add it as an action item in the postmortem (the blameless review after an incident: it asks what failed in the system, not who to blame).
- Communicate the risk. A short message to the service owner, the security team and the on-call manager: what is exposed, what compensates, when it ends, what triggers escalation. Plain wording, no blame. For example: "Since 14:10 today the production database port accepts connections from any internet address, a side effect of restoring failover. We have restricted logins and are alerting on outside connections. Fix is ticketed (SEC-2210), due Oct 5. If the alert fires, security will block the port immediately and we will take the failover replica offline until it is fixed."
- Make the expiry enforceable. Tag the change with an expiry date, and run an alert that fires when an expired tag is still present. A calendar reminder alone is not enough, because it depends on someone remembering. Concretely: put a label
expires=2026-10-05on the firewall rule, and run a daily job that lists every rule whoseexpiresdate is earlier than today and pages the owner. On 2026-10-06 the broad rule shows up in that list if it still exists, without anyone having to recall it. - Replace and verify. Apply the proper rule, remove the broad one, test the failover still works, and have someone other than the author confirm from outside that the port is no longer reachable. Attach the evidence and close both the record and the ticket.
What would change my call
If the permanent fix needs weeks (for example a network redesign), I stop calling it temporary: it becomes a formal, time-limited risk acceptance (a written, signed decision by the person who owns the risk that the organisation will live with it for a stated period), with stronger compensating controls and a renewal date.
Pitfalls
- Closing the incident and the ticket together, so nothing carries the exposure forward.
- Blame-heavy wording that discourages people from disclosing stopgaps next time.
- "Fixed" declared without confirming the old rule is gone.
Given a CSV export of remediation tickets (id, summary, owner, due_date, status), write a script that lists overdue open items with owner and days overdue. How do you deal with time zones and bad dates?
Sample Answer
Direct answer
Read the CSV with csv.DictReader, keep rows whose status is open-like, parse due_date as ISO 8601 (no guessing of other formats), and compare it to today's date in one declared "policy" time zone. Rows whose date cannot be parsed are never silently dropped or guessed at: they go to a separate "bad dates" list that is printed for a human to fix.
Terms. ISO 8601 is the standard date format year-month-day (2026-09-28), optionally followed by a time and an offset (T23:30:00-07:00, meaning 7 hours behind UTC, the world reference time). A datetime with an offset is called aware (it says exactly which instant it is); one with no zone information, including a bare date, is called naive (it could be any zone). A policy zone is the single zone the organisation declares as the meaning of "due", and America/New_York is an IANA zone name (the standard tz database name that also knows daylight-saving rules). A quarantined row is one set aside in a separate list for a human to fix instead of being processed.
Time-zone rules
- Pick one policy zone for what "due" means (here
America/New_York) and write it in the script. A date-only value (2026-09-28) means that calendar day in the policy zone. - A value with an offset (
2026-09-27T23:30:00-07:00) is converted to the policy zone first, and only then reduced to a date. Taking the date before converting gives the wrong day: that timestamp is 02:30 on 2026-09-28 in New York, so it is not overdue on 09-28, while a naive reading of the raw text (09-27) would call it 1 day overdue. - "Today" must come from the same zone (
datetime.now(POLICY_TZ).date()), otherwise items flip to overdue hours early or late. - Overdue means due date strictly before today; an item due today is not yet overdue.
Bad-date rules
- Empty, impossible (
2026-02-30) and ambiguous (09/12/2026could be 9 December or 12 September) values are rejected, not guessed. Guessing produces confident wrong numbers. - Every rejection is reported with its line number, ticket id and reason, so the count of overdue items is never quietly understated.
Script (Python 3.11 or newer, because earlier versions of datetime.fromisoformat accept only a narrow set of ISO forms; standard library only; today is pinned so the output is reproducible). Note that 3.11+ also accepts a space in place of the T (ticket 8, 2026-09-01 08:00:00), which is still ISO-like, so "strict" here means slash formats such as 09/12/2026 are refused, not that only one spelling passes.
import csv
import io
from datetime import date, datetime
from zoneinfo import ZoneInfo
POLICY_TZ = ZoneInfo("America/New_York") # the org's declared "due date" timezone
OPEN_STATUSES = {"open", "in progress", "blocked"}
CSV_TEXT = """id,summary,owner,due_date,status
1,Patch OpenSSL,ana,2026-09-20,Open
2,Rotate keys,raj,2026-09-27T23:30:00-07:00,open
3,Fix XSS,li,2026-09-28,open
4,Close S3 bucket,sam,2026-02-30,open
5,Old finding,ana,2026-08-01,Closed
6,Enable MFA,raj,09/12/2026,open
7,Update TLS,li,,In Progress
8,Harden SSH,sam,2026-09-01 08:00:00,blocked
"""
def parse_due(raw):
"""Return a date in POLICY_TZ, or raise ValueError. Refuses ambiguous formats."""
raw = raw.strip()
if not raw:
raise ValueError("empty due_date")
dt = datetime.fromisoformat(raw) # ISO 8601 only: 2026-09-20 or 2026-09-20T10:00:00+02:00
if dt.tzinfo is None: # date-only or naive: interpret in the policy zone
return dt.date()
return dt.astimezone(POLICY_TZ).date() # aware: convert first, then take the calendar date
def overdue(rows, today):
good, bad = [], []
for line_no, row in enumerate(rows, start=2): # line 1 is the header
if row["status"].strip().lower() not in OPEN_STATUSES:
continue
try:
due = parse_due(row["due_date"])
except ValueError as exc:
bad.append((line_no, row["id"], row["due_date"], str(exc)))
continue
if due < today:
good.append((row["id"], row["owner"], due, (today - due).days))
good.sort(key=lambda r: -r[3])
return good, bad
if __name__ == "__main__":
today = date(2026, 9, 28) # pinned; in production: datetime.now(POLICY_TZ).date()
good, bad = overdue(csv.DictReader(io.StringIO(CSV_TEXT)), today)
for tid, owner, due, days in good:
print(f"OVERDUE id={tid} owner={owner} due={due} days_overdue={days}")
for line_no, tid, raw, why in bad:
print(f"BAD_DATE line={line_no} id={tid} value={raw!r} reason={why}")
Output:
OVERDUE id=8 owner=sam due=2026-09-01 days_overdue=27
OVERDUE id=1 owner=ana due=2026-09-20 days_overdue=8
BAD_DATE line=5 id=4 value='2026-02-30' reason=day 30 must be in range 1..28 for month 2 in year 2026
BAD_DATE line=7 id=6 value='09/12/2026' reason=Invalid isoformat string: '09/12/2026'
BAD_DATE line=8 id=7 value='' reason=empty due_date
The reason text after reason= comes from the interpreter and varies by version: the wording shown for 2026-02-30 is from Python 3.14, while 3.11 to 3.13 print day is out of range for month. The rows flagged, their order and the day counts are identical on every version from 3.11.
Reading the result
- Ticket 1: 2026-09-28 minus 2026-09-20 is 8 days. Ticket 8: 09-01 to 09-28 is 27 days.
- Ticket 2 (offset timestamp) and ticket 3 (due today) are correctly NOT overdue. Ticket 5 is closed, so skipped.
- Three rows are quarantined, and the overdue list is sorted worst first.
Complexity and edge cases
- O(n log n) for the sort, O(n) memory; for a huge export stream rows and sort only the good list.
- Watch for a byte-order mark (an invisible marker some tools write at the start of a file, which would corrupt the first header name
id) or different header names/case in real exports, statuses with odd spelling ("Re-opened"), and due dates in the far past that hint at data-entry errors. In production, exit non-zero or alert if the bad-date count exceeds a threshold, so a broken export cannot masquerade as "nothing is overdue".
You inherit a backlog of 10,000 open findings across thousands of assets, and remediation capacity is small. How do you decide what gets fixed first, what can be automated, and how do you explain the plan to product and engineering?
Sample Answer
Direct answer
I would not try to rank 10,000 items one by one. I would shrink the list first (remove noise, group findings by shared root cause), rank the remaining findings by real risk, automate the repetitive fixes, and take a small human-sized slice to product and engineering as a plan with a clear capacity ask.
Step 1: Shrink the list
- Deduplicate and validate. The same flaw often appears once per scanner, per asset, per scan. Merge duplicates, rescan to drop items already fixed, and close confirmed false positives (findings that are not real).
- Group by root cause. A finding is a symptom; a fix-unit is the change that removes it, such as one base-image update or one library upgrade. Rank fix-units, not findings.
- Filter by exposure. Ask: is the asset reachable from the internet, does it hold sensitive data, is the vulnerable code path used?
Step 2: Rank what is left
Order by likelihood and impact together. Useful signals: is it known to be exploited (the US Cybersecurity and Infrastructure Security Agency's Known Exploited Vulnerabilities catalog, KEV), how likely is exploitation (the Exploit Prediction Scoring System, EPSS, gives a probability), how exposed is the asset, how sensitive is the data. Severity score alone is the weakest signal.
Step 3: Automate
- Dependency-update bots that open pull requests for library bumps.
- Rebuilding container base images on a schedule.
- Auto-assigning tickets from asset ownership tags.
- Auto-closing findings when a rescan no longer sees them.
- Fixing configuration findings once in infrastructure-as-code (infrastructure defined in version-controlled files) so the fix covers every copy.
Worked example (illustrative numbers)
| Stage | Findings left |
|---|---|
| Raw backlog | 10,000 |
| Minus duplicates, already fixed, false positives (say 3,000) | 7,000 |
| Minus low or medium on unreachable, non-sensitive assets, moved to scheduled hygiene (say 4,000) | 3,000 |
| Those 3,000 grouped by root cause | about 150 fix-units |
| Of those, exploited or internet-facing critical | about 8 fix-units |
The team's first two weeks go to those 8 fix-units. The ~140 others are scheduled by risk, with dependency bumps and base-image rebuilds automated. Each step is arithmetic on the stated assumptions; real proportions will vary and I would report the measured ones.
Explaining it to product and engineering
- Lead with the outcome: "8 changes remove the risks most likely to be exploited; 140 more are queued and mostly automated."
- Make a specific capacity ask: engineer-weeks per sprint, and what a smaller allocation would leave open.
- Show the deferred pile honestly, with the reason for deferral, so no one thinks it was ignored.
- Give teams a single prioritized list per team instead of a raw scanner export.
Trade-offs and pitfalls
- Over-filtering by "unreachable" is risky if the network diagram is out of date, so sample-check the filter.
- Automated pull requests can break builds, so start with low-risk updates and require passing tests.
- What would change my call: an active exploit campaign against a component would move that whole fix-unit to the top immediately.
Tell me about a time you chased security fixes to closure and made sure each one was actually verified. What did you do about ownership, evidence and follow-up? If you lack direct experience, walk me through how you would do it.
Sample Answer
Direct answer
Closure means the weakness is demonstrably gone, not that a ticket moved to Done. The story to tell is one where you gave every fix a named owner, agreed in advance what proof would count, collected that proof, and kept chasing on a schedule. If you have no direct experience, describe the same loop as what you would set up.
Story skeleton (illustrative; swap in your own details and numbers)
Situation. A penetration test (an authorised, simulated attack on a system to find weaknesses) of a customer portal produced 23 findings: 2 critical, 6 high, 15 medium or low. The report sat in a shared folder and, two weeks later, nothing had moved.
Ownership. I split the findings by the team that owns the code, using the service catalog (a registry of which team runs which system), and opened one ticket per finding assigned to a named engineer, with the due date set by severity (critical 7 days, high 30). I held a 30-minute walkthrough per team so each engineer understood the exploit, not only the title.
Evidence. For each critical and high finding I wrote the acceptance check (the written test that decides whether a fix counts) into the ticket: the exact request that had worked, and the response that would prove it fixed. A ticket could move to "Fixed" by the developer, but only I could set it to "Verified" after rerunning the test (a retest, meaning repeating the original attack steps against the fixed system) and attaching the result. The retest always comes before Verified, never after.
Follow-up. A weekly 15-minute review of overdue items, escalation to the engineering manager three days before a due date was missed, and a written risk acceptance (a signed decision by the business to live with a known risk, with an owner and an expiry date) for anything the business chose not to fix.
Result. Of the 8 critical and high findings, 7 passed my retest and were set to Verified by their deadlines. The eighth, a high, was marked "Fixed" by the developer, but my retest showed the same flaw still reproduced through a second endpoint. I did not set Verified. I sent it back with the new evidence, the team fixed both endpoints, and it passed a second retest a few days after its deadline, which I reported as one deadline miss. So the honest tally is 7 of 8 on time and 8 of 8 verified. Two mediums were formally accepted with expiry dates. I only caught the second endpoint because I reran the real request instead of accepting the developer's word.
If you lack direct experience
Say so plainly, then walk through the same four steps: assign an owning team per finding, define proof before work starts, verify independently, and chase on a fixed cadence with an escalation ladder. Naming what you would attach as evidence (the retest output, a configuration diff, a passing scan) shows you know what "verified" means.
A scripted version you can say aloud: "I haven't run this end to end, but here is how I would. First, I'd give every finding one named owner from the service catalog. Second, before any work starts, I'd write into the ticket what proof I will accept, for example the original request and the response that shows it is blocked. Third, the developer marks it Fixed, but I, or another person who did not make the change, rerun that request and attach the result before it can be Verified. Fourth, I'd review overdue items weekly and escalate to the manager before a deadline is missed. If I found the fix only worked on one URL, I'd send it back."
Pitfalls
- "The developer said it's fixed" is a status, not evidence.
- Verifying only the exact URL from the report misses the same flaw on sibling endpoints.
- A story with no checkable outcome sounds like activity, not closure.
Unlock Full Question Bank
Get access to all 14 Security Findings Management and Remediation Tracking interview questions and detailed answers.
Sign in to ContinueJoin thousands of developers preparing for their dream job.