Repository: bridgecrewio/checkov

Issue:
SCA Scan with Cyclonedx Output Causes Crash
**Describe the issue**

Running the following command is causing crash:

```sh
checkov --framework sca_package -d .  --bc-api-key $PRISMA_ACCESS_KEY::$PRISMA_SECRET_KEY --repo-id hagopj13/node-express-boilerplate -o cyclonedx
```

**Examples**

Using this repo as an example https://github.com/hagopj13/node-express-boilerplate

**Exception Trace**
Please share the trace for the exception and all relevant output by checkov.
To maximize the understanding, please run checkov with LOG_LEVEL set to debug
as follows:
```sh
 LOG_LEVEL=DEBUG checkov --framework sca_package -d .  --bc-api-key $PRISMA_ACCESS_KEY::$PRISMA_SECRET_KEY --repo-id hagopj13/node-express-boilerplate -o cyclonedx

TRUNCATED OUTPUT IS TOO LONG

2023-10-27 09:03:01,435 [MainThread  ] [INFO ]  Finalize repository hagopj13/node-express-boilerplate in bridgecrew's platform
2023-10-27 09:03:01,436 [MainThread  ] [DEBUG]  Getting exit code for report sca_package
2023-10-27 09:03:01,436 [MainThread  ] [DEBUG]  Soft fail severity threshold: None
2023-10-27 09:03:01,436 [MainThread  ] [DEBUG]  Soft fail checks: []
2023-10-27 09:03:01,436 [MainThread  ] [DEBUG]  Hard fail severity threshold: None
2023-10-27 09:03:01,436 [MainThread  ] [DEBUG]  Hard fail checks: []
2023-10-27 09:03:01,436 [MainThread  ] [DEBUG]  Use enforcement rules is FALSE
2023-10-27 09:03:01,436 [MainThread  ] [DEBUG]  In get_exit_code; exit code thresholds: {'soft_fail': False, 'soft_fail_checks': [], 'soft_fail_threshold': None, 'hard_fail_checks': [], 'hard_fail_threshold': None}, hard_fail_on_parsing_errors: False
2023-10-27 09:03:01,437 [MainThread  ] [DEBUG]  There are failed checks and all soft/hard fail args are empty - returning 1
2023-10-27 09:03:01,450 [MainThread  ] [ERROR]  Exception traceback:
Traceback (most recent call last):
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/main.py", line 554, in run
    exit_codes.append(self.print_results(
                      ^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/main.py", line 773, in print_results
    return runner_registry.print_reports(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/runners/runner_registry.py", line 526, in print_reports
    cyclonedx = CycloneDX(repo_id=metadata_integration.bc_integration.repo_id, reports=cyclonedx_reports)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/output/cyclonedx.py", line 64, in __init__
    self.bom = self.create_bom()
               ^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/output/cyclonedx.py", line 112, in create_bom
    vulnerability = self.create_vulnerability(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/output/cyclonedx.py", line 287, in create_vulnerability
    vulnerability = self.create_cve_vulnerability(resource=resource, component=component)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/output/cyclonedx.py", line 408, in create_cve_vulnerability
    affects=[BomTarget(ref=component.bom_ref.value)],
                           ^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'bom_ref'
Traceback (most recent call last):
  File "/opt/homebrew/bin/checkov", line 9, in <module>
    sys.exit(Checkov().run())
             ^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/main.py", line 554, in run
    exit_codes.append(self.print_results(
                      ^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/main.py", line 773, in print_results
    return runner_registry.print_reports(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/runners/runner_registry.py", line 526, in print_reports
    cyclonedx = CycloneDX(repo_id=metadata_integration.bc_integration.repo_id, reports=cyclonedx_reports)
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/output/cyclonedx.py", line 64, in __init__
    self.bom = self.create_bom()
               ^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/output/cyclonedx.py", line 112, in create_bom
    vulnerability = self.create_vulnerability(
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/output/cyclonedx.py", line 287, in create_vulnerability
    vulnerability = self.create_cve_vulnerability(resource=resource, component=component)
                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/opt/homebrew/lib/python3.11/site-packages/checkov/common/output/cyclonedx.py", line 408, in create_cve_vulnerability
    affects=[BomTarget(ref=component.bom_ref.value)],
                           ^^^^^^^^^^^^^^^^^
AttributeError: 'NoneType' object has no attribute 'bom_ref'
```

**Desktop (please complete the following information):**
 - OS: OSX
 - Checkov Version 3.0.7

**Additional context**
Scan works with CSV output


Discussion:
hey @hi-artem thanks for reaching out.

I can confirm this is a bug, resulting from a transitive dependency, which exists multiple times.

Resolving patch:
diff --git a/checkov/common/output/cyclonedx.py b/checkov/common/output/cyclonedx.py
--- a/checkov/common/output/cyclonedx.py
+++ b/checkov/common/output/cyclonedx.py
@@ -100,12 +100,8 @@ def create_bom(self) -> Bom:
                     continue
                 component = self.create_component(check_type=report.check_type, resource=check)
 
-                if bom.has_component(component=component):
-                    component = (
-                        bom.get_component_by_purl(  # type:ignore[assignment]  # the previous line checks, if exists
-                            purl=component.purl
-                        )
-                    )
+                if existing_component := bom.get_component_by_purl(purl=component.purl):
+                    component = existing_component
                 else:
                     bom.components.add(component)
 
@@ -121,7 +117,7 @@ def create_bom(self) -> Bom:
             for resource in sorted(report.extra_resources):
                 component = self.create_component(check_type=report.check_type, resource=resource)
 
-                if not bom.has_component(component=component):
+                if not bom.get_component_by_purl(purl=component.purl):
                     bom.components.add(component)
 
             if is_image_report:


Test patch:
diff --git a/tests/common/output/test_cyclonedx_report.py b/tests/common/output/test_cyclonedx_report.py
--- a/tests/common/output/test_cyclonedx_report.py
+++ b/tests/common/output/test_cyclonedx_report.py
@@ -197,6 +197,74 @@ def test_sca_packages_cyclonedx_bom():
     assert record.file_line_range == [2, 6]
     assert output
 
+
+def test_duplicate_sca_packages_cyclonedx_bom():
+    # given
+    rootless_file_path = "requirements.txt"
+    file_abs_path = "/path/to/requirements.txt"
+    check_class = "checkov.sca_package.scanner.Scanner"
+    vulnerability_details = {
+        "id": "CVE-2019-19844",
+        "status": "fixed in 3.0.1, 2.2.9, 1.11.27",
+        "cvss": 9.8,
+        "vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H",
+        "description": "Django before 1.11.27, 2.x before 2.2.9, and 3.x before 3.0.1 allows account takeover. ...",
+        "severity": "moderate",
+        "packageName": "django",
+        "packageVersion": "1.2",
+        "link": "https://nvd.nist.gov/vuln/detail/CVE-2019-19844",
+        "riskFactors": ["Attack complexity: low", "Attack vector: network", "Critical severity", "Has fix"],
+        "impactedVersions": ["<1.11.27"],
+        "publishedDate": "2019-12-18T20:15:00+01:00",
+        "discoveredDate": "2019-12-18T19:15:00Z",
+        "fixDate": "2019-12-18T20:15:00+01:00",
+    }
+    package_1 = {
+        "package_registry": "https://registry.npmjs.org/",
+        "is_private_registry": False,
+        "linesNumbers": [2, 3],
+    }
+    package_2 = {
+        "package_registry": "https://registry.npmjs.org/",
+        "is_private_registry": False,
+        "linesNumbers": [5, 6],
+    }
+
+    record_1 = create_report_cve_record(
+        rootless_file_path=rootless_file_path,
+        file_abs_path=file_abs_path,
+        check_class=check_class,
+        vulnerability_details=vulnerability_details,
+        licenses="OSI_BDS",
+        package=package_1,
+        file_line_range=get_package_lines(package_1),
+    )
+    record_2 = create_report_cve_record(
+        rootless_file_path=rootless_file_path,
+        file_abs_path=file_abs_path,
+        check_class=check_class,
+        vulnerability_details=vulnerability_details,
+        licenses="OSI_BDS",
+        package=package_2,
+        file_line_range=get_package_lines(package_2),
+    )
+
+    report = Report(CheckType.SCA_PACKAGE)
+    report.add_resource(record_1.resource)
+    report.add_record(record_1)
+    report.add_resource(record_2.resource)
+    report.add_record(record_2)
+
+    # when
+    cyclonedx = CycloneDX([report], "repoid/test")
+
+    # then
+    assert len(cyclonedx.bom.components) == 1
+
+    component = next(iter(cyclonedx.bom.components))
+    assert component.bom_ref.value == "pkg:pypi/repoid/test/requirements.txt/django@1.2"
+
+
 def test_create_schema_version_1_3(mocker: MockerFixture):
     # given
     test_file = Path(__file__).parent / "fixtures/main.tf"