diff --git a/example_remote_config_local/engine_core/ConfigTool.json b/example_remote_config_local/engine_core/ConfigTool.json index c8783beb..6501bd68 100644 --- a/example_remote_config_local/engine_core/ConfigTool.json +++ b/example_remote_config_local/engine_core/ConfigTool.json @@ -48,5 +48,8 @@ "ENGINE_CODE": { "ENABLED": "true", "TOOL": "BEARER" + }, + "REPORT_SONAR": { + "ENABLED": "true" } } \ No newline at end of file diff --git a/example_remote_config_local/report_sonar/ConfigTool.json b/example_remote_config_local/report_sonar/ConfigTool.json new file mode 100644 index 00000000..4aa5dcab --- /dev/null +++ b/example_remote_config_local/report_sonar/ConfigTool.json @@ -0,0 +1,11 @@ +{ + "PIPELINE_COMPONENTS": { + "EXAMPLE_MULTICOMPONENT_PIPELINE": [ + "component1", + "component2", + "component3", + "component4", + "component5" + ] + } +} diff --git a/tools/devsecops_engine_tools/engine_core/src/domain/model/gateway/vulnerability_management_gateway.py b/tools/devsecops_engine_tools/engine_core/src/domain/model/gateway/vulnerability_management_gateway.py index 98813d9a..e83e0925 100644 --- a/tools/devsecops_engine_tools/engine_core/src/domain/model/gateway/vulnerability_management_gateway.py +++ b/tools/devsecops_engine_tools/engine_core/src/domain/model/gateway/vulnerability_management_gateway.py @@ -1,7 +1,7 @@ from abc import ABCMeta, abstractmethod from devsecops_engine_tools.engine_core.src.domain.model.vulnerability_management import VulnerabilityManagement - +from devsecops_engine_tools.engine_core.src.domain.model.gateway.devops_platform_gateway import DevopsPlatformGateway class VulnerabilityManagementGateway(metaclass=ABCMeta): @abstractmethod diff --git a/tools/devsecops_engine_tools/engine_core/src/domain/model/report.py b/tools/devsecops_engine_tools/engine_core/src/domain/model/report.py index 872a60e0..e07d2b8b 100644 --- a/tools/devsecops_engine_tools/engine_core/src/domain/model/report.py +++ b/tools/devsecops_engine_tools/engine_core/src/domain/model/report.py @@ -31,4 +31,6 @@ def __init__(self, **kwargs): self.component_name = kwargs.get("component_name", "") self.component_version = kwargs.get("component_version", "") self.file_path = kwargs.get("file_path", "") - self.endpoints = kwargs.get("endpoints", "") \ No newline at end of file + self.endpoints = kwargs.get("endpoints", "") + self.unique_id_from_tool = kwargs.get("unique_id_from_tool", "") + self.out_of_scope = kwargs.get("out_of_scope", "") \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/defect_dojo/defect_dojo.py b/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/defect_dojo/defect_dojo.py index 9754fc99..86b7ec15 100644 --- a/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/defect_dojo/defect_dojo.py +++ b/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/defect_dojo/defect_dojo.py @@ -5,6 +5,9 @@ from devsecops_engine_tools.engine_core.src.domain.model.vulnerability_management import ( VulnerabilityManagement, ) +from devsecops_engine_tools.engine_core.src.domain.model.gateway.devops_platform_gateway import ( + DevopsPlatformGateway +) from devsecops_engine_tools.engine_utilities.defect_dojo import ( DefectDojo, ImportScanRequest, @@ -67,7 +70,8 @@ def send_vulnerability_management( "KUBESCAPE": "Kubescape Scanner", "KICS": "KICS Scanner", "BEARER": "Bearer CLI", - "DEPENDENCY_CHECK": "Dependency Check Scan" + "DEPENDENCY_CHECK": "Dependency Check Scan", + "SONARQUBE": "SonarQube API Import" } if any( @@ -426,6 +430,8 @@ def _create_report(self, finding): risk_accepted=finding.risk_accepted, false_p=finding.false_p, service=finding.service, + unique_id_from_tool=finding.unique_id_from_tool, + out_of_scope=finding.out_of_scope ) def _format_date_to_dd_format(self, date_string): diff --git a/tools/devsecops_engine_tools/engine_core/test/infrastructure/driven_adapters/defect_dojo/test_defect_dojo.py b/tools/devsecops_engine_tools/engine_core/test/infrastructure/driven_adapters/defect_dojo/test_defect_dojo.py index 3d22c78b..48ef094c 100644 --- a/tools/devsecops_engine_tools/engine_core/test/infrastructure/driven_adapters/defect_dojo/test_defect_dojo.py +++ b/tools/devsecops_engine_tools/engine_core/test/infrastructure/driven_adapters/defect_dojo/test_defect_dojo.py @@ -8,7 +8,9 @@ from devsecops_engine_tools.engine_core.src.domain.model.vulnerability_management import ( VulnerabilityManagement, ) - +from devsecops_engine_tools.engine_utilities.defect_dojo.domain.request_objects.import_scan import ( + ImportScanRequest +) class TestDefectDojoPlatform(unittest.TestCase): def setUp(self): diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/applications/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/applications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/applications/runner_report_sonar.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/applications/runner_report_sonar.py new file mode 100644 index 00000000..181783fa --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/applications/runner_report_sonar.py @@ -0,0 +1,110 @@ +from devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.aws.secrets_manager import ( + SecretsManager +) +from devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.azure.azure_devops import ( + AzureDevops +) +from devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.defect_dojo.defect_dojo import ( + DefectDojoPlatform +) +from devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.driven_adapters.sonarqube.sonarqube_report import( + SonarAdapter +) +from devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.aws.s3_manager import ( + S3Manager, +) +from devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.entry_points.entry_point_report_sonar import ( + init_report_sonar +) +import sys +import argparse +from devsecops_engine_tools.engine_utilities.utils.logger_info import MyLogger +from devsecops_engine_tools.engine_utilities import settings + +logger = MyLogger.__call__(**settings.SETTING_LOGGER).get_logger() + +def get_inputs_from_cli(args): + parser = argparse.ArgumentParser() + parser.add_argument( + "-rcf", + "--remote_config_repo", + type=str, + required=True, + help="Name of Config Repo", + ) + parser.add_argument( + "--use_secrets_manager", + choices=["true", "false"], + type=str, + required=True, + help="Use Secrets Manager to get the tokens", + ) + parser.add_argument( + "--send_metrics", + choices=["true", "false"], + type=str, + required=False, + help="Enable or Disable the send metrics to the driven adapter metrics", + ) + parser.add_argument( + "--sonar_url", + required=False, + help="Url to access sonar API", + ) + parser.add_argument( + "--token_cmdb", + required=False, + help="Token to connect to the CMDB" + ) + parser.add_argument( + "--token_vulnerability_management", + required=False, + help="Token to connect to the Vulnerability Management", + ) + parser.add_argument( + "--token_sonar", + required=False, + help="Token to access sonar server", + ) + + args = parser.parse_args() + return { + "remote_config_repo": args.remote_config_repo, + "use_secrets_manager": args.use_secrets_manager, + "send_metrics": args.send_metrics, + "sonar_url": args.sonar_url, + "token_cmdb": args.token_cmdb, + "token_vulnerability_management": args.token_vulnerability_management, + "token_sonar": args.token_sonar, + } + +def runner_report_sonar(): + try: + vulnerability_management_gateway = DefectDojoPlatform() + secrets_manager_gateway = SecretsManager() + devops_platform_gateway = AzureDevops() + sonar_gateway = SonarAdapter() + metrics_manager_gateway = S3Manager() + args = get_inputs_from_cli(sys.argv[1:]) + + init_report_sonar( + vulnerability_management_gateway=vulnerability_management_gateway, + secrets_manager_gateway=secrets_manager_gateway, + devops_platform_gateway=devops_platform_gateway, + sonar_gateway=sonar_gateway, + metrics_manager_gateway=metrics_manager_gateway, + args=args, + ) + + except Exception as e: + logger.error("Error report_sonar: {0} ".format(str(e))) + print( + devops_platform_gateway.message( + "error", "Error report_sonar: {0} ".format(str(e)) + ) + ) + print(devops_platform_gateway.result_pipeline("failed")) + + +if __name__ == "__main__": + runner_report_sonar() \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/model/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/model/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/model/gateways/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/model/gateways/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/model/gateways/sonar_gateway.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/model/gateways/sonar_gateway.py new file mode 100644 index 00000000..2470b72f --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/model/gateways/sonar_gateway.py @@ -0,0 +1,63 @@ +from abc import ( + ABCMeta, + abstractmethod +) + +class SonarGateway(metaclass=ABCMeta): + @abstractmethod + def get_project_keys( + self, + pipeline_name: str + ): + "get sonar project keys" + + @abstractmethod + def parse_project_key( + self, + file_path: str + ): + "find project key in metadata file" + + @abstractmethod + def create_task_report_from_string( + self, + file_content: str + ): + "make dict from metadata file" + + @abstractmethod + def filter_by_sonarqube_tag( + self, + findings: list + ): + "search for sonar findings" + + @abstractmethod + def change_finding_status( + self, + sonar_url: str, + sonar_token: str, + endpoint: str, + data: dict, + finding_type: str + ): + "use API to change vulnerabilities state in sonar" + + @abstractmethod + def get_findings( + self, + sonar_url: str, + sonar_token: str, + endpoint: str, + params: dict, + finding_type: str + ): + "use API to get project findings in sonar" + + @abstractmethod + def search_finding_by_id( + self, + findings: list, + finding_id: str + ): + "search a finding by id" \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/usecases/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/usecases/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/usecases/report_sonar.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/usecases/report_sonar.py new file mode 100644 index 00000000..e95e120b --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/domain/usecases/report_sonar.py @@ -0,0 +1,194 @@ +from devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.helpers.utils import ( + set_repository +) +from devsecops_engine_tools.engine_core.src.infrastructure.helpers.util import ( + define_env +) +from devsecops_engine_tools.engine_core.src.domain.model.gateway.vulnerability_management_gateway import ( + VulnerabilityManagementGateway +) +from devsecops_engine_tools.engine_core.src.domain.model.vulnerability_management import ( + VulnerabilityManagement +) +from devsecops_engine_tools.engine_core.src.domain.model.gateway.secrets_manager_gateway import ( + SecretsManagerGateway +) +from devsecops_engine_tools.engine_core.src.domain.model.gateway.devops_platform_gateway import ( + DevopsPlatformGateway +) +from devsecops_engine_tools.engine_utilities.sonarqube.src.domain.model.gateways.sonar_gateway import ( + SonarGateway +) +from devsecops_engine_tools.engine_core.src.domain.model.input_core import ( + InputCore +) +from devsecops_engine_tools.engine_utilities.utils.logger_info import MyLogger +from devsecops_engine_tools.engine_utilities import settings + +logger = MyLogger.__call__(**settings.SETTING_LOGGER).get_logger() + +class ReportSonar: + def __init__( + self, + vulnerability_management_gateway: VulnerabilityManagementGateway, + secrets_manager_gateway: SecretsManagerGateway, + devops_platform_gateway: DevopsPlatformGateway, + sonar_gateway: SonarGateway + ): + self.vulnerability_management_gateway = vulnerability_management_gateway + self.secrets_manager_gateway = secrets_manager_gateway + self.devops_platform_gateway = devops_platform_gateway + self.sonar_gateway = sonar_gateway + + def process(self, args): + pipeline_name = self.devops_platform_gateway.get_variable("pipeline_name") + branch = self.devops_platform_gateway.get_variable("branch_name") + input_core = InputCore( + [], + {}, + "", + "", + "", + self.devops_platform_gateway.get_variable("stage").capitalize(), + ) + + compact_remote_config_url = self.devops_platform_gateway.get_base_compact_remote_config_url(args["remote_config_repo"]) + source_code_management_uri = set_repository( + pipeline_name, + self.devops_platform_gateway.get_source_code_management_uri() + ) + config_tool = self.devops_platform_gateway.get_remote_config( + args["remote_config_repo"], + "/engine_core/ConfigTool.json" + ) + environment = define_env(None, branch) + + if args["use_secrets_manager"] == "true": + secret = self.secrets_manager_gateway.get_secret(config_tool) + else: + secret = args + + report_config_tool = self.devops_platform_gateway.get_remote_config( + args["remote_config_repo"], + "/report_sonar/ConfigTool.json" + ) + + get_components = report_config_tool["PIPELINE_COMPONENTS"].get(pipeline_name) + if get_components: + project_keys = [f"{pipeline_name}_{component}" for component in get_components] + print(f"Multiple project keys detected: {project_keys}") + logger.info(f"Multiple project keys detected: {project_keys}") + else: + project_keys = self.sonar_gateway.get_project_keys(pipeline_name) + + args["tool"] = "sonarqube" + vulnerability_manager = VulnerabilityManagement( + scan_type = "SONARQUBE", + input_core = input_core, + dict_args = args, + secret_tool = self.secrets_manager_gateway, + config_tool = config_tool, + source_code_management_uri = source_code_management_uri, + base_compact_remote_config_url = compact_remote_config_url, + access_token = self.devops_platform_gateway.get_variable("access_token"), + version = self.devops_platform_gateway.get_variable("build_execution_id"), + build_id = self.devops_platform_gateway.get_variable("build_id"), + branch_tag = branch, + commit_hash = self.devops_platform_gateway.get_variable("commit_hash"), + environment = environment + ) + + for project_key in project_keys: + try: + findings = self.vulnerability_management_gateway.get_all( + service=project_key, + dict_args=args, + secret_tool=self.secrets_manager_gateway, + config_tool=config_tool + )[0] + filtered_findings = self.sonar_gateway.filter_by_sonarqube_tag(findings) + + sonar_vulnerabilities = self.sonar_gateway.get_findings( + args["sonar_url"], + secret["token_sonar"], + "/api/issues/search", + { + "componentKeys": project_key, + "types": "VULNERABILITY", + "ps": 500, + "p": 1, + "s": "CREATION_DATE", + "asc": "false" + }, + "issues" + ) + sonar_hotspots = self.sonar_gateway.get_findings( + args["sonar_url"], + secret["token_sonar"], + "/api/hotspots/search", + { + "projectKey": project_key, + "ps": 100, + "p": 1, + }, + "hotspots" + ) + + sonar_findings = sonar_vulnerabilities + sonar_hotspots + + for finding in filtered_findings: + related_sonar_finding = self.sonar_gateway.search_finding_by_id( + sonar_findings, + finding.unique_id_from_tool + ) + status = None + if related_sonar_finding: + if related_sonar_finding.get("type") == "VULNERABILITY": + if finding.active and related_sonar_finding["status"] == "RESOLVED": status = "reopen" + elif related_sonar_finding["status"] != "RESOLVED": + if finding.false_p: status = "falsepositive" + elif finding.risk_accepted: status = "close" + elif finding.risk_accepted or finding.out_of_scope: status = "wontfix" + if status: + self.sonar_gateway.change_finding_status( + args["sonar_url"], + secret["token_sonar"], + "/api/issues/do_transition", + { + "issue": related_sonar_finding["key"], + "transition": status + }, + "issue" + ) + else: + resolution = None + if finding.active and related_sonar_finding["status"] == "REVIEWED": status = "TO_REVIEW" + elif related_sonar_finding["status"] == "TO_REVIEW": + if finding.false_p: resolution = "SAFE" + elif finding.risk_accepted or finding.out_of_scope: resolution = "ACKNOWLEDGED" + if resolution: status = "REVIEWED" + if status: + data = { + "hotspot": related_sonar_finding["key"], + "status": status, + "resolution": resolution + } + if not resolution: data.pop("resolution") + self.sonar_gateway.change_finding_status( + args["sonar_url"], + secret["token_sonar"], + "/api/hotspots/change_status", + data, + "hotspot" + ) + + except Exception as e: + logger.warning(f"It was not possible to synchronize Sonar and Vulnerability Manager: {e}") + + input_core.scope_pipeline = project_key + self.vulnerability_management_gateway.send_vulnerability_management( + vulnerability_management=vulnerability_manager + ) + + input_core.scope_pipeline = pipeline_name + return input_core \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/driven_adapters/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/driven_adapters/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/driven_adapters/sonarqube/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/driven_adapters/sonarqube/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/driven_adapters/sonarqube/sonarqube_report.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/driven_adapters/sonarqube/sonarqube_report.py new file mode 100644 index 00000000..1ccea587 --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/driven_adapters/sonarqube/sonarqube_report.py @@ -0,0 +1,112 @@ +from devsecops_engine_tools.engine_utilities.utils.utils import ( + Utils +) +from devsecops_engine_tools.engine_utilities.sonarqube.src.domain.model.gateways.sonar_gateway import ( + SonarGateway +) +import os +import re +import requests +from devsecops_engine_tools.engine_utilities.utils.logger_info import MyLogger +from devsecops_engine_tools.engine_utilities import settings + +logger = MyLogger.__call__(**settings.SETTING_LOGGER).get_logger() + +class SonarAdapter(SonarGateway): + def get_project_keys(self, pipeline_name): + project_keys = [pipeline_name] + sonar_scanner_params = os.getenv("SONARQUBE_SCANNER_PARAMS", "") + pattern = r'"sonar\.scanner\.metadataFilePath":"(.*?)"' + match_result = re.search(pattern, sonar_scanner_params) + + if match_result and match_result.group(1): + metadata_file_path = match_result.group(1) + project_key_found = self.parse_project_key(metadata_file_path) + + if project_key_found: + print(f"ProjectKey scanner params: {project_key_found}") + project_keys = [project_key_found] + + return project_keys + + def parse_project_key(self, file_path): + try: + with open(file_path, 'r', encoding='utf-8') as f: + file_content = f.read() + print(f"[SQ] Parse Task report file:\n{file_content}") + if not file_content or len(file_content) <= 0: + print("[SQ] Error reading file") + logger.warning("[SQ] Error reading file") + return None + try: + settings = self.create_task_report_from_string(file_content) + return settings.get("projectKey") + except Exception as err: + print(f"[SQ] Parse Task report error: {err}") + logger.warning(f"[SQ] Parse Task report error: {err}") + return None + except Exception as err: + logger.warning(f"[SQ] Error reading file: {str(err)}") + return None + + def create_task_report_from_string(self, file_content): + lines = file_content.replace('\r\n', '\n').split('\n') + settings = {} + for line in lines: + split_line = line.split('=') + if len(split_line) > 1: + settings[split_line[0]] = '='.join(split_line[1:]) + return settings + + def filter_by_sonarqube_tag(self, findings): + return [finding for finding in findings if "sonarqube" in finding.tags] + + def change_finding_status(self, sonar_url, sonar_token, endpoint, data, finding_type): + try: + response = requests.post( + f"{sonar_url}{endpoint}", + headers={ + "Authorization": f"Basic {Utils().encode_token_to_base64(sonar_token)}" + }, + data=data + ) + response.raise_for_status() + + if finding_type == "issue": + info = data["transition"] + else: + resolution_info = "" + if data.get("resolution"): resolution_info = f" ({data['resolution']})" + + info = f"{data['status']}{resolution_info}" + + print(f"The state of the {finding_type} {data[finding_type]} was changed to {info}.") + except Exception as e: + logger.warning(f"Unable to change the status of {finding_type} {data[finding_type]}. Error: {e}") + pass + + def get_findings(self, sonar_url, sonar_token, endpoint, params, finding_type): + findings = [] + try: + while True: + response = requests.get( + f"{sonar_url}{endpoint}", + headers={ + "Authorization": f"Basic {Utils().encode_token_to_base64(sonar_token)}" + }, + params=params + ) + response.raise_for_status() + data = response.json() + + findings.extend(data[finding_type]) + if len(data[finding_type]) < params["ps"]: break + params["p"] = params["p"] + 1 + + return findings + except Exception as e: + logger.warning(f"It was not possible to obtain the {finding_type}: {str(e)}") + return [] + + def search_finding_by_id(self, issues, issue_id): + return next((issue for issue in issues if issue["key"] in issue_id), None) \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/entry_points/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/entry_points/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/entry_points/entry_point_report_sonar.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/entry_points/entry_point_report_sonar.py new file mode 100644 index 00000000..cbe8e61f --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/entry_points/entry_point_report_sonar.py @@ -0,0 +1,37 @@ +from devsecops_engine_tools.engine_utilities.sonarqube.src.domain.usecases.report_sonar import ( + ReportSonar +) +from devsecops_engine_tools.engine_utilities.utils.printers import ( + Printers, +) +from devsecops_engine_tools.engine_core.src.domain.usecases.metrics_manager import ( + MetricsManager, +) +from devsecops_engine_tools.engine_utilities.utils.logger_info import MyLogger +from devsecops_engine_tools.engine_utilities import settings + +logger = MyLogger.__call__(**settings.SETTING_LOGGER).get_logger() + +def init_report_sonar(vulnerability_management_gateway, secrets_manager_gateway, devops_platform_gateway, sonar_gateway, metrics_manager_gateway, args): + config_tool = devops_platform_gateway.get_remote_config( + args["remote_config_repo"], "/engine_core/ConfigTool.json" + ) + Printers.print_logo_tool(config_tool["BANNER"]) + + if config_tool["REPORT_SONAR"]["ENABLED"] == "true": + input_core = ReportSonar( + vulnerability_management_gateway, + secrets_manager_gateway, + devops_platform_gateway, + sonar_gateway + ).process(args) + + if args["send_metrics"] == "true": + MetricsManager(devops_platform_gateway, metrics_manager_gateway).process( + config_tool, input_core, {"tool": "report_sonar"}, "" + ) + else: + print( + devops_platform_gateway.message( + "warning", "DevSecOps Engine Tool - {0} in maintenance...".format("report_sonar")), + ) \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/helpers/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/helpers/utils.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/helpers/utils.py new file mode 100644 index 00000000..796189ff --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/src/infrastructure/helpers/utils.py @@ -0,0 +1,8 @@ +import re + +def set_repository(pipeline_name, source_code_management): + if re.search('_MR_', pipeline_name) is None: + return source_code_management + else: + splittedPipeline = pipeline_name.split('_MR_') + return source_code_management + '?path=/' + splittedPipeline[1] diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/applications/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/applications/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/applications/test_runner_report_sonar.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/applications/test_runner_report_sonar.py new file mode 100644 index 00000000..67a41f9c --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/applications/test_runner_report_sonar.py @@ -0,0 +1,66 @@ +import unittest +from unittest.mock import patch +import sys +import argparse +from devsecops_engine_tools.engine_utilities.sonarqube.src.applications.runner_report_sonar import runner_report_sonar, get_inputs_from_cli + +class TestRunnerReportSonar(unittest.TestCase): + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.applications.runner_report_sonar.get_inputs_from_cli" + ) + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.applications.runner_report_sonar.init_report_sonar" + ) + def test_runner_report_sonar_success(self, mock_init_report_sonar, mock_get_inputs_from_cli): + # Act + runner_report_sonar() + + # Assert + mock_init_report_sonar.assert_called_once() + + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.applications.runner_report_sonar.get_inputs_from_cli" + ) + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.applications.runner_report_sonar.logger" + ) + def test_runner_report_sonar_exception(self, mock_logger, mock_get_inputs_from_cli): + # Arrange + mock_get_inputs_from_cli.side_effect = Exception("Test exception") + + # Act + runner_report_sonar() + + # Assert + mock_logger.error.assert_called_with("Error report_sonar: Test exception ") + + @patch( + "argparse.ArgumentParser.parse_args" + ) + def test_get_inputs_from_cli(self, mock_parse_args): + # Arrange + mock_parse_args.return_value = argparse.Namespace( + remote_config_repo="test_repo", + use_secrets_manager="false", + send_metrics="true", + sonar_url="https://sonar.com/", + token_cmdb="my_token_cmdb", + token_vulnerability_management="my_token_vm", + token_sonar="my_token_sonar", + ) + + expected_output = { + "remote_config_repo": "test_repo", + "use_secrets_manager": "false", + "send_metrics": "true", + "sonar_url": "https://sonar.com/", + "token_cmdb": "my_token_cmdb", + "token_vulnerability_management": "my_token_vm", + "token_sonar": "my_token_sonar", + } + + # Act + result = get_inputs_from_cli(sys.argv[1:]) + + # Assert + self.assertEqual(result, expected_output) \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/domain/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/domain/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/domain/usecases/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/domain/usecases/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/domain/usecases/test_report_sonar.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/domain/usecases/test_report_sonar.py new file mode 100644 index 00000000..4c6964a9 --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/domain/usecases/test_report_sonar.py @@ -0,0 +1,108 @@ +import unittest +from unittest import mock +from unittest.mock import MagicMock, patch, call +from devsecops_engine_tools.engine_utilities.sonarqube.src.domain.usecases.report_sonar import ( + ReportSonar +) + +class TestReportSonar(unittest.TestCase): + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.domain.usecases.report_sonar.set_repository" + ) + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.domain.usecases.report_sonar.define_env" + ) + def test_process_valid( + self, mock_define_env, mock_set_repository + ): + # Arrange + mock_vulnerability_gateway = MagicMock() + mock_secrets_manager_gateway = MagicMock() + mock_devops_platform_gateway = MagicMock() + mock_sonar_gateway = MagicMock() + + mock_devops_platform_gateway.get_variable.side_effect = [ + "pipeline_name", + "branch_name", + "repository", + "access_token", + "build_execution_id", + "build_id", + "commit_hash" + ] + mock_set_repository.return_value = "repository_uri" + mock_define_env.return_value = "dev" + mock_secrets_manager_gateway.get_secret.return_value = { + "token_sonar": "sonar_token" + } + + mock_devops_platform_gateway.get_remote_config.return_value = { + "PIPELINE_COMPONENTS": {} + } + + mock_sonar_gateway.get_project_keys.return_value = ["project_key_1"] + mock_sonar_gateway.filter_by_sonarqube_tag.return_value = [ + MagicMock(unique_id_from_tool="123", active=True, mitigated=False, false_p=False), + MagicMock(unique_id_from_tool="1234", active=False, mitigated=False, false_p=True) + ] + mock_sonar_gateway.search_finding_by_id.side_effect = [ + {"status": "RESOLVED", "key": "123", "type": "VULNERABILITY"}, + {"status": "REVIEWED", "key": "1234"} + ] + + report_sonar = ReportSonar( + vulnerability_management_gateway=mock_vulnerability_gateway, + secrets_manager_gateway=mock_secrets_manager_gateway, + devops_platform_gateway=mock_devops_platform_gateway, + sonar_gateway=mock_sonar_gateway, + ) + + args = {"remote_config_repo": "repo", "use_secrets_manager": "true", "sonar_url": "sonar_url"} + + # Act + report_sonar.process(args) + + # Assert + mock_sonar_gateway.get_findings.assert_has_calls( + [ + call("sonar_url", + "sonar_token", + "/api/issues/search", + { + "componentKeys": "project_key_1", + "types": "VULNERABILITY", + "ps": 500, + "p": 1, + "s": "CREATION_DATE", + "asc": "false" + }, + "issues" + ), + call("sonar_url", + "sonar_token", + "/api/hotspots/search", + { + "projectKey": "project_key_1", + "ps": 100, + "p": 1 + }, + "hotspots" + ) + ], + any_order=False + ) + mock_sonar_gateway.change_finding_status.assert_has_calls( + [ + call( + "sonar_url", + "sonar_token", + "/api/issues/do_transition", + { + "issue": "123", + "transition": "reopen" + }, + "issue" + ) + ], + any_order=False + ) \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/entry_points/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/entry_points/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/entry_points/test_entry_point_report_sonar.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/entry_points/test_entry_point_report_sonar.py new file mode 100644 index 00000000..bab204f4 --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/entry_points/test_entry_point_report_sonar.py @@ -0,0 +1,70 @@ +import unittest +from unittest.mock import MagicMock, patch +from devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.entry_points.entry_point_report_sonar import init_report_sonar + +class TestInitReportSonar(unittest.TestCase): + + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.entry_points.entry_point_report_sonar.ReportSonar" + ) + def test_init_report_sonar_calls_process(self, mock_report_sonar): + # Arrange + mock_vulnerability_management_gateway = MagicMock() + mock_secrets_manager_gateway = MagicMock() + mock_devops_platform_gateway = MagicMock() + mock_metrics_manager_gateway = MagicMock() + mock_sonar_gateway = MagicMock() + mock_devops_platform_gateway.get_remote_config.return_value = { + "REPORT_SONAR" : { + "ENABLED": "true" + }, + "BANNER": "DevSecOps" + } + args = {"remote_config_repo": "some_repo", "use_secrets_manager": "true", "send_metrics": "false"} + + # Act + init_report_sonar( + vulnerability_management_gateway=mock_vulnerability_management_gateway, + secrets_manager_gateway=mock_secrets_manager_gateway, + devops_platform_gateway=mock_devops_platform_gateway, + sonar_gateway=mock_sonar_gateway, + metrics_manager_gateway=mock_metrics_manager_gateway, + args=args, + ) + + # Assert + mock_report_sonar.assert_called_once_with( + mock_vulnerability_management_gateway, + mock_secrets_manager_gateway, + mock_devops_platform_gateway, + mock_sonar_gateway + ) + mock_report_sonar.return_value.process.assert_called_once_with(args) + + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.entry_points.entry_point_report_sonar.ReportSonar" + ) + def test_init_report_sonar_disabled(self, mock_report_sonar): + # Arrange + mock_devops_platform_gateway = MagicMock() + mock_metrics_manager_gateway = MagicMock() + mock_devops_platform_gateway.get_remote_config.return_value = { + "REPORT_SONAR" : { + "ENABLED": "false" + }, + "BANNER": "DevSecOps" + } + args = {"remote_config_repo": "some_repo", "use_secrets_manager": "true", "send_metrics": "false"} + + # Act + init_report_sonar( + vulnerability_management_gateway=MagicMock(), + secrets_manager_gateway=MagicMock(), + devops_platform_gateway=mock_devops_platform_gateway, + sonar_gateway=MagicMock(), + metrics_manager_gateway=mock_metrics_manager_gateway, + args=args, + ) + + # Assert + mock_report_sonar.assert_not_called() \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/helpers/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/helpers/test_utils.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/helpers/test_utils.py new file mode 100644 index 00000000..b3be6b92 --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/helpers/test_utils.py @@ -0,0 +1,26 @@ +import unittest +from devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.helpers.utils import set_repository + +class TestSonarUtils(unittest.TestCase): + + def test_set_repository_mr(self): + # Arrange + pipeline_name = "some_pipeline" + source_code_management = "https://example.com/repo" + + # Act + result = set_repository(pipeline_name, source_code_management) + + # Assert + self.assertEqual(result, source_code_management) + + def test_set_repository_not_mr(self): + # Arrange + pipeline_name = "some_pipeline_MR_123" + source_code_management = "https://example.com/repo" + + # Act + result = set_repository(pipeline_name, source_code_management) + + # Assert + self.assertEqual(result, "https://example.com/repo?path=/123") \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/sonar/__init__.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/sonar/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/sonar/test_report_sonar.py b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/sonar/test_report_sonar.py new file mode 100644 index 00000000..e26ec8ab --- /dev/null +++ b/tools/devsecops_engine_tools/engine_utilities/sonarqube/test/infrastructure/sonar/test_report_sonar.py @@ -0,0 +1,219 @@ +import unittest +from unittest.mock import patch, mock_open, MagicMock +from devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.driven_adapters.sonarqube.sonarqube_report import SonarAdapter + +class TestSonarAdapter(unittest.TestCase): + + @patch( + "os.getenv" + ) + def test_get_project_keys_from_env(self, mock_getenv): + # Arrange + adapter = SonarAdapter() + mock_getenv.return_value = '{"sonar.scanner.metadataFilePath":"path/to/metadata.json"}' + + with patch.object(adapter, 'parse_project_key', return_value="project_key_123") as mock_parse: + # Act + project_keys = adapter.get_project_keys("pipeline_name") + + # Assert + mock_parse.assert_called_once_with("path/to/metadata.json") + self.assertEqual(project_keys, ["project_key_123"]) + + @patch( + "os.getenv" + ) + def test_get_project_keys_no_match_in_env(self, mock_getenv): + # Arrange + adapter = SonarAdapter() + mock_getenv.return_value = "" + + # Act + project_keys = adapter.get_project_keys("pipeline_name") + + # Assert + self.assertEqual(project_keys, ["pipeline_name"]) + + @patch( + "os.getenv" + ) + def test_get_project_keys_no_project_key_found(self, mock_getenv): + # Arrange + adapter = SonarAdapter() + mock_getenv.return_value = '{"sonar.scanner.metadataFilePath":"path/to/metadata.json"}' + + with patch.object(adapter, "parse_project_key", return_value=None) as mock_parse: + # Act + project_keys = adapter.get_project_keys("pipeline_name") + + # Assert + mock_parse.assert_called_once_with("path/to/metadata.json") + self.assertEqual(project_keys, ["pipeline_name"]) + + @patch( + "builtins.open", + new_callable=mock_open, + read_data="projectKey=my_project_key" + ) + def test_parse_project_key_success(self, mock_file): + # Arrange + adapter = SonarAdapter() + + # Act + result = adapter.parse_project_key("path/to/metadata.json") + + # Assert + mock_file.assert_called_once_with("path/to/metadata.json", "r", encoding="utf-8") + self.assertEqual(result, "my_project_key") + + def test_parse_project_key_invalid_content(self): + # Arrange + adapter = SonarAdapter() + + # Act + result = adapter.parse_project_key("path/to/metadata.json") + + # Assert + self.assertIsNone(result) + + @patch( + "builtins.open", + side_effect=Exception("File not found") + ) + def test_parse_project_key_file_not_found(self, mock_file): + # Arrange + adapter = SonarAdapter() + + # Act + result = adapter.parse_project_key("path/to/nonexistent_file.json") + + # Assert + mock_file.assert_called_once_with("path/to/nonexistent_file.json", "r", encoding="utf-8") + self.assertIsNone(result) + + def test_create_task_report_from_string(self): + # Arrange + adapter = SonarAdapter() + file_content = "projectKey=my_project_key\nanotherSetting=some_value" + + # Act + result = adapter.create_task_report_from_string(file_content) + + # Assert + self.assertEqual(result["projectKey"], "my_project_key") + self.assertEqual(result["anotherSetting"], "some_value") + + @patch( + "requests.post" + ) + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.driven_adapters.sonarqube.sonarqube_report.Utils.encode_token_to_base64" + ) + def test_change_finding_status(self, mock_encode, mock_post): + # Arrange + mock_encode.return_value = "encoded_token" + mock_response = MagicMock() + mock_response.raise_for_status = MagicMock() + mock_post.return_value = mock_response + + sonar_adapter = SonarAdapter() + sonar_url = "https://sonar.example.com" + sonar_token = "my_token" + endpoint = "/api/issues/do_transition" + data = { + "issue": "123", + "transition": "reopen" + } + + # Act + sonar_adapter.change_finding_status( + sonar_url, + sonar_token, + endpoint, + data, + "issue" + ) + + # Assert + mock_post.assert_called_once_with( + f"{sonar_url}{endpoint}", + headers={"Authorization": "Basic encoded_token"}, + data={"issue": "123", "transition": "reopen"} + ) + mock_response.raise_for_status.assert_called_once() + + @patch( + "requests.get" + ) + @patch( + "devsecops_engine_tools.engine_utilities.sonarqube.src.infrastructure.driven_adapters.sonarqube.sonarqube_report.Utils.encode_token_to_base64" + ) + def test_get_findings(self, mock_encode, mock_get): + # Arrange + mock_encode.return_value = "encoded_token" + mock_response = MagicMock() + mock_response.json.return_value = { + "issues": [{"key": "123", "type": "VULNERABILITY"}] + } + mock_response.raise_for_status = MagicMock() + mock_get.return_value = mock_response + + report_sonar = SonarAdapter() + sonar_url = "https://sonar.example.com" + sonar_token = "my_token" + endpoint = "/api/issues/search" + params = { + "componentKeys": "my_project", + "types": "VULNERABILITY", + "ps": 500, + "p": 1, + "s": "CREATION_DATE", + "asc": "false" + } + + # Act + findings = report_sonar.get_findings( + sonar_url, + sonar_token, + endpoint, + params, + "issues" + ) + + # Assert + mock_get.assert_called_once_with( + f"{sonar_url}{endpoint}", + headers={"Authorization": "Basic encoded_token"}, + params=params + ) + mock_response.raise_for_status.assert_called_once() + self.assertEqual(findings, [{"key": "123", "type": "VULNERABILITY"}]) + + def test_search_finding_by_id(self): + # Arrange + report_sonar = SonarAdapter() + issues = [ + {"key": "123", "type": "VULNERABILITY"}, + {"key": "456", "type": "BUG"} + ] + issue_id = "123" + + # Act + result = report_sonar.search_finding_by_id(issues, issue_id) + + # Assert + self.assertEqual(result, {"key": "123", "type": "VULNERABILITY"}) + + def test_search_finding_by_id_not_found(self): + # Arrange + report_sonar = SonarAdapter() + issues = [ + {"key": "456", "type": "BUG"} + ] + issue_id = "999" + + # Act + result = report_sonar.search_finding_by_id(issues, issue_id) + + # Assert + self.assertIsNone(result) \ No newline at end of file diff --git a/tools/devsecops_engine_tools/engine_utilities/utils/utils.py b/tools/devsecops_engine_tools/engine_utilities/utils/utils.py index 32d90b1c..9f93d283 100644 --- a/tools/devsecops_engine_tools/engine_utilities/utils/utils.py +++ b/tools/devsecops_engine_tools/engine_utilities/utils/utils.py @@ -1,8 +1,13 @@ import zipfile - +import base64 class Utils: def unzip_file(self, zip_file_path, extract_path): with zipfile.ZipFile(zip_file_path, "r") as zip_ref: zip_ref.extractall(extract_path) + + def encode_token_to_base64(self, token): + token_bytes = f"{token}:".encode("utf-8") + base64_token = base64.b64encode(token_bytes).decode("utf-8") + return base64_token \ No newline at end of file diff --git a/tools/setup.py b/tools/setup.py index e89c1b57..d77e458b 100644 --- a/tools/setup.py +++ b/tools/setup.py @@ -33,7 +33,8 @@ def get_requirements(): packages=find_packages(exclude=["**test**"]), entry_points={ 'console_scripts': [ - 'devsecops-engine-tools=devsecops_engine_tools.engine_core.src.applications.runner_engine_core:application_core' + 'devsecops-engine-tools=devsecops_engine_tools.engine_core.src.applications.runner_engine_core:application_core', + 'devsecops-engine-tools.report-sonar=devsecops_engine_tools.engine_utilities.sonarqube.src.applications.runner_report_sonar:runner_report_sonar' ] }, classifiers=[