Repository: google/osv-scanner

Issue:
SARIF output does not contain `partialFingerprints` property, resulting in duplicate records eg. in GitHub output
Is there a reason SARIF output does not contain the `partialFingerprints` property? I see many duplicates in [GitHub's security tab](https://github.com/brabster/terraform-bootstrap-gcp/security) which I think would be resolved by this property being populated based [on GitHub's docs](https://github.com/brabster/terraform-bootstrap-gcp/security).

I can't see any mention of this property in the repo history, so checking whether there's a reason we don't generated it and whether populating it would be a welcome feature to add.

I am using the latest version 2.2.4 but this has been an issue for at least the past few versions.

Hints from issue comments:

Solution patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 33b87a11..78df38aa 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+# v2.3.0
+
+This release migrates to the new `osv.dev` and `osv-schema` proto bindings for its internal data models ([#2328](https://github.com/google/osv-scanner/pull/2328)). This is primarily an internal change and should not impact users.
+
+### Features:
+
+- [Feature #2321](https://github.com/google/osv-scanner/pull/2321) Add support for license checks for RubyGems.
+- [Feature #2294](https://github.com/google/osv-scanner/pull/2294) Replace `requirementsenhanceable` extractor with transitive enricher.
+- [Feature #2344](https://github.com/google/osv-scanner/pull/2344) Use `osduplicate` annotators.
+
+### Fixes:
+
+- [Bug #2329](https://github.com/google/osv-scanner/pull/2329) Add `--ignore-scripts` flag to npm lockfile generation.
+- [Bug #2311](https://github.com/google/osv-scanner/pull/2311) Improve logic for `--all-packages` flag.
+- [Bug #2309](https://github.com/google/osv-scanner/pull/2309) Exit with a non-zero code when showing help.
+- [Bug #2316](https://github.com/google/osv-scanner/pull/2316) Pre-commit hook now defaults to scanning current directory instead of failing.
+- [Bug #1507 (osv-scalibr)](https://github.com/google/osv-scalibr/pull/1507) Interpolate Maven projects before extracting repositories.
+
 # v2.2.4
 
 ### Features:
diff --git a/cmd/osv-scanner/__snapshots__/main_test.snap b/cmd/osv-scanner/__snapshots__/main_test.snap
index 454982fd..c1c53055 100755
--- a/cmd/osv-scanner/__snapshots__/main_test.snap
+++ b/cmd/osv-scanner/__snapshots__/main_test.snap
@@ -46,8 +46,8 @@ OPTIONS:
 ---
 
 [Test_run/version - 1]
-osv-scanner version: 2.2.4
-osv-scalibr version: 0.3.6
+osv-scanner version: 2.3.0
+osv-scalibr version: 0.4.0
 commit: n/a
 built at: n/a
 
diff --git a/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap b/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap
index 85fb9986..0120b620 100755
--- a/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap
+++ b/cmd/osv-scanner/scan/source/__snapshots__/command_test.snap
@@ -96,7 +96,7 @@ Loaded filter from: <rootdir>/testdata/locks-many/osv-scanner-test.toml
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -286,6 +286,9 @@ Total 2 packages affected by 7 known vulnerabilities (3 Critical, 4 High, 0 Medi
       "redactionTokens": [],
       "results": [
         {
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "attachments": [],
           "codeFlows": [],
           "fixes": [],
@@ -362,7 +365,7 @@ Total 2 packages affected by 7 known vulnerabilities (3 Critical, 4 High, 0 Medi
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -2698,6 +2701,9 @@ Total 1 package affected by 2 known vulnerabilities (0 Critical, 2 High, 0 Mediu
       "redactionTokens": [],
       "results": [
         {
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "attachments": [],
           "codeFlows": [],
           "fixes": [],
@@ -2731,6 +2737,9 @@ Total 1 package affected by 2 known vulnerabilities (0 Critical, 2 High, 0 Mediu
           "taxa": []
         },
         {
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "attachments": [],
           "codeFlows": [],
           "fixes": [],
@@ -2831,7 +2840,7 @@ Total 1 package affected by 2 known vulnerabilities (0 Critical, 2 High, 0 Mediu
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
diff --git a/docs/github-action.md b/docs/github-action.md
index ff62f68c..4bb0654f 100644
--- a/docs/github-action.md
+++ b/docs/github-action.md
@@ -54,7 +54,7 @@ permissions:
 
 jobs:
   scan-pr:
-    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.2.4"
+    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.0"
 ```
 
 ### View results
@@ -97,7 +97,7 @@ permissions:
 
 jobs:
   scan-scheduled:
-    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.2.4"
+    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.0"
 ```
 
 As written, the scanner will run on 12:30 pm UTC every Monday, and also on every push to the main branch. You can change the schedule by following the instructions [here](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule).
@@ -132,7 +132,7 @@ permissions:
 
 jobs:
   osv-scan:
-    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.2.4"
+    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable-pr.yml@v2.3.0"
     with:
       # Only scan the top level go.mod file without recursively scanning directories since
       # this is pipeline is about releasing the go module and binary
@@ -184,7 +184,7 @@ Examples
 ```yml
 jobs:
   scan-pr:
-    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.2.4"
+    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.0"
     with:
       scan-args: |-
         --lockfile=./path/to/lockfile1
@@ -196,7 +196,7 @@ jobs:
 ```yml
 jobs:
   scan-pr:
-    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.2.4"
+    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.0"
     with:
       scan-args: |-
         --recursive
@@ -222,7 +222,7 @@ jobs:
     name: Vulnerability scanning
     # makes sure the extraction step is completed before running the scanner
     needs: extract-deps
-    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.2.4"
+    uses: "google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@v2.3.0"
     with:
       # Download the artifact uploaded in extract-deps step
       download-artifact: converted-OSV-Scanner-deps
@@ -272,7 +272,7 @@ jobs:
           {target_arch: armhf},
           {target_arch: aarch64}
         ]
-    uses: "extract/osv-scanner/.github/workflows/osv-scanner-reusable.yml@v2.0.3"
+    uses: "extract/osv-scanner/.github/workflows/osv-scanner-reusable.yml@v2.3.0"
     with:
       download-artifact: "${{ matrix.platform.target_arch }}-OSV-Scanner-deps"
       matrix-property: "${{ matrix.platform.target_arch }}-"
diff --git a/go.mod b/go.mod
index d70723e1..073d57aa 100644
--- a/go.mod
+++ b/go.mod
@@ -18,7 +18,7 @@ require (
 	github.com/go-git/go-git/v5 v5.16.3
 	github.com/goccy/go-yaml v1.18.0
 	github.com/google/go-cmp v0.7.0
-	github.com/google/osv-scalibr v0.3.7-0.20251117160633-53fbb7fd648b
+	github.com/google/osv-scalibr v0.3.7-0.20251118161533-ed0917ecede1
 	github.com/ianlancetaylor/demangle v0.0.0-20251114061303-68c556c8ce09
 	github.com/jedib0t/go-pretty/v6 v6.7.2
 	github.com/modelcontextprotocol/go-sdk v1.1.0
diff --git a/go.sum b/go.sum
index 88ba62e5..c6e81f3a 100644
--- a/go.sum
+++ b/go.sum
@@ -256,8 +256,8 @@ github.com/google/go-cpy v0.0.0-20211218193943-a9c933c06932 h1:5/4TSDzpDnHQ8rKEE
 github.com/google/go-cpy v0.0.0-20211218193943-a9c933c06932/go.mod h1:cC6EdPbj/17GFCPDK39NRarlMI+kt+O60S12cNB5J9Y=
 github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q=
 github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE=
-github.com/google/osv-scalibr v0.3.7-0.20251117160633-53fbb7fd648b h1:lsj7w60oYSiAeoWx1MsZMHNMzfJFfmMQ81Gp/coTXAM=
-github.com/google/osv-scalibr v0.3.7-0.20251117160633-53fbb7fd648b/go.mod h1:9Ze2W6nQmu1WX2s95ezOAVZhPDbcA6ZGuEHgFT/sQEU=
+github.com/google/osv-scalibr v0.3.7-0.20251118161533-ed0917ecede1 h1:ORtXAjEo6xCsJnH5G+YxK61B5+KCwxEc45we2upW55Q=
+github.com/google/osv-scalibr v0.3.7-0.20251118161533-ed0917ecede1/go.mod h1:9Ze2W6nQmu1WX2s95ezOAVZhPDbcA6ZGuEHgFT/sQEU=
 github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8=
 github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
 github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
diff --git a/internal/output/__snapshots__/sarif_test.snap b/internal/output/__snapshots__/sarif_test.snap
index a5e20ba5..8abaf6ee 100755
--- a/internal/output/__snapshots__/sarif_test.snap
+++ b/internal/output/__snapshots__/sarif_test.snap
@@ -63,6 +63,9 @@
             "arguments": [],
             "text": "Package 'regex@1.5.1' is vulnerable to 'CVE-2022-24713' (also known as 'RUSTSEC-2022-0013', 'GHSA-m5pq-gvj9-9vr8')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "CVE-2022-24713",
@@ -96,6 +99,9 @@
             "arguments": [],
             "text": "Package 'github.com/gogo/protobuf@1.3.1' is vulnerable to 'CVE-2021-3121' (also known as 'GO-2021-0053', 'GHSA-c3h9-896r-86jm')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "CVE-2021-3121",
@@ -129,6 +135,9 @@
             "arguments": [],
             "text": "Package 'regex@1.5.1' is vulnerable to 'CVE-2022-24713' (also known as 'RUSTSEC-2022-0013', 'GHSA-m5pq-gvj9-9vr8')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "CVE-2022-24713",
@@ -203,7 +212,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -253,7 +262,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -303,7 +312,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -353,7 +362,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -403,7 +412,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -453,7 +462,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -503,7 +512,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -553,7 +562,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -603,7 +612,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -653,7 +662,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -703,7 +712,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -753,7 +762,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -803,7 +812,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -853,7 +862,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -903,7 +912,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -953,7 +962,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -1003,7 +1012,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -1053,7 +1062,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -1140,6 +1149,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -1173,6 +1185,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -1206,6 +1221,9 @@
             "arguments": [],
             "text": "Package 'mine2@3.2.5' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -1273,7 +1291,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -1360,6 +1378,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -1393,6 +1414,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -1426,6 +1450,9 @@
             "arguments": [],
             "text": "Package 'mine2@3.2.5' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -1493,7 +1520,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -1580,6 +1607,9 @@
             "arguments": [],
             "text": "Package 'mine1@abcxzy' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -1613,6 +1643,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -1646,6 +1679,9 @@
             "arguments": [],
             "text": "Package 'mine2@abc123' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -1713,7 +1749,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -1782,6 +1818,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -1829,7 +1868,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -1898,6 +1937,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -1945,7 +1987,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -2014,6 +2056,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -2061,7 +2106,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -2130,6 +2175,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -2177,7 +2225,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -2246,6 +2294,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -2293,7 +2344,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -2371,6 +2422,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.2' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -2404,6 +2458,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -2437,6 +2494,9 @@
             "arguments": [],
             "text": "Package 'mine2@3.2.5' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -2470,6 +2530,9 @@
             "arguments": [],
             "text": "Package 'mine3@0.4.1' is vulnerable to 'OSV-3'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-3",
@@ -2503,6 +2566,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -2536,6 +2602,9 @@
             "arguments": [],
             "text": "Package 'mine3@0.4.1' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -2643,7 +2712,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -2721,6 +2790,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.2' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -2754,6 +2826,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -2787,6 +2862,9 @@
             "arguments": [],
             "text": "Package 'mine2@3.2.5' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -2820,6 +2898,9 @@
             "arguments": [],
             "text": "Package 'mine3@0.4.1' is vulnerable to 'OSV-3'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-3",
@@ -2853,6 +2934,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -2886,6 +2970,9 @@
             "arguments": [],
             "text": "Package 'mine3@0.4.1' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -2993,7 +3080,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -3043,7 +3130,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -3130,6 +3217,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -3163,6 +3253,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -3196,6 +3289,9 @@
             "arguments": [],
             "text": "Package 'mine2@3.2.5' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -3263,7 +3359,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -3341,6 +3437,9 @@
             "arguments": [],
             "text": "Package 'author1/mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -3374,6 +3473,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.2' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -3407,6 +3509,9 @@
             "arguments": [],
             "text": "Package 'mine2@3.2.5' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -3440,6 +3545,9 @@
             "arguments": [],
             "text": "Package 'author3/mine3@0.4.1' is vulnerable to 'OSV-3'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-3",
@@ -3473,6 +3581,9 @@
             "arguments": [],
             "text": "Package 'author1/mine1@1.2.3' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -3506,6 +3617,9 @@
             "arguments": [],
             "text": "Package 'author3/mine3@0.4.1' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -3613,7 +3727,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -3691,6 +3805,9 @@
             "arguments": [],
             "text": "Package 'author1/mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -3724,6 +3841,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.2' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -3757,6 +3877,9 @@
             "arguments": [],
             "text": "Package 'mine2@3.2.5' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -3790,6 +3913,9 @@
             "arguments": [],
             "text": "Package 'author3/mine3@0.4.1' is vulnerable to 'OSV-3'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-3",
@@ -3823,6 +3949,9 @@
             "arguments": [],
             "text": "Package 'author1/mine1@1.2.3' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -3856,6 +3985,9 @@
             "arguments": [],
             "text": "Package 'author3/mine3@0.4.1' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -3963,7 +4095,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4041,6 +4173,9 @@
             "arguments": [],
             "text": "Package 'author1/mine1@123abc' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -4074,6 +4209,9 @@
             "arguments": [],
             "text": "Package 'mine1@abcxyz' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -4107,6 +4245,9 @@
             "arguments": [],
             "text": "Package 'mine2@3.2.5' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -4140,6 +4281,9 @@
             "arguments": [],
             "text": "Package 'author3/mine3@0.4.1' is vulnerable to 'OSV-3'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-3",
@@ -4173,6 +4317,9 @@
             "arguments": [],
             "text": "Package 'author1/mine1@123abc' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -4206,6 +4353,9 @@
             "arguments": [],
             "text": "Package 'author3/mine3@0.4.1' is vulnerable to 'OSV-5'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-5",
@@ -4313,7 +4463,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4363,7 +4513,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4413,7 +4563,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4463,7 +4613,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4513,7 +4663,7 @@
           "rules": [],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4582,6 +4732,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'GHSA-123'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "GHSA-123",
@@ -4615,6 +4768,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -4682,7 +4838,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4751,6 +4907,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -4798,7 +4957,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4867,6 +5026,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -4914,7 +5076,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -4983,6 +5145,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5030,7 +5195,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -5099,6 +5264,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5146,7 +5314,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -5215,6 +5383,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5262,7 +5433,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -5331,6 +5502,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1' (also known as 'GHSA-123')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5364,6 +5538,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1' (also known as 'GHSA-123')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5412,7 +5589,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -5481,6 +5658,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1' (also known as 'GHSA-123')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5514,6 +5694,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1' (also known as 'GHSA-123')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5562,7 +5745,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -5631,6 +5814,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1' (also known as 'GHSA-123')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5664,6 +5850,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1' (also known as 'GHSA-123')."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5712,7 +5901,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -5781,6 +5970,9 @@
             "arguments": [],
             "text": "Package 'mine1@abc123' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5828,7 +6020,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -5897,6 +6089,9 @@
             "arguments": [],
             "text": "Package 'mine1@abc123' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -5944,7 +6139,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -6013,6 +6208,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -6046,6 +6244,9 @@
             "arguments": [],
             "text": "Package 'mine3@0.10.2-rc' is vulnerable to 'OSV-2'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-2",
@@ -6113,7 +6314,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -6182,6 +6383,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -6229,7 +6433,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
@@ -6307,6 +6511,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -6340,6 +6547,9 @@
             "arguments": [],
             "text": "Package 'mine1@1.2.3' is vulnerable to 'OSV-1'."
           },
+          "partialFingerprints": {
+            "primaryLocationLineHash": "[line-hash]"
+          },
           "rank": -1,
           "relatedLocations": [],
           "ruleId": "OSV-1",
@@ -6387,7 +6597,7 @@
           ],
           "supportedTaxonomies": [],
           "taxa": [],
-          "version": "2.2.4"
+          "version": "2.3.0"
         },
         "extensions": []
       },
diff --git a/internal/output/sarif.go b/internal/output/sarif.go
index e2489006..b6c62804 100644
--- a/internal/output/sarif.go
+++ b/internal/output/sarif.go
@@ -1,6 +1,8 @@
 package output
 
 import (
+	"crypto/sha256"
+	"encoding/hex"
 	"fmt"
 	"io"
 	"log"
@@ -143,6 +145,31 @@ func stripGitHubWorkspace(path string) string {
 	return strings.TrimPrefix(path, "/github/workspace/")
 }
 
+// createSARIFFingerprint generates a stable fingerprint for a SARIF result
+// to help GitHub deduplicate findings across scans.
+//
+// The fingerprint is computed from three components to ensure uniqueness while maintaining stability:
+//  1. vulnID: The vulnerability identifier (e.g., "CVE-2022-24713") - ensures different vulnerabilities
+//     produce different fingerprints even for the same package
+//  2. artifactPath: The path to the lockfile (e.g., "/path/to/package.json") - distinguishes the same
+//     vulnerability in different parts of a monorepo or different projects
+//  3. pkg: The package information (name, version, or commit) - differentiates the same vulnerability
+//     across different versions or instances of a package
+//
+// These three components are combined because they uniquely identify a specific vulnerability finding:
+// the same vulnerability (vulnID) in the same package (pkg) detected in the same location (artifactPath)
+// should always be considered the same finding and produce the same fingerprint across scans.
+func createSARIFFingerprint(vulnID string, artifactPath string, pkg models.PackageInfo) string {
+	// Create a stable string representation
+	pkgStr := results.PkgToString(pkg)
+	fingerprintData := fmt.Sprintf("%s:%s:%s", vulnID, artifactPath, pkgStr)
+
+	// Hash the data to create a stable fingerprint
+	hash := sha256.Sum256([]byte(fingerprintData))
+
+	return hex.EncodeToString(hash[:])
+}
+
 // createSARIFHelpText returns the text for SARIF rule's help field
 func createSARIFHelpText(gv *groupedSARIFFinding) string {
 	backtickSARIFTemplate := strings.ReplaceAll(strings.TrimSpace(SARIFTemplate), `""`, "`")
@@ -305,6 +332,9 @@ func PrintSARIFReport(vulnResult *models.VulnerabilityResults, outputWriter io.W
 				alsoKnownAsStr = fmt.Sprintf(" (also known as '%s')", strings.Join(gv.AliasedIDList[1:], "', '"))
 			}
 
+			// Generate a stable fingerprint for deduplication
+			fingerprint := createSARIFFingerprint(gv.DisplayID, artifactPath, pws.Package)
+
 			run.CreateResultForRule(gv.DisplayID).
 				WithLevel("warning").
 				WithMessage(
@@ -319,7 +349,23 @@ func PrintSARIFReport(vulnResult *models.VulnerabilityResults, outputWriter io.W
 					sarif.NewLocationWithPhysicalLocation(
 						sarif.NewPhysicalLocation().
 							WithArtifactLocation(sarif.NewSimpleArtifactLocation(artifactPath)),
-					))
+					)).
+				WithPartialFingerprints(map[string]string{
+					// Use "primaryLocationLineHash" as the key for the fingerprint.
+					// This is the standard key that GitHub Advanced Security uses to deduplicate
+					// code scanning alerts across multiple runs.
+					//
+					// Reference: https://docs.github.com/en/code-security/code-scanning/integrating-with-code-scanning/sarif-support-for-code-scanning#preventing-duplicate-alerts-using-fingerprints
+					//
+					// GitHub's documentation states: "GitHub uses the primaryLocationLineHash property
+					// to detect results that are logically the same, so they can be shown only once,
+					// in the correct branch and pull request."
+					//
+					// For dependency scanning (as opposed to source code analysis), we don't have
+					// line numbers in the traditional sense, so our fingerprint is based on the
+					// combination of vulnerability ID, package, and location rather than source code lines.
+					"primaryLocationLineHash": fingerprint,
+				})
 		}
 	}
 
diff --git a/internal/spdx/licenses.go b/internal/spdx/licenses.go
index c96d07eb..743029c5 100644
--- a/internal/spdx/licenses.go
+++ b/internal/spdx/licenses.go
@@ -110,6 +110,7 @@ var IDs = map[string]bool{
 	"bsd-systemics":                        true,
 	"bsd-systemics-w3works":                true,
 	"bsl-1.0":                              true,
+	"buddy":                                true,
 	"busl-1.1":                             true,
 	"bzip2-1.0.5":                          true,
 	"bzip2-1.0.6":                          true,
@@ -457,6 +458,7 @@ var IDs = map[string]bool{
 	"mit-khronos-old":                      true,
 	"mit-modern-variant":                   true,
 	"mit-open-group":                       true,
+	"mit-stk":                              true,
 	"mit-testregex":                        true,
 	"mit-wu":                               true,
 	"mitnfa":                               true,
@@ -691,6 +693,7 @@ var IDs = map[string]bool{
 	"wxwindows":                            true,
 	"x11":                                  true,
 	"x11-distribute-modifications-variant": true,
+	"x11-no-permit-persons":                true,
 	"x11-swapped":                          true,
 	"xdebug-1.03":                          true,
 	"xerox":                                true,
diff --git a/internal/version/version.go b/internal/version/version.go
index 50a9c6ca..21c3e26d 100644
--- a/internal/version/version.go
+++ b/internal/version/version.go
@@ -2,4 +2,4 @@
 package version
 
 // OSVVersion is the current release version, you should update this variable when doing a release
-const OSVVersion = "2.2.4"
+const OSVVersion = "2.3.0"



Test patch:
diff --git a/cmd/osv-scanner/internal/testcmd/case.go b/cmd/osv-scanner/internal/testcmd/case.go
index b8219afe..f2edccd6 100644
--- a/cmd/osv-scanner/internal/testcmd/case.go
+++ b/cmd/osv-scanner/internal/testcmd/case.go
@@ -3,6 +3,8 @@ package testcmd
 
 import (
 	"strings"
+
+	"github.com/google/osv-scanner/v2/internal/testutility"
 )
 
 type Case struct {
@@ -11,7 +13,7 @@ type Case struct {
 	Exit int
 
 	// ReplaceRules are only used for JSON output
-	ReplaceRules []JSONReplaceRule
+	ReplaceRules []testutility.JSONReplaceRule
 }
 
 // findFirstValueOfFlag returns the value of the first instance of the given flag
diff --git a/cmd/osv-scanner/internal/testcmd/run.go b/cmd/osv-scanner/internal/testcmd/run.go
index 96537b86..9081d3d7 100644
--- a/cmd/osv-scanner/internal/testcmd/run.go
+++ b/cmd/osv-scanner/internal/testcmd/run.go
@@ -86,11 +86,11 @@ func RunAndMatchSnapshots(t *testing.T, tc Case) {
 }
 
 // normalizeJSON runs the given JSONReplaceRules on the given JSON input and returns the normalized JSON string
-func normalizeJSON(t *testing.T, jsonInput string, jsonReplaceRules ...JSONReplaceRule) string {
+func normalizeJSON(t *testing.T, jsonInput string, jsonReplaceRules ...testutility.JSONReplaceRule) string {
 	t.Helper()
 
 	for _, rule := range jsonReplaceRules {
-		jsonInput = replaceJSONInput(t, jsonInput, rule.Path, rule.ReplaceFunc)
+		jsonInput = testutility.ReplaceJSONInput(t, jsonInput, rule.Path, rule.ReplaceFunc)
 	}
 
 	jsonFormatted := bytes.Buffer{}
diff --git a/cmd/osv-scanner/scan/image/command_test.go b/cmd/osv-scanner/scan/image/command_test.go
index 17d9a5a3..a38a4fa7 100644
--- a/cmd/osv-scanner/scan/image/command_test.go
+++ b/cmd/osv-scanner/scan/image/command_test.go
@@ -336,49 +336,49 @@ func TestCommand_OCIImage_JSONFormat(t *testing.T) {
 			Name: "Scanning python image with some packages",
 			Args: []string{"", "image", "--archive", "--format=json", "./testdata/test-python-full.tar"},
 			Exit: 1,
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.GroupsAsArrayLen,
-				testcmd.OnlyIDVulnsRule,
-				testcmd.OnlyFirstBaseImage,
-				testcmd.AnyDiffID,
-				testcmd.NormalizeHistoryCommand,
-				testcmd.ShortenHistoryCommandLength,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.GroupsAsArrayLen,
+				testutility.OnlyIDVulnsRule,
+				testutility.OnlyFirstBaseImage,
+				testutility.AnyDiffID,
+				testutility.NormalizeHistoryCommand,
+				testutility.ShortenHistoryCommandLength,
 			},
 		},
 		{
 			Name: "scanning node_modules using npm with some packages",
 			Args: []string{"", "image", "--archive", "--format=json", "./testdata/test-node_modules-npm-full.tar"},
 			Exit: 1,
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.GroupsAsArrayLen,
-				testcmd.OnlyIDVulnsRule,
-				testcmd.OnlyFirstBaseImage,
-				testcmd.AnyDiffID,
-				testcmd.NormalizeHistoryCommand,
-				testcmd.ShortenHistoryCommandLength,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.GroupsAsArrayLen,
+				testutility.OnlyIDVulnsRule,
+				testutility.OnlyFirstBaseImage,
+				testutility.AnyDiffID,
+				testutility.NormalizeHistoryCommand,
+				testutility.ShortenHistoryCommandLength,
 			},
 		},
 		{
 			Name: "scanning image with go binary",
 			Args: []string{"", "image", "--archive", "--all-packages", "--format=json", "./testdata/test-go-binary.tar"},
 			Exit: 1,
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.GroupsAsArrayLen,
-				testcmd.OnlyIDVulnsRule,
-				testcmd.OnlyFirstBaseImage,
-				testcmd.AnyDiffID,
-				testcmd.NormalizeHistoryCommand,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.GroupsAsArrayLen,
+				testutility.OnlyIDVulnsRule,
+				testutility.OnlyFirstBaseImage,
+				testutility.AnyDiffID,
+				testutility.NormalizeHistoryCommand,
 			},
 		},
 		{
 			Name: "scanning ubuntu image",
 			Args: []string{"", "image", "--archive", "--format=json", "./testdata/test-ubuntu.tar"},
 			Exit: 1,
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.GroupsAsArrayLen,
-				testcmd.OnlyIDVulnsRule,
-				testcmd.OnlyFirstBaseImage,
-				testcmd.AnyDiffID,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.GroupsAsArrayLen,
+				testutility.OnlyIDVulnsRule,
+				testutility.OnlyFirstBaseImage,
+				testutility.AnyDiffID,
 			},
 		},
 		{
@@ -386,11 +386,11 @@ func TestCommand_OCIImage_JSONFormat(t *testing.T) {
 			Name: "ubuntu_image_with_go_OS_packages_json",
 			Args: []string{"", "image", "--archive", "--format=json", "./testdata/test-ubuntu-with-packages.tar"},
 			Exit: 1,
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.GroupsAsArrayLen,
-				testcmd.OnlyIDVulnsRule,
-				testcmd.OnlyFirstBaseImage,
-				testcmd.AnyDiffID,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.GroupsAsArrayLen,
+				testutility.OnlyIDVulnsRule,
+				testutility.OnlyFirstBaseImage,
+				testutility.AnyDiffID,
 			},
 		},
 		{
@@ -402,11 +402,11 @@ func TestCommand_OCIImage_JSONFormat(t *testing.T) {
 				"--archive", "./testdata/test-alpine-etcshadow.tar",
 			},
 			Exit: 1,
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.GroupsAsArrayLen,
-				testcmd.OnlyIDVulnsRule,
-				testcmd.OnlyFirstBaseImage,
-				testcmd.AnyDiffID,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.GroupsAsArrayLen,
+				testutility.OnlyIDVulnsRule,
+				testutility.OnlyFirstBaseImage,
+				testutility.AnyDiffID,
 			},
 		},
 		{
@@ -418,11 +418,11 @@ func TestCommand_OCIImage_JSONFormat(t *testing.T) {
 				"--archive", "./testdata/test-alpine-etcshadow.tar",
 			},
 			Exit: 1,
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.GroupsAsArrayLen,
-				testcmd.OnlyIDVulnsRule,
-				testcmd.OnlyFirstBaseImage,
-				testcmd.AnyDiffID,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.GroupsAsArrayLen,
+				testutility.OnlyIDVulnsRule,
+				testutility.OnlyFirstBaseImage,
+				testutility.AnyDiffID,
 			},
 		},
 	}
diff --git a/cmd/osv-scanner/scan/source/command_test.go b/cmd/osv-scanner/scan/source/command_test.go
index 2c769f51..edba3450 100644
--- a/cmd/osv-scanner/scan/source/command_test.go
+++ b/cmd/osv-scanner/scan/source/command_test.go
@@ -165,11 +165,17 @@ func TestCommand(t *testing.T) {
 		{
 			Name: "Empty sarif output",
 			Args: []string{"", "source", "--format", "sarif", "./testdata/locks-many/composer.lock"},
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.ReplacePartialFingerprintHash,
+			},
 			Exit: 0,
 		},
 		{
 			Name: "Sarif with vulns",
 			Args: []string{"", "source", "--format", "sarif", "./testdata/locks-many-with-insecure/package-lock.json"},
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.ReplacePartialFingerprintHash,
+			},
 			Exit: 1,
 		},
 		// output format: gh-annotations
@@ -215,16 +221,16 @@ func TestCommand(t *testing.T) {
 		{
 			Name: "Empty spdx 2.3 output",
 			Args: []string{"", "source", "--format", "spdx-2-3", "./testdata/locks-many/composer.lock"},
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.NormalizeCreateDateSPDX,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.NormalizeCreateDateSPDX,
 			},
 			Exit: 0,
 		},
 		{
 			Name: "spdx 2.3 output", // SPDX does not support outputting vulnerabilties
 			Args: []string{"", "source", "--config=./testdata/osv-scanner-empty-config.toml", "--format", "spdx-2-3", "--all-packages", "./testdata/locks-insecure"},
-			ReplaceRules: []testcmd.JSONReplaceRule{
-				testcmd.NormalizeCreateDateSPDX,
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.NormalizeCreateDateSPDX,
 			},
 			Exit: 1,
 		},
@@ -881,6 +887,9 @@ func TestCommand_GithubActions(t *testing.T) {
 		{
 			Name: "scanning osv-scanner custom format output json",
 			Args: []string{"", "source", "-L", "osv-scanner:./testdata/locks-insecure/osv-scanner-flutter-deps.json", "--format=sarif"},
+			ReplaceRules: []testutility.JSONReplaceRule{
+				testutility.ReplacePartialFingerprintHash,
+			},
 			Exit: 1,
 		},
 	}
diff --git a/internal/output/sarif_fingerprint_test.go b/internal/output/sarif_fingerprint_test.go
new file mode 100644
index 00000000..56c47d64
--- /dev/null
+++ b/internal/output/sarif_fingerprint_test.go
@@ -0,0 +1,92 @@
+package output
+
+import (
+	"fmt"
+	"testing"
+
+	"github.com/google/osv-scanner/v2/pkg/models"
+)
+
+// Common test parameter values used by both test functions
+var (
+	testVulnIDs       = []string{"CVE-2021-1234", "CVE-2022-5678"}
+	testArtifactPaths = []string{"/path/to/package.json", "/different/path/go.mod"}
+	testPackages      = []models.PackageInfo{
+		{Name: "pkg1", Version: "1.0.0"},
+		{Name: "pkg2", Version: "1.0.0"},
+		{Name: "pkg1", Version: "2.0.0"},
+		{Name: "pkg1", Commit: "abc123"},
+	}
+)
+
+func Test_createSARIFFingerprint(t *testing.T) {
+	t.Parallel()
+
+	// Generate all combinations from common test parameters
+	for i, vulnID := range testVulnIDs {
+		for j, artifactPath := range testArtifactPaths {
+			for k, pkg := range testPackages {
+				testName := fmt.Sprintf("vuln_%d_path_%d_pkg_%d", i, j, k)
+
+				t.Run(testName, func(t *testing.T) {
+					t.Parallel()
+
+					got := createSARIFFingerprint(vulnID, artifactPath, pkg)
+
+					// Verify it returns a 64-character hex string (SHA-256 produces 32 bytes = 64 hex chars)
+					if len(got) != 64 {
+						t.Errorf("createSARIFFingerprint() returned fingerprint of length %d, want 64", len(got))
+					}
+
+					// Verify determinism by calling it again with the same inputs
+					got2 := createSARIFFingerprint(vulnID, artifactPath, pkg)
+					if got != got2 {
+						t.Errorf("createSARIFFingerprint() is not deterministic: first call = %v, second call = %v", got, got2)
+					}
+				})
+			}
+		}
+	}
+}
+
+func Test_createSARIFFingerprint_DifferentInputs(t *testing.T) {
+	t.Parallel()
+
+	// Generate all combinations from common test parameters and their fingerprints
+	type testCase struct {
+		vulnID       string
+		artifactPath string
+		pkg          models.PackageInfo
+		fingerprint  string
+	}
+
+	var testCases []testCase
+	for _, vulnID := range testVulnIDs {
+		for _, artifactPath := range testArtifactPaths {
+			for _, pkg := range testPackages {
+				fp := createSARIFFingerprint(vulnID, artifactPath, pkg)
+				testCases = append(testCases, testCase{
+					vulnID:       vulnID,
+					artifactPath: artifactPath,
+					pkg:          pkg,
+					fingerprint:  fp,
+				})
+			}
+		}
+	}
+
+	// Verify that all fingerprints are unique
+	for i := range testCases {
+		for j := i + 1; j < len(testCases); j++ {
+			if testCases[i].fingerprint == testCases[j].fingerprint {
+				t.Errorf("Expected different fingerprints but got same:\n"+
+					"  Input 1: vulnID=%q, path=%q, pkg=%+v\n"+
+					"  Input 2: vulnID=%q, path=%q, pkg=%+v\n"+
+					"  Fingerprint: %s",
+					testCases[i].vulnID, testCases[i].artifactPath, testCases[i].pkg,
+					testCases[j].vulnID, testCases[j].artifactPath, testCases[j].pkg,
+					testCases[i].fingerprint)
+			}
+		}
+	}
+}
diff --git a/internal/output/sarif_test.go b/internal/output/sarif_test.go
index 261e078c..99e54b43 100644
--- a/internal/output/sarif_test.go
+++ b/internal/output/sarif_test.go
@@ -123,8 +123,15 @@ func buildJSONSarifReport(t *testing.T, res *models.VulnerabilityResults) map[st
 		t.Errorf("Error writing SARIF output: %s", err)
 	}
 
+	replacedJSON := testutility.ReplaceJSONInput(
+		t,
+		outputWriter.String(),
+		testutility.ReplacePartialFingerprintHash.Path,
+		testutility.ReplacePartialFingerprintHash.ReplaceFunc,
+	)
+
 	jsonStructure := map[string]any{}
-	err = json.NewDecoder(outputWriter).Decode(&jsonStructure)
+	err = json.NewDecoder(bytes.NewBufferString(replacedJSON)).Decode(&jsonStructure)
 	if err != nil {
 		t.Errorf("Error decoding SARIF output: %s", err)
 	}
diff --git a/cmd/osv-scanner/internal/testcmd/jsonreplace.go b/internal/testutility/jsonreplace.go
similarity index 90%
rename from cmd/osv-scanner/internal/testcmd/jsonreplace.go
rename to internal/testutility/jsonreplace.go
index 657a70cb..82c578ce 100644
--- a/cmd/osv-scanner/internal/testcmd/jsonreplace.go
+++ b/internal/testutility/jsonreplace.go
@@ -1,4 +1,4 @@
-package testcmd
+package testutility
 
 import (
 	"strconv"
@@ -89,6 +89,17 @@ var (
 			return "2025-01-01T01:01:01Z"
 		},
 	}
+
+	ReplacePartialFingerprintHash = JSONReplaceRule{
+		Path: "runs.#.results.#.partialFingerprints.primaryLocationLineHash",
+		ReplaceFunc: func(toReplace gjson.Result) any {
+			if len(toReplace.String()) > 0 {
+				return "[line-hash]"
+			}
+
+			return "[empty]"
+		},
+	}
 )
 
 func expandArrayPaths(t *testing.T, jsonInput string, path string) []string {
@@ -130,8 +141,8 @@ func expandArrayPaths(t *testing.T, jsonInput string, path string) []string {
 	return paths
 }
 
-// replaceJSONInput takes a gjson path and replaces all elements the path matches with the output of matcher
-func replaceJSONInput(t *testing.T, jsonInput string, path string, replacer func(toReplace gjson.Result) any) string {
+// ReplaceJSONInput takes a gjson path and replaces all elements the path matches with the output of matcher
+func ReplaceJSONInput(t *testing.T, jsonInput string, path string, replacer func(toReplace gjson.Result) any) string {
 	t.Helper()
 
 	var err error
diff --git a/cmd/osv-scanner/internal/testcmd/jsonreplace_test.go b/internal/testutility/jsonreplace_test.go
similarity index 97%
rename from cmd/osv-scanner/internal/testcmd/jsonreplace_test.go
rename to internal/testutility/jsonreplace_test.go
index b92f2c91..523e9f37 100644
--- a/cmd/osv-scanner/internal/testcmd/jsonreplace_test.go
+++ b/internal/testutility/jsonreplace_test.go
@@ -1,4 +1,4 @@
-package testcmd
+package testutility
 
 import (
 	"bytes"
@@ -384,7 +384,7 @@ func Test_replaceJSONInput(t *testing.T) {
 			t.Run(fmt.Sprintf("%d-%s", i, path), func(t *testing.T) {
 				t.Parallel()
 
-				got := replaceJSONInput(t, tt.input, path, matcher)
+				got := ReplaceJSONInput(t, tt.input, path, matcher)
 				if !gjson.Valid(got) {
 					t.Fatalf("Output not valid: \n%s", got)
 				}
@@ -400,7 +400,7 @@ func Test_replaceJSONInput(t *testing.T) {
 				_ = json.Indent(&gotPretty, []byte(got), "", "  ")
 
 				if diff := cmp.Diff(wantPretty.String(), gotPretty.String()); diff != "" {
-					t.Errorf("replaceJSONInput() diff (-want +got): %s", diff)
+					t.Errorf("ReplaceJSONInput() diff (-want +got): %s", diff)
 				}
 			})
 		}
@@ -545,7 +545,7 @@ func Test_replaceJSONInput_More(t *testing.T) {
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 
-			got := replaceJSONInput(t, tt.args.jsonInput, tt.args.path, tt.args.matcher)
+			got := ReplaceJSONInput(t, tt.args.jsonInput, tt.args.path, tt.args.matcher)
 			if !gjson.Valid(got) {
 				t.Fatalf("Output not valid: \n%s", got)
 			}
@@ -561,7 +561,7 @@ func Test_replaceJSONInput_More(t *testing.T) {
 			_ = json.Indent(&gotPretty, []byte(got), "", "\t")
 
 			if diff := cmp.Diff(wantPretty.String(), gotPretty.String()); diff != "" {
-				t.Errorf("replaceJSONInput() diff (-want +got): %s", diff)
+				t.Errorf("ReplaceJSONInput() diff (-want +got): %s", diff)
 			}
 		})
 	}