{"slug": "emit-sarif-2-1-0-from-your-linter-so-findings-show-up-in-github-code-scanning", "title": "Emit SARIF 2.1.0 From Your Linter So Findings Show Up in GitHub Code Scanning", "summary": "A developer behind the mcp-audit security scanner documented how emitting SARIF 2.1.0 output lets existing linter findings surface as pull-request annotations and GitHub Security tab alerts without changing any scanning logic. The writeup details the format's tool/rules/results structure and a two-track severity mapping — a coarse SARIF level plus a numeric security-severity score — so critical and high findings stay distinguishable in the GitHub UI.", "body_md": "I build small security scanners, and for a long time each one printed findings to a terminal that nobody read twice. The moment I taught them to emit SARIF, the same findings started appearing as annotations in pull requests and as alerts in the GitHub Security tab. Nothing about the scanning logic changed. I just changed the output format.\n\nThis post teaches that technique. I will use my own tool, mcp-audit, as the concrete reference, but the goal is that you can wire SARIF into whatever linter or scanner you already have.\n\nSARIF (Static Analysis Results Interchange Format) is a JSON schema that OASIS standardizes and GitHub consumes. Version 2.1.0 is the one code scanning ingests. At its core a SARIF file has one idea: a **tool** declares its **rules**, and then reports **results** that each point back to a rule by id. Get that relationship right and most of the work is done.\n\nThe smallest useful shape looks like this:\n\n```\n{\n  \"$schema\": \"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json\",\n  \"version\": \"2.1.0\",\n  \"runs\": [\n    {\n      \"tool\": {\n        \"driver\": {\n          \"name\": \"mcp-audit\",\n          \"informationUri\": \"https://github.com/AgentPostmortem/mcp-audit\",\n          \"version\": \"0.1.0\",\n          \"rules\": []\n        }\n      },\n      \"results\": []\n    }\n  ]\n}\n```\n\nEverything else is filling in `rules` and `results`.\n\nEach rule your scanner knows about becomes a **rule descriptor** under `tool.driver.rules`. The two properties GitHub cares about most are `defaultConfiguration.level` and the `security-severity` score. The level drives whether an alert renders as error, warning, or note. The `security-severity` string (a number from 0 to 10, as text) drives the Low/Medium/High/Critical label in the Security tab.\n\nHere is one descriptor:\n\n```\n{\n  \"id\": \"MCP001\",\n  \"name\": \"UnauthenticatedToolInvocation\",\n  \"shortDescription\": { \"text\": \"Unauthenticated tool invocation\" },\n  \"fullDescription\": { \"text\": \"The server exposes a tool that can be called without any authentication check.\" },\n  \"defaultConfiguration\": { \"level\": \"error\" },\n  \"properties\": {\n    \"category\": \"auth\",\n    \"security-severity\": \"8.0\",\n    \"tags\": [\"security\", \"mcp\", \"auth\"]\n  }\n}\n```\n\nA detail worth internalizing: your internal severity vocabulary is almost never SARIF's vocabulary. In mcp-audit I have `critical`, `high`, `medium`, `low`. SARIF has three levels. So I map. Critical and high both become `error`; medium and low become `warning`; anything else becomes `note`. Separately I translate each severity to a numeric score so the Security tab still distinguishes critical from high:\n\n``` php\ncritical -> level \"error\", security-severity \"9.5\"\nhigh     -> level \"error\", security-severity \"8.0\"\nmedium   -> level \"warning\", security-severity \"5.5\"\nlow      -> level \"warning\", security-severity \"3.0\"\n```\n\nThat two-track mapping (a coarse `level` plus a fine `security-severity`) is the single most useful trick I learned. Without the score, every error-level finding collapses into one bucket in the UI.\n\nA result references a rule by `ruleId`, carries its own `level`, a human message, and a location:\n\n```\n{\n  \"ruleId\": \"MCP001\",\n  \"level\": \"error\",\n  \"message\": { \"text\": \"Tool 'run_shell' is exposed without auth. Remediation: require a signed session token before dispatch.\" },\n  \"properties\": { \"security-severity\": \"8.0\" },\n  \"locations\": [\n    {\n      \"logicalLocations\": [\n        {\n          \"name\": \"server.tools.run_shell\",\n          \"fullyQualifiedName\": \"server.tools.run_shell\"\n        }\n      ]\n    }\n  ]\n}\n```\n\nNotice I used a **logical location** rather than a physical file and line. Code scanning strongly prefers `physicalLocation` with an `artifactLocation.uri` and a `region` so it can annotate the exact line in a diff. Use that whenever your scanner knows the file and line:\n\n```\n\"locations\": [\n  {\n    \"physicalLocation\": {\n      \"artifactLocation\": { \"uri\": \"src/server.ts\" },\n      \"region\": { \"startLine\": 42, \"startColumn\": 3 }\n    }\n  }\n]\n```\n\nmcp-audit often audits a running server or a manifest rather than a source line, so it falls back to logical locations. That is legal SARIF and it uploads fine, which brings me to the honest caveat below.\n\nOne design choice that keeps the file clean: I only emit rule descriptors for rules that actually produced a finding. I collect the set of rule ids present in the results, then filter the full rule list down to that set before writing descriptors. A SARIF file with 80 declared rules and 2 results is valid, but it clutters the tool inventory GitHub shows. Declaring only what you used keeps the report honest and small.\n\nGitHub ships a first-party action, `github/codeql-action/upload-sarif`, that works for any SARIF file regardless of who generated it. You do not need CodeQL itself. Here is a complete job:\n\n```\nname: security-scan\non:\n  push:\n    branches: [main]\n  pull_request:\n\npermissions:\n  security-events: write\n  contents: read\n\njobs:\n  audit:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n      - run: npm ci\n      - run: npx mcp-audit ./manifest.json --format sarif --output results.sarif\n      - uses: github/codeql-action/upload-sarif@v3\n        with:\n          sarif_file: results.sarif\n```\n\nThe two things people miss: the `security-events: write` permission is required or the upload silently fails, and the upload step should run even when the scan finds problems. If your scanner exits non-zero on findings, add `if: always()` to the upload step so results still reach the Security tab.\n\nLogical locations do not annotate pull request diffs. When a finding has no file and line, GitHub still creates a Security tab alert, but it cannot draw the inline comment on the changed code, so reviewers may never notice it. If your findings map to real source positions, spend the effort to emit `physicalLocation` with a `region`. The difference between an alert nobody opens and an annotation on the exact offending line is entirely in that one field.\n\nThe whole technique is three moves: declare rules, report results that point at them, and map your severities onto SARIF's `level` plus a numeric `security-severity`. Once your tool speaks SARIF, GitHub code scanning is a free distribution channel for everything it already knows.\n\nIf you want a full working reference, the SARIF reporter in mcp-audit is a single readable file that does exactly what this post describes, severity mapping and all. It lives at [github.com/AgentPostmortem/mcp-audit](https://github.com/AgentPostmortem/mcp-audit). Copy the shape, swap in your rules, and let your scanner start showing up where reviewers actually look.", "url": "https://wpnews.pro/news/emit-sarif-2-1-0-from-your-linter-so-findings-show-up-in-github-code-scanning", "canonical_source": "https://dev.to/royalpinto007/emit-sarif-210-from-your-linter-so-findings-show-up-in-github-code-scanning-301f", "published_at": "2026-10-03 09:30:29+00:00", "updated_at": "2026-10-03 09:38:26.139969+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-agents"], "entities": ["GitHub", "mcp-audit", "OASIS", "SARIF", "AgentPostmortem"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/emit-sarif-2-1-0-from-your-linter-so-findings-show-up-in-github-code-scanning", "markdown": "https://wpnews.pro/news/emit-sarif-2-1-0-from-your-linter-so-findings-show-up-in-github-code-scanning.md", "text": "https://wpnews.pro/news/emit-sarif-2-1-0-from-your-linter-so-findings-show-up-in-github-code-scanning.txt", "jsonld": "https://wpnews.pro/news/emit-sarif-2-1-0-from-your-linter-so-findings-show-up-in-github-code-scanning.jsonld"}}