Repository: bridgecrewio/checkov

Issue:
CSV file blank - No results added
**Describe the issue**
I am trying to generate an Output CSV file upon a successful scan It created CSV files but results are not getting added inside CSV.


**Additional context**
Please check attached image 
<img width="879" alt="issuecsv" src="https://user-images.githubusercontent.com/25578218/180148709-484acd15-c5d2-4bf7-ab9e-02108908ee59.png">

pipenv run bridgecrew --version
2.1.65




Discussion:
hey @Gaurav-Jadhav thanks for reaching out. I actually found 2 problems, which needs to be fixed.

1. support for `--output-file-path` with `csv` output
2. actually add the packages to the generated `csv` output 😄 

Container image information will be added soon, so please don't be surprised about it being empty 😄 

Resolving patch:
diff --git a/checkov/common/output/csv.py b/checkov/common/output/csv.py
--- a/checkov/common/output/csv.py
+++ b/checkov/common/output/csv.py
@@ -1,67 +1,160 @@
 from __future__ import annotations
 
 import csv
+import itertools
+import logging
 import os
 from datetime import datetime
-from typing import Any
+from typing import Any, TYPE_CHECKING
 
+from checkov.common.models.enums import CheckResult
+from checkov.common.output.record import Record
 from checkov.common.output.report import Report, CheckType
 
+if TYPE_CHECKING:
+    from checkov.common.output.extra_resource import ExtraResource
+
 date_now = f'{datetime.now().strftime("%Y%m%d-%H%M%S")}'
-FILE_NAME_OSS_PACKAGES = f'{date_now}_oss_packages.csv'
-HEADER_OSS_PACKAGES = ["Package", "Version", "Path", "git org", "git repository", "Vulnerability", "Severity",
-                       "License"]
+FILE_NAME_OSS_PACKAGES = f"{date_now}_oss_packages.csv"
+HEADER_OSS_PACKAGES = [
+    "Package",
+    "Version",
+    "Path",
+    "git org",
+    "git repository",
+    "Vulnerability",
+    "Severity",
+    "License",
+]
 HEADER_CONTAINER_IMAGE = HEADER_OSS_PACKAGES
-FILE_NAME_CONTAINER_IMAGES = f'{date_now}_container_images.csv'
+FILE_NAME_CONTAINER_IMAGES = f"{date_now}_container_images.csv"
 
-FILE_NAME_IAC = f'{date_now}_iac.csv'
+FILE_NAME_IAC = f"{date_now}_iac.csv"
 HEADER_IAC = ["Resource", "Path", "git org", "git repository", "Misconfigurations", "Severity"]
 
-CTA_NO_API_KEY = "SCA, image and runtime findings are only available with Bridgecrew. Signup at " \
-                 "https://www.bridgecrew.cloud/login/signUp and add your API key to include those findings. "
-
+CTA_NO_API_KEY = (
+    "SCA, image and runtime findings are only available with Bridgecrew. Signup at "
+    "https://www.bridgecrew.cloud/login/signUp and add your API key to include those findings. "
+)
 
-class CSVSBOM():
 
+class CSVSBOM:
     def __init__(self) -> None:
         self.iac_rows: list[dict[str, Any]] = []
         self.container_rows: list[dict[str, Any]] = []
         self.package_rows: list[dict[str, Any]] = []
 
+        self.iac_resource_cache: set[str] = set()  # used to check, if a resource was already added
+
     def add_report(self, report: Report, git_org: str, git_repository: str) -> None:
-        if report.check_type != CheckType.SCA_IMAGE and report.check_type != CheckType.SCA_PACKAGE:
-            for failed_record in report.failed_checks:
-                self.iac_rows.append(
-                    {"Resource": failed_record.resource, "Path": failed_record.file_path, "git org": git_org,
-                     "git repository": git_repository, "Misconfigurations": failed_record.check_id,
-                     "Severity": failed_record.severity})
-            for extra_resource in report.extra_resources:
-                self.iac_rows.append(
-                    {"Resource": extra_resource.resource, "Path": extra_resource.file_path, "git org": git_org,
-                     "git repository": git_repository, "Misconfigurations": "",
-                     "Severity": ""})
+        if report.check_type == CheckType.SCA_PACKAGE:
+            for record in itertools.chain(report.failed_checks, report.passed_checks, report.skipped_checks):
+                self.add_sca_package_resources(resource=record, git_org=git_org, git_repository=git_repository)
+            for resource in report.extra_resources:
+                self.add_sca_package_resources(resource=resource, git_org=git_org, git_repository=git_repository)
+        elif report.check_type == CheckType.SCA_IMAGE:
+            # needs to be implemented separately
+            return
+        else:
+            for record in itertools.chain(report.failed_checks, report.passed_checks, report.skipped_checks):
+                self.add_iac_resources(resource=record, git_org=git_org, git_repository=git_repository)
+            for resource in report.extra_resources:
+                self.add_iac_resources(resource=resource, git_org=git_org, git_repository=git_repository)
+
+
+    def add_sca_package_resources(self, resource: Record | ExtraResource, git_org: str, git_repository: str) -> None:
+        if not resource.vulnerability_details:
+            # this shouldn't happen
+            logging.error(f"Resource {resource.resource} doesn't have 'vulnerability_details' set")
+            return
+
+        severity = None
+        if isinstance(resource, Record) and resource.severity is not None:
+            # ExtraResource don't have a CVE/Severity
+            severity = resource.severity.name
+
+        self.package_rows.append(
+            {
+                "Package": resource.vulnerability_details["package_name"],
+                "Version": resource.vulnerability_details["package_version"],
+                "Path": resource.file_path,
+                "git org": git_org,
+                "git repository": git_repository,
+                "Vulnerability": resource.vulnerability_details.get("id"),
+                "Severity": severity,
+                "License": None,  # will be added later
+            }
+        )
+
+    def add_iac_resources(self, resource: Record | ExtraResource, git_org: str, git_repository: str) -> None:
+        resource_id = f"{git_org}/{git_repository}/{resource.file_path}/{resource.resource}"
+
+        misconfig = None
+        severity = None
+        if isinstance(resource, Record) and resource.check_result["result"] == CheckResult.FAILED:
+            # only failed resources should be added with their misconfiguration
+            misconfig = resource.check_id
+            if resource.severity is not None:
+                severity = resource.severity.name
+        elif resource_id in self.iac_resource_cache:
+            # IaC resources shouldn't be added multiple times, if they don't have any misconfiguration
+            return
+
+        self.iac_rows.append(
+            {
+                "Resource": resource.resource,
+                "Path": resource.file_path,
+                "git org": git_org,
+                "git repository": git_repository,
+                "Misconfigurations": misconfig,
+                "Severity": severity,
+            }
+        )
+        self.iac_resource_cache.add(resource_id)
 
     def persist_report(self, is_api_key: bool, output_path: str = "") -> None:
+        output_path = output_path or ""
+
         self.persist_report_iac(file_name=FILE_NAME_IAC, output_path=output_path)
-        self.persist_report_container_images(file_name=FILE_NAME_CONTAINER_IMAGES, is_api_key=is_api_key, output_path=output_path)
-        self.persist_report_oss_packages(file_name=FILE_NAME_OSS_PACKAGES, is_api_key=is_api_key, output_path=output_path)
+        self.persist_report_container_images(
+            file_name=FILE_NAME_CONTAINER_IMAGES,
+            is_api_key=is_api_key,
+            output_path=output_path,
+        )
+        self.persist_report_oss_packages(
+            file_name=FILE_NAME_OSS_PACKAGES,
+            is_api_key=is_api_key,
+            output_path=output_path,
+        )
 
     def persist_report_iac(self, file_name: str, output_path: str = "") -> None:
-        CSVSBOM.write_section(file=os.path.join(output_path, file_name), header=HEADER_IAC, rows=self.iac_rows,
-                              is_api_key=True)
+        CSVSBOM.write_section(
+            file=os.path.join(output_path, file_name),
+            header=HEADER_IAC,
+            rows=self.iac_rows,
+            is_api_key=True,
+        )
 
     def persist_report_container_images(self, file_name: str, is_api_key: bool, output_path: str = "") -> None:
-        CSVSBOM.write_section(file=os.path.join(output_path, file_name), header=HEADER_CONTAINER_IMAGE,
-                              rows=self.container_rows, is_api_key=is_api_key)
+        CSVSBOM.write_section(
+            file=os.path.join(output_path, file_name),
+            header=HEADER_CONTAINER_IMAGE,
+            rows=self.container_rows,
+            is_api_key=is_api_key,
+        )
 
     def persist_report_oss_packages(self, file_name: str, is_api_key: bool, output_path: str = "") -> None:
-        CSVSBOM.write_section(file=os.path.join(output_path, file_name), header=HEADER_OSS_PACKAGES,
-                              rows=self.package_rows, is_api_key=is_api_key)
+        CSVSBOM.write_section(
+            file=os.path.join(output_path, file_name),
+            header=HEADER_OSS_PACKAGES,
+            rows=self.package_rows,
+            is_api_key=is_api_key,
+        )
 
     @staticmethod
     def write_section(file: str, header: list[str], rows: list[dict[str, Any]], is_api_key: bool) -> None:
-        with open(file, 'w', newline='') as f:
-            print(f'Persisting SBOM to {os.path.abspath(file)}')
+        with open(file, "w", newline="") as f:
+            print(f"Persisting SBOM to {os.path.abspath(file)}")
             if is_api_key:
                 dict_writer = csv.DictWriter(f, fieldnames=header)
                 dict_writer.writeheader()
diff --git a/checkov/common/runners/runner_registry.py b/checkov/common/runners/runner_registry.py
--- a/checkov/common/runners/runner_registry.py
+++ b/checkov/common/runners/runner_registry.py
@@ -234,7 +234,7 @@ def print_reports(
             is_api_key = False
             if 'bc_api_key' in config and config.bc_api_key is not None:
                 is_api_key = True
-            csv_sbom_report.persist_report(is_api_key)
+            csv_sbom_report.persist_report(is_api_key=is_api_key, output_path=config.output_file_path)
 
         # Save output to file
         file_names = {'cli': 'results_cli.txt',


Test patch:
diff --git a/tests/common/output/test_bom_report.py b/tests/common/output/test_bom_report.py
--- a/tests/common/output/test_bom_report.py
+++ b/tests/common/output/test_bom_report.py
@@ -1,10 +1,14 @@
-import pytest
-import pathlib
-
 import os
 import io
+from pathlib import Path
 from unittest.mock import patch
 from checkov.cloudformation.runner import Runner as cfn_runner
+from checkov.common.bridgecrew.severities import Severities, BcSeverities
+from checkov.common.models.enums import CheckResult
+from checkov.common.output.csv import CSVSBOM
+from checkov.common.output.extra_resource import ExtraResource
+from checkov.common.output.record import Record
+from checkov.common.output.report import Report, CheckType
 from checkov.common.runners.runner_registry import RunnerRegistry
 from checkov.common.util.banner import banner
 from checkov.kubernetes.runner import Runner as k8_runner
@@ -13,7 +17,7 @@
 
 
 class TestBomOutput:
-    def test_csv_output(self, tmp_path: pathlib.Path):
+    def test_csv_output(self, tmp_path: Path):
         test_files_dir = os.path.dirname(os.path.realpath(__file__)) + "/../runner_registry/example_s3_tf"
         runner_filter = RunnerFilter(framework=None, checks=None, skip_checks=None)
         runner_registry = RunnerRegistry(
@@ -32,3 +36,61 @@ def test_csv_output(self, tmp_path: pathlib.Path):
             assert 'Resource,Path,git org,git repository,Misconfigurations,Severity\n' == header
             rows = content[1:]
             assert 'aws_s3_bucket' in rows[0]
+
+    def test_sca_package_csv_output(self, tmp_path: Path):
+        # given
+        file_name = "oss_packages.csv"
+        csv_sbom_report = CSVSBOM()
+        report = Report(CheckType.SCA_PACKAGE)
+
+        cve_id = "CVE-2019-1010083"
+        rootless_file_path = "requirements.txt"
+        file_abs_path = f"example/{rootless_file_path}"
+
+        details = {
+            "id": cve_id,
+            "severity": "high",
+            "package_name": "flask",
+            "package_version": "0.6",
+        }
+        record = Record(
+            check_id=f"CKV_{cve_id.replace('-', '_')}",
+            bc_check_id=f"BC_{cve_id.replace('-', '_')}",
+            check_name="SCA package scan",
+            check_result={"result": CheckResult.FAILED},
+            code_block=[(0,"")],
+            file_path=f"/{rootless_file_path}",
+            file_line_range=[0, 0],
+            resource="requirements.txt.flask",
+            check_class="",
+            evaluations=None,
+            file_abs_path=file_abs_path,
+            severity=Severities.get(BcSeverities.HIGH),
+            vulnerability_details=details,
+        )
+        extra_resource = ExtraResource(
+            file_abs_path=file_abs_path,
+            file_path=f"/{rootless_file_path}",
+            resource=f"{rootless_file_path}.requests",
+            vulnerability_details={
+                "package_name": "requests",
+                "package_version": "",
+            }
+        )
+
+        report.add_record(record)
+        report.extra_resources.add(extra_resource)
+        csv_sbom_report.add_report(report=report, git_org="acme", git_repository="bridgecrewio/example")
+
+        # when
+        csv_sbom_report.persist_report_oss_packages(file_name=file_name, is_api_key=True, output_path=str(tmp_path))
+
+        # then
+        output_file_path = tmp_path / file_name
+        csv_output = output_file_path.read_text()
+
+        assert csv_output == (
+            "Package,Version,Path,git org,git repository,Vulnerability,Severity,License\n"
+            "flask,0.6,/requirements.txt,acme,bridgecrewio/example,CVE-2019-1010083,HIGH,\n"
+            "requests,,/requirements.txt,acme,bridgecrewio/example,,,\n"
+        )