diff --git a/docs/Docusaurus/docs/modules/engine_core.md b/docs/Docusaurus/docs/modules/engine_core.md index 4afc9ed65..231a46151 100644 --- a/docs/Docusaurus/docs/modules/engine_core.md +++ b/docs/Docusaurus/docs/modules/engine_core.md @@ -142,9 +142,7 @@ Configuration of the driven adapters in the main layer and management of on/off "RECURSE": true, "INSTALL_DEPENDENCIES": true, "DEBUG_PIPELINES": ["pipeline_name1", "pipeline_name2"], - "LIFECYCLE_PIPELINES": { - "pipeline_name1": "pre-build" - }, + "REQUIRED_ONLY_PIPELINES": ["pipeline_name1", "pipeline_name2"], "BREAK_ON_BUILD_FAILURE": true, "BUILD_FAILURE_PATTERNS": ["BUILD FAILED", "BUILD FAILURE", "npm ERR!"], "OVERRIDE_REGISTRIES": false, diff --git a/docs/Docusaurus/docs/modules/engine_sca/engine_container.md b/docs/Docusaurus/docs/modules/engine_sca/engine_container.md index be1527881..50c26aab0 100755 --- a/docs/Docusaurus/docs/modules/engine_sca/engine_container.md +++ b/docs/Docusaurus/docs/modules/engine_sca/engine_container.md @@ -221,6 +221,7 @@ Defines exclusion rules for repositories and specific vulnerability findings. "where": "all", "create_date": "24012023", "expired_date": "22092023", + "severity": "high", "hu": "345345", "reason": "False Positive" } @@ -243,6 +244,7 @@ Defines exclusion rules for repositories and specific vulnerability findings. "cve_id": "CVE-2023-6237", "expired_date": "21092024", "create_date": "24012023", + "severity": "high", "hu": "345345" } ] @@ -263,6 +265,7 @@ Defines exclusion rules for repositories and specific vulnerability findings. "cve_id": "CVE-2023-6237", "expired_date": "21092024", "create_date": "24012023", + "severity": "high", "hu": "345345" } ] @@ -292,6 +295,8 @@ Defines exclusion rules for repositories and specific vulnerability findings. - `where`: Scope of exclusion (`"all"` for global or specific image/path) - `create_date`: Date when exclusion was created (format: DDMMYYYY) - `expired_date`: Expiration date for the exclusion (format: DDMMYYYY) +- `severity`: Vulnerability severity level in lowercase (e.g., `"critical"`, `"high"`, `"medium"`, `"low"`) used to match against the finding's severity +- `priority`: Alternative to `severity` per the `Exclusions` model - equally interchangeable, since an exclusion is applied when either `severity` or `priority` matches the finding (e.g., `"Very Critical"`, `"Critical"`, `"High"`, `"Medium Low"`) - `hu`: Human user identifier for audit trail - `reason`: Justification for exclusion (e.g., `"False Positive"`, `"Business Risk Accepted"`) diff --git a/docs/Docusaurus/docs/modules/engine_sca/engine_dependencies.md b/docs/Docusaurus/docs/modules/engine_sca/engine_dependencies.md index a4cb439df..4ad08c8ae 100644 --- a/docs/Docusaurus/docs/modules/engine_sca/engine_dependencies.md +++ b/docs/Docusaurus/docs/modules/engine_sca/engine_dependencies.md @@ -111,6 +111,7 @@ Defines exclusion rules for repositories and specific vulnerability findings. "where": "all", "create_date": "19022024", "expired_date": "undefined", + "severity": "high", "hu": "4662904" } ] @@ -132,6 +133,7 @@ Defines exclusion rules for repositories and specific vulnerability findings. "where": "all", "create_date": "19022024", "expired_date": "undefined", + "severity": "high", "hu": "4662904" } ] @@ -158,6 +160,8 @@ Each exclusion entry contains: - `where`: Scope of exclusion (`"all"` for global or specific dependency/path) - `create_date`: Date when exclusion was created (format: DDMMYYYY) - `expired_date`: Expiration date for the exclusion (`"undefined"` for permanent exclusions) +- `severity`: Vulnerability severity level in lowercase (e.g., `"critical"`, `"high"`, `"medium"`, `"low"`) used to match against the finding's severity +- `priority`: Alternative to `severity` per the `Exclusions` model - equally interchangeable, since an exclusion is applied when either `severity` or `priority` matches the finding (e.g., `"Very Critical"`, `"Critical"`, `"High"`, `"Medium Low"`) - `hu`: Human user identifier for audit trail ## Main Responsibilities diff --git a/example_remote_config_local/engine_core/ConfigTool.json b/example_remote_config_local/engine_core/ConfigTool.json index 68156fe29..595ba35af 100644 --- a/example_remote_config_local/engine_core/ConfigTool.json +++ b/example_remote_config_local/engine_core/ConfigTool.json @@ -121,9 +121,7 @@ "EXCLUDE_PATHS": ["**/test/**"], "RECURSE": true, "DEBUG_PIPELINES": ["pipeline_name1", "pipeline_name2"], - "LIFECYCLE_PIPELINES": { - "pipeline_name1": "pre-build" - }, + "REQUIRED_ONLY_PIPELINES": ["pipeline_name1", "pipeline_name2"], "BREAK_ON_BUILD_FAILURE": true, "BUILD_FAILURE_PATTERNS": ["BUILD FAILED", "BUILD FAILURE", "npm ERR!"], "OVERRIDE_REGISTRIES": false, diff --git a/example_remote_config_local/engine_sca/engine_container/Exclusions.json b/example_remote_config_local/engine_sca/engine_container/Exclusions.json index 59bd16ec7..5b3e576ee 100755 --- a/example_remote_config_local/engine_sca/engine_container/Exclusions.json +++ b/example_remote_config_local/engine_sca/engine_container/Exclusions.json @@ -6,6 +6,7 @@ "where": "all", "create_date": "24012023", "expired_date": "22092023", + "severity": "high", "hu": "345345", "reason": "False Positive" } @@ -28,6 +29,7 @@ "cve_id": "CVE-2023-6237", "expired_date": "21092024", "create_date": "24012023", + "severity": "high", "hu": "345345" } ] @@ -48,6 +50,7 @@ "cve_id": "CVE-2023-6237", "expired_date": "21092024", "create_date": "24012023", + "severity": "high", "hu": "345345" } ] diff --git a/example_remote_config_local/engine_sca/engine_dependencies/Exclusions.json b/example_remote_config_local/engine_sca/engine_dependencies/Exclusions.json index 87d80b439..a6f00b60f 100644 --- a/example_remote_config_local/engine_sca/engine_dependencies/Exclusions.json +++ b/example_remote_config_local/engine_sca/engine_dependencies/Exclusions.json @@ -7,6 +7,7 @@ "where": "all", "create_date": "19022024", "expired_date": "undefined", + "severity": "high", "hu": "4662904" } ] @@ -28,6 +29,7 @@ "where": "all", "create_date": "19022024", "expired_date": "undefined", + "severity": "high", "hu": "4662904" } ] diff --git a/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/cdxgen/cdxgen.py b/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/cdxgen/cdxgen.py index 33c70b94c..ca6c24d74 100644 --- a/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/cdxgen/cdxgen.py +++ b/tools/devsecops_engine_tools/engine_core/src/infrastructure/driven_adapters/cdxgen/cdxgen.py @@ -34,7 +34,7 @@ def get_components(self, artifact, config, service_name) -> "list[Component]": recurse = config["CDXGEN"].get("RECURSE", True) install_deps = config["CDXGEN"].get("INSTALL_DEPENDENCIES", True) debug_pipelines = config["CDXGEN"].get("DEBUG_PIPELINES", []) - lifecycle_pipelines = config["CDXGEN"].get("LIFECYCLE_PIPELINES", {}) + required_only_pipelines = config["CDXGEN"].get("REQUIRED_ONLY_PIPELINES", []) spec_version = config["CDXGEN"].get("SPEC_VERSION", "1.6") break_on_build_failure = config["CDXGEN"].get("BREAK_ON_BUILD_FAILURE", True) build_failure_patterns = config["CDXGEN"].get("BUILD_FAILURE_PATTERNS", []) @@ -90,50 +90,20 @@ def get_components(self, artifact, config, service_name) -> "list[Component]": logger.warning(f"{os_platform} is not supported.") return None - result_sbom = self._run_cdxgen(command_prefix, artifact, service_name, exclude_types, exclude_paths, recurse, install_deps, lifecycle_pipelines, enable_debug, spec_version, failure_patterns) + required_only = service_name in required_only_pipelines if required_only_pipelines else False + result_sbom = self._run_cdxgen(command_prefix, artifact, service_name, exclude_types, exclude_paths, recurse, install_deps, required_only, enable_debug, spec_version, failure_patterns) return get_list_component(result_sbom, config["CDXGEN"]["OUTPUT_FORMAT"]) except Exception as e: logger.error(f"Error generating SBOM: {e}") return None - def _run_cdxgen(self, command_prefix, artifact, service_name, exclude_types, exclude_paths, recurse, install_deps, lifecycle_pipelines, enable_debug=False, spec_version="1.6", failure_patterns=None): + def _run_cdxgen(self, command_prefix, artifact, service_name, exclude_types, exclude_paths, recurse, install_deps, required_only=False, enable_debug=False, spec_version="1.6", failure_patterns=None): failure_patterns = failure_patterns or [] result_file = f"{service_name}_SBOM.json" - command = [ - command_prefix, - artifact, - "-o", - result_file, - "--spec-version", - spec_version - ] - - if exclude_types: - for ex in exclude_types: - command.extend( - ["--exclude-type", ex] - ) - - if exclude_paths: - for ex in exclude_paths: - command.extend( - ["--exclude", ex] - ) - - if lifecycle_pipelines.get(service_name): - command.extend( - ["--lifecycle", lifecycle_pipelines.get(service_name)] - ) - - if not recurse: - command.append( - "--no-recurse" - ) - - if not install_deps: - command.append( - "--no-install-deps" - ) + command = self._build_cdxgen_command( + command_prefix, artifact, result_file, spec_version, + exclude_types, exclude_paths, required_only, recurse, install_deps + ) try: result = subprocess.run( @@ -142,23 +112,9 @@ def _run_cdxgen(self, command_prefix, artifact, service_name, exclude_types, exc stderr=subprocess.PIPE, text=True ) - - if enable_debug: - if result.stdout: - logger.info(f"CDXGEN stdout: {result.stdout}") - if result.stderr: - logger.info(f"CDXGEN stderr: {result.stderr}") - matched_pattern = self._detect_build_failure( - f"{result.stdout or ''}\n{result.stderr or ''}", failure_patterns - ) - if matched_pattern: - self._remove_incomplete_sbom(result_file) - raise Exception( - f"Detected build failure pattern '{matched_pattern}' in cdxgen output for " - f"'{service_name}'. The underlying build likely failed; aborting to avoid " - "generating an incomplete or empty SBOM." - ) + self._log_debug_output(result, enable_debug) + self._check_build_failure(result, failure_patterns, service_name, result_file) if result.returncode == 0: print(f"SBOM generated and saved to: {result_file}") @@ -169,6 +125,53 @@ def _run_cdxgen(self, command_prefix, artifact, service_name, exclude_types, exc except Exception as e: logger.error(f"Error running cdxgen: {e}") + def _build_cdxgen_command(self, command_prefix, artifact, result_file, spec_version, exclude_types, exclude_paths, required_only, recurse, install_deps): + command = [ + command_prefix, + artifact, + "-o", + result_file, + "--spec-version", + spec_version + ] + + for ex in exclude_types or []: + command.extend(["--exclude-type", ex]) + + for ex in exclude_paths or []: + command.extend(["--exclude", ex]) + + if required_only: + command.append("--required-only") + + if not recurse: + command.append("--no-recurse") + + if not install_deps: + command.append("--no-install-deps") + + return command + + def _log_debug_output(self, result, enable_debug): + if not enable_debug: + return + if result.stdout: + logger.info(f"CDXGEN stdout: {result.stdout}") + if result.stderr: + logger.info(f"CDXGEN stderr: {result.stderr}") + + def _check_build_failure(self, result, failure_patterns, service_name, result_file): + matched_pattern = self._detect_build_failure( + f"{result.stdout or ''}\n{result.stderr or ''}", failure_patterns + ) + if matched_pattern: + self._remove_incomplete_sbom(result_file) + raise Exception( + f"Detected build failure pattern '{matched_pattern}' in cdxgen output for " + f"'{service_name}'. The underlying build likely failed; aborting to avoid " + "generating an incomplete or empty SBOM." + ) + def _detect_build_failure(self, output, patterns): """Return the first configured regex pattern that matches the cdxgen output, if any.""" if not output or not patterns: diff --git a/tools/devsecops_engine_tools/engine_core/test/infrastructure/driven_adapters/cdxgen/test_cdxgen.py b/tools/devsecops_engine_tools/engine_core/test/infrastructure/driven_adapters/cdxgen/test_cdxgen.py index bb4724085..369e27acf 100644 --- a/tools/devsecops_engine_tools/engine_core/test/infrastructure/driven_adapters/cdxgen/test_cdxgen.py +++ b/tools/devsecops_engine_tools/engine_core/test/infrastructure/driven_adapters/cdxgen/test_cdxgen.py @@ -22,7 +22,7 @@ def setUp(self): "EXCLUDE_PATHS": [], "RECURSE": True, "DEBUG_PIPELINES": [], - "LIFECYCLE_PIPELINES": {} + "REQUIRED_ONLY_PIPELINES": [] } } @@ -35,7 +35,7 @@ def setUp(self): "EXCLUDE_PATHS": [], "RECURSE": True, "DEBUG_PIPELINES": [], - "LIFECYCLE_PIPELINES": {} + "REQUIRED_ONLY_PIPELINES": [] } } @@ -66,7 +66,7 @@ def test_get_components_linux_success(self, mock_platform, mock_get_list_compone "https://github.com/CycloneDX/cdxgen/releases/download/v10.2.0/cdxgen-linux-amd64", "cdxgen" ) - mock_run.assert_called_once_with('./cdxgen-linux-amd64', self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', []) + mock_run.assert_called_once_with('./cdxgen-linux-amd64', self.artifact, self.service_name, [], [], True, True, False, False, '1.6', []) mock_get_list_component.assert_called_once_with('test_service_SBOM.json', 'json') @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.platform.machine') @@ -92,7 +92,7 @@ def test_get_components_linux_success_arm64(self, mock_platform, mock_get_list_c "https://github.com/CycloneDX/cdxgen/releases/download/v10.2.0/cdxgen-linux-arm64", "cdxgen" ) - mock_run.assert_called_once_with('./cdxgen-linux-arm64', self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', []) + mock_run.assert_called_once_with('./cdxgen-linux-arm64', self.artifact, self.service_name, [], [], True, True, False, False, '1.6', []) mock_get_list_component.assert_called_once_with('test_service_SBOM.json', 'json') @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.get_list_component') @@ -224,7 +224,7 @@ def test_run_cdxgen_success(self, mock_subprocess): recurse = True install_deps = True enable_debug = False - lifecycle_pipelines = {} + required_only = False mock_result = Mock(returncode=0, stdout="", stderr="") mock_subprocess.return_value = mock_result expected_result_file = f"{self.service_name}_SBOM.json" @@ -232,7 +232,7 @@ def test_run_cdxgen_success(self, mock_subprocess): # Act with patch('builtins.print') as mock_print: - result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, lifecycle_pipelines, enable_debug) + result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, required_only, enable_debug) # Assert self.assertEqual(result, expected_result_file) @@ -254,12 +254,12 @@ def test_run_cdxgen_failure(self, mock_subprocess, mock_logger): recurse = True install_deps = True enable_debug = False - lifecycle_pipelines = {} + required_only = False error_message = "Command execution failed" mock_subprocess.side_effect = Exception(error_message) # Act - result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, lifecycle_pipelines, enable_debug) + result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, required_only, enable_debug) # Assert self.assertIsNone(result) @@ -275,14 +275,14 @@ def test_run_cdxgen_debug_mode_enabled(self, mock_logger, mock_subprocess): recurse = True install_deps = True enable_debug = True - lifecycle_pipelines = {} + required_only = False mock_result = Mock(returncode=0, stdout="Debug stdout output", stderr="Debug stderr output") mock_subprocess.return_value = mock_result expected_result_file = f"{self.service_name}_SBOM.json" # Act with patch('builtins.print') as mock_print: - result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, lifecycle_pipelines, enable_debug) + result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, required_only, enable_debug) # Assert self.assertEqual(result, expected_result_file) @@ -312,7 +312,7 @@ def test_get_components_debug_mode_service_in_list(self, mock_logger, mock_platf "RECURSE": True, "INSTALL_DEPENDENCIES": False, "DEBUG_PIPELINES": ["test_service", "another_service"], - "LIFECYCLE_PIPELINES": {} + "REQUIRED_ONLY_PIPELINES": [] } } @@ -325,7 +325,7 @@ def test_get_components_debug_mode_service_in_list(self, mock_logger, mock_platf # Assert self.assertEqual(result, self.mock_components) mock_logger.info.assert_called_with(f"Enabling debug mode for pipeline: {self.service_name}") - mock_run.assert_called_once_with('./cdxgen-linux-amd64', self.artifact, self.service_name, [], [], True, False, {}, True, '1.6', []) + mock_run.assert_called_once_with('./cdxgen-linux-amd64', self.artifact, self.service_name, [], [], True, False, False, True, '1.6', []) @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.get_list_component') @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.platform.system') @@ -344,7 +344,7 @@ def test_get_components_debug_mode_service_not_in_list(self, mock_platform, mock "EXCLUDE_PATHS": [], "RECURSE": True, "DEBUG_PIPELINES": ["other_service", "another_service"], - "LIFECYCLE_PIPELINES": {} + "REQUIRED_ONLY_PIPELINES": [] } } @@ -356,7 +356,36 @@ def test_get_components_debug_mode_service_not_in_list(self, mock_platform, mock # Assert self.assertEqual(result, self.mock_components) - mock_run.assert_called_once_with('./cdxgen-linux-amd64', self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', []) + mock_run.assert_called_once_with('./cdxgen-linux-amd64', self.artifact, self.service_name, [], [], True, True, False, False, '1.6', []) + + @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.get_list_component') + @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.platform.system') + def test_get_components_required_only_service_in_list(self, mock_platform, mock_get_list_component): + # Arrange + mock_platform.return_value = "Linux" + mock_get_list_component.return_value = self.mock_components + + required_only_config = { + "CDXGEN": { + "CDXGEN_VERSION": "10.2.0", + "SLIM_BINARY": False, + "OUTPUT_FORMAT": "json", + "EXCLUDE_TYPES": [], + "EXCLUDE_PATHS": [], + "RECURSE": True, + "DEBUG_PIPELINES": [], + "REQUIRED_ONLY_PIPELINES": ["test_service", "another_service"] + } + } + + with patch.object(self.cdxgen, '_check_cdxgen_in_path', return_value=None): + with patch.object(self.cdxgen, '_install_tool_unix', return_value='./cdxgen-linux-amd64'): + with patch.object(self.cdxgen, '_run_cdxgen', return_value='test_service_SBOM.json') as mock_run: + result = self.cdxgen.get_components(self.artifact, required_only_config, self.service_name) + + # Assert: required_only=True because service_name is in REQUIRED_ONLY_PIPELINES + self.assertEqual(result, self.mock_components) + mock_run.assert_called_once_with('./cdxgen-linux-amd64', self.artifact, self.service_name, [], [], True, True, True, False, '1.6', []) @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.get_list_component') @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.platform.system') @@ -375,7 +404,7 @@ def test_get_components_fetch_license_enabled_sets_env(self, mock_platform, mock "RECURSE": True, "FETCH_LICENSE": True, "DEBUG_PIPELINES": [], - "LIFECYCLE_PIPELINES": {} + "REQUIRED_ONLY_PIPELINES": [] } } @@ -389,7 +418,7 @@ def test_get_components_fetch_license_enabled_sets_env(self, mock_platform, mock self.assertEqual(result, self.mock_components) self.assertEqual(os.environ.get("FETCH_LICENSE"), "true") mock_install.assert_called_once() - mock_run.assert_called_once_with('/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', []) + mock_run.assert_called_once_with('/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, False, False, '1.6', []) @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.get_list_component') @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.platform.system') @@ -408,7 +437,7 @@ def test_get_components_fetch_license_disabled_does_not_set_env(self, mock_platf "RECURSE": True, "FETCH_LICENSE": False, "DEBUG_PIPELINES": [], - "LIFECYCLE_PIPELINES": {} + "REQUIRED_ONLY_PIPELINES": [] } } @@ -422,7 +451,7 @@ def test_get_components_fetch_license_disabled_does_not_set_env(self, mock_platf self.assertEqual(result, self.mock_components) self.assertIsNone(os.environ.get("FETCH_LICENSE")) mock_install.assert_called_once() - mock_run.assert_called_once_with('/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', []) + mock_run.assert_called_once_with('/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, False, False, '1.6', []) @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.subprocess.run') def test_run_cdxgen_with_exclude_types_list(self, mock_subprocess): @@ -433,7 +462,7 @@ def test_run_cdxgen_with_exclude_types_list(self, mock_subprocess): recurse = True install_deps = True enable_debug = False - lifecycle_pipelines = {} + required_only = False mock_result = Mock(returncode=0, stdout="", stderr="") mock_subprocess.return_value = mock_result expected_result_file = f"{self.service_name}_SBOM.json" @@ -441,7 +470,7 @@ def test_run_cdxgen_with_exclude_types_list(self, mock_subprocess): # Act with patch('builtins.print') as mock_print: - result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, lifecycle_pipelines, enable_debug) + result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, required_only, enable_debug) # Assert self.assertEqual(result, expected_result_file) @@ -461,7 +490,7 @@ def test_run_cdxgen_with_exclude_paths_list(self, mock_subprocess): recurse = True install_deps = True enable_debug = False - lifecycle_pipelines = {} + required_only = False mock_result = Mock(returncode=0, stdout="", stderr="") mock_subprocess.return_value = mock_result expected_result_file = f"{self.service_name}_SBOM.json" @@ -469,7 +498,7 @@ def test_run_cdxgen_with_exclude_paths_list(self, mock_subprocess): # Act with patch('builtins.print') as mock_print: - result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, lifecycle_pipelines, enable_debug) + result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, required_only, enable_debug) # Assert self.assertEqual(result, expected_result_file) @@ -489,7 +518,7 @@ def test_run_cdxgen_no_recurse(self, mock_subprocess): recurse = False enable_debug = False install_deps = True - lifecycle_pipelines = {} + required_only = False mock_result = Mock(returncode=0, stdout="", stderr="") mock_subprocess.return_value = mock_result expected_result_file = f"{self.service_name}_SBOM.json" @@ -497,7 +526,7 @@ def test_run_cdxgen_no_recurse(self, mock_subprocess): # Act with patch('builtins.print') as mock_print: - result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, lifecycle_pipelines, enable_debug) + result = self.cdxgen._run_cdxgen(command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, required_only, enable_debug) # Assert self.assertEqual(result, expected_result_file) @@ -611,7 +640,7 @@ def test_get_components_uses_cdxgen_from_path(self, mock_logger, mock_platform, # Assert self.assertEqual(result, self.mock_components) mock_logger.info.assert_called_with(f"Using cdxgen from PATH: {cdxgen_path}") - mock_run.assert_called_once_with(cdxgen_path, self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', []) + mock_run.assert_called_once_with(cdxgen_path, self.artifact, self.service_name, [], [], True, True, False, False, '1.6', []) @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.subprocess.run') def test_install_tool_unix_success(self, mock_subprocess): @@ -741,7 +770,7 @@ def test_get_components_override_registries(self, mock_platform, mock_get_list_c "RECURSE": True, "INSTALL_DEPENDENCIES": True, "DEBUG_PIPELINES": [], - "LIFECYCLE_PIPELINES": {}, + "REQUIRED_ONLY_PIPELINES": [], "OVERRIDE_REGISTRIES": True, "REGISTRIES": {"MY_PRIVATE_REG": "http://my-registry.example.com"}, } @@ -754,25 +783,25 @@ def test_get_components_override_registries(self, mock_platform, mock_get_list_c self.assertEqual(os.environ.get("MY_PRIVATE_REG"), "http://my-registry.example.com") @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.subprocess.run') - def test_run_cdxgen_with_lifecycle_pipeline(self, mock_subprocess): - """Covers lifecycle_pipelines branch (line 112).""" + def test_run_cdxgen_with_required_only(self, mock_subprocess): + """Covers required_only=True branch: --required-only flag is appended to the command.""" command_prefix = "/usr/local/bin/cdxgen" exclude_types = [] exclude_paths = [] recurse = True install_deps = True enable_debug = False - lifecycle_pipelines = {self.service_name: "post-build"} + required_only = True mock_result = Mock(returncode=0, stdout="", stderr="") mock_subprocess.return_value = mock_result expected_result_file = f"{self.service_name}_SBOM.json" - expected_command = [command_prefix, self.artifact, "-o", expected_result_file, "--spec-version", "1.6", "--lifecycle", "post-build"] + expected_command = [command_prefix, self.artifact, "-o", expected_result_file, "--spec-version", "1.6", "--required-only"] with patch('builtins.print'): result = self.cdxgen._run_cdxgen( command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, - lifecycle_pipelines, enable_debug + required_only, enable_debug ) self.assertEqual(result, expected_result_file) @@ -792,7 +821,7 @@ def test_run_cdxgen_no_install_deps(self, mock_subprocess): recurse = True install_deps = False enable_debug = False - lifecycle_pipelines = {} + required_only = False mock_result = Mock(returncode=0, stdout="", stderr="") mock_subprocess.return_value = mock_result expected_result_file = f"{self.service_name}_SBOM.json" @@ -802,7 +831,7 @@ def test_run_cdxgen_no_install_deps(self, mock_subprocess): result = self.cdxgen._run_cdxgen( command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, - lifecycle_pipelines, enable_debug + required_only, enable_debug ) self.assertEqual(result, expected_result_file) @@ -823,14 +852,14 @@ def test_run_cdxgen_nonzero_returncode(self, mock_subprocess, mock_logger): recurse = True install_deps = True enable_debug = False - lifecycle_pipelines = {} + required_only = False mock_result = Mock(returncode=1, stdout="", stderr="some cdxgen error") mock_subprocess.return_value = mock_result result = self.cdxgen._run_cdxgen( command_prefix, self.artifact, self.service_name, exclude_types, exclude_paths, recurse, install_deps, - lifecycle_pipelines, enable_debug + required_only, enable_debug ) self.assertIsNone(result) @@ -853,7 +882,7 @@ def test_run_cdxgen_gradle_build_failed_pattern_breaks_execution( result = self.cdxgen._run_cdxgen( command_prefix, self.artifact, self.service_name, - [], [], True, True, {}, False, "1.6", ["BUILD FAILED"] + [], [], True, True, False, False, "1.6", ["BUILD FAILED"] ) self.assertIsNone(result) @@ -877,7 +906,7 @@ def test_run_cdxgen_maven_build_failure_pattern_case_insensitive(self, mock_subp result = self.cdxgen._run_cdxgen( command_prefix, self.artifact, self.service_name, - [], [], True, True, {}, False, "1.6", ["BUILD FAILURE"] + [], [], True, True, False, False, "1.6", ["BUILD FAILURE"] ) self.assertIsNone(result) @@ -898,7 +927,7 @@ def test_run_cdxgen_regex_failure_pattern(self, mock_subprocess, mock_exists, mo result = self.cdxgen._run_cdxgen( command_prefix, self.artifact, self.service_name, - [], [], True, True, {}, False, "1.6", [r"npm ERR!\s+code\s+E\d+"] + [], [], True, True, False, False, "1.6", [r"npm ERR!\s+code\s+E\d+"] ) self.assertIsNone(result) @@ -916,7 +945,7 @@ def test_run_cdxgen_invalid_regex_pattern_is_skipped(self, mock_subprocess, mock with patch('builtins.print'): result = self.cdxgen._run_cdxgen( command_prefix, self.artifact, self.service_name, - [], [], True, True, {}, False, "1.6", ["("] + [], [], True, True, False, False, "1.6", ["("] ) self.assertEqual(result, expected_result_file) @@ -933,7 +962,7 @@ def test_run_cdxgen_no_failure_patterns_configured(self, mock_subprocess): with patch('builtins.print'): result = self.cdxgen._run_cdxgen( command_prefix, self.artifact, self.service_name, - [], [], True, True, {}, False, "1.6", [] + [], [], True, True, False, False, "1.6", [] ) self.assertEqual(result, expected_result_file) @@ -993,7 +1022,7 @@ def test_get_components_break_on_build_failure_config_disabled(self, mock_platfo "EXCLUDE_PATHS": [], "RECURSE": True, "DEBUG_PIPELINES": [], - "LIFECYCLE_PIPELINES": {}, + "REQUIRED_ONLY_PIPELINES": [], "BUILD_FAILURE_PATTERNS": ["BUILD FAILED"], "BREAK_ON_BUILD_FAILURE": False } @@ -1005,7 +1034,7 @@ def test_get_components_break_on_build_failure_config_disabled(self, mock_platfo self.assertEqual(result, self.mock_components) mock_run.assert_called_once_with( - '/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', [] + '/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, False, False, '1.6', [] ) @patch('devsecops_engine_tools.engine_core.src.infrastructure.driven_adapters.cdxgen.cdxgen.get_list_component') @@ -1024,7 +1053,7 @@ def test_get_components_build_failure_patterns_passed_through(self, mock_platfor "EXCLUDE_PATHS": [], "RECURSE": True, "DEBUG_PIPELINES": [], - "LIFECYCLE_PIPELINES": {}, + "REQUIRED_ONLY_PIPELINES": [], "BUILD_FAILURE_PATTERNS": ["BUILD FAILED", r"npm ERR!\s+code\s+E\d+"] } } @@ -1035,7 +1064,7 @@ def test_get_components_build_failure_patterns_passed_through(self, mock_platfor self.assertEqual(result, self.mock_components) mock_run.assert_called_once_with( - '/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', + '/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, False, False, '1.6', ["BUILD FAILED", r"npm ERR!\s+code\s+E\d+"] ) @@ -1052,5 +1081,5 @@ def test_get_components_no_build_failure_patterns_configured(self, mock_platform self.assertEqual(result, self.mock_components) mock_run.assert_called_once_with( - '/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, {}, False, '1.6', [] + '/usr/local/bin/cdxgen', self.artifact, self.service_name, [], [], True, True, False, False, '1.6', [] )