Introduction
Security scanning is useful only when developers can understand what the results actually mean.
In a real repository, a security scanner may report hundreds of alerts over time. Some are new, some are already being fixed, and others may have been investigated and determined to be acceptable risks for a particular application.
This creates an important challenge:
How do you remove an alert from the active security queue without pretending that the underlying risk never existed?
GitHub's code-scanning experience introduces more detailed handling for mitigated alerts, including additional reasoning around why an alert was dismissed or otherwise treated as no longer requiring active remediation.
This distinction is important for security teams because:
Alert removed from active queue
≠
Security risk never existed
A mature security process should preserve the reasoning behind a decision instead of simply making an alert disappear.
What Is a Code-Scanning Alert?
A code-scanning alert represents a potential security problem identified during static analysis.
For example, imagine a .NET application containing:
public string BuildQuery(string customerId)
{
return "SELECT * FROM Customers WHERE Id = '"
+ customerId
+ "'";
}
A security analyzer may identify a potential SQL injection problem.
The important information is not only:
SQL Injection Detected
It also includes:
Where the problem exists
How the data flows
What code is affected
What severity is assigned
Whether the issue is reachable
Whether remediation is required
A useful security workflow therefore looks like:
Code
|
v
Security Analysis
|
v
Alert
|
v
Investigation
|
+---- True Risk ----> Remediate
|
+---- Acceptable ----> Document Decision
|
+---- False Positive -> Dismiss With Reason
Why Alert Management Matters
Imagine a repository with:
1,000 security alerts
If every alert remains active forever, developers eventually face alert fatigue.
They may stop paying attention to the dashboard.
That creates another security problem:
Too Many Alerts
|
v
Less Attention
|
v
Important Alerts Get Missed
The answer is not to suppress everything.
The answer is to distinguish between:
Active Risk
Resolved Risk
Accepted Risk
False Positive
This allows teams to focus attention where it matters.
What Does "Mitigated" Mean?
A mitigated alert is an alert where the immediate security concern has been addressed or is no longer considered an active remediation item.
That does not necessarily mean:
"This code was always safe."
It can instead mean:
"The organization investigated this alert
and has a documented reason for its current state."
For example, suppose a scanner reports a potential vulnerability in a legacy integration.
After investigation, the team determines that:
The vulnerable path is unreachable
The input is constrained
A compensating control exists
The component is isolated
The system is scheduled for replacement
The team may decide that immediate remediation is not required.
That decision should be recorded.
Mitigated vs False Positive
These concepts should not be treated as identical.
False Positive
A false positive means the scanner identified something that is not actually a security problem in the application's context.
For example:
Scanner:
Potential SQL Injection
Investigation:
Input is never user-controlled and is
generated from a fixed internal enumeration.
The alert may be a false positive.
Mitigated Risk
A mitigated risk means there was a legitimate security concern or potential concern, but another control or change reduces the risk to an acceptable level.
For example:
Potentially dangerous operation
|
v
Additional validation
|
v
Risk reduced
The distinction is important for auditability.
Why Dismissal Reasons Matter
Suppose an alert is dismissed with:
Reason: Won't Fix
That tells the next developer very little.
A better record might explain:
This endpoint is accessible only from the
internal processing network. The input is
validated against the approved customer
identifier format before reaching the query.
The application is also scheduled for migration
to parameterized queries.
Now another security engineer can understand the decision.
Good security records answer:
What was found?
Why was it investigated?
What did we determine?
What control exists?
Why is remediation not happening now?
A Practical .NET Example
Consider:
public async Task<Customer?> FindCustomerAsync(
string customerId)
{
var sql =
$"SELECT * FROM Customers WHERE Id = '{customerId}'";
return await database.QuerySingleOrDefaultAsync<Customer>(
sql);
}
A security scanner could identify this as a potential injection vulnerability.
The preferred fix is to parameterize the query:
public async Task<Customer?> FindCustomerAsync(
string customerId)
{
const string sql =
"SELECT * FROM Customers WHERE Id = @CustomerId";
return await database.QuerySingleOrDefaultAsync<Customer>(
sql,
new { CustomerId = customerId });
}
After the change:
Security Alert
|
v
Code Fixed
|
v
Scan Again
|
v
Alert Resolved
This is different from simply dismissing the alert.
Whenever practical, fixing the underlying vulnerability should remain the preferred option.
When Should an Alert Be Mitigated?
There are legitimate situations where immediate code modification may not be the best answer.
Examples include:
Legacy System
A critical legacy component may be scheduled for replacement.
Current System
|
v
Temporary Compensating Control
|
v
Migration
Unreachable Code Path
The scanner may identify a potentially dangerous path that cannot be reached under the application's actual architecture.
External Protection
A vulnerability may be mitigated by a control outside the application.
For example:
Application
|
v
Internal Network
|
v
Restricted Service
Accepted Risk
The organization may consciously accept a documented risk based on business or technical considerations.
The important part is that acceptance should be deliberate, not automatic.
Never Use Mitigation to Hide a Problem
There is a major difference between:
Investigate → Document → Mitigate
and:
Alert appears → Dismiss → Forget
The second process creates security debt.
A mature process should require enough information to justify the decision.
For example:
Alert
|
+--> Owner
+--> Severity
+--> Investigation
+--> Decision
+--> Reason
+--> Compensating Control
+--> Review Date
This creates traceability.
Security Alert Lifecycle
A useful lifecycle is:
Detected
|
v
Triaged
|
+---- False Positive
| |
| v
| Document
|
+---- Real Risk
|
v
Remediation
|
v
Verify
|
v
Resolved
There can also be another path:
Real Risk
|
v
Temporary Mitigation
|
v
Accepted / Deferred
|
v
Periodic Review
|
v
Remediate
The second path is useful for risks that cannot be fixed immediately.
Security Debt Is Still Debt
A mitigated alert should not automatically disappear from engineering awareness.
Suppose a team has:
10 mitigated alerts
That may look good.
But if all 10 are relying on temporary workarounds, the organization still has security debt.
A better dashboard separates:
| State | Meaning |
|---|
| Open | Requires investigation or remediation |
| In Progress | Remediation underway |
| Resolved | Underlying issue fixed |
| False Positive | Alert does not represent a real issue |
| Mitigated | Risk reduced through another control |
| Accepted | Risk intentionally retained |
This provides much better visibility than simply counting active alerts.
Use Severity With Context
Security severity provides useful prioritization.
For example:
Critical
High
Medium
Low
But severity should not be considered in isolation.
Consider:
High Severity
+
Public Internet
+
Authentication Bypass
This deserves immediate attention.
Compare that with:
High Severity
+
Unreachable Internal Code
+
Strong Network Isolation
The technical context is different.
This is why security triage requires both automated analysis and human investigation.
Don't Ignore Business Context
Security decisions sometimes depend on how the application is actually used.
Suppose a scanner identifies:
Potential command injection
A security engineer should ask:
Where does the input originate?
Is the endpoint publicly accessible?
Is authentication required?
Is authorization enforced?
What permissions does the process have?
Is the code path reachable?
Is the input validated?
What data can the process access?
The alert is the starting point.
It is not the final security conclusion.
Code Scanning and Pull Requests
Code scanning becomes particularly useful when integrated with pull requests.
Consider:
Developer
|
v
Pull Request
|
v
CodeQL Analysis
|
+---- New Alert
| |
| v
| Review
|
v
No New Security Finding
|
v
Human Approval
A new security finding should receive attention before the change reaches the main branch.
This is especially important for security-sensitive code such as:
Preventing Alert Fatigue
Alert fatigue is one of the biggest problems with security tooling.
Suppose developers see:
Monday 15 alerts
Tuesday 22 alerts
Wednesday 18 alerts
Thursday 30 alerts
Friday 25 alerts
If most alerts are low-value or poorly understood, developers may eventually stop investigating them.
A better strategy is:
Detect
|
v
Prioritize
|
v
Investigate
|
v
Remediate or Document
Quality matters more than raw alert volume.
A Practical Security Triage Process
When a new alert appears, follow a repeatable process.
Step 1: Understand the Alert
Identify:
Rule
File
Line
Severity
Data Flow
Step 2: Confirm Reachability
Ask whether an attacker can actually control the relevant input.
Step 3: Understand the Impact
Determine what could happen if the issue were exploited.
Step 4: Check Existing Controls
Look for:
Validation
Authorization
Network Restrictions
Sandboxing
Encoding
Parameterization
Step 5: Choose an Action
The result should normally be one of:
Fix
Mitigate
Accept
False Positive
Step 6: Document the Decision
Record enough information for another engineer to understand the reasoning.
Step 7: Review Later if Necessary
Temporary mitigations should have an owner and, where appropriate, a review date.
Example: File Path Vulnerability
Consider:
public async Task<byte[]> ReadFileAsync(string fileName)
{
var path = Path.Combine(
"/app/uploads",
fileName);
return await File.ReadAllBytesAsync(path);
}
A scanner may identify a potential path traversal vulnerability.
An attacker could potentially attempt:
../../sensitive-file
A better implementation validates the requested path.
For example:
public async Task<byte[]> ReadFileAsync(string fileName)
{
var root = Path.GetFullPath("/app/uploads");
var path = Path.GetFullPath(
Path.Combine(root, fileName));
if (!path.StartsWith(
root,
StringComparison.Ordinal))
{
throw new SecurityException(
"Invalid file path.");
}
return await File.ReadAllBytesAsync(path);
}
The exact validation strategy should be designed carefully for the application.
The important lesson is that a real vulnerability should normally be fixed rather than dismissed.
Temporary Mitigation Example
Suppose a legacy service cannot be modified immediately.
A compensating control might be:
Internet
|
v
Web Application Firewall
|
v
Restricted Endpoint
|
v
Legacy Service
If the control meaningfully reduces exposure, the security team may temporarily classify the alert as mitigated.
But the mitigation should be documented:
Risk:
Path traversal in legacy endpoint.
Mitigation:
Endpoint accessible only through internal network.
Owner:
Platform Security Team.
Follow-up:
Replace legacy file handler.
The mitigation is a risk-management decision, not a declaration that the vulnerable code is safe.
Common Mistakes
Mistake 1: Dismissing Every Difficult Alert
A difficult alert is not automatically a false positive.
Mistake 2: Using "Won't Fix" Without Explanation
Future developers need to understand the decision.
Mistake 3: Treating Mitigation as Permanent
A temporary control can eventually become forgotten technical debt.
Mistake 4: Ignoring Alert Severity
Prioritize security work based on impact and exploitability.
Mistake 5: Fixing Only the Reported Line
Security vulnerabilities often involve a broader data flow.
Mistake 6: Forgetting Compensating Controls
If a mitigation depends on infrastructure, document that dependency.
Mistake 7: Never Reviewing Old Decisions
Security assumptions can change as applications evolve.
Troubleshooting
| Problem | What to Check |
|---|
| Too many alerts | Review rule configuration and prioritization |
| Alert appears to be false | Trace the input and execution path |
| Same alert keeps returning | Verify whether the underlying code was actually changed |
| Developers ignore alerts | Reduce noise and improve triage |
| Mitigation is unclear | Document the exact compensating control |
| Risk becomes higher later | Reassess the alert after architecture changes |
| Alert cannot be resolved | Check whether the vulnerable code path still exists |
| Security dashboard looks clean but risk remains | Review mitigated and accepted alerts separately |
Best Practices
Fix Real Vulnerabilities Whenever Practical
Mitigation should not become an excuse to avoid remediation.
Document Security Decisions
Every non-remediated security alert should have a meaningful explanation.
Separate False Positives From Accepted Risks
These represent different security decisions.
Track Mitigated Alerts
Do not allow them to disappear from organizational visibility.
Assign Ownership
Someone should be responsible for reviewing important security decisions.
Review Temporary Mitigations
Compensating controls can become outdated.
Use Pull Request Analysis
Detect new security problems before they reach the main branch.
Prioritize by Risk
Consider severity, exploitability, exposure, and business impact.
Combine Automated and Human Analysis
Static analysis finds patterns; engineers provide context.
Advantages
Reduced Security Noise
Proper alert management helps teams focus on meaningful risks.
Better Auditability
Documented decisions provide a history of how security issues were handled.
More Effective Prioritization
Developers can distinguish active vulnerabilities from already-managed risks.
Supports Legacy Systems
Temporary mitigation can provide a practical path when immediate remediation is not possible.
Better Security Governance
Security decisions become explicit rather than informal.
Disadvantages and Limitations
Risk of Overuse
If teams classify too many alerts as mitigated, real security debt can become hidden.
Documentation Overhead
Good security decisions require investigation and explanation.
Mitigations Can Expire
A compensating control may disappear during future architecture changes.
Human Judgment Is Required
A scanner cannot determine every organization's acceptable risk.
Security Status Can Become Stale
An alert that was safe to defer six months ago may become important after the system changes.
Building a Security Alert Review Process
A practical organization-wide process can look like:
New Alert
|
v
Security Triage
|
+---- False Positive
| |
| v
| Document
|
+---- Real Risk
|
+---- Fix
| |
| v
| Verify
|
+---- Mitigate
| |
| v
| Monitor
|
+---- Accept
|
v
Review
This creates a consistent process without requiring every alert to follow exactly the same remediation path.
A Simple Security Decision Template
For every alert that is not immediately fixed, record:
Alert:
[Security rule and description]
Risk:
[What could happen?]
Affected Component:
[Service / repository / endpoint]
Decision:
[False Positive / Mitigated / Accepted]
Reason:
[Why this decision was made]
Compensating Control:
[If applicable]
Owner:
[Responsible team]
Follow-Up:
[Future remediation or review]
This is simple enough to use consistently and detailed enough to preserve context.
Conclusion
Security alerts are useful only when teams can manage them responsibly. A clean code-scanning dashboard does not necessarily mean a completely secure application, just as a large number of alerts does not automatically mean the application is unsafe. The important part is understanding why each significant alert exists and what happened after it was discovered. Real vulnerabilities should be fixed whenever possible, while legitimate false positives, temporary mitigations, and accepted risks should be clearly distinguished and documented. Treat mitigated alerts as part of the security history rather than simply hiding them, and review important decisions again when the application or its threat model changes. This approach gives development and security teams a much clearer picture of the actual risk in their codebase.