An alert says a package you depend on has a published vulnerability. It does not say whether you are affected.
The gap between those two statements is where this lesson lives, and closing it badly produces the two failures that make vulnerability management ineffective: patching everything indiscriminately until the team stops caring, or dismissing on instinct until something real goes through.
The short answer
Section titled “The short answer”Severity describes the vulnerability. Risk describes your system. Triage is the work of converting one into the other, and it is not automatable.
A workable prioritisation, in order of weight:
- Is the vulnerable code path reachable from our usage? The largest single filter.
- Is it exploitable in our deployment? Network position, authentication, input sources.
- What is the impact if exploited? Data, availability, privilege.
- Is it being exploited in the wild? Known exploitation changes the timeline sharply.
- How hard is the fix? A patch release is different from a major version migration.
- What is the CVSS score? Last, deliberately. It is an input, not the answer.
Most organisations run that list in roughly the opposite order, because the score is the number printed on the alert.
Where alerts come from
Section titled “Where alerts come from”An alert is the join of two things: your dependency graph, and the GitHub Advisory Database.
The advisory database aggregates published vulnerability data — CVEs from the national databases, advisories published by maintainers through GitHub’s security advisory workflow, and entries curated by GitHub’s security team. Each advisory names affected packages, affected version ranges and, where one exists, a patched version.
Two consequences follow directly.
An alert fires when either side changes. A new advisory matching what you already have, or a new dependency matching an existing advisory. Most alerts are the first kind, which is why they arrive in bursts unrelated to anything you did.
Coverage is a property of the database. A vulnerability nobody has reported produces no advisory and no alert. This is not a gap you can close with configuration — it is the reason dependency review’s age and adoption signals matter, and why dependency security covers threats that never become advisories.
Reachability is the biggest filter
Section titled “Reachability is the biggest filter”Most vulnerability alerts on a mature codebase are not exploitable in that codebase, and the reason is usually simple: you do not call the affected code.
A library with forty modules where you import two. An advisory concerning its XML parser, in an application that only uses its HTTP client. The dependency is present, the version matches, and the vulnerable function is never reached.
That is not a reason to ignore alerts. It is a reason to read the advisory rather than the severity, because the advisory usually names the affected function or feature, and answering “do we call that?” takes minutes.
The other four questions
Section titled “The other four questions”Exploitability in your deployment. A vulnerability requiring a crafted request is a different risk in a public API and in a batch job behind three network boundaries. A vulnerability requiring local filesystem access is nearly irrelevant in a container that has none.
Impact. What an attacker gains. Remote code execution in a service holding customer data is not comparable to a denial of service in a build tool, whatever their scores say.
Known exploitation. An advisory with observed exploitation in the wild moves to the front of the queue regardless of anything else. This is the single strongest signal available, because it converts a theoretical risk into an observed one.
Fix cost. A patch release you can take today is worth taking even for a low-severity issue — cheap, permanent, and it removes a decision. A fix that only exists in a major version is a migration project, and it deserves an explicit decision rather than sitting in a queue accumulating age.
CVSS, and why it comes last
Section titled “CVSS, and why it comes last”CVSS scores a vulnerability’s characteristics in the abstract: attack vector, complexity, privileges required, impact on confidentiality, integrity and availability.
It is genuinely useful for what it is — a consistent, comparable description of a vulnerability. What it cannot describe is the thing you need: your exposure.
The score is computed without knowledge of whether you call the affected code, whether the input reaches it, or what your deployment looks like. Two organisations with the same dependency and the same advisory can have completely different real risk, and CVSS assigns them the same number.
Using it as the primary filter produces both errors at once: high-scoring alerts about unreachable code consume the team’s attention, and a medium-scoring alert on your authentication path waits behind them.
Use it as a tie-breaker among alerts you have already assessed, and as a coarse threshold for what gets assessed at all.
Dismissing well
Section titled “Dismissing well”The dismissal reason is the only artefact of the analysis. Everything else disappears.
| Reason | Correct use |
|---|---|
| A fix has already been started | The upgrade is in flight |
| This alert is inaccurate | The advisory does not apply — wrong version, wrong package |
| The vulnerable code is not actually used | Reachability. Say which function you checked |
| No bandwidth to fix this | An honest deferral. Needs a date and an owner |
| Risk is tolerable to this project | An accepted risk. Needs the reasoning |
The last two are honest and are the two most often misused, because they are what people select when the real reason is “I do not want to look at this again”.
Auto-triage rules
Section titled “Auto-triage rules”Rules that automatically dismiss or snooze alerts matching criteria. GitHub provides a default rule, and organisations can define custom ones.
The legitimate case is a class of alert that genuinely does not apply, evaluated once rather than repeatedly — low-severity issues in development-only dependencies, for instance, where the vulnerability requires an attacker-facing deployment that a test runner does not have.
The illegitimate case is any rule broad enough that you can no longer say what it is suppressing.
If you use them:
- Keep them narrow. Specific ecosystems, scopes and severity bands, not blanket rules.
- Record why each exists. The rule is a standing decision and needs the same reasoning a dismissal does.
- Review them. Quarterly is enough. A rule written for a repository’s old architecture applies perfectly well to its new one.
- Watch what they suppress. A rule dismissing forty alerts a month is doing something significant, and nobody is looking at any of it.
A triage rhythm that holds
Section titled “A triage rhythm that holds”Prioritisation principles are easy to agree with and hard to sustain. What makes them stick is a routine small enough that nobody negotiates it.
Daily, five minutes. Look at new alerts only. Anything critical or with known exploitation gets handled today; everything else gets a quick reachability read and either a fix, a dismissal with a reason, or a place in the weekly list. Five minutes is achievable because new alerts are few — the volume everyone dreads is the backlog, and the backlog is not in this step.
Weekly, thirty minutes. Work the list from the daily passes. Merge the Dependabot pull requests that CI has validated. Look at anything that has been open more than a week and decide whether it is deferred deliberately or just old.
Quarterly. Review dismissals and auto-triage rules. Ask whether the reachability judgements still hold and whether the rules still describe the architecture. This is the step that stops the accumulated exceptions becoming an invisible policy nobody wrote.
Continuously. The backlog, as a campaign with an owner and a deadline, entirely separate from the above.
The structural point is that only the last one is a project. The first three are a routine, and a routine that takes five minutes a day survives contact with a busy quarter in a way that a monthly two-hour triage meeting does not.
Alert fatigue is a security failure
Section titled “Alert fatigue is a security failure”Worth stating as a finding rather than as an operational annoyance.
A team with four hundred open alerts has the same effective posture as a team with alerting disabled, minus the time spent not reading the tab. Detection that produces nothing actionable is not detection; it is a record that would have been useful.
The mechanisms that keep the queue readable are all legitimate and all under-used:
- Grouped updates — one pull request instead of twelve
- Severity thresholds on what blocks a merge
- Scope filtering, separating runtime from development dependencies
- Auto-triage rules, narrowly
- Campaigns for the historical backlog, so it is a project rather than a queue
The key structural move is separating new alerts from the historical backlog. New alerts need a short response time and are few. The backlog is a project with an owner and a deadline. Mixing them produces a single list that is both too long to work and too important to ignore, which is the condition under which people stop looking.
Organisational management
Section titled “Organisational management”At scale the useful questions are comparative.
Security overview aggregates alerts across repositories, which is where you notice that one team has ninety open alerts and another has none — usually a fact about enablement rather than about hygiene.
Security campaigns turn a filtered set of alerts into tracked remediation with an owner and a deadline. See Security campaigns.
The metric worth tracking is not open alert count. It is median time from alert to resolution for new alerts, tracked separately from the backlog. The first measures whether the process works. The second measures a project’s progress. Reporting them as one number tells you nothing about either.
Publishing your own advisories
Section titled “Publishing your own advisories”The other side of the database, and relevant if anyone consumes what you publish.
GitHub’s repository security advisories let a maintainer draft an advisory privately, collaborate on a fix in a private fork, and publish both together. Publishing routes the advisory into the GitHub Advisory Database, which means every downstream consumer gets a Dependabot alert.
Three things about that process are worth knowing even if you never use it.
Private disclosure is the point. Fixing a vulnerability in a public repository publishes the vulnerability, in the commit, before anyone has upgraded. The private fork exists to avoid that.
Publishing is what generates downstream alerts. A fixed vulnerability that was never disclosed protects only the people who upgraded for other reasons. If you maintain something others depend on, publishing is part of fixing.
Private vulnerability reporting gives researchers a channel that is not a public issue. Without one, the realistic alternatives are a public issue disclosing the vulnerability, or an email nobody reads.
If you publish libraries, actions or container images, this is the mechanism by which your consumers find out — and it is the same mechanism you rely on when somebody else’s dependency is the problem.
When an advisory is genuinely urgent
Section titled “When an advisory is genuinely urgent”Occasionally an advisory arrives that everybody is talking about, and the response is different from routine triage.
-
Answer “are we affected?” first, across every repository. This is what the dependency graph is for, and the scripted version of the question is worth having written before you need it.
-
Include what the graph cannot see — container base images, vendored code, build-time downloads. For a widely-exploited advisory these are frequently where it actually is.
-
Determine reachability for the repositories that are affected. The same analysis as routine triage, done urgently.
-
Patch what is reachable, and mitigate what cannot be patched immediately — a configuration change, a network control, a feature disabled.
-
Record what you checked, including the negatives. “We confirmed these forty repositories are not affected” is the answer to the question somebody will ask next week.
Step 1 is the one that separates a prepared organisation from an unprepared one, and the difference is measured in hours at the front of the response.
The threat model
Section titled “The threat model”Threat. A known, published vulnerability in a dependency is exploited against your system, because it was never assessed or never fixed.
Attack surface. Every dependency in the tree with a published advisory, weighted by whether the vulnerable path is reachable from your usage and exposed by your deployment.
Impact. Set by the vulnerability and by your architecture together. Note that the most common real impact is not a single exploited CVE — it is the response failure when an urgent advisory arrives and the team cannot tell which of four hundred open alerts is the one that matters.
Control. A short, current queue of assessed alerts, with the historical backlog managed separately as a project. Documented reasoning on every dismissal. Time-to-resolution tracked rather than count.
Verification. Pick a package you depend on and answer, without looking it up: is it affected by anything open, has anybody assessed it, and when? Then ask how long it would take to answer that for your entire estate.
That last question is the one worth timing. An organisation that can answer “are we affected?” in fifteen minutes has a different incident from one that needs three days, and the difference is preparation rather than tooling.
Common mistakes
Section titled “Common mistakes”Prioritising by CVSS. It scores the vulnerability, not your exposure, and it is the number most readily available.
Patching everything. Unsustainable, and it trains the team to treat alerts as noise.
Dismissing without a reason. The reason is the entire record of the analysis.
Dismissing on reachability and forgetting. The judgement is about today’s code.
Broad auto-triage rules. A permanent, invisible reduction in what you are told.
Treating an open pull request as remediation. The vulnerability is present until it merges and deploys.
Mixing new alerts with the historical backlog. One list that is too long to work and too important to ignore.
Measuring open alert count. It rewards dismissal. Time-to-resolution rewards fixing.
Mental model
Section titled “Mental model”An alert is a hypothesis: this vulnerability may affect you. Triage is testing it. The output is not “fixed” or “ignored” — it is a decision with a reason attached, which is the only part that survives to be useful later.
What you learned
Section titled “What you learned”- Severity describes the vulnerability; risk describes your system; triage converts one to the other
- Alerts are the join of your dependency graph and the GitHub Advisory Database
- Most alerts fire because of a new advisory, not a change you made
- Reachability is the largest single filter, and it is a statement about today’s code
- Known exploitation in the wild is the strongest prioritisation signal available
- CVSS is a tie-breaker, not a priority, because it knows nothing about your deployment
- The dismissal reason is the only surviving artefact of the analysis
- Auto-triage rules are standing decisions and need narrow scope and a review date
- Separate new alerts from the historical backlog, and measure time to resolution rather than count
Exercise
Section titled “Exercise”Use a disposable repository with a real dependency manifest.
-
Add a dependency with a known advisory. Wait for the alert.
-
Open the alert and read the advisory, not the summary. Identify which function or feature is affected.
-
Predict: does your code call it? Check.
-
Look at the CVSS score, then at your answer to step 3. Predict: would score-based prioritisation have given you the same answer?
-
Dismiss the alert with a reason detailed enough for a stranger. Read it back and ask whether it would let somebody re-evaluate.
-
Add a second dependency with an advisory affecting a code path you do use. Compare the two alerts. Predict: does anything in the interface distinguish them?
-
If you administer a test organisation, create a narrow auto-triage rule and observe what it suppresses.
-
Delete the repository.