Skip to content

Baseline Management

Complete guide for managing baselines of accepted findings.


Table of Contents

  1. What is a Baseline
  2. Creating a Baseline
  3. Using a Baseline
  4. Viewing Baseline Contents
  5. Merging Baselines
  6. Maintaining the Baseline
  7. Management Strategies
  8. 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

mcp-scan baseline show baseline.json

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

mcp-scan baseline show baseline.json --output json
{
  "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:

baseline-dev.json      # Development
baseline-qa.json       # QA
baseline-legacy.json   # Legacy code

Merge Command

mcp-scan baseline merge \
  baseline-dev.json \
  baseline-qa.json \
  baseline-legacy.json \
  --output baseline-combined.json

Merge Behavior

  1. Deduplication: Duplicate entries are removed
  2. Conflicts: The most recent entry is kept
  3. 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:

{
  "entries": [
    {"rule_id": "MCP-E001", "file": "src/config.py", "line": 47}
  ]
}

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:

  1. PR with findings: Scan fails with new findings
  2. Security review: Security team reviews
  3. Documented approval: Reason is documented
  4. 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:

# Validate JSON
jq . baseline.json

# Verify structure
jq 'has("entries")' baseline.json


Previous: Output Formats | Next: CI/CD Integration