# Static Analysis Results Interchange Format (SARIF)

> 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

Source: https://socket.dev/glossary/static-analysis-results-interchange-format-sarif

Last updated: 2026-10-31

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](https://socket.dev/glossary/malicious-package). By the time a SARIF report exists, CI has already installed the dependency.

## What is a SARIF file?

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](https://oasis-open.org/?p=3002), 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](https://osv.dev/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.

```json
{
  "$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.

> SARIF 2.1.0 object model: a log contains runs; each run has a tool driver with rules and a results array; each result references a rule by ruleId and points at a file and line.

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:

```json
{"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](https://docs.github.com/en/code-security/reference/code-scanning/sarif-files/sarif-support) 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](https://github.com/microsoft/sarif-tutorials/blob/master/docs/1-Introduction.md) 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](https://socket.dev/glossary/static-application-security-testing) engines, linters, secret scanners, and [software composition analysis](https://socket.dev/glossary/software-composition-analysis) tools all emit SARIF, even though its data model was built for the first group.

## How does SARIF represent a dependency finding?

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](https://socket.dev/glossary/requirements-txt), `package.json`, or the [lockfile](https://socket.dev/glossary/lockfile) that pulls the package in.

Socket's Python CLI is a concrete, public example. In [socketsecurity](https://github.com/SocketDev/socket-python-cli) 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:

```json
{
  "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.

## How do SARIF fingerprints work?

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](https://docs.oasis-open.org/sarif/sarif/v2.1.0/errata01/os/schemas/sarif-schema-2.1.0.json). `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](https://github.com/github/codeql-action/blob/main/src/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:

```bash
$ 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:1
```

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](https://docs.gitlab.com/user/application_security/detect/sarif/), 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](https://socket.dev/blog/opengrep-restores-fingerprinting-in-json-and-sarif-outputs), 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.

## What are GitHub's SARIF upload limits?

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](https://docs.github.com/en/code-security/reference/code-scanning/sarif-files/sarif-support) 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](https://github.blog/changelog/2025-07-21-code-scanning-will-stop-combining-multiple-sarif-runs-uploaded-in-the-same-sarif-file/), 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](https://socket.dev/glossary/reachability-analysis) checks whether your code can call the vulnerable function at all, and Socket reports that [Tier 1 reachability](https://socket.dev/blog/introducing-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](https://github.com/SocketDev/socket-python-cli/blob/main/docs/cli-reference.md):

```bash
socketcli --reach --sarif-file results.sarif --sarif-scope full --sarif-reachability reachable
```

## What SARIF cannot do for supply chain security

A 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](https://socket.dev/blog/pnpm-10-0-0-blocks-lifecycle-scripts-by-default) stopped running dependency lifecycle scripts by default in January 2025, and [npm v12](https://socket.dev/blog/npm-12) did the same for `preinstall`, `install`, and `postinstall` scripts in July 2026. Older npm versions still run [install scripts](https://socket.dev/glossary/postinstall-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](https://www.oasis-open.org/2026/10/20/invitation-to-comment-on-static-analysis-results-interchange-format-sarif-version-2-2-csd01-ends-18-november-2026/), with comments open until 18 November 2026. Its [change list](https://docs.oasis-open.org/sarif/sarif/v2.2/csd01/sarif-v2.2-csd01.md) 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.
