From 0131bf37b8c076ebdbdad7bba1aaf5d2efc6c4a8 Mon Sep 17 00:00:00 2001 From: Jayashree Huttanagoudar Date: Thu, 23 Jul 2026 15:13:37 +0530 Subject: [PATCH 1/3] [OCPERT-437] Port blocking sec-alerts check command to main for Konflux release flow --- .claude/skills/release-workflow/SKILL.md | 84 +++++++--------------- mcp_server/server.py | 17 +++++ oar/cli/cmd_check_blocking_sec_alerts.py | 87 +++++++++++++++++++++++ oar/cli/cmd_group.py | 2 + oar/core/advisory.py | 4 +- oar/core/const.py | 4 ++ tests/test_advisory.py | 88 +++++++++++++++++++++++- 7 files changed, 222 insertions(+), 64 deletions(-) create mode 100644 oar/cli/cmd_check_blocking_sec_alerts.py diff --git a/.claude/skills/release-workflow/SKILL.md b/.claude/skills/release-workflow/SKILL.md index 7029cf5e691c..dd4878eefd25 100644 --- a/.claude/skills/release-workflow/SKILL.md +++ b/.claude/skills/release-workflow/SKILL.md @@ -56,7 +56,7 @@ take-ownership ↓ check-cve-tracker-bug (always passes, notifies ART) ↓ -check-rhcos-security-alerts (Konflux only - checks blocking security alerts) +check-blocking-sec-alerts (checks blocking security alerts on RHSA advisories) ↓ ├─→ push-to-cdn-staging (async - runs independently in parallel) └─→ [WAIT FOR BUILD PROMOTION - check API until phase == "Accepted"] @@ -86,7 +86,7 @@ analyze-candidate-build (conditionally - only if accepted == false) - **Sequential:** Most tasks run one after another - **Parallel Execution:** - `analyze-candidate-build` runs independently (tests already completed when flow starts) - - `push-to-cdn-staging` starts immediately after check-rhcos-security-alerts (runs while waiting for build promotion) + - `push-to-cdn-staging` starts immediately after check-blocking-sec-alerts (runs while waiting for build promotion) - **ENHANCED:** 2 async tasks (image-consistency-check, stage-testing) triggered immediately after build promotion is detected, running in parallel with test result analysis - **Build Promotion Checkpoint:** Critical decision point - once detected, async tasks trigger immediately - **Test Result Checkpoints:** Must wait for file existence and aggregation (runs in parallel with async tasks) @@ -327,87 +327,49 @@ stdout contains: "task [Check CVE tracker bugs] status is changed to [Pass]" **Expected Duration:** 1 minute -**Next Action:** Proceed to check-rhcos-security-alerts +**Next Action:** Proceed to check-blocking-sec-alerts --- -### 4. check-rhcos-security-alerts +### 4. check-blocking-sec-alerts -**Purpose:** Check for blocking security alerts on RHCOS advisory (Konflux flow only) - -**When to run:** Konflux release flow only (releases with shipment_mr in metadata) +**Purpose:** Check for blocking security alerts across all RHSA advisories **Prerequisites:** check-cve-tracker-bug completed -**Implementation:** Uses curl with Kerberos authentication (no existing MCP tool) - -**Execution Steps:** - -**Step 1: Verify Kerberos ticket exists** -```bash -klist -``` +**MCP Tool:** `oar_check_blocking_sec_alerts(release)` -If no ticket or ticket expired: -- Report to user: "No valid Kerberos ticket found. Please run: kinit $kid@$domain" -- STOP task execution +**Input:** +- `release`: Z-stream version -**Step 2: Get RHCOS advisory ID** +**Execution:** ```python -metadata = oar_get_release_metadata(release) -rhcos_advisory_id = metadata.advisories.rhcos +result = oar_check_blocking_sec_alerts(release=release) ``` -**Step 3: Fetch security alerts from Errata Tool** -```bash -curl -s -u : --negotiate 'https://errata.devel.redhat.com/api/v1/erratum/{rhcos_advisory_id}/security_alerts' +**Output Parsing:** ``` +IF "BLOCKING SECURITY ALERTS FOUND" in stdout: + Report blocking advisory details to user + Advise user to contact secalert@redhat.com + Task status will be Fail - a blocking issue is added to StateBox -**Step 4: Parse response and check for blocking alerts** -```python -response = json.loads(curl_output) - -# Filter blocking alerts from the alerts array -blocking_alerts = [alert for alert in response.alerts.alerts if alert.blocking == true] - -IF len(blocking_alerts) > 0: - Report to user with alert details and ask to email secalert@redhat.com - # Continue pipeline - this is not a hard blocker, but requires follow-up - -ELSE: - Report to user: "No blocking security alerts found on RHCOS advisory" +IF "No blocking security alerts found" in stdout: + Report: "No blocking security alerts found" + Task status will be Pass ``` **Success Detection:** ``` -Task always passes - this is an informational check -Blocking alerts require manual follow-up but don't stop the pipeline +stdout contains: "task [Check Blocking Security Alerts] status is changed to [Pass]" ``` -**Expected Duration:** 10 seconds - -**Errata Tool API Response Format:** -```json -{ - "alerts": { - "alerts": [ - { - "name": "erratum_missing_notes_link", - "text": "...", - "description": "...", - "how_to_resolve": "...", - "blocking": false - } - ], - "blocking": false - } -} +**Failure Detection:** +``` +stdout contains: "task [Check Blocking Security Alerts] status is changed to [Fail]" ``` -**Key Fields:** -- `.alerts.alerts[]` (array) - List of individual alerts -- `.alerts.alerts[].blocking` (boolean) - Per-alert blocking status (THIS is what we check) -- `.alerts.blocking` (boolean) - Top-level blocking status (informational only) +**Expected Duration:** 30 seconds **Next Action:** - Trigger push-to-cdn-staging (async) diff --git a/mcp_server/server.py b/mcp_server/server.py index ffb7de733bd5..10205dce61ff 100644 --- a/mcp_server/server.py +++ b/mcp_server/server.py @@ -670,6 +670,23 @@ async def oar_check_cve_tracker_bug(release: str, notify: bool = False) -> str: return format_result(result) +@mcp.tool() +async def oar_check_blocking_sec_alerts(release: str) -> str: + """ + Check for blocking security alerts across all RHSA advisories. + + WRITE OPERATION: Creates blocking issue in StateBox when alerts are found. + + Args: + release: Z-stream release version (e.g., "4.19.1") + + Returns: + Blocking security alert check results + """ + result = await invoke_oar_command_async(release, "check-blocking-sec-alerts", []) + return format_result(result) + + @mcp.tool() async def oar_image_signed_check(release: str) -> str: """ diff --git a/oar/cli/cmd_check_blocking_sec_alerts.py b/oar/cli/cmd_check_blocking_sec_alerts.py new file mode 100644 index 000000000000..acc40922bc43 --- /dev/null +++ b/oar/cli/cmd_check_blocking_sec_alerts.py @@ -0,0 +1,87 @@ +import logging + +import click + +from oar.core.advisory import AdvisoryManager +from oar.core.const import * +from oar.core.statebox import StateBox +from oar.core.exceptions import StateBoxException +from oar.core import util + +logger = logging.getLogger(__name__) + + +@click.command() +@click.pass_context +def check_blocking_sec_alerts(ctx): + """ + Check for blocking security alerts across all RHSA advisories. + Reports blocking alerts and updates StateBox task status. + """ + cs = ctx.obj["cs"] + + try: + util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_INPROGRESS) + + am = AdvisoryManager(cs) + statebox = StateBox(cs) + advisories = am.get_advisories() + + blocking_advisories = [] + rhsa_found = 0 + rhsa_checked = 0 + check_errors = {} + + for advisory in advisories: + try: + if advisory.errata_type == "RHSA": + rhsa_found += 1 + if advisory.has_blocking_security_alert(): + blocking_advisories.append(advisory) + rhsa_checked += 1 + except Exception as e: + check_errors[advisory.errata_id] = str(e) + logger.error(f"Error checking advisory {advisory.errata_id}: {e}") + + if rhsa_found == 0: + logger.info("No RHSA advisories found, skipping security alert check") + util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) + return + elif rhsa_checked == 0: + error_details = "; ".join(f"{eid}: {err}" for eid, err in check_errors.items()) + logger.warning(f"Found {rhsa_found} RHSA advisory(ies) but all checks failed: {error_details}") + util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + return + + if blocking_advisories: + logger.warning("BLOCKING SECURITY ALERTS FOUND:") + advisory_details = [] + for advisory in blocking_advisories: + link = util.get_advisory_link(str(advisory.errata_id)) + logger.warning(f" RHSA advisory {advisory.errata_id} - {link}") + advisory_details.append(f"- RHSA advisory {advisory.errata_id}: {link}") + + issue_description = ( + f"Found blocking security alerts in {len(blocking_advisories)} RHSA advisory(ies)\n\n" + f"Affected advisories:\n" + "\n".join(advisory_details) + ) + try: + statebox.add_issue( + issue=issue_description, + blocker=True, + related_tasks=[TASK_CHECK_BLOCKING_SEC_ALERTS], + auto_save=True, + ) + logger.info(f"Created blocking issue in StateBox for {len(blocking_advisories)} RHSA advisory(ies)") + except StateBoxException as e: + logger.warning(f"Could not add StateBox issue (may already exist): {e}") + + util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + else: + logger.info("No blocking security alerts found") + util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) + + except Exception as e: + logger.exception("check blocking sec-alerts failed") + util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + raise diff --git a/oar/cli/cmd_group.py b/oar/cli/cmd_group.py index 8723db133c58..ed8a3b690b36 100644 --- a/oar/cli/cmd_group.py +++ b/oar/cli/cmd_group.py @@ -6,6 +6,7 @@ import oar.core.util as util from oar import version from oar.cli.cmd_change_advisory_status import change_advisory_status +from oar.cli.cmd_check_blocking_sec_alerts import check_blocking_sec_alerts from oar.cli.cmd_check_cve_tracker_bug import check_cve_tracker_bug from oar.cli.cmd_create_test_report import create_test_report from oar.cli.cmd_image_consistency_check import image_consistency_check @@ -227,6 +228,7 @@ def cli(ctx, release, debug): cli.add_command(take_ownership) cli.add_command(image_consistency_check) cli.add_command(check_cve_tracker_bug) +cli.add_command(check_blocking_sec_alerts) cli.add_command(push_to_cdn_staging) cli.add_command(stage_testing) cli.add_command(image_signed_check) diff --git a/oar/core/advisory.py b/oar/core/advisory.py index a68da66f0084..d60a71a10eb3 100644 --- a/oar/core/advisory.py +++ b/oar/core/advisory.py @@ -176,7 +176,7 @@ def change_advisory_status(self, target_status=AD_STATUS_REL_PREP): logger.warning( f"cannot change state of advisory {ad.errata_id} from {target_status} to {ad.get_state()}, skip") continue - if ad.has_blocking_secruity_alert(): + if ad.has_blocking_security_alert(): raise AdvisoryException( f"advisory {ad.errata_id} has blocking secalerts, please contact prodsec team") ad.set_state(target_status.strip()) @@ -654,7 +654,7 @@ def refresh_security_alerts(self): f"RHBA advisory {self.errata_id} does not have secalerts") return None - def has_blocking_secruity_alert(self): + def has_blocking_security_alert(self): """ Check RHSA advisory has blocking security alert """ diff --git a/oar/core/const.py b/oar/core/const.py index 9c2c4d2e3a43..0b6a44b6b6e7 100644 --- a/oar/core/const.py +++ b/oar/core/const.py @@ -64,6 +64,7 @@ "analyze-promoted-build", "check-greenwave-cvp-tests", "check-cve-tracker-bug", + "check-blocking-sec-alerts", "push-to-cdn-staging", "stage-testing", "image-signed-check", @@ -80,6 +81,7 @@ "analyze-candidate-build", "analyze-promoted-build", "check-cve-tracker-bug", + "check-blocking-sec-alerts", "push-to-cdn-staging", "stage-testing", "image-signed-check", @@ -108,6 +110,7 @@ TASK_IMAGE_SIGNED_CHECK = "image-signed-check" TASK_DROP_BUGS = "drop-bugs" TASK_CHANGE_ADVISORY_STATUS = "change-advisory-status" +TASK_CHECK_BLOCKING_SEC_ALERTS = "check-blocking-sec-alerts" # Task to human-readable display name mapping TASK_DISPLAY_NAMES = { @@ -122,6 +125,7 @@ TASK_IMAGE_SIGNED_CHECK: "Image Signed Check", TASK_DROP_BUGS: "Drop Bugs", TASK_CHANGE_ADVISORY_STATUS: "Change Advisory Status", + TASK_CHECK_BLOCKING_SEC_ALERTS: "Check Blocking Security Alerts", } # env variables diff --git a/tests/test_advisory.py b/tests/test_advisory.py index af5753d17826..2988b9f54be3 100644 --- a/tests/test_advisory.py +++ b/tests/test_advisory.py @@ -1,9 +1,14 @@ import unittest +from unittest.mock import Mock, patch, call + +from click.testing import CliRunner from oar.core.advisory import Advisory from oar.core.advisory import AdvisoryManager from oar.core.configstore import ConfigStore from oar.core.const import * +from oar.core.exceptions import StateBoxException +from oar.cli.cmd_check_blocking_sec_alerts import check_blocking_sec_alerts class TestAdvisoryManager(unittest.TestCase): @@ -68,7 +73,7 @@ def test_get_security_alerts(self): self.me = AdvisoryManager(ConfigStore("4.12.61")) ads = self.me.get_advisories() for ad in ads: - self.assertFalse(ad.has_blocking_secruity_alert(), f"advisory {ad.errata_id} has blocking security alerts") + self.assertFalse(ad.has_blocking_security_alert(), f"advisory {ad.errata_id} has blocking security alerts") def test_kernel_tag(self): self.assertTrue(Advisory(errata_id=144853, impetus='image').check_kernel_tag()) @@ -79,3 +84,84 @@ def test_kernel_tag(self): def test_finished_jiras(self): self.assertTrue(self.am.has_finished_all_advisories_jiras()) + +class TestCheckBlockingSecAlerts(unittest.TestCase): + def setUp(self): + self.runner = CliRunner() + self.mock_cs = Mock(spec=ConfigStore) + + def _make_advisory(self, errata_id, errata_type, has_blocking=False, raises=None): + ad = Mock() + ad.errata_id = errata_id + ad.errata_type = errata_type + if raises: + ad.has_blocking_security_alert.side_effect = raises + else: + ad.has_blocking_security_alert.return_value = has_blocking + return ad + + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_no_blocking_alerts(self, mock_am_cls, mock_sb_cls, mock_util): + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(11111, "RHSA", has_blocking=False), + ] + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) + mock_sb_cls.return_value.add_issue.assert_not_called() + + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_blocking_alert_found(self, mock_am_cls, mock_sb_cls, mock_util): + mock_util.get_advisory_link.return_value = "https://errata.devel.redhat.com/advisory/11111" + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(11111, "RHSA", has_blocking=True), + ] + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + mock_sb_cls.return_value.add_issue.assert_called_once() + call_kwargs = mock_sb_cls.return_value.add_issue.call_args + self.assertTrue(call_kwargs[1]["blocker"]) + self.assertIn(TASK_CHECK_BLOCKING_SEC_ALERTS, call_kwargs[1]["related_tasks"]) + + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_no_rhsa_advisories(self, mock_am_cls, mock_sb_cls, mock_util): + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(22222, "RHBA"), + ] + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) + mock_sb_cls.return_value.add_issue.assert_not_called() + + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_all_checks_fail_with_exception(self, mock_am_cls, mock_sb_cls, mock_util): + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(33333, "RHSA", raises=ConnectionError("Errata API unreachable")), + ] + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + mock_sb_cls.return_value.add_issue.assert_not_called() + + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_statebox_add_issue_raises(self, mock_am_cls, mock_sb_cls, mock_util): + mock_util.get_advisory_link.return_value = "https://errata.devel.redhat.com/advisory/44444" + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(44444, "RHSA", has_blocking=True), + ] + mock_sb_cls.return_value.add_issue.side_effect = StateBoxException("duplicate blocker") + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + From f7fe1d593cc10f1a832eda65d0bace6707e67596 Mon Sep 17 00:00:00 2001 From: Jayashree Huttanagoudar Date: Fri, 24 Jul 2026 15:31:21 +0530 Subject: [PATCH 2/3] Resolve stale StateBox blockers on re-run and handle partial check failures in sec-alerts command --- .claude/skills/release-workflow/SKILL.md | 6 ++-- oar/cli/cmd_check_blocking_sec_alerts.py | 46 ++++++++++++++---------- tests/test_advisory.py | 19 +++++++++- 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/.claude/skills/release-workflow/SKILL.md b/.claude/skills/release-workflow/SKILL.md index dd4878eefd25..c50a4bd780dc 100644 --- a/.claude/skills/release-workflow/SKILL.md +++ b/.claude/skills/release-workflow/SKILL.md @@ -348,7 +348,7 @@ result = oar_check_blocking_sec_alerts(release=release) ``` **Output Parsing:** -``` +```text IF "BLOCKING SECURITY ALERTS FOUND" in stdout: Report blocking advisory details to user Advise user to contact secalert@redhat.com @@ -360,12 +360,12 @@ IF "No blocking security alerts found" in stdout: ``` **Success Detection:** -``` +```text stdout contains: "task [Check Blocking Security Alerts] status is changed to [Pass]" ``` **Failure Detection:** -``` +```text stdout contains: "task [Check Blocking Security Alerts] status is changed to [Fail]" ``` diff --git a/oar/cli/cmd_check_blocking_sec_alerts.py b/oar/cli/cmd_check_blocking_sec_alerts.py index acc40922bc43..ce40aac25d28 100644 --- a/oar/cli/cmd_check_blocking_sec_alerts.py +++ b/oar/cli/cmd_check_blocking_sec_alerts.py @@ -3,7 +3,12 @@ import click from oar.core.advisory import AdvisoryManager -from oar.core.const import * +from oar.core.const import ( + TASK_CHECK_BLOCKING_SEC_ALERTS, + TASK_STATUS_FAIL, + TASK_STATUS_INPROGRESS, + TASK_STATUS_PASS, +) from oar.core.statebox import StateBox from oar.core.exceptions import StateBoxException from oar.core import util @@ -27,31 +32,32 @@ def check_blocking_sec_alerts(ctx): statebox = StateBox(cs) advisories = am.get_advisories() + rhsa_advisories = [ad for ad in advisories if ad.errata_type == "RHSA"] + if not rhsa_advisories: + logger.info("No RHSA advisories found, skipping security alert check") + util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) + return + + logger.info(f"Found {len(rhsa_advisories)} RHSA advisory(ies) to check: " + f"{', '.join(str(ad.errata_id) for ad in rhsa_advisories)}") + blocking_advisories = [] - rhsa_found = 0 - rhsa_checked = 0 check_errors = {} - for advisory in advisories: + for advisory in rhsa_advisories: try: - if advisory.errata_type == "RHSA": - rhsa_found += 1 - if advisory.has_blocking_security_alert(): - blocking_advisories.append(advisory) - rhsa_checked += 1 + if advisory.has_blocking_security_alert(): + blocking_advisories.append(advisory) except Exception as e: check_errors[advisory.errata_id] = str(e) logger.error(f"Error checking advisory {advisory.errata_id}: {e}") - if rhsa_found == 0: - logger.info("No RHSA advisories found, skipping security alert check") - util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) - return - elif rhsa_checked == 0: - error_details = "; ".join(f"{eid}: {err}" for eid, err in check_errors.items()) - logger.warning(f"Found {rhsa_found} RHSA advisory(ies) but all checks failed: {error_details}") - util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) - return + existing_blocker = statebox.get_task_blocker(TASK_CHECK_BLOCKING_SEC_ALERTS) + if existing_blocker: + statebox.resolve_issue( + issue=existing_blocker["issue"], + resolution="Re-checked blocking security alerts", + ) if blocking_advisories: logger.warning("BLOCKING SECURITY ALERTS FOUND:") @@ -76,6 +82,10 @@ def check_blocking_sec_alerts(ctx): except StateBoxException as e: logger.warning(f"Could not add StateBox issue (may already exist): {e}") + util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + elif check_errors: + error_details = "; ".join(f"{eid}: {err}" for eid, err in check_errors.items()) + logger.warning(f"Security-alert checks failed for: {error_details}") util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) else: logger.info("No blocking security alerts found") diff --git a/tests/test_advisory.py b/tests/test_advisory.py index 2988b9f54be3..c5f602d726e2 100644 --- a/tests/test_advisory.py +++ b/tests/test_advisory.py @@ -6,7 +6,12 @@ from oar.core.advisory import Advisory from oar.core.advisory import AdvisoryManager from oar.core.configstore import ConfigStore -from oar.core.const import * +from oar.core.const import ( + AD_STATUS_DROPPED_NO_SHIP, + TASK_CHECK_BLOCKING_SEC_ALERTS, + TASK_STATUS_FAIL, + TASK_STATUS_PASS, +) from oar.core.exceptions import StateBoxException from oar.cli.cmd_check_blocking_sec_alerts import check_blocking_sec_alerts @@ -152,6 +157,18 @@ def test_all_checks_fail_with_exception(self, mock_am_cls, mock_sb_cls, mock_uti mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) mock_sb_cls.return_value.add_issue.assert_not_called() + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_mixed_success_and_error_fails(self, mock_am_cls, mock_sb_cls, mock_util): + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(11111, "RHSA", has_blocking=False), + self._make_advisory(22222, "RHSA", raises=ConnectionError("Errata API unreachable")), + ] + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') From 9ca1b5653231417dda7554a0d0f76d185c18f774 Mon Sep 17 00:00:00 2001 From: Jayashree Huttanagoudar Date: Mon, 27 Jul 2026 13:37:35 +0530 Subject: [PATCH 3/3] Resolve stale StateBox blockers on re-run and handle partial check failures in sec-alerts command --- oar/cli/cmd_check_blocking_sec_alerts.py | 47 ++++++++++----- tests/test_advisory.py | 76 ++++++++++++++++++++++++ 2 files changed, 107 insertions(+), 16 deletions(-) diff --git a/oar/cli/cmd_check_blocking_sec_alerts.py b/oar/cli/cmd_check_blocking_sec_alerts.py index ce40aac25d28..63b0a637a30e 100644 --- a/oar/cli/cmd_check_blocking_sec_alerts.py +++ b/oar/cli/cmd_check_blocking_sec_alerts.py @@ -52,12 +52,9 @@ def check_blocking_sec_alerts(ctx): check_errors[advisory.errata_id] = str(e) logger.error(f"Error checking advisory {advisory.errata_id}: {e}") - existing_blocker = statebox.get_task_blocker(TASK_CHECK_BLOCKING_SEC_ALERTS) - if existing_blocker: - statebox.resolve_issue( - issue=existing_blocker["issue"], - resolution="Re-checked blocking security alerts", - ) + existing_issues = statebox.get_issues( + unresolved_only=True, task_name=TASK_CHECK_BLOCKING_SEC_ALERTS + ) if blocking_advisories: logger.warning("BLOCKING SECURITY ALERTS FOUND:") @@ -71,16 +68,28 @@ def check_blocking_sec_alerts(ctx): f"Found blocking security alerts in {len(blocking_advisories)} RHSA advisory(ies)\n\n" f"Affected advisories:\n" + "\n".join(advisory_details) ) - try: - statebox.add_issue( - issue=issue_description, - blocker=True, - related_tasks=[TASK_CHECK_BLOCKING_SEC_ALERTS], - auto_save=True, - ) - logger.info(f"Created blocking issue in StateBox for {len(blocking_advisories)} RHSA advisory(ies)") - except StateBoxException as e: - logger.warning(f"Could not add StateBox issue (may already exist): {e}") + + existing_blocker = next( + (i for i in existing_issues if i.get("blocker", False)), None + ) + if existing_blocker and existing_blocker["issue"].strip().lower() == issue_description.strip().lower(): + logger.info("Blocking security alerts unchanged from previous check") + else: + if existing_blocker: + statebox.resolve_issue( + issue=existing_blocker["issue"], + resolution="Replaced with updated blocking security alert findings", + ) + try: + statebox.add_issue( + issue=issue_description, + blocker=True, + related_tasks=[TASK_CHECK_BLOCKING_SEC_ALERTS], + auto_save=True, + ) + logger.info(f"Created blocking issue in StateBox for {len(blocking_advisories)} RHSA advisory(ies)") + except StateBoxException as e: + logger.warning(f"Could not add StateBox issue: {e}") util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) elif check_errors: @@ -88,6 +97,12 @@ def check_blocking_sec_alerts(ctx): logger.warning(f"Security-alert checks failed for: {error_details}") util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) else: + for issue in existing_issues: + statebox.resolve_issue( + issue=issue["issue"], + resolution="No blocking security alerts found on re-check", + ) + logger.info(f"Resolved previous StateBox issue: {issue['issue'][:80]}") logger.info("No blocking security alerts found") util.log_task_status(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) diff --git a/tests/test_advisory.py b/tests/test_advisory.py index c5f602d726e2..7ebf88958ad1 100644 --- a/tests/test_advisory.py +++ b/tests/test_advisory.py @@ -109,6 +109,7 @@ def _make_advisory(self, errata_id, errata_type, has_blocking=False, raises=None @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') def test_no_blocking_alerts(self, mock_am_cls, mock_sb_cls, mock_util): + mock_sb_cls.return_value.get_issues.return_value = [] mock_am_cls.return_value.get_advisories.return_value = [ self._make_advisory(11111, "RHSA", has_blocking=False), ] @@ -122,6 +123,7 @@ def test_no_blocking_alerts(self, mock_am_cls, mock_sb_cls, mock_util): @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') def test_blocking_alert_found(self, mock_am_cls, mock_sb_cls, mock_util): mock_util.get_advisory_link.return_value = "https://errata.devel.redhat.com/advisory/11111" + mock_sb_cls.return_value.get_issues.return_value = [] mock_am_cls.return_value.get_advisories.return_value = [ self._make_advisory(11111, "RHSA", has_blocking=True), ] @@ -149,6 +151,7 @@ def test_no_rhsa_advisories(self, mock_am_cls, mock_sb_cls, mock_util): @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') def test_all_checks_fail_with_exception(self, mock_am_cls, mock_sb_cls, mock_util): + mock_sb_cls.return_value.get_issues.return_value = [] mock_am_cls.return_value.get_advisories.return_value = [ self._make_advisory(33333, "RHSA", raises=ConnectionError("Errata API unreachable")), ] @@ -161,6 +164,7 @@ def test_all_checks_fail_with_exception(self, mock_am_cls, mock_sb_cls, mock_uti @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') def test_mixed_success_and_error_fails(self, mock_am_cls, mock_sb_cls, mock_util): + mock_sb_cls.return_value.get_issues.return_value = [] mock_am_cls.return_value.get_advisories.return_value = [ self._make_advisory(11111, "RHSA", has_blocking=False), self._make_advisory(22222, "RHSA", raises=ConnectionError("Errata API unreachable")), @@ -174,6 +178,7 @@ def test_mixed_success_and_error_fails(self, mock_am_cls, mock_sb_cls, mock_util @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') def test_statebox_add_issue_raises(self, mock_am_cls, mock_sb_cls, mock_util): mock_util.get_advisory_link.return_value = "https://errata.devel.redhat.com/advisory/44444" + mock_sb_cls.return_value.get_issues.return_value = [] mock_am_cls.return_value.get_advisories.return_value = [ self._make_advisory(44444, "RHSA", has_blocking=True), ] @@ -182,3 +187,74 @@ def test_statebox_add_issue_raises(self, mock_am_cls, mock_sb_cls, mock_util): self.assertEqual(result.exit_code, 0) mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_rerun_pass_resolves_existing_blocker(self, mock_am_cls, mock_sb_cls, mock_util): + mock_sb_cls.return_value.get_issues.return_value = [ + { + "issue": "Found blocking security alerts in 1 RHSA advisory(ies)", + "blocker": True, + "resolved": False, + }, + ] + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(11111, "RHSA", has_blocking=False), + ] + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) + mock_sb_cls.return_value.resolve_issue.assert_called_once() + mock_sb_cls.return_value.add_issue.assert_not_called() + + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_rerun_same_blocker_skips_update(self, mock_am_cls, mock_sb_cls, mock_util): + mock_util.get_advisory_link.return_value = "https://errata.devel.redhat.com/advisory/11111" + issue_text = ( + "Found blocking security alerts in 1 RHSA advisory(ies)\n\n" + "Affected advisories:\n" + "- RHSA advisory 11111: https://errata.devel.redhat.com/advisory/11111" + ) + mock_sb_cls.return_value.get_issues.return_value = [ + { + "issue": issue_text, + "blocker": True, + "resolved": False, + }, + ] + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(11111, "RHSA", has_blocking=True), + ] + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_FAIL) + mock_sb_cls.return_value.add_issue.assert_not_called() + mock_sb_cls.return_value.resolve_issue.assert_not_called() + + @patch('oar.cli.cmd_check_blocking_sec_alerts.util') + @patch('oar.cli.cmd_check_blocking_sec_alerts.StateBox') + @patch('oar.cli.cmd_check_blocking_sec_alerts.AdvisoryManager') + def test_rerun_pass_resolves_all_issues(self, mock_am_cls, mock_sb_cls, mock_util): + mock_sb_cls.return_value.get_issues.return_value = [ + { + "issue": "Found blocking security alerts in 1 RHSA advisory(ies)", + "blocker": True, + "resolved": False, + }, + { + "issue": "Prow job timeout during sec-alert check", + "blocker": False, + "resolved": False, + }, + ] + mock_am_cls.return_value.get_advisories.return_value = [ + self._make_advisory(11111, "RHSA", has_blocking=False), + ] + result = self.runner.invoke(check_blocking_sec_alerts, obj={"cs": self.mock_cs}, catch_exceptions=False) + self.assertEqual(result.exit_code, 0) + mock_util.log_task_status.assert_any_call(TASK_CHECK_BLOCKING_SEC_ALERTS, TASK_STATUS_PASS) + self.assertEqual(mock_sb_cls.return_value.resolve_issue.call_count, 2) + mock_sb_cls.return_value.add_issue.assert_not_called() +