Repository: bridgecrewio/checkov

Issue:
junit ouptut format - add context information
**Is your feature request related to a problem? Please describe.**
We're using CI to run the cli checkov analysis. It works good but with bigger code bases it takes a lot of scrolling to find the actually violation. 
We started using junit report which can be parsed and presented in a nicer form.
![image](https://user-images.githubusercontent.com/742104/92414400-5db65a00-f154-11ea-8730-f44cd97baf7f.png)


The problem now is how to locate the violation. The junit report contains no context information where exactly should we look into. We end up again using grep/find


**Describe the solution you'd like**
It would be great if the junit report contained context information where exactly did the violation occur.


**Describe alternatives you've considered**
One option would be to use multiple output at the same time cli+junit but this doesn't work as expected since junit doesn't create a dedicated ouptut file but rather you have to pipe it yourself e.g.

`checkov  -o junitxml > analysis.xml`





Discussion:
Hello! 

We have exactly the same use case and would be more than happy if the violated policy IDs would be visible in the junit reports. 

Thank you!

Hi @tantweiler @jglapa can you supply a mock of the suggestion?
Hi @schosterbarak! Just an example --> It would be ok for me if I could see the ID (e.g. CKV_AZURE_6) in the suite column:

Output in GitLab:

 
![image](https://user-images.githubusercontent.com/48676838/104896142-07b1b880-5977-11eb-8f22-e8d7749fc4ee.png)

@tantweiler as concatenated value? 

Btw what system is the screenshot from? 
Hi @schosterbarak,

That is GitLab! Concatenated value should work I would say. 
@tantweiler I think that be done by adding modifying the following line:
https://github.com/bridgecrewio/checkov/blob/feea72d817db0f0a1fbb9689164858f356325213/checkov/common/output/report.py#L117
to have `record.check_id` concatenated as string. 

I haven't done the testing, if you want to give it a try - we would accept a PR there. 
I've been playing around with this a little together with tfsec

What's odd is that using [warnings-ng](https://github.com/jenkinsci/warnings-ng-plugin) to integrate this with github checks it detects findings from tfsec but none from checkov.

the format also differs slightly

tfsec:

```xml
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="tfsec" failures="1" tests="1">
	<testcase classname="/REDACTED/grafana/ec2/security_groups.tf" name="[AWS008][WARNING]" time="0">
		<failure message="Resource &#39;aws_security_group.alb&#39; defines a fully open ingress security group." type="">/REDACTED/grafana/ec2/security_groups.tf:11&#xA;       8 |     from_port   = 80&#xA;       9 |     to_port     = 80&#xA;      10 |     protocol    = &#34;tcp&#34;&#xA;      11 |     cidr_blocks = [&#34;0.0.0.0/0&#34;] # tfsec:ignore:AWS009&#xA;      12 |   }&#xA;      13 | &#xA;      14 |   ingress {&#xA;&#xA;See https://tfsec.dev/docs/aws/AWS008/ for more information.</failure>
	</testcase>
</testsuite>
```

checkov:

```xml
<?xml version="1.0" ?>
<testsuites disabled="0" errors="0" failures="16" tests="924" time="0.0">
    <testsuite disabled="0" errors="0" failures="2" name="Ensure that load balancer is using TLS 1.2" package="checkov.terraform.checks.resource.aws.AppLoadBalancerTLS12" skipped="0" tests="14" time="0">
		<testcase classname="checkov.terraform.checks.resource.aws.AppLoadBalancerTLS12" file="/REDACTED/ec2/int_nlb.tf" name="terraform Ensure that load balancer is using TLS 1.2 aws_lb_listener.internal">
			<failure message="Resource &quot;aws_lb_listener.internal&quot; failed in check &quot;Ensure that load balancer is using TLS 1.2&quot;" type="failure"/>
		</testcase>
	</testsuite>
</testsuites>
```

right now it's basically impossible to figure out which module the results relate to in Jenkins.

**note** i'm not saying tfsec does this better/correctly in any way - I have no idea which behaviour is supposed to be correct. This is how it displays in github in the end though, which is pretty damn awesome.

![image](https://user-images.githubusercontent.com/1730315/107997987-66a13680-6fe4-11eb-9a1f-05e1f5f46a6a.png)

and this is how it shows up in jenkins

![image](https://user-images.githubusercontent.com/1730315/108038677-6c763680-703b-11eb-8c96-0e3d9ae7b456.png)
![image](https://user-images.githubusercontent.com/1730315/108038724-7861f880-703b-11eb-8a27-eaccb0b6b8c5.png)

from screenshots above it seems to me that jenkins is not presenting the `file` attribute but reading the content of the tag. I don't think that's standard...

I would also like to see the check ID presented though, as it would make skipping a lot easier.

Further - it would be good if there was some docs for each check that could be linked to, justifying the reason for the check and acceptable use cases for skipping it. tfsec have done this really well.


Resolving patch:
diff --git a/checkov/common/output/record.py b/checkov/common/output/record.py
--- a/checkov/common/output/record.py
+++ b/checkov/common/output/record.py
@@ -105,17 +105,18 @@ def _is_expression_in_code_lines(self, expression):
         return any(stripped_expression in self._trim_special_chars(line) for (_, line) in self.code_block)
 
     @staticmethod
-    def _code_line_string(code_block):
-        string_block = ""
-        last_line_number, _ = code_block[-1]
+    def _code_line_string(code_block: List[Tuple[int, str]], colorized: bool = True) -> str:
+        code_output = []
+        color_codes = (Fore.WHITE if colorized else "", Fore.YELLOW if colorized else "")
+        last_line_number_len = len(str(code_block[-1][0]))
 
-        for (line_num, line) in code_block:
-            spaces = " " * (len(str(last_line_number)) - len(str(line_num)))
+        for line_num, line in code_block:
+            spaces = " " * (last_line_number_len - len(str(line_num)))
             if line.lstrip().startswith("#"):
-                string_block += "\t\t" + Fore.WHITE + str(line_num) + spaces + " | " + line
+                code_output.append(f"\t\t{color_codes[0]}{line_num}{spaces} | {line}")
             else:
-                string_block += "\t\t" + Fore.WHITE + str(line_num) + spaces + " | " + Fore.YELLOW + line
-        return string_block
+                code_output.append(f"\t\t{color_codes[0]}{line_num}{spaces} | {color_codes[1]}{line}")
+        return "".join(code_output)
 
     def to_string(self, compact: bool = False, use_bc_ids: bool = False) -> str:
         status = ""
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
@@ -1,8 +1,8 @@
+import argparse
 import fnmatch
 import itertools
 import json
 import sys
-from collections import defaultdict
 from collections.abc import Iterable
 from dataclasses import dataclass
 from typing import List, Dict, Union, Any, Optional, Set
@@ -274,11 +274,6 @@ def print_console(
     def _print_parsing_error_console(file: str) -> None:
         print(colored(f"Error parsing file {file}", "red"))
 
-    def print_junit_xml(self, use_bc_ids: bool = False):
-        ts = self.get_test_suites(use_bc_ids)
-        xml_string = self.get_junit_xml_string(ts)
-        return xml_string
-
     def get_sarif_json(self, tool) -> Dict[str, Any]:
         runs = []
         rules = []
@@ -412,38 +407,123 @@ def print_failed_github_md(self, use_bc_ids=False) -> str:
         print(output_data)
         return output_data
 
-    def get_test_suites(self, use_bc_ids=False) -> List[TestSuite]:
-        test_cases = defaultdict(list)
+    def get_test_suite(self, properties: Optional[Dict[str, Any]] = None, use_bc_ids: bool = False) -> TestSuite:
+        """Creates a test suite for the JUnit XML report"""
 
+        test_cases = []
+        
         records = self.passed_checks + self.failed_checks + self.skipped_checks
         for record in records:
-            check_name = f"{record.get_output_id(use_bc_ids)}/{record.check_name}"
+            severity = (record.severity or "none").upper()
 
-            test_name = f"{self.check_type} {check_name} {record.resource}"
-            test_case = TestCase(
-                name=test_name, file=record.file_path, classname=record.check_class
-            )
+            if self.check_type == CheckType.SCA_PACKAGE:
+                check_id = record.vulnerability_details["id"]
+                test_name_detail = f"{record.vulnerability_details['package_name']}: {record.vulnerability_details['package_version']}"
+                class_name = f"{record.file_path}.{record.vulnerability_details['package_name']}"
+            else:
+                check_id = record.bc_check_id if use_bc_ids else record.check_id
+                test_name_detail = record.check_name
+                class_name = f"{record.file_path}.{record.resource}"
+
+            test_name = f"[{severity}][{check_id}] {test_name_detail}"
+
+            test_case = TestCase(name=test_name, file=record.file_path, classname=class_name)
             if record.check_result["result"] == CheckResult.FAILED:
-                if record.file_path and record.file_line_range:
-                    test_case.add_failure_info(
-                        f"Resource {record.resource} failed in check {check_name} - {record.file_path}:{record.file_line_range} - Guideline: {record.guideline}"
-                    )
-                else:
-                    test_case.add_failure_info(
-                        f"Resource {record.resource} failed in check {check_name}"
-                    )
-            if record.check_result["result"] == CheckResult.SKIPPED:
-                test_case.add_skipped_info(
-                    f'Resource {record.resource} skipped in check {check_name} \n Suppress comment: {record.check_result["suppress_comment"]} - Guideline: {record.guideline}'
+                test_case.add_failure_info(
+                    message=record.check_name,
+                    output=self._create_test_case_failure_output(record)
                 )
+            if record.check_result["result"] == CheckResult.SKIPPED:
+                if self.check_type == CheckType.SCA_PACKAGE:
+                    test_case.add_skipped_info(f"{check_id} skipped for {test_name_detail}")
+                else:
+                    test_case.add_skipped_info(record.check_result["suppress_comment"])
 
-            test_cases[check_name].append(test_case)
-        test_suites = [
-            TestSuite(name=key, test_cases=value, package=value[0].classname)
-            for key, value in test_cases.items()
-        ]
-        
-        return test_suites
+            test_cases.append(test_case)
+
+        test_suite = TestSuite(name=f"{self.check_type} scan", test_cases=test_cases, properties=properties)
+        return test_suite
+
+    @staticmethod
+    def create_test_suite_properties_block(config: argparse.Namespace) -> Dict[str, Any]:
+        """Creates a dictionary without 'None' values for the JUnit XML properties block"""
+
+        properties = {k: v for k, v in config.__dict__.items() if v is not None}
+        return properties
+
+
+    def _create_test_case_failure_output(self, record: Record) -> str:
+        """Creates the failure output for a JUnit XML test case
+
+        IaC example:
+            Resource: azurerm_network_security_rule.fail_rdp
+            File: /main.tf: 71-83
+            Guideline: https://docs.bridgecrew.io/docs/bc_azr_networking_2
+
+                    71 | resource "azurerm_network_security_rule" "fail_rdp" {
+                    72 |   resource_group_name = azurerm_resource_group.example.name
+                    73 |   network_security_group_name=azurerm_network_security_group.example_rdp.name
+                    74 |   name                       = "fail_security_rule"
+                    75 |   direction                  = "Inbound"
+                    76 |   access                     = "Allow"
+                    77 |   protocol                   = "TCP"
+                    78 |   source_port_range          = "*"
+                    79 |   destination_port_range     = "3389"
+                    80 |   source_address_prefix      = "*"
+                    81 |   destination_address_prefix = "*"
+                    82 |   priority = 120
+                    83 | }
+
+        SCA example:
+            Description: Django before 1.11.27, 2.x before 2.2.9, and 3.x before 3.0.1 allows account takeover.
+            Link: https://nvd.nist.gov/vuln/detail/CVE-2019-19844
+            Published Date: 2019-12-18T20:15:00+01:00
+            Base Score: 9.8
+            Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H
+            Risk Factors: ['Attack complexity: low', 'Attack vector: network', 'Critical severity', 'Has fix']
+
+            Resource: requirements.txt.django
+            File: /requirements.txt: 0-0
+
+                    0 | django: 1.2
+        """
+
+        failure_output = []
+
+        if self.check_type == CheckType.SCA_PACKAGE:
+            failure_output.extend(
+                [
+                    "",
+                    f"Description: {record.description}",
+                    f"Link: {record.vulnerability_details.get('link')}",
+                    f"Published Date: {record.vulnerability_details.get('published_date')}",
+                    f"Base Score: {record.vulnerability_details.get('cvss')}",
+                    f"Vector: {record.vulnerability_details.get('vector')}",
+                    f"Risk Factors: {record.vulnerability_details.get('risk_factors')}",
+                ]
+            )
+
+        failure_output.extend(
+            [
+                "",
+                f"Resource: {record.resource}",
+            ]
+        )
+
+        if record.file_path:
+            file_line = f"File: {record.file_path}"
+            if record.file_line_range:
+                file_line += f": {record.file_line_range[0]}-{record.file_line_range[1]}"
+            failure_output.append(file_line)
+
+        if self.check_type != CheckType.SCA_PACKAGE:
+            failure_output.append(f"Guideline: {record.guideline}")
+
+        if record.code_block:
+            failure_output.append("")
+            failure_output.append(record._code_line_string(code_block=record.code_block, colorized=False))
+
+        return "\n".join(failure_output)
 
     def print_json(self) -> None:
         print(self.get_json())
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
@@ -115,7 +115,6 @@ def print_reports(
                     report_jsons.append(report.get_dict(is_quiet=config.quiet, url=url))
                 if "junitxml" in config.output:
                     junit_reports.append(report)
-                    # report.print_junit_xml()
                 if "github_failed_only" in config.output:
                     data_outputs["github_failed_only"] += report.print_failed_github_md(use_bc_ids=config.output_bc_ids)
                 if "sarif" in config.output:
@@ -179,17 +178,19 @@ def print_reports(
             if output_formats:
                 print(OUTPUT_DELIMITER)
         if "junitxml" in config.output:
-            if len(junit_reports) == 1:
-                data_outputs['junitxml'] = junit_reports[0].print_junit_xml(use_bc_ids=config.output_bc_ids)
-                print(data_outputs['junitxml'])
+            properties = Report.create_test_suite_properties_block(config)
+
+            if junit_reports:
+                test_suites = [
+                    report.get_test_suite(properties=properties, use_bc_ids=config.output_bc_ids)
+                    for report in junit_reports
+                ]
             else:
-                master_report = Report(None)
-                for report in junit_reports:
-                    master_report.skipped_checks += report.skipped_checks
-                    master_report.passed_checks += report.passed_checks
-                    master_report.failed_checks += report.failed_checks
-                data_outputs['junitxml'] = master_report.print_junit_xml(use_bc_ids=config.output_bc_ids)
-                print(data_outputs['junitxml'])
+                test_suites = [Report(None).get_test_suite(properties=properties)]
+
+            data_outputs['junitxml'] = Report.get_junit_xml_string(test_suites)
+            print(data_outputs['junitxml'])
+
             output_formats.remove("junitxml")
             if output_formats:
                 print(OUTPUT_DELIMITER)


Test patch:
diff --git a/docs/4.Integrations/jenkins_test_results.png b/docs/4.Integrations/jenkins_test_results.png
Binary files a/docs/4.Integrations/jenkins_test_results.png and b/docs/4.Integrations/jenkins_test_results.png differ
diff --git a/tests/common/output/fixtures/main.tf b/tests/common/output/fixtures/main.tf
--- a/tests/common/output/fixtures/main.tf
+++ b/tests/common/output/fixtures/main.tf
@@ -1,4 +1,5 @@
 resource "aws_s3_bucket" "destination" {
+  # checkov:skip=CKV_AWS_19: no encryption needed
   bucket = "tf-test-bucket-destination-12345"
   acl = var.acl
   versioning {
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
@@ -24,7 +24,7 @@ def test_valid_cyclonedx_bom(self):
         # outputter.output_to_file(filename='/tmp/test.xml', allow_overwrite=True)
         self.assertEqual(len(cyclonedx_bom.get_components()), 1)
         first_component = cyclonedx_bom.get_components()[0]
-        self.assertEqual(len(first_component.get_vulnerabilities()), 5)
+        self.assertEqual(len(first_component.get_vulnerabilities()), 4)
 
 
 if __name__ == '__main__':
diff --git a/tests/common/output/test_junit_report.py b/tests/common/output/test_junit_report.py
--- a/tests/common/output/test_junit_report.py
+++ b/tests/common/output/test_junit_report.py
@@ -1,44 +1,107 @@
+import argparse
 import unittest
-import os
+import xml
 import xml.etree.ElementTree as ET
-
+from pathlib import Path
 
 from checkov.common.models.enums import CheckResult
 from checkov.common.output.report import Report
 from checkov.common.output.record import Record
+from checkov.common.runners.runner_registry import RunnerRegistry
+from checkov.runner_filter import RunnerFilter
+from checkov.terraform.runner import Runner as TerrafomrRunner
 
 
 class TestJunitReport(unittest.TestCase):
-
     def test_valid_passing_valid_testcases(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'
-                         })
+        record1 = Record(
+            check_id="CKV_AWS_21",
+            check_name="Some Check",
+            check_result={"result": CheckResult.FAILED},
+            code_block=[],
+            file_path="./s3.tf",
+            file_line_range=[1, 3],
+            resource="aws_s3_bucket.operations",
+            evaluations=None,
+            check_class="",
+            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=[],
+            file_path="./ec2.tf",
+            file_line_range=[1, 3],
+            resource="aws_ebs_volume.web_host_storage",
+            evaluations=None,
+            check_class="",
+            file_abs_path=",.",
+            entity_tags={"tag1": "value1"},
+        )
 
         r = Report("terraform")
         r.add_record(record=record1)
         r.add_record(record=record2)
-        ts = r.get_test_suites()
-        xml_string = r.get_junit_xml_string(ts)
+        ts = r.get_test_suite()
+        xml_string = r.get_junit_xml_string([ts])
         root = ET.fromstring(xml_string)
-        self.assertEqual(root.attrib['errors'], '0')
+        self.assertEqual(root.attrib["errors"], "0")
+
+    def test_get_junit_xml_string_with_terraform(self):
+        # given
+        test_file = Path(__file__).parent / "fixtures/main.tf"
+        checks = ["CKV_AWS_18", "CKV_AWS_19", "CKV_AWS_21"]  # 1 pass, 1 fail, 1 skip
+        config = argparse.Namespace(file="fixtures/main.tf", framework=["terraform"])
+        report = TerrafomrRunner().run(
+            root_folder="", files=[str(test_file)], runner_filter=RunnerFilter(checks=checks)
+        )
+
+        # then
+        properties = Report.create_test_suite_properties_block(config=config)
+        test_suite = report.get_test_suite(properties=properties)
+        xml_string = Report.get_junit_xml_string([test_suite])
+
+        # then
+        assert (
+            xml.dom.minidom.parseString(xml_string).toprettyxml()
+            == xml.dom.minidom.parseString(
+                "".join(
+                    [
+                        '<?xml version="1.0" ?>\n',
+                        '<testsuites disabled="0" errors="0" failures="1" tests="3" time="0.0">\n',
+                        '\t<testsuite disabled="0" errors="0" failures="1" name="terraform scan" skipped="1" tests="3" time="0">\n',
+                        "\t\t<properties>\n",
+                        '\t\t\t<property name="file" value="fixtures/main.tf"/>\n',
+                        '\t\t\t<property name="framework" value="[\'terraform\']"/>\n',
+                        "\t\t</properties>\n",
+                        '\t\t<testcase name="[NONE][CKV_AWS_21] Ensure all data stored in the S3 bucket have versioning enabled" classname="/main.tf.aws_s3_bucket.destination" file="/main.tf"/>\n',
+                        '\t\t<testcase name="[NONE][CKV_AWS_18] Ensure the S3 bucket has access logging enabled" classname="/main.tf.aws_s3_bucket.destination" file="/main.tf">\n',
+                        '\t\t\t<failure type="failure" message="Ensure the S3 bucket has access logging enabled">\n',
+                        "Resource: aws_s3_bucket.destination\n",
+                        "File: /main.tf: 1-8\n",
+                        "Guideline: None\n",
+                        "\n",
+                        "\t\t1 | resource &quot;aws_s3_bucket&quot; &quot;destination&quot; {\n",
+                        "\t\t2 |   # checkov:skip=CKV_AWS_19: no encryption needed\n",
+                        "\t\t3 |   bucket = &quot;tf-test-bucket-destination-12345&quot;\n",
+                        "\t\t4 |   acl = var.acl\n",
+                        "\t\t5 |   versioning {\n",
+                        "\t\t6 |     enabled = var.is_enabled\n",
+                        "\t\t7 |   }\n",
+                        "\t\t8 | }</failure>\n",
+                        "\t\t</testcase>\n",
+                        '\t\t<testcase name="[NONE][CKV_AWS_19] Ensure all data stored in the S3 bucket is securely encrypted at rest" classname="/main.tf.aws_s3_bucket.destination" file="/main.tf">\n',
+                        '\t\t\t<skipped type="skipped" message=" no encryption needed"/>\n',
+                        "\t\t</testcase>\n",
+                        "\t</testsuite>\n",
+                        "</testsuites>\n",
+                    ]
+                )
+            ).toprettyxml()
+        )
 
 
-if __name__ == '__main__':
+if __name__ == "__main__":
     unittest.main()
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
@@ -41,7 +41,6 @@ def test_valid_passing_valid_testcases(self):
         r = Report("terraform")
         r.add_record(record=record1)
         r.add_record(record=record2)
-        r.get_test_suites()
         json_structure = r.get_sarif_json("")
         print(json.dumps(json_structure))
         self.assertEqual(
@@ -186,7 +185,6 @@ def test_multiple_instances_of_same_rule_do_not_break_schema(self):
         r.add_record(record=record7)
         r.add_record(record=record8)
         r.add_record(record=record9)
-        r.get_test_suites()
         json_structure = r.get_sarif_json("")
         print(json.dumps(json_structure))
         self.assertEqual(
diff --git a/tests/sca_package/test_output_reports.py b/tests/sca_package/test_output_reports.py
--- a/tests/sca_package/test_output_reports.py
+++ b/tests/sca_package/test_output_reports.py
@@ -299,55 +299,122 @@ def test_get_junit_xml_string(mocker: MockerFixture, scan_result):
     report = Runner().run(root_folder=EXAMPLES_DIR, runner_filter=runner_filter)
 
     # when
-    test_suites = report.get_test_suites()
+    test_suites = [report.get_test_suite()]
     junit_xml_output = report.get_junit_xml_string(test_suites)
 
     # then
-    assert xml.dom.minidom.parseString(junit_xml_output).toprettyxml() == xml.dom.minidom.parseString("".join(
-        [
-            '<?xml version="1.0" ?>\n',
-            '<testsuites disabled="0" errors="0" failures="7" tests="8" time="0.0">\n',
-            '\t<testsuite disabled="0" errors="0" failures="1" name="CKV_CVE_2019_19844/SCA package scan" skipped="0" tests="1" time="0" package="mock.mock.MagicMock">\n',
-            '\t\t<testcase name="sca_package CKV_CVE_2019_19844/SCA package scan path/to/requirements.txt.django" classname="mock.mock.MagicMock" file="/path/to/requirements.txt">\n',
-            '\t\t\t<failure type="failure" message="Resource path/to/requirements.txt.django failed in check CKV_CVE_2019_19844/SCA package scan - /path/to/requirements.txt:[0, 0] - Guideline: None"/>\n',
-            "\t\t</testcase>\n",
-            "\t</testsuite>\n",
-            '\t<testsuite disabled="0" errors="0" failures="1" name="CKV_CVE_2016_6186/SCA package scan" skipped="0" tests="1" time="0" package="mock.mock.MagicMock">\n',
-            '\t\t<testcase name="sca_package CKV_CVE_2016_6186/SCA package scan path/to/requirements.txt.django" classname="mock.mock.MagicMock" file="/path/to/requirements.txt">\n',
-            '\t\t\t<failure type="failure" message="Resource path/to/requirements.txt.django failed in check CKV_CVE_2016_6186/SCA package scan - /path/to/requirements.txt:[0, 0] - Guideline: None"/>\n',
-            "\t\t</testcase>\n",
-            "\t</testsuite>\n",
-            '\t<testsuite disabled="0" errors="0" failures="1" name="CKV_CVE_2016_7401/SCA package scan" skipped="0" tests="1" time="0" package="mock.mock.MagicMock">\n',
-            '\t\t<testcase name="sca_package CKV_CVE_2016_7401/SCA package scan path/to/requirements.txt.django" classname="mock.mock.MagicMock" file="/path/to/requirements.txt">\n',
-            '\t\t\t<failure type="failure" message="Resource path/to/requirements.txt.django failed in check CKV_CVE_2016_7401/SCA package scan - /path/to/requirements.txt:[0, 0] - Guideline: None"/>\n',
-            "\t\t</testcase>\n",
-            "\t</testsuite>\n",
-            '\t<testsuite disabled="0" errors="0" failures="1" name="CKV_CVE_2021_33203/SCA package scan" skipped="0" tests="1" time="0" package="mock.mock.MagicMock">\n',
-            '\t\t<testcase name="sca_package CKV_CVE_2021_33203/SCA package scan path/to/requirements.txt.django" classname="mock.mock.MagicMock" file="/path/to/requirements.txt">\n',
-            '\t\t\t<failure type="failure" message="Resource path/to/requirements.txt.django failed in check CKV_CVE_2021_33203/SCA package scan - /path/to/requirements.txt:[0, 0] - Guideline: None"/>\n',
-            "\t\t</testcase>\n",
-            "\t</testsuite>\n",
-            '\t<testsuite disabled="0" errors="0" failures="1" name="CKV_CVE_2019_1010083/SCA package scan" skipped="0" tests="1" time="0" package="mock.mock.MagicMock">\n',
-            '\t\t<testcase name="sca_package CKV_CVE_2019_1010083/SCA package scan path/to/requirements.txt.flask" classname="mock.mock.MagicMock" file="/path/to/requirements.txt">\n',
-            '\t\t\t<failure type="failure" message="Resource path/to/requirements.txt.flask failed in check CKV_CVE_2019_1010083/SCA package scan - /path/to/requirements.txt:[0, 0] - Guideline: None"/>\n',
-            "\t\t</testcase>\n",
-            "\t</testsuite>\n",
-            '\t<testsuite disabled="0" errors="0" failures="1" name="CKV_CVE_2018_1000656/SCA package scan" skipped="0" tests="1" time="0" package="mock.mock.MagicMock">\n',
-            '\t\t<testcase name="sca_package CKV_CVE_2018_1000656/SCA package scan path/to/requirements.txt.flask" classname="mock.mock.MagicMock" file="/path/to/requirements.txt">\n',
-            '\t\t\t<failure type="failure" message="Resource path/to/requirements.txt.flask failed in check CKV_CVE_2018_1000656/SCA package scan - /path/to/requirements.txt:[0, 0] - Guideline: None"/>\n',
-            "\t\t</testcase>\n",
-            "\t</testsuite>\n",
-            '\t<testsuite disabled="0" errors="0" failures="1" name="CKV_CVE_2020_26160/SCA package scan" skipped="0" tests="1" time="0" package="mock.mock.MagicMock">\n',
-            '\t\t<testcase name="sca_package CKV_CVE_2020_26160/SCA package scan path/to/go.sum.github.com/dgrijalva/jwt-go" classname="mock.mock.MagicMock" file="/path/to/go.sum">\n',
-            '\t\t\t<failure type="failure" message="Resource path/to/go.sum.github.com/dgrijalva/jwt-go failed in check CKV_CVE_2020_26160/SCA package scan - /path/to/go.sum:[0, 0] - Guideline: None"/>\n',
-            "\t\t</testcase>\n",
-            "\t</testsuite>\n",
-            '\t<testsuite disabled="0" errors="0" failures="0" name="CKV_CVE_2020_29652/SCA package scan" skipped="1" tests="1" time="0" package="mock.mock.MagicMock">\n',
-            '\t\t<testcase name="sca_package CKV_CVE_2020_29652/SCA package scan path/to/go.sum.golang.org/x/crypto" classname="mock.mock.MagicMock" file="/path/to/go.sum">\n',
-            '\t\t\t<skipped type="skipped" message="Resource path/to/go.sum.golang.org/x/crypto skipped in check CKV_CVE_2020_29652/SCA package scan \n',
-            ' Suppress comment: CVE-2020-29652 is skipped - Guideline: None"/>\n',
-            "\t\t</testcase>\n",
-            "\t</testsuite>\n",
-            "</testsuites>\n",
-        ]
-    )).toprettyxml()
+    assert (
+        xml.dom.minidom.parseString(junit_xml_output).toprettyxml()
+        == xml.dom.minidom.parseString(
+            "".join(
+                [
+                    '<?xml version="1.0" ?>\n',
+                    '<testsuites disabled="0" errors="0" failures="7" tests="8" time="0.0">\n',
+                    '\t<testsuite disabled="0" errors="0" failures="7" name="sca_package scan" skipped="1" tests="8" time="0">\n',
+                    '\t\t<testcase name="[CRITICAL][CVE-2019-19844] django: 1.2" classname="/path/to/requirements.txt.django" file="/path/to/requirements.txt">\n',
+                    '\t\t\t<failure type="failure" message="SCA package scan">\n',
+                    "Description: Django before 1.11.27, 2.x before 2.2.9, and 3.x before 3.0.1 allows account takeover. A suitably crafted email address (that is equal to an existing user\\'s email address after case transformation of Unicode characters) would allow an attacker to be sent a password reset token for the matched user account. (One mitigation in the new releases is to send password reset tokens only to the registered user email address.)\n",
+                    "Link: https://nvd.nist.gov/vuln/detail/CVE-2019-19844\n",
+                    "Published Date: 2019-12-18T20:15:00+01:00\n",
+                    "Base Score: 9.8\n",
+                    "Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\n",
+                    "Risk Factors: ['Attack complexity: low', 'Attack vector: network', 'Critical severity', 'Has fix']\n",
+                    "\n",
+                    "Resource: path/to/requirements.txt.django\n",
+                    "File: /path/to/requirements.txt: 0-0\n",
+                    "\n",
+                    "\t\t0 | django: 1.2</failure>\n",
+                    "\t\t</testcase>\n",
+                    '\t\t<testcase name="[MEDIUM][CVE-2016-6186] django: 1.2" classname="/path/to/requirements.txt.django" file="/path/to/requirements.txt">\n',
+                    '\t\t\t<failure type="failure" message="SCA package scan">\n',
+                    "Description: Cross-site scripting (XSS) vulnerability in the dismissChangeRelatedObjectPopup function in contrib/admin/static/admin/js/admin/RelatedObjectLookups.js in Django before 1.8.14, 1.9.x before 1.9.8, and 1.10.x before 1.10rc1 allows remote attackers to inject arbitrary web script or HTML via vectors involving unsafe usage of Element.innerHTML.\n",
+                    "Link: https://nvd.nist.gov/vuln/detail/CVE-2016-6186\n",
+                    "Published Date: 2016-08-05T17:59:00+02:00\n",
+                    "Base Score: 6.1\n",
+                    "Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N\n",
+                    "Risk Factors: ['Attack complexity: low', 'Attack vector: network', 'Exploit exists', 'Has fix', 'Medium severity']\n",
+                    "\n",
+                    "Resource: path/to/requirements.txt.django\n",
+                    "File: /path/to/requirements.txt: 0-0\n",
+                    "\n",
+                    "\t\t0 | django: 1.2</failure>\n",
+                    "\t\t</testcase>\n",
+                    '\t\t<testcase name="[HIGH][CVE-2016-7401] django: 1.2" classname="/path/to/requirements.txt.django" file="/path/to/requirements.txt">\n',
+                    '\t\t\t<failure type="failure" message="SCA package scan">\n',
+                    "Description: The cookie parsing code in Django before 1.8.15 and 1.9.x before 1.9.10, when used on a site with Google Analytics, allows remote attackers to bypass an intended CSRF protection mechanism by setting arbitrary cookies.\n",
+                    "Link: https://nvd.nist.gov/vuln/detail/CVE-2016-7401\n",
+                    "Published Date: 2016-10-03T20:59:00+02:00\n",
+                    "Base Score: 7.5\n",
+                    "Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N\n",
+                    "Risk Factors: ['High severity', 'Attack complexity: low', 'Attack vector: network', 'Has fix']\n",
+                    "\n",
+                    "Resource: path/to/requirements.txt.django\n",
+                    "File: /path/to/requirements.txt: 0-0\n",
+                    "\n",
+                    "\t\t0 | django: 1.2</failure>\n",
+                    "\t\t</testcase>\n",
+                    '\t\t<testcase name="[MEDIUM][CVE-2021-33203] django: 1.2" classname="/path/to/requirements.txt.django" file="/path/to/requirements.txt">\n',
+                    '\t\t\t<failure type="failure" message="SCA package scan">\n',
+                    "Description: Django before 2.2.24, 3.x before 3.1.12, and 3.2.x before 3.2.4 has a potential directory traversal via django.contrib.admindocs. Staff members could use the TemplateDetailView view to check the existence of arbitrary files. Additionally, if (and only if) the default admindocs templates have been customized by application developers to also show file contents, then not only the existence but also the file contents would have been exposed. In other words, there is directory traversal outside of the template root directories.\n",
+                    "Link: https://nvd.nist.gov/vuln/detail/CVE-2021-33203\n",
+                    "Published Date: 2021-06-08T20:15:00+02:00\n",
+                    "Base Score: 4.9\n",
+                    "Vector: CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:U/C:H/I:N/A:N\n",
+                    "Risk Factors: ['Attack complexity: low', 'Attack vector: network', 'Has fix', 'Medium severity', 'Recent vulnerability']\n",
+                    "\n",
+                    "Resource: path/to/requirements.txt.django\n",
+                    "File: /path/to/requirements.txt: 0-0\n",
+                    "\n",
+                    "\t\t0 | django: 1.2</failure>\n",
+                    "\t\t</testcase>\n",
+                    '\t\t<testcase name="[HIGH][CVE-2019-1010083] flask: 0.6" classname="/path/to/requirements.txt.flask" file="/path/to/requirements.txt">\n',
+                    '\t\t\t<failure type="failure" message="SCA package scan">\n',
+                    "Description: The Pallets Project Flask before 1.0 is affected by: unexpected memory usage. The impact is: denial of service. The attack vector is: crafted encoded JSON data. The fixed version is: 1. NOTE: this may overlap CVE-2018-1000656.\n",
+                    "Link: https://nvd.nist.gov/vuln/detail/CVE-2019-1010083\n",
+                    "Published Date: 2019-07-17T16:15:00+02:00\n",
+                    "Base Score: 7.5\n",
+                    "Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n",
+                    "Risk Factors: ['Attack complexity: low', 'Attack vector: network', 'DoS', 'Has fix', 'High severity']\n",
+                    "\n",
+                    "Resource: path/to/requirements.txt.flask\n",
+                    "File: /path/to/requirements.txt: 0-0\n",
+                    "\n",
+                    "\t\t0 | flask: 0.6</failure>\n",
+                    "\t\t</testcase>\n",
+                    '\t\t<testcase name="[HIGH][CVE-2018-1000656] flask: 0.6" classname="/path/to/requirements.txt.flask" file="/path/to/requirements.txt">\n',
+                    '\t\t\t<failure type="failure" message="SCA package scan">\n',
+                    "Description: The Pallets Project flask version Before 0.12.3 contains a CWE-20: Improper Input Validation vulnerability in flask that can result in Large amount of memory usage possibly leading to denial of service. This attack appear to be exploitable via Attacker provides JSON data in incorrect encoding. This vulnerability appears to have been fixed in 0.12.3. NOTE: this may overlap CVE-2019-1010083.\n",
+                    "Link: https://nvd.nist.gov/vuln/detail/CVE-2018-1000656\n",
+                    "Published Date: 2018-08-20T21:31:00+02:00\n",
+                    "Base Score: 7.5\n",
+                    "Vector: CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H\n",
+                    "Risk Factors: ['Attack complexity: low', 'Attack vector: network', 'DoS', 'Has fix', 'High severity']\n",
+                    "\n",
+                    "Resource: path/to/requirements.txt.flask\n",
+                    "File: /path/to/requirements.txt: 0-0\n",
+                    "\n",
+                    "\t\t0 | flask: 0.6</failure>\n",
+                    "\t\t</testcase>\n",
+                    '\t\t<testcase name="[HIGH][CVE-2020-26160] github.com/dgrijalva/jwt-go: v3.2.0" classname="/path/to/go.sum.github.com/dgrijalva/jwt-go" file="/path/to/go.sum">\n',
+                    '\t\t\t<failure type="failure" message="SCA package scan">\n',
+                    "Description: jwt-go before 4.0.0-preview1 allows attackers to bypass intended access restrictions in situations with []string{} for m[\\&quot;aud\\&quot;] (which is allowed by the specification). Because the type assertion fails, \\&quot;\\&quot; is the value of aud. This is a security problem if the JWT token is presented to a service that lacks its own audience check.\n",
+                    "Link: https://nvd.nist.gov/vuln/detail/CVE-2020-26160\n",
+                    "Published Date: 2020-09-30T20:15:00+02:00\n",
+                    "Base Score: 7.7\n",
+                    "Vector: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N\n",
+                    "Risk Factors: ['High severity', 'Attack complexity: low', 'Attack vector: network', 'Has fix']\n",
+                    "\n",
+                    "Resource: path/to/go.sum.github.com/dgrijalva/jwt-go\n",
+                    "File: /path/to/go.sum: 0-0\n",
+                    "\n",
+                    "\t\t0 | github.com/dgrijalva/jwt-go: v3.2.0</failure>\n",
+                    "\t\t</testcase>\n",
+                    '\t\t<testcase name="[HIGH][CVE-2020-29652] golang.org/x/crypto: v0.0.0-20200622213623-75b288015ac9" classname="/path/to/go.sum.golang.org/x/crypto" file="/path/to/go.sum">\n',
+                    '\t\t\t<skipped type="skipped" message="CVE-2020-29652 skipped for golang.org/x/crypto: v0.0.0-20200622213623-75b288015ac9"/>\n',
+                    "\t\t</testcase>\n",
+                    "\t</testsuite>\n",
+                    "</testsuites>\n",
+                ]
+            )
+        ).toprettyxml()
+    )
diff --git a/tests/terraform/runner/test_plan_runner.py b/tests/terraform/runner/test_plan_runner.py
--- a/tests/terraform/runner/test_plan_runner.py
+++ b/tests/terraform/runner/test_plan_runner.py
@@ -17,12 +17,12 @@ def test_runner_two_checks_only(self):
             root_folder=None,
             files=[valid_plan_path],
             external_checks_dir=None,
-            runner_filter=RunnerFilter(framework="all", checks=checks_allowlist),
+            runner_filter=RunnerFilter(framework=["all"], checks=checks_allowlist),
         )
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
@@ -39,12 +39,12 @@ def test_runner_child_modules(self):
             root_folder=None,
             files=[valid_plan_path],
             external_checks_dir=None,
-            runner_filter=RunnerFilter(framework="all"),
+            runner_filter=RunnerFilter(framework=["all"]),
         )
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
@@ -60,12 +60,12 @@ def test_runner_nested_child_modules(self):
             root_folder=None,
             files=[valid_plan_path],
             external_checks_dir=[current_dir + "/extra_yaml_checks"],
-            runner_filter=RunnerFilter(framework="all"),
+            runner_filter=RunnerFilter(framework=["all"]),
         )
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
@@ -92,12 +92,12 @@ def test_runner_root_module_resources_no_values(self):
             root_folder=None,
             files=[valid_plan_path],
             external_checks_dir=None,
-            runner_filter=RunnerFilter(framework="all"),
+            runner_filter=RunnerFilter(framework=["all"]),
         )
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
@@ -136,12 +136,12 @@ def test_runner_data_resource_partial_values(self):
             root_folder=None,
             files=[valid_plan_path],
             external_checks_dir=None,
-            runner_filter=RunnerFilter(framework="all"),
+            runner_filter=RunnerFilter(framework=["all"]),
         )
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
@@ -168,7 +168,7 @@ def test_runner_root_dir(self):
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
@@ -194,7 +194,7 @@ def test_record_relative_path_with_relative_dir(self):
         report = runner.run(
             root_folder=dir_rel_path,
             external_checks_dir=None,
-            runner_filter=RunnerFilter(framework="terraform", checks=checks_allowlist),
+            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist),
         )
 
         all_checks = report.failed_checks + report.passed_checks
@@ -217,7 +217,7 @@ def test_record_relative_path_with_abs_dir(self):
         report = runner.run(
             root_folder=dir_abs_path,
             external_checks_dir=None,
-            runner_filter=RunnerFilter(framework="terraform", checks=checks_allowlist),
+            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist),
         )
 
         all_checks = report.failed_checks + report.passed_checks
@@ -242,7 +242,7 @@ def test_record_relative_path_with_relative_file(self):
             root_folder=None,
             external_checks_dir=None,
             files=[file_rel_path],
-            runner_filter=RunnerFilter(framework="terraform", checks=checks_allowlist),
+            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist),
         )
 
         all_checks = report.failed_checks + report.passed_checks
@@ -266,7 +266,7 @@ def test_record_relative_path_with_abs_file(self):
             root_folder=None,
             external_checks_dir=None,
             files=[file_abs_path],
-            runner_filter=RunnerFilter(framework="terraform", checks=checks_allowlist),
+            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist),
         )
 
         all_checks = report.failed_checks + report.passed_checks
@@ -282,12 +282,12 @@ def test_runner_unexpected_eks_node_group_remote_access(self):
             root_folder=None,
             files=[valid_plan_path],
             external_checks_dir=None,
-            runner_filter=RunnerFilter(framework="all"),
+            runner_filter=RunnerFilter(framework=["all"]),
         )
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 0)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
@@ -303,13 +303,13 @@ def test_runner_with_resource_reference(self):
             root_folder=None,
             files=[valid_plan_path],
             external_checks_dir=None,
-            runner_filter=RunnerFilter(framework="all", checks=allowed_checks),
+            runner_filter=RunnerFilter(framework=["all"], checks=allowed_checks),
         )
 
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 0)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
diff --git a/tests/terraform/runner/test_runner.py b/tests/terraform/runner/test_runner.py
--- a/tests/terraform/runner/test_runner.py
+++ b/tests/terraform/runner/test_runner.py
@@ -26,11 +26,11 @@ def test_runner_two_checks_only(self):
         runner = Runner()
         checks_allowlist = ['CKV_AWS_41', 'CKV_AZURE_1']
         report = runner.run(root_folder=valid_dir_path, external_checks_dir=None,
-                            runner_filter=RunnerFilter(framework='all', checks=checks_allowlist))
+                            runner_filter=RunnerFilter(framework=["all"], checks=checks_allowlist))
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         for record in report.failed_checks:
             self.assertIn(record.check_id, checks_allowlist)
 
@@ -40,11 +40,11 @@ def test_runner_denylist_checks(self):
         runner = Runner()
         checks_denylist = ['CKV_AWS_41', 'CKV_AZURE_1']
         report = runner.run(root_folder=valid_dir_path, external_checks_dir=None,
-                            runner_filter=RunnerFilter(framework='all', skip_checks=checks_denylist))
+                            runner_filter=RunnerFilter(framework=["all"], skip_checks=checks_denylist))
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
         for record in report.failed_checks:
@@ -58,7 +58,7 @@ def test_runner_valid_tf(self):
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
         summary = report.get_summary()
@@ -69,7 +69,6 @@ def test_runner_valid_tf(self):
         report.print_console()
         report.print_console(is_quiet=True)
         report.print_console(is_quiet=True, is_compact=True)
-        report.print_junit_xml()
         report.print_failed_github_md()
 
     def test_runner_passing_valid_tf(self):
@@ -83,7 +82,7 @@ def test_runner_passing_valid_tf(self):
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(False), 1)
         summary = report.get_summary()
         self.assertGreaterEqual(summary['passed'], 1)
@@ -118,7 +117,7 @@ def test_runner_extra_check(self):
                 resource_registry.checks["aws_s3_bucket"].remove(check)
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
 
         passing_custom = 0
         failed_custom = 0
@@ -148,7 +147,7 @@ def test_runner_extra_yaml_check(self):
                 resource_registry.checks["aws_s3_bucket"].remove(check)
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
 
         passing_custom = 0
         failed_custom = 0
@@ -174,7 +173,7 @@ def test_runner_specific_file(self):
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         # self.assertEqual(report.get_exit_code(), 0)
         summary = report.get_summary()
         self.assertGreaterEqual(summary['passed'], 1)
@@ -818,11 +817,11 @@ def test_failure_in_resolved_module(self):
         runner = Runner()
         checks_allowlist = ['CKV_AWS_20']
         report = runner.run(root_folder=valid_dir_path, external_checks_dir=None,
-                            runner_filter=RunnerFilter(framework='terraform', checks=checks_allowlist))
+                            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist))
         report_json = report.get_json()
         self.assertIsInstance(report_json, str)
         self.assertIsNotNone(report_json)
-        self.assertIsNotNone(report.get_test_suites())
+        self.assertIsNotNone(report.get_test_suite())
         self.assertEqual(report.get_exit_code(soft_fail=False), 1)
         self.assertEqual(report.get_exit_code(soft_fail=True), 0)
 
@@ -847,7 +846,7 @@ def test_record_relative_path_with_relative_dir(self):
         runner = Runner()
         checks_allowlist = ['CKV_AWS_20']
         report = runner.run(root_folder=dir_rel_path, external_checks_dir=None,
-                            runner_filter=RunnerFilter(framework='terraform', checks=checks_allowlist))
+                            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist))
 
         all_checks = report.failed_checks + report.passed_checks
 
@@ -871,7 +870,7 @@ def test_record_relative_path_with_abs_dir(self):
         runner = Runner()
         checks_allowlist = ['CKV_AWS_20']
         report = runner.run(root_folder=dir_abs_path, external_checks_dir=None,
-                            runner_filter=RunnerFilter(framework='terraform', checks=checks_allowlist))
+                            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist))
 
         all_checks = report.failed_checks + report.passed_checks
 
@@ -895,7 +894,7 @@ def test_record_relative_path_with_relative_file(self):
         runner = Runner()
         checks_allowlist = ['CKV_AWS_20']
         report = runner.run(root_folder=None, external_checks_dir=None, files=[file_rel_path],
-                            runner_filter=RunnerFilter(framework='terraform', checks=checks_allowlist))
+                            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist))
 
         all_checks = report.failed_checks + report.passed_checks
 
@@ -919,7 +918,7 @@ def test_record_relative_path_with_abs_file(self):
         runner = Runner()
         checks_allowlist = ['CKV_AWS_20']
         report = runner.run(root_folder=None, external_checks_dir=None, files=[file_abs_path],
-                            runner_filter=RunnerFilter(framework='terraform', checks=checks_allowlist))
+                            runner_filter=RunnerFilter(framework=["terraform"], checks=checks_allowlist))
 
         all_checks = report.failed_checks + report.passed_checks
 
@@ -1133,7 +1132,7 @@ def test_resource_values_dont_exist(self):
         runner = Runner()
         report = runner.run(root_folder=None, external_checks_dir=None,
                             files=list(map(lambda f: f'{resources_path}/{f}', source_files)),
-                            runner_filter=RunnerFilter(framework='terraform',
+                            runner_filter=RunnerFilter(framework=["terraform"],
                                                        checks=checks_allow_list, skip_checks=skip_checks))
 
         self.assertEqual(len(report.passed_checks), 7)
@@ -1149,7 +1148,7 @@ def test_resource_values_do_exist(self):
         runner = Runner()
         report = runner.run(root_folder=None, external_checks_dir=None,
                             files=list(map(lambda f: f'{resources_path}/{f}', source_files)),
-                            runner_filter=RunnerFilter(framework='terraform',
+                            runner_filter=RunnerFilter(framework=["terraform"],
                                                        checks=checks_allow_list, skip_checks=skip_checks))
 
         self.assertEqual(len(report.passed_checks), 5)
@@ -1181,7 +1180,7 @@ def test_resource_negative_values_do_exist(self):
         runner = Runner()
         report = runner.run(root_folder=None, external_checks_dir=None,
                             files=list(map(lambda f: f'{resources_path}/{f}', source_files)),
-                            runner_filter=RunnerFilter(framework='terraform',
+                            runner_filter=RunnerFilter(framework=["terraform"],
                                                        checks=checks_allow_list, skip_checks=skip_checks))
 
         self.assertEqual(len(report.passed_checks), 3)
@@ -1192,7 +1191,7 @@ def test_no_duplicate_results(self):
             os.path.dirname(os.path.realpath(__file__)), "resources", "duplicate_violations")
         runner = Runner()
         report = runner.run(root_folder=resources_path, external_checks_dir=None,
-                            runner_filter=RunnerFilter(framework='terraform'))
+                            runner_filter=RunnerFilter(framework=["terraform"]))
 
         unique_checks = []
         for record in report.passed_checks:
@@ -1252,14 +1251,14 @@ def test_runner_exclude_file(self):
         current_dir = os.path.dirname(os.path.realpath(__file__))
         path_to_scan = os.path.join(current_dir, 'resources', 'nested_dir', 'dir1')
         runner = Runner()
-        report = runner.run(root_folder=path_to_scan, external_checks_dir=None, runner_filter=RunnerFilter(framework='terraform', excluded_paths=['example.tf']))
+        report = runner.run(root_folder=path_to_scan, external_checks_dir=None, runner_filter=RunnerFilter(framework=["terraform"], excluded_paths=['example.tf']))
         self.assertEqual(0, len(report.resources))
 
     def test_runner_exclude_dir(self):
         current_dir = os.path.dirname(os.path.realpath(__file__))
         path_to_scan = os.path.join(current_dir, 'resources', 'nested_dir')
         runner = Runner()
-        report = runner.run(root_folder=path_to_scan, external_checks_dir=None, runner_filter=RunnerFilter(framework='terraform', excluded_paths=['dir1']))
+        report = runner.run(root_folder=path_to_scan, external_checks_dir=None, runner_filter=RunnerFilter(framework=["terraform"], excluded_paths=['dir1']))
         self.assertEqual(1, len(report.resources))
 
     def test_runner_merge_operator(self):