Machine-Readable Diffs Guide
Compare two security scans to identify new, resolved, and modified findings.
The jmo diff command enables intelligent comparison of scan results using fingerprint-based matching, supporting PR reviews, CI/CD gates, remediation tracking, and trend analysis.
Table of Contents
- Key Features
- Two Comparison Modes
- Directory Mode (Primary)
- SQLite Mode (Historical)
- CLI Reference
- Output Formats
- JSON (Machine-Readable)
- Markdown (PR Comments)
- HTML (Interactive Dashboard)
- SARIF 2.1.0 (Code Scanning)
- Modification Detection
- CI/CD Integration Examples
- GitHub Actions (PR Comments)
- GitLab CI (Merge Request Comments)
- Common Workflows
- Performance
- Troubleshooting
- Related Documentation
Key Features
| Feature | Description |
|---|---|
| Fingerprint Matching | O(n) performance with stable finding IDs |
| Four Classifications | NEW, RESOLVED, UNCHANGED, MODIFIED |
| Modification Detection | Tracks severity upgrades, compliance changes, priority shifts |
| Four Output Formats | JSON, Markdown (PR comments), HTML (interactive), SARIF 2.1.0 |
| Flexible Filtering | By severity, tool, category, or combination |
| CI/CD Ready | GitHub Actions and GitLab CI examples included |
Two Comparison Modes
1. Directory Mode (Primary)
Compare findings from two results directories:
# Basic comparison
jmo diff baseline-results/ current-results/ --format md --output pr-diff.md
# With filtering
jmo diff baseline/ current/ \
--format json \
--severity CRITICAL,HIGH \
--only new \
--output critical-findings.json
Use Cases:
- PR reviews: Compare main branch vs feature branch
- Release validation: Compare previous release vs current
- Sprint tracking: Compare sprint start vs sprint end
2. SQLite Mode (Historical)
Compare two scan IDs from history database:
# Compare historical scans
jmo diff \
--scan abc123-baseline \
--scan def456-current \
--format md \
--output diff.md
# Custom database location
jmo diff \
--scan scan-id-1 \
--scan scan-id-2 \
--db /custom/path/history.db \
--format json
Use Cases:
- Long-term trend analysis
- Quarterly compliance reporting
- Regression detection across releases
CLI Reference
Positional Arguments (Directory Mode)
| Argument | Description |
|---|---|
BASELINE |
Baseline results directory |
CURRENT |
Current results directory |
SQLite Mode Options
| Option | Description |
|---|---|
--scan SCAN_ID |
Scan ID (provide twice: baseline, current) |
--db PATH |
History database path (default: .jmo/history.db) |
Output Options
| Option | Description |
|---|---|
--output PATH |
Output file path (extension added by format) |
--format FORMAT |
json, md, html, sarif (can specify multiple) |
Filtering Options
| Option | Description |
|---|---|
--severity SEV [SEV...] |
CRITICAL, HIGH, MEDIUM, LOW, INFO |
--tool TOOL [TOOL...] |
Filter by tool names |
--only CATEGORY |
new, resolved, modified |
--no-modifications |
Skip modification detection (faster) |
Behavior Options
| Option | Description |
|---|---|
--fail-on SEV |
Exit 1 if new findings at severity level |
--quiet |
Suppress summary output |
Output Formats
JSON (Machine-Readable)
Schema with metadata wrapper:
{
"meta": {
"diff_version": "1.0.0",
"jmo_version": "1.0.0",
"timestamp": "2025-11-05T10:30:00Z",
"baseline": {},
"current": {}
},
"statistics": {
"total_new": 12,
"total_resolved": 20,
"total_unchanged": 120,
"total_modified": 2,
"net_change": -8,
"trend": "improving",
"new_by_severity": {},
"resolved_by_severity": {}
},
"findings": {
"new": [],
"resolved": [],
"modified": []
}
}
Use Case: CI/CD automation, programmatic analysis
Markdown (PR Comments)
Human-readable format with collapsible details:
# Security Diff Report
## Summary
| Metric | Count | Change |
|--------|-------|--------|
| **New Findings** | 12 | +12 |
| **Resolved Findings** | 20 | -20 |
| **Net Change** | -8 | Improving |
## New Findings (12)
### CRITICAL (1)
<details>
<summary><b>SQL Injection in user query handler</b></summary>
**Rule:** `semgrep.sql-injection`
**File:** `src/database.py:127`
**Message:** Unsanitized user input flows into SQL query...
</details>
Use Case: GitHub/GitLab PR comments, team reviews
HTML (Interactive Dashboard)
Self-contained interactive dashboard with:
- Severity filtering
- Search/filter by rule, tool, path
- Side-by-side comparison for modified findings
- Collapsible finding cards
- Dark mode support
Use Case: Visual exploration, management reporting
SARIF 2.1.0 (Code Scanning)
GitHub/GitLab Code Scanning integration with baselineState annotations:
{
"runs": [{
"results": [{
"baselineState": "new",
"properties": {
"diff_category": "new",
"baseline_severity": null,
"current_severity": "error"
}
}]
}]
}
Use Case: GitHub Security tab, GitLab SAST dashboard
Modification Detection
Enabled by default - detects 5 types of changes:
| Change Type | Description |
|---|---|
| Severity Changes | MEDIUM -> HIGH (upgrade/downgrade) |
| Priority Changes | EPSS/KEV updates (risk delta) |
| Compliance Changes | New framework mappings added |
| CWE Changes | CWE classification updates |
| Message Changes | Finding description updates |
Example:
{
"fingerprint": "abc123...",
"changes": {
"severity": ["MEDIUM", "HIGH"],
"priority": [45.2, 78.9],
"compliance_frameworks": [
["owasp"],
["owasp", "pci_dss"]
]
}
}
Disable for performance:
CI/CD Integration Examples
GitHub Actions (PR Comments)
name: Security Diff on PR
on:
pull_request:
branches: [main]
jobs:
security-diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
# Scan baseline (main branch)
- name: Checkout main
run: git checkout main
- name: Scan main branch
run: jmo scan --repo . --profile balanced --results-dir baseline-results
# Scan current PR
- name: Checkout PR
run: git checkout ${{ github.event.pull_request.head.sha }}
- name: Scan PR branch
run: jmo scan --repo . --profile balanced --results-dir current-results
# Generate diff
- name: Generate diff
run: |
jmo diff baseline-results/ current-results/ \
--format md \
--output pr-diff.md \
--fail-on HIGH
# Post PR comment
- name: Post PR comment
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
const diff = fs.readFileSync('pr-diff.md', 'utf8');
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: diff
});
Complete example: docs/examples/github-actions-diff.yml
GitLab CI (Merge Request Comments)
security-diff:
stage: test
script:
# Scan baseline
- git checkout $CI_MERGE_REQUEST_TARGET_BRANCH_NAME
- jmo scan --repo . --profile balanced --results-dir baseline/
# Scan current
- git checkout $CI_COMMIT_SHA
- jmo scan --repo . --profile balanced --results-dir current/
# Generate diff
- jmo diff baseline/ current/ --format md --output diff.md
# Post MR comment via GitLab API
- |
curl --request POST \
--header "PRIVATE-TOKEN: $GITLAB_TOKEN" \
--data-urlencode "body@diff.md" \
"$CI_API_V4_URL/projects/$CI_PROJECT_ID/merge_requests/$CI_MERGE_REQUEST_IID/notes"
only:
- merge_requests
Complete example: docs/examples/gitlab-ci-diff.yml
Common Workflows
1. PR Review (Show Only New Issues)
# Compare branches
jmo diff baseline/ current/ --format md --only new --severity HIGH,CRITICAL
# CI gate: Block if new HIGH/CRITICAL
jmo diff baseline/ current/ --format json --output diff.json
NEW_COUNT=$(jq '(.statistics.new_by_severity.CRITICAL // 0) + (.statistics.new_by_severity.HIGH // 0)' diff.json)
[ "$NEW_COUNT" -eq 0 ] || exit 1
2. Sprint Remediation Tracking
# Track fixes between sprint start and end
jmo diff \
--scan sprint-start-abc123 \
--scan sprint-end-def456 \
--format json \
--output sprint-kpis.json
# Extract remediation stats
jq '.statistics.resolved_by_severity' sprint-kpis.json
3. Release Validation
# Compare previous release vs current
jmo diff \
--scan v0.9.0-scan-id \
--scan v1.0.0-scan-id \
--format html \
--output release-validation.html
# Fail if regression (more new than resolved)
NET=$(jq '.statistics.net_change' diff.json)
[ "$NET" -le 0 ] || exit 1
4. Compliance Regression Detection
# Check if PR introduces new OWASP Top 10 findings
jmo diff baseline/ current/ --format json --only new --output diff.json
jq '[.findings.new[] | select(.compliance.owaspTop10_2021 != null)]' diff.json
# Fail if any OWASP findings
COUNT=$(jq '[.findings.new[] | select(.compliance.owaspTop10_2021 != null)] | length' diff.json)
[ "$COUNT" -eq 0 ] || exit 1
Performance
Targets:
| Metric | Target |
|---|---|
| 1,000 findings diff | <500ms |
| 10,000 findings diff | <2s |
| Complexity | O(n) fingerprint set operations |
Optimization Tips:
- Use
--no-modificationsfor faster diffs (30% speedup) - Filter early:
--severity HIGH,CRITICALreduces processing - SQLite mode is slightly faster (indexed queries)
Troubleshooting
"Baseline directory not found"
- Ensure
baseline-results/exists and containssummaries/findings.json - Run
jmo scanto generate baseline first
"Scan ID not found in database"
- List available scans:
jmo history list - Check database path:
--db .jmo/history.db
"No findings in diff output"
- Check filtering: Remove
--severityor--onlyflags - Verify scans actually differ:
diff baseline/summaries/findings.json current/summaries/findings.json
"Modified findings not detected"
- Ensure
--no-modificationsnot set - Modification detection requires same fingerprint with different metadata
"Diff taking too long"
- Use
--no-modificationsfor 30% speedup - Filter by severity:
--severity CRITICAL,HIGH - Check scan sizes:
wc -l baseline/summaries/findings.json
Related Documentation
- Trend Analysis Guide - Statistical trend analysis over time
- Historical Storage Guide - Database storage for scan results
- Results Guide - Understanding scan output formats
- CI/CD Integration - CI/CD integration help
- Diff Workflows Examples - Complete workflow examples
- User Guide - Complete reference documentation