Repository: bridgecrewio/checkov

Issue:
GitHub CodeQL Upload SARIF Results Failure
Uploading the SARIF formatted output to the GH repo fails. It does not appear to be valid SARIF according to the parser. Using the `github/codeql-action/upload-sarif@v1` produces an error message with invalid data.

**To Reproduce**
Steps to reproduce the behavior:
1. Create a GH Action with the following steps to run the checkov scan and generate a `checkov.sarif` results file:

```
- name: Set up Python 3.7
   uses: actions/setup-python@v2
   with:
     python-version: 3.7.7
 - name: Run checkov analysis
    run: |
      python --version
      python -m pip install --upgrade pip setuptools
      python -m pip install --upgrade 'checkov==2.0.360'
      checkov --version
      checkov --framework terraform -s -d src/ -o sarif > checkov.sarif
```

2. Add another action to upload the SARIF results to the repo:

```
- name: Upload SARIF file
  uses: github/codeql-action/upload-sarif@v1
  with:
    sarif_file: checkov.sarif
```

3. See error message:

```
Error: Unable to upload "checkov.sarif" as it is not valid SARIF:
- instance.runs[0].tool.driver.rules contains duplicate item
Error: Unable to upload "checkov.sarif" as it is not valid SARIF:
- instance.runs[0].tool.driver.rules contains duplicate item
    at validateSarifFileSchema (/home/runner/work/_actions/github/codeql-action/v1/lib/upload-lib.js:196:15)
    at uploadFiles (/home/runner/work/_actions/github/codeql-action/v1/lib/upload-lib.js:256:9)
    at Object.uploadFromActions (/home/runner/work/_actions/github/codeql-action/v1/lib/upload-lib.js:132:18)
    at async run (/home/runner/work/_actions/github/codeql-action/v1/lib/upload-sarif-action.js:49:29)
    at async runWrapper (/home/runner/work/_actions/github/codeql-action/v1/lib/upload-sarif-action.js:61:9)
```

**Expected behavior**
SARIF results are uploaded to GH and display on the Security tab.



Discussion:
Further analysis of the JSON file shows that the rule ids appear to be duplicated based on how many "results" are in the findings. For example, I'm seeing two `CKV_AWS_121` ids in the `rules` array. Also 17 `CKV_AWS_111` rule ids in the array. 

I believe the `rules` array is intended to be a distinct list of all rules supported by the scanner. The `results` array is intended to hold the  1 ---> many list of instances. Each result references the corresponding `ruleId` found in the distinct list of `rules`.
@ejohn20  you're absolutely right!

We'd love a contribution, should be around here somewhere:
https://github.com/bridgecrewio/checkov/blob/d181aa0c29d2b5220e24c8f24f3f426870ff9dd3/checkov/common/output/report.py#L192

Resolving patch:
diff --git a/checkov/common/output/report.py b/checkov/common/output/report.py
--- a/checkov/common/output/report.py
+++ b/checkov/common/output/report.py
@@ -193,6 +193,7 @@ def get_sarif_json(self) -> Dict[str, Any]:
         runs = []
         rules = []
         results = []
+        ruleset = set()
         for idx, record in enumerate(self.failed_checks):
             rule = {
                 "id": record.check_id,
@@ -204,6 +205,11 @@ def get_sarif_json(self) -> Dict[str, Any]:
                 },
                 "defaultConfiguration": {"level": "error"},
             }
+            if record.check_id not in ruleset:
+                ruleset.add(record.check_id)
+                rules.append(rule)
+            else:
+                idx = rules.index(rule)
             if record.file_line_range[0] == 0:
                 record.file_line_range[0] = 1
             if record.file_line_range[1] == 0:
@@ -225,7 +231,6 @@ def get_sarif_json(self) -> Dict[str, Any]:
                     }
                 ],
             }
-            rules.append(rule)
             results.append(result)
 
         runs.append({


Test patch:
diff --git a/tests/common/output/test_sarif_report.py b/tests/common/output/test_sarif_report.py
--- a/tests/common/output/test_sarif_report.py
+++ b/tests/common/output/test_sarif_report.py
@@ -50,6 +50,60 @@ def test_valid_passing_valid_testcases(self):
             jsonschema.validate(instance=json_structure, schema=get_sarif_schema()),
         )
 
+    def test_multiples_of_same_rule_do_not_break_schema(self):
+        record1 = Record(
+            check_id="CKV_AWS_21",
+            check_name="Some Check",
+            check_result={"result": CheckResult.FAILED},
+            code_block=None,
+            file_path="./s3.tf",
+            file_line_range=[1, 3],
+            resource="aws_s3_bucket.operations",
+            evaluations=None,
+            check_class=None,
+            file_abs_path=",.",
+            entity_tags={"tag1": "value1"},
+        )
+
+        record2 = Record(
+            check_id="CKV_AWS_3",
+            check_name="Ensure all data stored in the EBS is securely encrypted",
+            check_result={"result": CheckResult.FAILED},
+            code_block=None,
+            file_path="./ec2.tf",
+            file_line_range=[1, 3],
+            resource="aws_ebs_volume.web_host_storage",
+            evaluations=None,
+            check_class=None,
+            file_abs_path=",.",
+            entity_tags={"tag1": "value1"},
+        )
+
+        record3 = Record(
+            check_id="CKV_AWS_3",
+            check_name="Ensure all data stored in the EBS is securely encrypted",
+            check_result={"result": CheckResult.FAILED},
+            code_block=None,
+            file_path="./ec2.tf",
+            file_line_range=[7, 10],
+            resource="aws_ebs_volume.web_host_storage",
+            evaluations=None,
+            check_class=None,
+            file_abs_path=",.",
+            entity_tags={"tag1": "value1"},
+        )
+
+        r = Report("terraform")
+        r.add_record(record=record1)
+        r.add_record(record=record2)
+        r.add_record(record=record3)
+        json_structure = r.get_sarif_json()
+        print(json.dumps(json_structure))
+        self.assertEqual(
+            None,
+            jsonschema.validate(instance=json_structure, schema=get_sarif_schema()),
+        )
+
 
 def get_sarif_schema():
     file_name, headers = urllib.request.urlretrieve(