Baseline Management¶
Complete guide for managing baselines of accepted findings.
Table of Contents¶
- What is a Baseline
- Creating a Baseline
- Using a Baseline
- Viewing Baseline Contents
- Merging Baselines
- Maintaining the Baseline
- Management Strategies
- Programmatic API
What is a Baseline¶
A baseline is a file containing findings that have been reviewed and accepted. These findings are excluded from future scans.
Use Cases¶
| Case | Description |
|---|---|
| False positives | Incorrect findings that are not vulnerabilities |
| Accepted risk | Known vulnerabilities with accepted risk |
| Pending | Issues that will be fixed in the future |
| Legacy | Inherited code that cannot be modified |
| Exceptions | Documented special cases |
Baseline Structure¶
{
"version": "1.0",
"created_at": "2026-01-23T10:00:00Z",
"updated_at": "2026-01-23T10:00:00Z",
"entries": [
{
"rule_id": "MCP-E001",
"location_hash": "sha256:abc123...",
"file": "src/config.py",
"line": 47,
"reason": "Development API key, not used in production",
"accepted_by": "security@example.com",
"timestamp": "2026-01-23T10:00:00Z"
}
]
}
Entry Fields¶
| Field | Type | Description |
|---|---|---|
rule_id |
string | Rule ID |
location_hash |
string | Hash of file + line |
file |
string | File path |
line |
int | Line number |
reason |
string | Acceptance reason |
accepted_by |
string | Who accepted |
timestamp |
string | When it was accepted |
Creating a Baseline¶
Method 1: From Scan Results¶
# 1. Run scan and save results
mcp-scan scan . --output json > scan-results.json
# 2. Review results
cat scan-results.json | jq '.findings[] | "\(.rule_id): \(.title) @ \(.location.file):\(.location.line)"'
# 3. Generate baseline
mcp-scan baseline generate --from scan-results.json
# 4. The baseline.json file is created
Method 2: With Metadata¶
mcp-scan baseline generate \
--from scan-results.json \
--output my-baseline.json \
--reason "Initial security review" \
--accepted-by "security-team@example.com"
Method 3: Selective Baseline¶
After generating, manually edit to include only some findings:
# Generate complete baseline
mcp-scan baseline generate --from scan-results.json
# Edit to keep only accepted ones
nano baseline.json
Generation Options¶
| Option | Alias | Default | Description |
|---|---|---|---|
--from |
-f |
- | JSON results file |
--output |
-o |
baseline.json |
Output file |
--reason |
-r |
"" |
Reason for all entries |
--accepted-by |
-a |
"" |
Who accepts |
Using a Baseline¶
In Scans¶
# Normal scan with baseline
mcp-scan scan . --baseline baseline.json
# Findings in baseline are filtered out
Output with Baseline¶
MCP-Scan v2.0.0
================================================================================
FINDINGS (excluding baselined)
================================================================================
HIGH: 1
- MCP-C002: Unvalidated URL in request (NEW)
File: src/api.py
Line: 18
================================================================================
Total: 1 new finding (1 high)
Baselined: 3 findings filtered
Exit code: 1
In CI/CD¶
# .github/workflows/security.yml
- name: Security Scan
run: |
mcp-scan scan . \
--baseline baseline.json \
--fail-on high \
--output sarif > results.sarif
Verify Baseline Applied¶
# See how many findings were filtered
mcp-scan scan . --baseline baseline.json --output json | jq '.summary.baselined'
Viewing Baseline Contents¶
Show Command¶
Example Output¶
BASELINE: baseline.json
=======================
Version: 1.0
Created: 2026-01-23T10:00:00Z
Updated: 2026-01-23T15:30:00Z
Total Entries: 5
BY RULE ID:
MCP-E001: 3 entries
- src/config.py:47 (Development API key)
- src/config.py:48 (Development DB password)
- src/legacy/old.py:12 (Legacy token)
MCP-A003: 1 entry
- scripts/admin.py:25 (Internal admin tool, not exposed)
MCP-N001: 1 entry
- . (Lockfile managed externally)
BY FILE:
src/config.py: 2 entries
src/legacy/old.py: 1 entry
scripts/admin.py: 1 entry
.: 1 entry
RECENT ENTRIES (last 5):
1. MCP-E001 @ src/config.py:47
Reason: Development API key, not used in production
Accepted by: security@example.com
Date: 2026-01-23
2. MCP-E001 @ src/config.py:48
Reason: Development DB password
Accepted by: security@example.com
Date: 2026-01-23
...
JSON Output¶
{
"file": "baseline.json",
"version": "1.0",
"created_at": "2026-01-23T10:00:00Z",
"updated_at": "2026-01-23T15:30:00Z",
"total_entries": 5,
"stats": {
"by_rule_id": {
"MCP-E001": 3,
"MCP-A003": 1,
"MCP-N001": 1
},
"by_file": {
"src/config.py": 2,
"src/legacy/old.py": 1,
"scripts/admin.py": 1,
".": 1
}
},
"entries": [...]
}
Merging Baselines¶
Scenario¶
You have multiple baselines from different teams or branches:
Merge Command¶
mcp-scan baseline merge \
baseline-dev.json \
baseline-qa.json \
baseline-legacy.json \
--output baseline-combined.json
Merge Behavior¶
- Deduplication: Duplicate entries are removed
- Conflicts: The most recent entry is kept
- Metadata: Reasons and accepters are combined
Merging baselines...
baseline-dev.json: 10 entries
baseline-qa.json: 5 entries
baseline-legacy.json: 8 entries
Result:
Total entries: 18 (5 duplicates removed)
Written to: baseline-combined.json
Merge Example¶
baseline-dev.json:
baseline-qa.json:
{
"entries": [
{"rule_id": "MCP-E001", "file": "src/config.py", "line": 47}, // Duplicate
{"rule_id": "MCP-B002", "file": "src/handlers.py", "line": 33}
]
}
baseline-combined.json (result):
{
"entries": [
{"rule_id": "MCP-E001", "file": "src/config.py", "line": 47},
{"rule_id": "MCP-B002", "file": "src/handlers.py", "line": 33}
]
}
Maintaining the Baseline¶
Remove Specific Entries¶
Manually edit the JSON file:
# Open for editing
nano baseline.json
# Or with jq (remove specific entry)
jq 'del(.entries[] | select(.rule_id == "MCP-E001" and .file == "src/config.py"))' \
baseline.json > baseline-updated.json
Clean Obsolete Entries¶
If the code has changed, some entries may be obsolete:
# Scan and see which entries no longer match
mcp-scan scan . --baseline baseline.json --output json | \
jq '.summary.baselined'
# If it's 0, the baseline may need cleanup
Update Reasons¶
# Manually edit
nano baseline.json
# Update timestamp
jq '.updated_at = now | todate' baseline.json > baseline-updated.json
Audit Baseline¶
# View statistics
mcp-scan baseline show baseline.json
# Export for review
mcp-scan baseline show baseline.json --output json > baseline-audit.json
# Review old entries
jq '.entries | sort_by(.timestamp) | .[0:5]' baseline.json
Management Strategies¶
Strategy 1: Baseline per Environment¶
baselines/
baseline-dev.json # Development - permissive
baseline-staging.json # Staging - moderate
baseline-prod.json # Production - strict
# Development
mcp-scan scan . --baseline baselines/baseline-dev.json
# CI for staging
mcp-scan scan . --baseline baselines/baseline-staging.json --fail-on high
# Release to production
mcp-scan scan . --baseline baselines/baseline-prod.json --fail-on medium
Strategy 2: Baseline per Type¶
baselines/
baseline-false-positives.json # Only false positives
baseline-accepted-risk.json # Accepted risk
baseline-legacy.json # Legacy code
# Combine for scanning
mcp-scan baseline merge \
baselines/baseline-false-positives.json \
baselines/baseline-accepted-risk.json \
--output .mcp-scan-baseline.json
mcp-scan scan . --baseline .mcp-scan-baseline.json
Strategy 3: Periodic Review¶
# Every quarter, review baseline
mcp-scan baseline show baseline.json > baseline-review-Q1-2026.txt
# Clean old entries (> 6 months)
jq --arg cutoff "2025-07-01T00:00:00Z" \
'.entries |= map(select(.timestamp > $cutoff))' \
baseline.json > baseline-cleaned.json
Strategy 4: Required Approval¶
Review process for adding to baseline:
- PR with findings: Scan fails with new findings
- Security review: Security team reviews
- Documented approval: Reason is documented
- Baseline update: Added with metadata
# Add with documentation
mcp-scan baseline generate \
--from new-findings.json \
--reason "Reviewed in JIRA-1234, accepted risk" \
--accepted-by "security-lead@example.com"
Programmatic API¶
Using the Go Library¶
package main
import (
"github.com/mcphub/mcp-scan/internal/baseline"
"github.com/mcphub/mcp-scan/pkg/scanner"
)
func main() {
// Load existing baseline
bl, err := baseline.Load("baseline.json")
if err != nil {
log.Fatal(err)
}
// Run scan
s := scanner.New(scanner.Config{Mode: "fast"})
result, err := s.Scan(context.Background(), "./src")
if err != nil {
log.Fatal(err)
}
// Apply baseline
filtered := s.ApplyBaseline(result, bl)
// View non-baselined findings
fmt.Printf("New findings: %d\n", len(filtered.Findings))
// Generate new baseline
newBaseline := s.GenerateBaseline(result)
// Merge with existing
merged := baseline.Merge(bl, newBaseline)
// Save
err = baseline.Save(merged, "baseline-updated.json")
}
Baseline Statistics¶
// Get statistics
stats := bl.GetStats()
fmt.Printf("Total: %d\n", stats.Total)
fmt.Printf("By Rule:\n")
for rule, count := range stats.ByRuleID {
fmt.Printf(" %s: %d\n", rule, count)
}
Remove Entries¶
// Remove by rule and location
bl.Remove("MCP-E001", "src/config.py", 47)
// Remove by finding ID
bl.RemoveFinding(finding)
Check if Baselined¶
// Check if a finding is in baseline
if bl.Contains(finding) {
fmt.Println("Finding is baselined")
}
Best Practices¶
1. Always Document¶
{
"rule_id": "MCP-E001",
"file": "src/config.py",
"line": 47,
"reason": "Development API key for local testing. Production uses env vars. See JIRA-1234.",
"accepted_by": "jane.doe@example.com"
}
2. Review Periodically¶
- Quarterly: Review all entries
- Monthly: Review critical entries
- Each release: Validate that baseline is still relevant
3. Version Control Baseline¶
# Keep baseline in version control
git add baseline.json
git commit -m "Update baseline: add accepted risk for MCP-E001 in config.py"
4. Separate by Criticality¶
{
"entries": [
{
"rule_id": "MCP-A003",
"severity_at_baseline": "critical",
"requires_review_by": "2026-04-01"
}
]
}
5. Automate Validation¶
# CI: Verify baseline doesn't grow unchecked
- name: Check baseline size
run: |
ENTRIES=$(jq '.entries | length' baseline.json)
if [ $ENTRIES -gt 50 ]; then
echo "Baseline has too many entries ($ENTRIES). Please review."
exit 1
fi
Troubleshooting¶
Baseline Not Applied¶
Symptom: Findings are not being filtered.
Causes: 1. File moved/renamed 2. Line changed 3. Incorrect baseline format
Solution:
# Verify that the hash matches
mcp-scan scan . --output json | jq '.findings[0].id'
jq '.entries[0].location_hash' baseline.json
Too Many False Positives¶
Symptom: The baseline grows too large.
Solutions: 1. Use custom rules for specific cases 2. Adjust allowlist configuration 3. Review if the rules are appropriate
Corrupt Baseline¶
Symptom: Error loading baseline.
Solution:
Previous: Output Formats | Next: CI/CD Integration