diff --git a/.github/workflows/k8s-tests.yml b/.github/workflows/k8s-tests.yml index 3bb01d6171e..dc30f685793 100644 --- a/.github/workflows/k8s-tests.yml +++ b/.github/workflows/k8s-tests.yml @@ -27,7 +27,7 @@ jobs: # are tested (https://docs.aws.amazon.com/eks/latest/userguide/kubernetes-versions.html#available-versions) - databases: pgsql brokers: redis - k8s: 'v1.34.1' + k8s: 'v1.34.0' os: debian steps: - name: Checkout diff --git a/docs/content/en/changelog/changelog.md b/docs/content/en/changelog/changelog.md index 2cc71a0a6f7..f45d0a04005 100644 --- a/docs/content/en/changelog/changelog.md +++ b/docs/content/en/changelog/changelog.md @@ -10,6 +10,10 @@ For Open Source release notes, please see the [Releases page on GitHub](https:// ## Sept 2025: v2.50 +### Sept 15, 2025: v2.50.3 + +* **(Pro UI)** Added support for [CVSSv4.0](https://www.first.org/cvss/v4-0/) vector strings. + ### Sept 15, 2025: v2.50.2 * **(Pro UI)** Added Any/All status filtering. Filtering by status allows you to apply either AND (inner join) logic, or OR (outer join) logic to the filter. diff --git a/dojo/forms.py b/dojo/forms.py index a460a09f722..5e7c2026edc 100644 --- a/dojo/forms.py +++ b/dojo/forms.py @@ -343,10 +343,6 @@ class ProductForm(forms.ModelForm): product_manager = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) technical_contact = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) team_manager = forms.ModelChoiceField(queryset=Dojo_User.objects.exclude(is_active=False).order_by("first_name", "last_name"), required=False) - tags = TagField( - required=False, - help_text="Add tags that help describe this product. Choose from the list or add new tags. Press Enter key to add.", - ) def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -1018,10 +1014,6 @@ class EngForm(forms.ModelForm): queryset=None, required=True, label="Testing Lead") test_strategy = forms.URLField(required=False, label="Test Strategy URL") - tags = TagField( - required=False, - help_text="Add tags that help describe this engagement. Choose from the list or add new tags. Press Enter key to add.", - ) def __init__(self, *args, **kwargs): cicd = False @@ -1104,10 +1096,6 @@ class TestForm(forms.ModelForm): lead = forms.ModelChoiceField( queryset=None, required=False, label="Testing Lead") - tags = TagField( - required=False, - help_text="Add tags that help describe this test. Choose from the list or add new tags. Press Enter key to add.", - ) def __init__(self, *args, **kwargs): obj = None @@ -1464,10 +1452,6 @@ class FindingForm(forms.ModelForm): choices=EFFORT_FOR_FIXING_CHOICES, error_messages={ "invalid_choice": EFFORT_FOR_FIXING_INVALID_CHOICE}) - tags = TagField( - required=False, - help_text="Add tags that help describe this finding. Choose from the list or add new tags. Press Enter key to add.", - ) # the only reliable way without hacking internal fields to get predicatble ordering is to make it explicit field_order = ("title", "group", "date", "sla_start_date", "sla_expiration_date", "cwe", "vulnerability_ids", "severity", "cvss_info", "cvssv3", @@ -1739,11 +1723,6 @@ class Meta: class EditEndpointForm(forms.ModelForm): - tags = TagField( - required=False, - help_text="Add tags that help describe this endpoint. Choose from the list or add new tags. Press Enter key to add.", - ) - class Meta: model = Endpoint exclude = ["product", "inherited_tags"] diff --git a/dojo/jira_link/helper.py b/dojo/jira_link/helper.py index a9bae4b762d..9dbbd6deeee 100644 --- a/dojo/jira_link/helper.py +++ b/dojo/jira_link/helper.py @@ -1401,14 +1401,16 @@ def update_epic(engagement, **kwargs): jira = get_jira_connection(jira_instance) j_issue = get_jira_issue(engagement) issue = jira.issue(j_issue.jira_id) - epic_name = kwargs.get("epic_name") if not epic_name: epic_name = engagement.name - + description = epic_name + branch_tag = engagement.branch_tag + if branch_tag: + description += "\nBranch: " + branch_tag jira_issue_update_kwargs = { "summary": epic_name, - "description": epic_name, + "description": description, } if (epic_priority := kwargs.get("epic_priority")) is not None: jira_issue_update_kwargs["priority"] = {"name": epic_priority} @@ -1443,12 +1445,16 @@ def add_epic(engagement, **kwargs): epic_issue_type_name = getattr(jira_project, "epic_issue_type_name", "Epic") if not epic_name: epic_name = engagement.name + description = epic_name + branch_tag = engagement.branch_tag + if branch_tag: + description += "\nBranch: " + branch_tag issue_dict = { "project": { "key": jira_project.project_key, }, "summary": epic_name, - "description": epic_name, + "description": description, "issuetype": { "name": epic_issue_type_name, }, diff --git a/dojo/tools/api_sonarqube/api_client.py b/dojo/tools/api_sonarqube/api_client.py index 9c04ceea648..91a7673c812 100644 --- a/dojo/tools/api_sonarqube/api_client.py +++ b/dojo/tools/api_sonarqube/api_client.py @@ -334,6 +334,31 @@ def get_hotspot_rule(self, rule_id): self.rules_cache.update({rule_id: rule}) return rule + def get_hotspot(self, rule_id): + """ + Get detailed information about a hotspot + :param rule_id: + :return: + """ + rule = self.rules_cache.get(rule_id) + if not rule: + response = self.session.get( + url=f"{self.sonar_api_url}/hotspots/search", + params={"hotspots": rule_id}, + headers=self.default_headers, + timeout=settings.REQUESTS_TIMEOUT, + ) + if not response.ok: + msg = ( + f"Unable to get the hotspot rule {rule_id} " + f"due to {response.status_code} - {response.content}" + ) + raise Exception(msg) + + rule = response.json()["hotspots"][0] + self.rules_cache.update({rule_id: rule}) + return rule + def transition_issue(self, issue_key, transition): """ Do workflow transition on an issue. Requires authentication and Browse permission on project. @@ -375,6 +400,46 @@ def transition_issue(self, issue_key, transition): ) raise Exception(msg) + def transition_hotspot(self, issue_key, status, resolution=None): + """ + Do workflow transition on an issue. Requires authentication and Browse permission on project. + The transitions 'wontfix' and 'falsepositive' require the permission 'Administer Issues'. + The transitions involving security hotspots (except 'requestreview') require + the permission 'Administer Security Hotspot'. + + Possible resolution values: + - FIXED + - SAFE + - ACKNOWLEDGED + + Possible status values: + - TO_REVIEW + - REVIEWED + + :param issue_key: + :param status: + :param resolution: + :return: + """ + data = {"hotspot": issue_key, "status": status} + + if resolution: + data["resolution"] = resolution + + response = self.session.post( + url=f"{self.sonar_api_url}/hotspots/change_status", + data=data, + headers=self.default_headers, + timeout=settings.REQUESTS_TIMEOUT, + ) + + if not response.ok: + msg = ( + f"Unable to change status {status} / resolution {resolution} the issue {issue_key} " + f'due to {response.status_code} - {response.content.decode("utf-8")}' + ) + raise Exception(msg) + def add_comment(self, issue_key, text): """ Add a comment. diff --git a/dojo/tools/api_sonarqube/updater.py b/dojo/tools/api_sonarqube/updater.py index 18d547fea05..860c1bbb491 100644 --- a/dojo/tools/api_sonarqube/updater.py +++ b/dojo/tools/api_sonarqube/updater.py @@ -52,6 +52,45 @@ class SonarQubeApiUpdater: }, ] + MAPPING_SONARQUBE_HOTSPOT_STATUS_TRANSITION = [ + { + "from": ["TO_REVIEW"], + "to": "RESOLVED / FALSE-POSITIVE", + "transition": "REVIEWED", + "resolution": "SAFE", + }, + { + "from": ["TO_REVIEW"], + "to": "RESOLVED / FIXED", + "transition": "REVIEWED", + "resolution": "FIXED", + }, + { + "from": ["TO_REVIEW"], + "to": "RESOLVED / WONTFIX", + "transition": "REVIEWED", + "resolution": "ACKNOWLEDGED", + }, + { + "from": ["REVIEWED"], + "to": "OPEN", + "transition": "TO_REVIEW", + "resolution": None, + }, + { + "from": ["REVIEWED"], + "to": "REOPENED", + "transition": "TO_REVIEW", + "resolution": None, + }, + { + "from": ["REVIEWED"], + "to": "CONFIRMED", + "transition": "TO_REVIEW", + "resolution": None, + }, + ] + @staticmethod def get_sonarqube_status_for(finding): target_status = None @@ -66,16 +105,22 @@ def get_sonarqube_status_for(finding): return target_status def get_sonarqube_required_transitions_for( - self, current_status, target_status, - ): + self, current_status, target_status, is_hotspot): # If current and target is the same... do nothing if current_status == target_status: return None + # Select the appropriate mapping based on issue type + mapping = ( + self.MAPPING_SONARQUBE_HOTSPOT_STATUS_TRANSITION + if is_hotspot + else self.MAPPING_SONARQUBE_STATUS_TRANSITION + ) + # Check if there is at least one transition from current_status... if not [ x - for x in self.MAPPING_SONARQUBE_STATUS_TRANSITION + for x in mapping if current_status in x.get("from") ]: return None @@ -84,7 +129,7 @@ def get_sonarqube_required_transitions_for( # can transition to target_status transitions = [ x - for x in self.MAPPING_SONARQUBE_STATUS_TRANSITION + for x in mapping if target_status == x.get("to") ] if transitions: @@ -92,26 +137,38 @@ def get_sonarqube_required_transitions_for( # There is a direct transition from current status... if current_status in transition.get("from"): t = transition.get("transition") + if is_hotspot: + return [{"status": t, "resolution": transition.get("resolution")}] if t else None return [t] if t else None - # We have the last transition to get to our target status but there - # is no direct transition - transitions_result = deque() - transitions_result.appendleft(transitions[0].get("transition")) + # Handle complex transitions for regular issues + if not is_hotspot: + # We have the last transition to get to our target status but there + # is no direct transition + transitions_result = deque() + transitions_result.appendleft(transitions[0].get("transition")) - # Find out previous transitions that would finish in any FROM of a - # previous to use as target - for transition in transitions: - for t_from in transition.get("from"): - possible_transition = ( - self.get_sonarqube_required_transitions_for( - current_status, t_from, + # Find out previous transitions that would finish in any FROM of a + # previous to use as target + for transition in transitions: + for t_from in transition.get("from"): + possible_transition = ( + self.get_sonarqube_required_transitions_for( + current_status, t_from, is_hotspot, + ) ) - ) - if possible_transition: - transitions_result.extendleft(possible_transition) - return list(transitions_result) - return None + if possible_transition: + transitions_result.extendleft(possible_transition) + return list(transitions_result) + else: + # SQ code is too complicated for ISSUES, there is no such thing for HOTSPOTS, + # there are only 2 states: TO_REVIEW and REVIEWED + transitions_result = deque() + transitions_result.appendleft( + {"status": transitions[0].get("transition"), + "resolution": transitions[0].get("resolution")}, + ) + return list(transitions_result) return None def update_sonarqube_finding(self, finding): @@ -128,44 +185,56 @@ def update_sonarqube_finding(self, finding): # during import target_status = self.get_sonarqube_status_for(finding) + is_hotspot = sonarqube_issue.type == "SECURITY_HOTSPOT" - issue = client.get_issue(sonarqube_issue.key) - if ( - issue - ): # Issue could have disappeared in SQ because a previous scan has resolved the issue as fixed - if issue.get("resolution"): - current_status = "{} / {}".format( - issue.get("status"), issue.get("resolution"), - ) - else: - current_status = issue.get("status") + issue = client.get_hotspot(sonarqube_issue.key) if is_hotspot else client.get_issue(sonarqube_issue.key) + + # Issue does not exist (could have disappeared in SQ because a previous scan resolved it) + if not issue: + return + + if is_hotspot: + current_status = issue.get("status") + elif issue.get("resolution"): + current_status = "{} / {}".format(issue.get("status"), issue.get("resolution")) + else: + current_status = issue.get("status") + # Get required transitions + transitions = self.get_sonarqube_required_transitions_for( + current_status, target_status, is_hotspot=is_hotspot, + ) + + if not transitions: logger.debug( "--> SQ Current status: %s. Current target status: %s", current_status, target_status, ) + return - transitions = self.get_sonarqube_required_transitions_for( - current_status, target_status, + logger.debug( + f"Updating finding '{finding}' transition {current_status} -> {target_status} in SonarQube", ) - if transitions: - logger.info( - "Updating finding '%s' in SonarQube", finding, - ) - for transition in transitions: - client.transition_issue(sonarqube_issue.key, transition) - - # Track Defect Dojo has updated the SonarQube issue - Sonarqube_Issue_Transition.objects.create( - sonarqube_issue=finding.sonarqube_issue, - # not sure if this is needed, but looks like the original author decided to send display status - # to sonarqube we changed Accepted into Risk Accepted, but we change it back to be sure we don't - # break the integration - finding_status=finding.status().replace( - "Risk Accepted", "Accepted", - ) - if finding.status() - else finding.status(), - sonarqube_status=current_status, - transitions=",".join(transitions), - ) + # Apply transitions + for transition in transitions: + if is_hotspot: + client.transition_hotspot(sonarqube_issue.key, + status=transition["status"], + resolution=transition["resolution"]) + else: + client.transition_issue(sonarqube_issue.key, transition) + + # Track that Defect Dojo has updated the SonarQube issue + Sonarqube_Issue_Transition.objects.create( + sonarqube_issue=finding.sonarqube_issue, + # not sure if this is needed, but looks like the original author decided to send display status + # to sonarqube we changed Accepted into Risk Accepted, but we change it back to be sure we don't + # break the integration + finding_status=finding.status().replace( + "Risk Accepted", "Accepted", + ) + if finding.status() + else finding.status(), + sonarqube_status=current_status, + transitions=",".join(transition["status"] if is_hotspot else transition for transition in transitions), + ) diff --git a/dojo/tools/blackduck_binary_analysis/parser.py b/dojo/tools/blackduck_binary_analysis/parser.py index 312ad018566..1a2087487b2 100644 --- a/dojo/tools/blackduck_binary_analysis/parser.py +++ b/dojo/tools/blackduck_binary_analysis/parser.py @@ -96,12 +96,14 @@ def ingest_findings(self, sorted_findings, test): component_version=i.version, unique_id_from_tool=unique_finding_key, ) - if cvss_v3: finding.cvssv3 = cvss_vectors else: finding.severity_justification = cvss_vectors - + if i.latest_version: + finding.fix_available = True + else: + finding.fix_available = False findings[unique_finding_key] = finding return list(findings.values()) diff --git a/dojo/tools/sonarqube/sonarqube_restapi_json.py b/dojo/tools/sonarqube/sonarqube_restapi_json.py index 205a5e14b1b..f56f362209c 100644 --- a/dojo/tools/sonarqube/sonarqube_restapi_json.py +++ b/dojo/tools/sonarqube/sonarqube_restapi_json.py @@ -13,7 +13,7 @@ def get_json_items(self, json_content, test, mode): rule = issue.get("rule") component = issue.get("component") project = issue.get("project") - line = str(issue.get("line")) + line = issue.get("line") textRange = issue.get("textRange", {}) flows = issue.get("flows", []) status = issue.get("status") @@ -28,7 +28,7 @@ def get_json_items(self, json_content, test, mode): description += "**rule:** " + rule + "\n" description += "**component:** " + component + "\n" description += "**project:** " + project + "\n" - description += "**line:** " + line + "\n" + description += "**line:** " + str(line) + "\n" if bool(textRange): res = [item + ": " + str(textRange[item]) for item in textRange] description += "**textRange:** " + ", ".join(res) + "\n" @@ -49,6 +49,7 @@ def get_json_items(self, json_content, test, mode): static_finding=True, dynamic_finding=False, tags=["bug"], + line=line, ) elif issue.get("type") == "VULNERABILITY": key = issue.get("key") @@ -58,6 +59,7 @@ def get_json_items(self, json_content, test, mode): flows = issue.get("flows", []) status = issue.get("status") message = issue.get("message") + line = issue.get("line") cwe = None if "Category: CWE-" in message: cwe_pattern = r"Category: CWE-\d{1,5}" @@ -91,6 +93,7 @@ def get_json_items(self, json_content, test, mode): description += "**rule:** " + rule + "\n" description += "**component:** " + component + "\n" description += "**project:** " + project + "\n" + description += "**line:** " + str(line) + "\n" if flows != []: description += "**flows:** " + str(flows) + "\n" description += "**status:** " + status + "\n" @@ -115,6 +118,7 @@ def get_json_items(self, json_content, test, mode): cvssv3_score=cvss, file_path=component, tags=["vulnerability"], + line=line, ) vulnids = [] if "Reference: CVE" in message: @@ -141,7 +145,7 @@ def get_json_items(self, json_content, test, mode): rule = issue.get("rule") component = issue.get("component") project = issue.get("project") - line = str(issue.get("line")) + line = issue.get("line") textRange = issue.get("textRange", {}) flows = issue.get("flows", []) status = issue.get("status") @@ -154,7 +158,7 @@ def get_json_items(self, json_content, test, mode): description += "**rule:** " + rule + "\n" description += "**component:** " + component + "\n" description += "**project:** " + project + "\n" - description += "**line:** " + line + "\n" + description += "**line:** " + str(line) + "\n" if bool(textRange): res = [] for item in textRange: @@ -180,6 +184,7 @@ def get_json_items(self, json_content, test, mode): dynamic_finding=False, file_path=component, tags=["code_smell"], + line=line, ) items.append(item) if json_content.get("hotspots"): @@ -189,7 +194,7 @@ def get_json_items(self, json_content, test, mode): project = hotspot.get("project") securityCategory = hotspot.get("securityCategory") status = hotspot.get("status") - line = str(hotspot.get("line")) + line = hotspot.get("line") message = hotspot.get("message") textRange = hotspot.get("textRange", {}) flows = hotspot.get("flows", []) @@ -201,7 +206,7 @@ def get_json_items(self, json_content, test, mode): description += "**project:** " + project + "\n" description += "**securityCategory:** " + securityCategory + "\n" description += "**status:** " + status + "\n" - description += "**line:** " + line + "\n" + description += "**line:** " + str(line) + "\n" description += "**message:** " + message + "\n" if bool(textRange): res = [] @@ -223,6 +228,7 @@ def get_json_items(self, json_content, test, mode): dynamic_finding=False, file_path=component, tags=["hotspot"], + line=line, ) items.append(item) return items diff --git a/dojo/utils.py b/dojo/utils.py index 858d24c6bb6..20d8a8ff82d 100644 --- a/dojo/utils.py +++ b/dojo/utils.py @@ -1602,7 +1602,8 @@ def calculate_grade(product, *args, **kwargs): product.prod_numeric_grade = prod_numeric_grade super(Product, product).save() else: - logger.debug("Product %s grade %i is up to date", product.id, prod_numeric_grade) + # Use %s to safely handle None grades without formatter errors + logger.debug("Product %s grade %s is up to date", product.id, prod_numeric_grade) def get_celery_worker_status(): diff --git a/requirements.txt b/requirements.txt index 27e6c513689..15909aaa28f 100644 --- a/requirements.txt +++ b/requirements.txt @@ -10,7 +10,7 @@ django-dbbackup==5.0.0 django-environ==0.12.0 django-filter==25.1 django-imagekit==5.0.0 -django-multiselectfield==0.1.13 +django-multiselectfield==1.0.1 django-polymorphic==4.1.0 django-crispy-forms==2.4 django_extensions==4.1 diff --git a/unittests/test_api_sonarqube_updater.py b/unittests/test_api_sonarqube_updater.py index 52ee6ed8f9f..4fb6377e784 100644 --- a/unittests/test_api_sonarqube_updater.py +++ b/unittests/test_api_sonarqube_updater.py @@ -12,86 +12,147 @@ def setUp(self): self.updater = SonarQubeApiUpdater() - def test_transitions_for_sonarqube_from_open_1(self): + def test_transitions_for_sonarqube_issue_from_open_to_confirmed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("OPEN", "CONFIRMED"), + self.updater.get_sonarqube_required_transitions_for("OPEN", "CONFIRMED", is_hotspot=False), ["confirm"], ) - def test_transitions_for_sonarqube_from_open_2(self): + def test_transitions_for_sonarqube_issue_from_open_to_resolved_fixed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("OPEN", "RESOLVED / FIXED"), + self.updater.get_sonarqube_required_transitions_for("OPEN", "RESOLVED / FIXED", is_hotspot=False), ["resolve"], ) - def test_transitions_for_sonarqube_from_reopened_1(self): + def test_transitions_for_sonarqube_issue_from_reopened_to_resolved_fixed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("REOPENED", "RESOLVED / FIXED"), + self.updater.get_sonarqube_required_transitions_for("REOPENED", "RESOLVED / FIXED", is_hotspot=False), ["resolve"], ) - def test_transitions_for_sonarqube_from_reopened_2(self): + def test_transitions_for_sonarqube_issue_from_reopened_to_confirmed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("REOPENED", "CONFIRMED"), + self.updater.get_sonarqube_required_transitions_for("REOPENED", "CONFIRMED", is_hotspot=False), ["confirm"], ) - def test_transitions_for_sonarqube_from_resolved_1(self): + def test_transitions_for_sonarqube_issue_from_resolved_fixed_to_confirmed(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "CONFIRMED"), + self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "CONFIRMED", is_hotspot=False), ["reopen", "confirm"], ) - def test_transitions_for_sonarqube_from_resolved_2(self): + def test_transitions_for_sonarqube_issue_from_resolved_fixed_to_resolved_falsepositive(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "RESOLVED / FALSE-POSITIVE"), + self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "RESOLVED / FALSE-POSITIVE", is_hotspot=False), ["reopen", "falsepositive"], ) - def test_transitions_for_sonarqube_from_resolved_3(self): + def test_transitions_for_sonarqube_issue_from_resolved_fixed_to_resolved_wontfix(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "RESOLVED / WONTFIX"), + self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "RESOLVED / WONTFIX", is_hotspot=False), ["reopen", "wontfix"], ) - def test_transitions_for_sonarqube_fake_target_origin(self): + def test_transitions_for_sonarqube_issue_from_confirmed_to_reopened(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("FAKE_STATUS", "RESOLVED / FIXED"), + self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "REOPENED", is_hotspot=False), + ["unconfirm"], + ) + + def test_transitions_for_sonarqube_issue_from_confirmed_to_resolved_fixed(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "RESOLVED / FIXED", is_hotspot=False), + ["resolve"], + ) + + def test_transitions_for_sonarqube_issue_from_confirmed_to_resolved_wontfix(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "RESOLVED / WONTFIX", is_hotspot=False), + ["wontfix"], + ) + + def test_transitions_for_sonarqube_issue_from_confirmed_to_resolved_falsepositive(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "RESOLVED / FALSE-POSITIVE", is_hotspot=False), + ["falsepositive"], + ) + + def test_transitions_for_sonarqube_issue_open_reopen_status_same(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("OPEN", "OPEN", is_hotspot=False), None, ) - def test_transitions_for_sonarqube_fake_target_status(self): + def test_transitions_for_sonarqube_issue_open_reopen_status_different(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "FAKE_STATUS"), + self.updater.get_sonarqube_required_transitions_for("OPEN", "REOPENED", is_hotspot=False), None, ) - def test_transitions_for_sonarqube_from_confirmed_1(self): + def test_transitions_for_sonarqube_issue_fake_status(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "REOPENED"), - ["unconfirm"], + self.updater.get_sonarqube_required_transitions_for("FAKE_STATUS", "RESOLVED / FIXED", is_hotspot=False), + None, ) - def test_transitions_for_sonarqube_from_confirmed_2(self): + def test_transitions_for_sonarqube_issue_fake_target(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("CONFIRMED", "RESOLVED / FIXED"), - ["resolve"], + self.updater.get_sonarqube_required_transitions_for("RESOLVED / FIXED", "FAKE_STATUS", is_hotspot=False), + None, + ) + + # Tests for hotspot transitions + def test_transitions_for_sonarqube_hotspot_from_to_review_to_resolved_falsepositive(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "RESOLVED / FALSE-POSITIVE", is_hotspot=True), + [{"status": "REVIEWED", "resolution": "SAFE"}], + ) + + def test_transitions_for_sonarqube_hotspot_from_to_review_to_resolved_fixed(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "RESOLVED / FIXED", is_hotspot=True), + [{"status": "REVIEWED", "resolution": "FIXED"}], + ) + + def test_transitions_for_sonarqube_hotspot_from_to_review_to_resolved_wontfix(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "RESOLVED / WONTFIX", is_hotspot=True), + [{"status": "REVIEWED", "resolution": "ACKNOWLEDGED"}], + ) + + def test_transitions_for_sonarqube_hotspot_from_reviewed_to_open(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("REVIEWED", "OPEN", is_hotspot=True), + [{"status": "TO_REVIEW", "resolution": None}], + ) + + def test_transitions_for_sonarqube_hotspot_from_reviewed_to_reopened(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("REVIEWED", "REOPENED", is_hotspot=True), + [{"status": "TO_REVIEW", "resolution": None}], + ) + + def test_transitions_for_sonarqube_hotspot_from_reviewed_to_confirmed(self): + self.assertEqual( + self.updater.get_sonarqube_required_transitions_for("REVIEWED", "CONFIRMED", is_hotspot=True), + [{"status": "TO_REVIEW", "resolution": None}], ) - def test_transitions_for_open_reopen_status_1(self): + def test_transitions_for_sonarqube_hotspot_same_status(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("OPEN", "REOPENED"), + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "TO_REVIEW", is_hotspot=True), None, ) - def test_transitions_for_open_reopen_status_2(self): + def test_transitions_for_sonarqube_hotspot_fake_status(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("REOPENED", "OPEN"), + self.updater.get_sonarqube_required_transitions_for("FAKE_STATUS", "REVIEWED", is_hotspot=True), None, ) - def test_transitions_for_open_reopen_status_3(self): + def test_transitions_for_sonarqube_hotspot_fake_target(self): self.assertEqual( - self.updater.get_sonarqube_required_transitions_for("REOPENED", "REOPENED"), + self.updater.get_sonarqube_required_transitions_for("TO_REVIEW", "FAKE_STATUS", is_hotspot=True), None, ) diff --git a/unittests/tools/test_blackduck_binary_analysis_parser.py b/unittests/tools/test_blackduck_binary_analysis_parser.py index d378de0567d..4d74c8914b7 100644 --- a/unittests/tools/test_blackduck_binary_analysis_parser.py +++ b/unittests/tools/test_blackduck_binary_analysis_parser.py @@ -22,26 +22,21 @@ def test_parse_one_vuln(self): "instrument.dll: zlib 1.2.13 Vulnerable to CVE-2023-45853", finding.title, ) - + self.assertEqual(True, finding.fix_available) self.assertIsNotNone(finding.description) self.assertIsNotNone(finding.severity) self.assertEqual("Critical", finding.severity) - self.assertIsNotNone(finding.component_name) self.assertEqual("zlib", finding.component_name) - self.assertIsNotNone(finding.component_version) self.assertEqual("1.2.13", finding.component_version) - self.assertIsNotNone(finding.file_path) self.assertEqual( "JRE.msi:JRE.msi-30276-90876123.cab:instrument.dll", finding.file_path, ) - self.assertIsNotNone(finding.vuln_id_from_tool) self.assertEqual("CVE-2023-45853", finding.vuln_id_from_tool) - self.assertIsNotNone(finding.unique_id_from_tool) def test_parse_many_vulns(self):