Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/content/issue_tracking/jira/OS__jira_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@ Here is an example of a **jira\_full** Issue:

#### Component

If you manage your Jira Space using Components, you can assign the appropriate Component for DefectDojo here.
If you manage your Jira Space using Components, you can assign the appropriate Component for DefectDojo here. To assign more than one Component, enter a comma-separated list (for example, `Security, DevSecOps`); each value is sent to Jira as a separate component.

**Custom fields**

Expand Down
2 changes: 1 addition & 1 deletion docs/content/issue_tracking/jira/PRO__jira_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ Here is an example of a **jira\_full** Issue:

#### Component

If you manage your Jira Space using Components, you can assign the appropriate Component for DefectDojo here.
If you manage your Jira Space using Components, you can assign the appropriate Component for DefectDojo here. To assign more than one Component, enter a comma-separated list (for example, `Security, DevSecOps`); each value is sent to Jira as a separate component.

#### Custom fields

Expand Down
10 changes: 9 additions & 1 deletion docs/content/releases/os_upgrading/3.1.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: 'Upgrading to DefectDojo Version 3.1.x'
toc_hide: true
weight: -20260617
description: Blank Finding components are now normalized to NULL so component-less findings group together.
description: Blank Finding components are now normalized to NULL so component-less findings group together; JIRA project configurations now support multiple comma-separated components.
---

## Blank Finding components normalized to NULL
Expand All @@ -14,3 +14,11 @@ Findings without a component now consistently store `NULL`. Blank values are nor
### What you need to do

Nothing — the change is applied automatically by the database migration included in this release. After upgrading, component-less findings will group together under a single "None" entry.

## Multiple JIRA components per project

The `Component` field on a JIRA project configuration now supports assigning more than one Jira component. Separate multiple component names with a comma (for example, `Security,DevSecOps`), and each name is sent to Jira as a distinct component. A single value without commas continues to behave as before.

### What you need to do

Nothing is required. Note that a component name that legitimately contains a comma will be split into separate components — component names rarely contain commas, so this is an accepted limitation.
5 changes: 5 additions & 0 deletions dojo/jira/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,11 @@ def __init__(self, *args, **kwargs):
self.engagement = kwargs.pop("engagement", None)
super().__init__(*args, **kwargs)

self.fields["component"].help_text = (
"Comma-separate multiple components to assign more than one to the JIRA issue, "
"e.g. 'Security, DevSecOps'."
)

logger.debug("self.target: %s, self.product: %s, self.instance: %s", self.target, self.product, self.instance)
logger.debug("data: %s", self.data)
if self.target == "engagement":
Expand Down
7 changes: 6 additions & 1 deletion dojo/jira/helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -867,7 +867,12 @@ def prepare_jira_issue_fields(
}

if component_name:
fields["components"] = [{"name": component_name}]
# The component field holds a comma-separated list of component names, so split it
# into the list of components Jira expects ([{"name": "A"}, {"name": "B"}]). A single
# value without commas yields a single component. (SC-13173)
components = [{"name": name.strip()} for name in component_name.split(",") if name.strip()]
if components:
fields["components"] = components

if custom_fields:
fields.update(custom_fields)
Expand Down
46 changes: 46 additions & 0 deletions unittests/test_jira_helper.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,49 @@ def test_issue_from_jira_is_active_with_unknown_status(self):
def test_issue_from_jira_is_active_defaults_to_active_on_missing_attribute(self):
"""AttributeError anywhere in the fields.status.statusCategory.key chain defaults to active."""
self.assertTrue(jira_helper.issue_from_jira_is_active(Mock(spec=[])))


class JIRAComponentFieldTest(TestCase):

"""
SC-13173: the JIRA project `component` field holds a comma-separated list of
component names. prepare_jira_issue_fields must split it into multiple Jira
components so Jira receives [{"name": "A"}, {"name": "B"}] instead of a single
component named "A,B".
"""

def _fields(self, component_name):
return jira_helper.prepare_jira_issue_fields(
project_key="PROJ",
issuetype_name="Bug",
summary="summary",
description="description",
component_name=component_name,
)

def test_single_component(self):
fields = self._fields("Security")
self.assertEqual([{"name": "Security"}], fields["components"])

def test_multiple_components_split_on_comma(self):
fields = self._fields("Security,DevSecOps")
self.assertEqual([{"name": "Security"}, {"name": "DevSecOps"}], fields["components"])

def test_multiple_components_whitespace_trimmed(self):
fields = self._fields("Security, DevSecOps , Platform")
self.assertEqual(
[{"name": "Security"}, {"name": "DevSecOps"}, {"name": "Platform"}],
fields["components"],
)

def test_empty_entries_dropped(self):
fields = self._fields("Security,,DevSecOps,")
self.assertEqual([{"name": "Security"}, {"name": "DevSecOps"}], fields["components"])

def test_no_component_omits_field(self):
fields = self._fields("")
self.assertNotIn("components", fields)

def test_only_separators_omits_field(self):
fields = self._fields(" , , ")
self.assertNotIn("components", fields)
Loading