result.ruleId matches rules[].id.SARIF is the OASIS-standard JSON format that code analysis and security tools use to report findings, with rules, file locations, and fingerprints.
Also known as SARIF file, SARIF 2.1.0, SARIF report
SARIF (Static Analysis Results Interchange Format) is an OASIS-standard JSON format for the output of code analysis tools. A SARIF file lists each tool run, the rules the tool checked, and every result, anchored to a file, line, and rule ID. GitHub code scanning and GitLab ingest SARIF to deduplicate, triage, and display findings from any tool.
Writing a valid SARIF file is easy. The schema requires a version, a list of runs, a tool name per run, and a message per result, and nothing else. The hard problem is identity: a dashboard must decide whether today's result is the same one it showed yesterday, so that yesterday's triage still applies. SARIF leaves that decision to fingerprints, and GitHub and GitLab compute identity in incompatible ways. For dependency findings, which are anchored to short manifest lines, GitHub's default fingerprint changes when an unrelated package on the next line is upgraded.
Our position: treat SARIF as a transport for findings, not as a control. Producers should set their own stable fingerprints instead of letting the uploader guess, findings should be filtered by reachability before upload, and nobody should count on a code scanning dashboard to stop a malicious package. By the time a SARIF report exists, CI has already installed the dependency.
A SARIF file is a JSON document that validates against the SARIF 2.1.0 schema, usually saved as .sarif or .sarif.json. SARIF 2.1.0 was approved as an OASIS Standard on 27 March 2020, and its errata release, 2.1.0 Plus Errata 01, is the current standard. GitHub and GitLab both build their SARIF ingestion on 2.1.0.
Here is a complete file for one dependency finding: requests 2.31.0 is affected by GHSA-9wx4-h78v-vm56, fixed in 2.32.0. We validated it against the official 2.1.0 schema with a Draft 4 JSON Schema validator.
{
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
"version": "2.1.0",
"runs": [
{
"tool": {
"driver": {
"name": "Example SCA",
"rules": [
{
"id": "GHSA-9wx4-h78v-vm56",
"shortDescription": { "text": "requests Session skips TLS verification after verify=False" },
"properties": { "security-severity": "5.6" }
}
]
}
},
"results": [
{
"ruleId": "GHSA-9wx4-h78v-vm56",
"level": "warning",
"message": { "text": "requests 2.31.0 is affected. Upgrade to 2.32.0." },
"locations": [
{
"physicalLocation": {
"artifactLocation": { "uri": "requirements.txt" },
"region": { "startLine": 3 }
}
}
]
}
]
}
]
}The object model has four levels. A log holds runs. Each run names one tool and carries that tool's rules and results. Each result points at a rule through ruleId and at code through locations. A location is an artifact URI plus a region, usually a start line.
result.ruleId matches rules[].id.The diagram shows the same structure as the file above. The top-level log requires version and runs. Each run requires tool, and the tool's driver requires name. The driver's rules array defines each rule by id, here GHSA-9wx4-h78v-vm56, with a short description and a security-severity property. Each entry in results requires only message; it links to its rule because result.ruleId equals rules[].id, carries a level, and has a locations array whose first physicalLocation names the file (requirements.txt) and the start line (3). The optional partialFingerprints object carries the identity data discussed below.
The SARIF schema is permissive by design. In the 2.1.0 schema, a result's only required property is message, so this is a valid log:
{"version":"2.1.0","runs":[{"tool":{"driver":{"name":"x"}},"results":[{"message":{"text":"hi"}}]}]}Consumers are stricter than the schema. GitLab skips any result without a ruleId or a physical location, and GitHub's SARIF reference says a result needs at least one location to be displayed.
Severity is split in two. The level property has four values, none, note, warning, and error, and none of them means critical. Security tools therefore add a numeric security-severity string to each rule. GitHub maps scores above 9.0 to critical, 7.0 to 8.9 to high, 4.0 to 6.9 to medium, and 0.1 to 3.9 to low. GitLab multiplies the same property by 10 and buckets it on a 0 to 100 scale.
The reason the format exists is arithmetic. Microsoft's SARIF tutorial states the original problem: "Historically, every static analysis tool has defined its own output format." With N tools and M dashboards, bespoke formats need N times M parsers, and a shared format needs N plus M. That is why static application security testing engines, linters, secret scanners, and software composition analysis tools all emit SARIF, even though its data model was built for the first group.
SARIF represents a dependency finding as a result whose location is the manifest line that declares the package. That choice follows from the spec. The 2.1.0 schema tells producers to "specify only one location unless the problem indicated by the result can only be corrected by making a change at every specified location." A SARIF location is where the fix goes.
For a bug in your own code, the fix goes on the line with the bug. For a vulnerable or malicious dependency, the faulty code is not in your repository at all. The only place you can fix it is the line in requirements.txt, package.json, or the lockfile that pulls the package in.
Socket's Python CLI is a concrete, public example. In socketsecurity 2.11.6, the current release as of October 2026, socketcli --sarif-file results.sarif writes one result per alert per manifest. The rule ID is the package coordinate plus the manifest path, the location is the manifest line where the package appears, and Socket severities map to SARIF levels: low to note, medium to warning, high and critical to error. This is the shape of one result, taken from create_security_comment_sarif in that release, with the message text shortened:
{
"ruleId": "requests==2.31.0 (requirements.txt)",
"message": { "text": "...Suggested Action:<br/>...<br/><a href=\"https://socket.dev/pypi/package/requests/alerts/2.31.0\">..." },
"locations": [{
"physicalLocation": {
"artifactLocation": { "uri": "requirements.txt" },
"region": { "startLine": 3, "snippet": { "text": "requests==2.31.0" } }
}
}]
}Anchoring to manifest lines makes dependency findings render in pull request diffs and code scanning views built for source code. It also hands the consumer a very short line to fingerprint, and that is where the trouble starts.
SARIF fingerprints are strings a consumer compares across runs to decide whether two results are the same issue. The schema defines two properties. fingerprints is "a set of strings each of which individually defines a stable, unique identity for the result," in the words of the OASIS SARIF 2.1.0 schema. partialFingerprints holds strings that only contribute to identity, which the consumer combines however it likes.
GitHub code scanning uses exactly one value: partialFingerprints.primaryLocationLineHash. If an uploaded file lacks it, github/codeql-action/upload-sarif computes it from the checked-out source in fingerprints.ts. The algorithm is a base-37 rolling hash over the first 100 characters starting at the result's start line, skipping spaces and tabs but counting newlines. A :N suffix numbers repeats of the same hash within a file.
The hash window looks forward from the finding, not backward. To see what that means for a dependency finding, we extracted the hash function from codeql-action, compiled it with the same long library it depends on, and ran it against four versions of one file:
$ printf 'certifi==2024.8.30\nidna==3.7\nrequests==2.31.0\nurllib3==2.2.2\ncharset-normalizer==3.3.2\n' > a.txt
$ node hash.js a.txt 3
line 3: 3c04c1fcd75d7e62:1
$ sed 's/urllib3==2.2.2/urllib3==2.2.3/' a.txt > b.txt && node hash.js b.txt 3
line 3: e6650d62999f6c37:1requests==2.31.0. Hashes were computed with the hash function from github/codeql-action's fingerprints.ts.The figure steps through four versions of the same requirements.txt, with the finding on requests==2.31.0. The table lists what GitHub and GitLab would use as the finding's location identity in each case.
| Edit to requirements.txt | GitHub primaryLocationLineHash | GitLab location | Which identity changed |
|---|---|---|---|
| Baseline: requests on line 3 | 3c04c1fcd75d7e62:1 | requirements.txt, line 3 | Reference |
| Bump urllib3 2.2.2 to 2.2.3 on line 4 | e6650d62999f6c37:1 | requirements.txt, line 3 | GitHub |
| Insert a comment on line 1, requests moves to line 4 | 3c04c1fcd75d7e62:1 | requirements.txt, line 4 | GitLab |
| Bump idna 3.7 to 3.8 on line 2 | 3c04c1fcd75d7e62:1 | requirements.txt, line 3 | Neither |
Upgrading urllib3 on line 4 changed the fingerprint of the requests finding on line 3. Nothing about the requests vulnerability changed. The window simply reached the next line. For source code this design is reasonable, because a 100-character window usually stays inside one statement and survives code moving up or down. Pinned requirements are often 10 to 30 characters per line, so the window spans several neighboring packages.
When the hash changes, GitHub cannot match the result to the alert it already has. GitHub's reference says fingerprints exist to avoid duplicate alerts, and that API uploads without them may produce duplicates. The practical effect is that routine upgrades of neighboring packages can reopen triage on findings that did not change.
GitLab's SARIF ingestion, generally available in GitLab Ultimate since GitLab 19.2, fails the opposite way. Its documentation describes identity as a fingerprint of the ruleId plus a location of the form file:startLine:endLine, and it does not mention partialFingerprints. Inserting a line above the finding moves it and changes its identity, while a neighbor's upgrade does not.
Static analyzers have hit the same wall. When Opengrep restored fingerprints in its JSON and SARIF output, we wrote that "a finding without a stable fingerprint is practically noise."
| Property | GitHub default | GitLab | Producer-supplied semantic hash on GitHub |
|---|---|---|---|
| Identity | ruleId plus hash of 100 characters from the start line | ruleId plus file, start line, end line | ruleId plus a hash of ecosystem, package, version, advisory, and manifest path |
| Survives a line inserted above | Yes | No | Yes |
| Survives a neighbor's upgrade | No | Yes | Yes |
| Cost | None for the producer | None for the producer | upload-sarif logs a mismatch warning for each result whose supplied hash differs from its own |
Verdict: if you write a SARIF producer for dependency findings, set primaryLocationLineHash yourself from the package coordinate, the advisory or alert type, and the manifest path. GitHub asks producers that upload through its API to populate partialFingerprints, and upload-sarif keeps a supplied value rather than overwriting it. On GitLab there is no equivalent hook today, so keep findings on the line that declares the package and expect reformatting a manifest to reopen findings.
GitHub code scanning accepts a gzip-compressed SARIF file of up to 10 MB, with at most 20 runs. A run may contain 25,000 results, but GitHub only includes the top 5,000 by severity. Exceeding any maximum rejects the entire file, not just the excess. These limits are from GitHub's SARIF reference as of October 2026; GitLab's are similar.
| Limit | GitHub code scanning | GitLab (19.2+) |
|---|---|---|
| File size | 10 MB, gzip-compressed | 10 MB artifact, configurable |
| Runs per file | 20 | 20 |
| Results per run | 25,000; only the top 5,000 by severity are included | 5,000 |
| Rules per run | 25,000 | 25,000 |
| Tags per rule | 20; only 10 are included | 10 |
Since 22 July 2025, GitHub also rejects a file containing multiple runs with the same tool and category, where it previously merged them. A producer that splits results across runs, one per manifest for example, must give each run a distinct category through runAutomationDetails.id. GitHub reads that value as category/run-id and splits it at the last slash.
For dependency scanning, 5,000 results per run is a real ceiling. A monorepo with dozens of manifests and hundreds of transitive packages each can exceed it, and GitHub keeps the 5,000 it ranks highest by severity score, not the ones an attacker can reach. Severity is the wrong sort key. Filter before upload on evidence instead: reachability analysis checks whether your code can call the vulnerable function at all, and Socket reports that Tier 1 reachability typically filters out about 80% of vulnerabilities as irrelevant.
In the Socket Python CLI, --reach runs that analysis, and --sarif-reachability reachable keeps only findings whose vulnerable code is reachable. The default --sarif-scope diff emits only alerts introduced by the change under review, which keeps per-PR uploads small. Both flags are documented in the CLI reference:
socketcli --reach --sarif-file results.sarif --sarif-scope full --sarif-reachability reachableA SARIF report is a record of what a tool found after the fact. For dependency findings, after the fact is usually after the code ran. A typical pipeline installs dependencies, runs the scanner, and then uploads the SARIF file, so the scanner sees the package only after the CI runner has fetched and installed it.
Package managers have narrowed that window but not closed it. pnpm 10 stopped running dependency lifecycle scripts by default in January 2025, and npm v12 did the same for preinstall, install, and postinstall scripts in July 2026. Older npm versions still run install scripts by default, and so do projects that allowlist a package. Building a Python source distribution executes its build backend, and any test that imports a package executes its module code.
A SARIF alert about a malicious package is an incident notification, not a prevention. The control belongs before install, where a package can be blocked at resolution time. SARIF is the right way to record what was blocked and what remains for review.
SARIF also carries state that consumers mostly ignore. The baselineState property lets a producer mark each result as new, unchanged, updated, or absent, but GitHub's SARIF reference does not mention it. The suppressions property records a dismissal inside the file; GitLab skips suppressed results but, per its documentation, does not turn suppressions into dismissals.
The open problem is identity for things that are not lines of code. SARIF 2.2 entered its first public review on 19 October 2026, with comments open until 18 November 2026. Its change list adds a log guid, a scannedFile artifact role, and a suppression justificationType, and changes nothing about fingerprints. Neither the 2.1.0 schema nor the 2.2 draft schema has a property for a package URL (purl). A standard, purl-based identity for dependency results would let every consumer agree on what a finding is, instead of guessing from the 100 characters that happen to follow it in a manifest.
Manifest confusion is a mismatch between an npm package's registry manifest and the package.json in its tarball, two copies that tools trust inconsistently.
A software bill of materials (SBOM) is a machine-readable inventory of the components, versions, and dependency relationships inside a piece of software.
Software composition analysis (SCA) inventories the third-party code in an application and checks each component for vulnerabilities, licenses, and malware.