Skip to content

Commit 9858da2

Browse files
wip
1 parent dd78b6b commit 9858da2

44 files changed

Lines changed: 1910 additions & 57584 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

dojo/jira_link/helper.py

Lines changed: 39 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -119,27 +119,25 @@ def _safely_get_obj_status_for_jira(obj: Finding | Finding_Group, *, isenforced:
119119
return obj.status
120120

121121
if isinstance(obj, Finding_Group):
122-
jira_minimum_threshold = 0
123-
if System_Settings.objects.get().jira_minimum_severity:
124-
jira_minimum_threshold = Finding.get_numerical_severity(System_Settings.objects.get().jira_minimum_severity)
125-
126-
findings = obj.findings.all().filter(numerical_severity__gte=jira_minimum_threshold)
122+
findings = get_finding_group_findings_above_threshold(obj)
127123
if not findings:
128124
return ["Empty"]
129125

126+
for find in findings:
127+
logger.error(f"Finding {find.id} status {find.active} {find.verified} {find.is_mitigated}")
128+
130129
# This iterates 3 times over the list of findings, but any code doing 1 iteration would looke it's from 1990
131-
has_active = any(find.active for find in findings)
132-
has_verified = any(find.verified for find in findings)
133-
all_mitigated = all(find.is_mitigated for find in findings) if findings else False
130+
if any(find.active for find in findings):
131+
status += ["Active"]
132+
133+
if any((find.active and find.verified) for find in findings):
134+
status += ["Verified"]
134135

135-
if has_active:
136-
status.append("Active")
137-
if has_verified:
138-
status.append("Verified")
139-
if all_mitigated:
140-
status.append("Mitigated")
136+
if all(find.is_mitigated for find in findings):
137+
status += ["Mitigated", "Inactive"]
141138

142-
return status
139+
# if no active findings are found, we must assume the status is inactive
140+
return status or ["Inactive"]
143141

144142

145143
# checks if a finding can be pushed to JIRA
@@ -193,6 +191,7 @@ def can_be_pushed_to_jira(obj, form=None):
193191
return False, f"Finding below the minimum JIRA severity threshold ({System_Settings.objects.get().jira_minimum_severity}).", "error_below_minimum_threshold"
194192
elif isinstance(obj, Finding_Group):
195193
finding_group_status = _safely_get_obj_status_for_jira(obj)
194+
logger.error(f"Finding group status: {finding_group_status}")
196195
if "Empty" in finding_group_status:
197196
return False, f"{to_str_typed(obj)} cannot be pushed to jira as it contains no findings above minimum treshold.", "error_empty"
198197

@@ -673,7 +672,17 @@ def jira_description(obj):
673672

674673

675674
def jira_priority(obj):
676-
return get_jira_instance(obj).get_priority(obj.severity)
675+
if isinstance(obj, Finding):
676+
return get_jira_instance(obj).get_priority(obj.severity)
677+
if isinstance(obj, Finding_Group):
678+
finding_group_severity_for_jira = get_finding_group_findings_above_threshold(obj)
679+
680+
max_number_severity = max(Finding.get_number_severity(find.severity) for find in finding_group_severity_for_jira)
681+
return Finding.get_severity(max_number_severity)
682+
683+
logger.error("unsupported object passed to push_to_jira: %s %i %s", obj.__name__, obj.id, obj)
684+
msg = f"Unsupported object passed to push_to_jira: {type(obj)}"
685+
raise RuntimeError(msg)
677686

678687

679688
def jira_environment(obj):
@@ -1122,8 +1131,7 @@ def push_status_to_jira(obj, jira_instance, jira, issue, *, save=False):
11221131
status_list = _safely_get_obj_status_for_jira(obj)
11231132
issue_closed = False
11241133
updated = False
1125-
logger.debug("item check: %s", "Active" in "Active") # noqa: PLR0133
1126-
logger.debug("any item resolved: %s", any(item in status_list for item in RESOLVED_STATUS))
1134+
logger.debug("pushing status to JIRA for %d:%s status:%s", obj.id, to_str_typed(obj), status_list)
11271135
# check RESOLVED_STATUS first to avoid corner cases with findings that are Inactive, but verified
11281136
if not updated and any(item in status_list for item in RESOLVED_STATUS):
11291137
if issue_from_jira_is_active(issue):
@@ -1777,3 +1785,16 @@ def save_and_push_to_jira(finding):
17771785
# the updated data of the finding is pushed as part of the group
17781786
if push_to_jira_decision and finding_in_group:
17791787
push_to_jira(finding.finding_group)
1788+
1789+
1790+
def get_finding_group_findings_above_threshold(finding_group):
1791+
"""Get the findings that are above the minimum threshold"""
1792+
jira_minimum_threshold = 0
1793+
if System_Settings.objects.get().jira_minimum_severity:
1794+
jira_minimum_threshold = Finding.get_numerical_severity(System_Settings.objects.get().jira_minimum_severity)
1795+
1796+
findings = finding_group.findings.filter(numerical_severity__lte=jira_minimum_threshold)
1797+
# TODO: JIRA REMOVE
1798+
logger.error(findings.query)
1799+
logger.error(f"count: {findings.count()}")
1800+
return findings

dojo/templates/issue-trackers/jira_full/jira-finding-group-description.tpl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,14 @@ A group of Findings has been pushed to JIRA to be investigated and fixed:
1010
h2. Group
1111
*Group*: [{{ finding_group.name|jiraencode}}|{{ finding_group_url|full_url }}] in [{{ finding_group.test.engagement.product.name|jiraencode }}|{{ product_url|full_url }}] / [{{ finding_group.test.engagement.name|jiraencode }}|{{ engagement_url|full_url }}] / [{{ finding_group.test|stringformat:'s'|jiraencode }}|{{ test_url|full_url }}]
1212

13-
13+
# TODO: JIRA only include active/verified findings above threshold
1414
|| Severity || CVE || CWE || Component || Version || Title || Status ||{% for finding in finding_group.findings.all %}
1515
| {{finding.severity}} | {% if finding.cve %}[{{finding.cve}}|{{finding.cve|vulnerability_url}}]{% else %}None{% endif %} | [{{finding.cwe}}|{{finding.cwe|cwe_url}}] | {{finding.component_name|jiraencode_component}} | {{finding.component_version}} | {% url 'view_finding' finding.id as finding_url %}[{{ finding.title|jiraencode}}|{{ finding_url|full_url }}] | {{ finding.status }} |{% endfor %}
1616

17+
# TODO: JIRA only include active/verified findings above threshold
1718
*Severity:* {{ finding_group.severity }}
1819

20+
# TODO: JIRA only include active/verified findings above threshold
1921
{% if finding_group.sla_deadline %} *Due Date:* {{ finding_group.sla_deadline }} {% endif %}
2022

2123
{% if finding_group.test.engagement.branch_tag %}

run-unittest.sh

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
#!/usr/bin/env bash
2+
set -x
23

34
unset TEST_CASE
45

@@ -51,7 +52,7 @@ then
5152
fi
5253

5354
echo "Running docker compose unit tests with test case $TEST_CASE ..."
54-
# Compose V2 integrates compose functions into the Docker platform, continuing to support
55-
# most of the previous docker-compose features and flags. You can run Compose V2 by
55+
# Compose V2 integrates compose functions into the Docker platform, continuing to support
56+
# most of the previous docker-compose features and flags. You can run Compose V2 by
5657
# replacing the hyphen (-) with a space, using docker compose, instead of docker-compose.
57-
docker compose exec uwsgi bash -c "python manage.py test $TEST_CASE -v2 --keepdb"
58+
docker compose exec -e JIRA_PAT_DD=$JIRA_PAT_DD=$JIRA_PAT_DD=$JIRA_PAT_DD uwsgi bash -c "python manage.py test $TEST_CASE -v2 --keepdb --failfast"

unittests/dojo_test_case.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,26 @@ def wrapper(*args, **kwargs):
6565
return decorator
6666

6767

68+
def with_system_setting(field, value):
69+
"""Decorator to temporarily set a value in System Settings."""
70+
71+
def decorator(test_func):
72+
@wraps(test_func)
73+
def wrapper(*args, **kwargs):
74+
old_value = getattr(System_Settings.objects.get(), field)
75+
# Set the flag to the specified value
76+
System_Settings.objects.update(**{field: value})
77+
try:
78+
return test_func(*args, **kwargs)
79+
finally:
80+
# Reset the flag to its original state after the test
81+
System_Settings.objects.update(**{field: old_value})
82+
83+
return wrapper
84+
85+
return decorator
86+
87+
6888
class DojoTestUtilsMixin:
6989

7090
def get_test_admin(self, *args, **kwargs):

unittests/scans/npm_audit/many_vuln_with_groups.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@
188188
"recommendation": "Update to version 0.6.1 or later.",
189189
"references": "",
190190
"access": "public",
191-
"severity": "high",
191+
"severity": "moderate",
192192
"cwe": "CWE-400",
193193
"metadata": {
194194
"module_type": "Network.Library",
@@ -317,7 +317,7 @@
317317
"recommendation": "Update to version 0.5.2 or later.",
318318
"references": "",
319319
"access": "public",
320-
"severity": "high",
320+
"severity": "moderate",
321321
"cwe": "CWE-400",
322322
"metadata": {
323323
"module_type": "Multi.Library",
@@ -359,7 +359,7 @@
359359
"recommendation": "* Version 2.x.x: Update to version 2.11.2 or later.\n* Version 3.x.x: Update to version 3.6.4 or later.\n* Version 4.x.x: Update to version 4.5.7 or later.\n* Version 5.x.x: Update to version 5.2.1 or later.\n* Version 6.x.x: Update to version 6.4.2 or later. ( Note that versions 6.1.6, 6.2.5, and 6.3.3 are also patched. )\n* Version 7.x.x: Update to version 7.1.2 or later. ( Note that version 7.0.2 is also patched. )",
360360
"references": "[Node Postgres: Code Execution Vulnerability Announcement](https://node-postgres.com/announcements#2017-08-12-code-execution-vulnerability)",
361361
"access": "public",
362-
"severity": "high",
362+
"severity": "moderate",
363363
"cwe": "CWE-94",
364364
"metadata": {
365365
"module_type": "Network.Library",

unittests/test_jira_import_and_pushing_api.py

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
get_unit_tests_path,
1717
get_unit_tests_scans_path,
1818
toggle_system_setting_boolean,
19+
with_system_setting,
1920
)
2021

2122
logger = logging.getLogger(__name__)
@@ -49,12 +50,12 @@ def __init__(self, *args, **kwargs):
4950
DojoVCRAPITestCase.__init__(self, *args, **kwargs)
5051

5152
def assert_cassette_played(self):
52-
if True: # set to True when committing. set to False when recording new test cassettes
53+
if False: # set to True when committing. set to False when recording new test cassettes
5354
self.assertTrue(self.cassette.all_played)
5455

5556
def _get_vcr(self, **kwargs):
5657
my_vcr = super()._get_vcr(**kwargs)
57-
my_vcr.record_mode = "once"
58+
my_vcr.record_mode = "all"
5859
my_vcr.path_transformer = VCR.ensure_suffix(".yaml")
5960
my_vcr.filter_headers = ["Authorization", "X-Atlassian-Token"]
6061
my_vcr.cassette_library_dir = str(get_unit_tests_path() / "vcr" / "jira")
@@ -69,6 +70,7 @@ def setUp(self):
6970
self.testuser = User.objects.get(username="admin")
7071
self.testuser.usercontactinfo.block_execution = True
7172
self.testuser.usercontactinfo.save()
73+
7274
token = Token.objects.get(user=self.testuser)
7375
self.client = APIClient()
7476
self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
@@ -106,6 +108,29 @@ def test_import_with_groups_push_to_jira(self):
106108
# by asserting full cassette is played we know issues have been updated in JIRA
107109
self.assert_cassette_played()
108110

111+
@with_system_setting("jira_minimum_severity", "Critical")
112+
def test_import_with_groups_push_to_jira_minimum_critical(self):
113+
# No Critical findings in report, so expect no groups to be pushed
114+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=True)
115+
test_id = import0["test"]
116+
# all findings should be in a group, so no JIRA issues for individual findings
117+
self.assert_jira_issue_count_in_test(test_id, 0)
118+
self.assert_jira_group_issue_count_in_test(test_id, 0)
119+
# by asserting full cassette is played we know issues have been updated in JIRA
120+
self.assert_cassette_played()
121+
122+
@with_system_setting("jira_minimum_severity", "High")
123+
def test_import_with_groups_push_to_jira_minimum_high(self):
124+
# 7 findings, 5 unique component_name+component_version
125+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=True)
126+
test_id = import0["test"]
127+
# all findings should be in a group, so no JIRA issues for individual findings
128+
self.assert_jira_issue_count_in_test(test_id, 0)
129+
# fresh library has only medium findings, so only 2 instead of 3 groups expected
130+
self.assert_jira_group_issue_count_in_test(test_id, 2)
131+
# by asserting full cassette is played we know issues have been updated in JIRA
132+
self.assert_cassette_played()
133+
109134
def test_import_with_push_to_jira_epic_as_issue_type(self):
110135
jira_instance = JIRA_Instance.objects.get(id=2)
111136
# we choose issue type Epic and test if it can be created successfully.
@@ -456,7 +481,7 @@ def test_groups_create_edit_update_finding(self):
456481
del finding_details["push_to_jira"]
457482

458483
# push a finding should result in pushing the group instead
459-
self.patch_finding_api(findings["results"][0]["id"], {"push_to_jira": True})
484+
self.patch_finding_api(findings["results"][0]["id"], {"push_to_jira": True, "verified": True})
460485

461486
self.assert_jira_issue_count_in_test(test_id, 0)
462487
self.assert_jira_group_issue_count_in_test(test_id, 1)
@@ -628,7 +653,12 @@ def test_import_with_push_to_jira_not_verified_enforced_verified_globally_true_e
628653
import0 = self.import_scan_with_params(self.zap_sample5_filename, push_to_jira=True, verified=False)
629654
test_id = import0["test"]
630655
# This scan file has two active findings, so we should not push either of them
631-
self.assert_jira_issue_count_in_test(test_id, 0)
656+
self.assert_jira_group_issue_count_in_test(test_id, 0)
657+
658+
import0 = self.import_scan_with_params(self.zap_sample5_filename, push_to_jira=True, verified=True)
659+
test_id = import0["test"]
660+
self.assert_jira_group_issue_count_in_test(test_id, 2)
661+
632662
# by asserting full cassette is played we know all calls to JIRA have been made as expected
633663
self.assert_cassette_played()
634664

@@ -639,7 +669,12 @@ def test_import_with_push_to_jira_not_verified_enforced_verified_globally_true_e
639669
test_id = import0["test"]
640670
# This scan file has two active findings, so we should not push either of them
641671
self.assert_jira_issue_count_in_test(test_id, 0)
672+
673+
import0 = self.import_scan_with_params(self.zap_sample5_filename, push_to_jira=True, verified=True)
674+
test_id = import0["test"]
675+
self.assert_jira_issue_count_in_test(test_id, 2)
642676
# by asserting full cassette is played we know all calls to JIRA have been made as expected
677+
643678
self.assert_cassette_played()
644679

645680
@toggle_system_setting_boolean("enforce_verified_status", False) # noqa: FBT003
@@ -649,6 +684,11 @@ def test_import_with_push_to_jira_not_verified_enforced_verified_globally_false_
649684
test_id = import0["test"]
650685
# This scan file has two active findings, so we should not push either of them
651686
self.assert_jira_issue_count_in_test(test_id, 0)
687+
688+
import0 = self.import_scan_with_params(self.zap_sample5_filename, push_to_jira=True, verified=True)
689+
test_id = import0["test"]
690+
self.assert_jira_issue_count_in_test(test_id, 2)
691+
652692
# by asserting full cassette is played we know all calls to JIRA have been made as expected
653693
self.assert_cassette_played()
654694

@@ -662,6 +702,61 @@ def test_import_with_push_to_jira_not_verified_enforced_verified_globally_false_
662702
# by asserting full cassette is played we know all calls to JIRA have been made as expected
663703
self.assert_cassette_played()
664704

705+
@toggle_system_setting_boolean("enforce_verified_status", True) # noqa: FBT003
706+
@toggle_system_setting_boolean("enforce_verified_status_jira", True) # noqa: FBT003
707+
def test_groups_import_with_push_to_jira_not_verified_enforced_verified_globally_true_enforced_verified_jira_true(self):
708+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=False)
709+
test_id = import0["test"]
710+
# No verified findings, means no groups pushed to JIRA
711+
self.assert_jira_group_issue_count_in_test(test_id, 0)
712+
713+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=True)
714+
test_id = import0["test"]
715+
self.assert_jira_group_issue_count_in_test(test_id, 3)
716+
717+
# by asserting full cassette is played we know all calls to JIRA have been made as expected
718+
self.assert_cassette_played()
719+
720+
@toggle_system_setting_boolean("enforce_verified_status", True) # noqa: FBT003
721+
@toggle_system_setting_boolean("enforce_verified_status_jira", False) # noqa: FBT003
722+
def test_groups_import_with_push_to_jira_not_verified_enforced_verified_globally_true_enforced_verified_jira_false(self):
723+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=False)
724+
test_id = import0["test"]
725+
# No verified findings, means no groups pushed to JIRA
726+
self.assert_jira_group_issue_count_in_test(test_id, 0)
727+
728+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=True)
729+
test_id = import0["test"]
730+
self.assert_jira_group_issue_count_in_test(test_id, 3)
731+
# by asserting full cassette is played we know all calls to JIRA have been made as expected
732+
733+
self.assert_cassette_played()
734+
735+
@toggle_system_setting_boolean("enforce_verified_status", False) # noqa: FBT003
736+
@toggle_system_setting_boolean("enforce_verified_status_jira", True) # noqa: FBT003
737+
def test_groups_import_with_push_to_jira_not_verified_enforced_verified_globally_false_enforced_verified_jira_true(self):
738+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=False)
739+
test_id = import0["test"]
740+
# No verified findings, means no groups pushed to JIRA
741+
self.assert_jira_group_issue_count_in_test(test_id, 0)
742+
743+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=True)
744+
test_id = import0["test"]
745+
self.assert_jira_group_issue_count_in_test(test_id, 3)
746+
747+
# by asserting full cassette is played we know all calls to JIRA have been made as expected
748+
self.assert_cassette_played()
749+
750+
@toggle_system_setting_boolean("enforce_verified_status", False) # noqa: FBT003
751+
@toggle_system_setting_boolean("enforce_verified_status_jira", False) # noqa: FBT003
752+
@with_system_setting("jira_minimum_severity", "Low")
753+
def test_groups_import_with_push_to_jira_not_verified_enforced_verified_globally_false_enforced_verified_jira_false(self):
754+
import0 = self.import_scan_with_params(self.npm_groups_sample_filename, scan_type="NPM Audit Scan", group_by="component_name+component_version", push_to_jira=True, verified=True)
755+
test_id = import0["test"]
756+
self.assert_jira_group_issue_count_in_test(test_id, 3)
757+
# by asserting full cassette is played we know all calls to JIRA have been made as expected
758+
self.assert_cassette_played()
759+
665760
def test_engagement_epic_creation(self):
666761
eng = self.get_engagement(3)
667762
# Set epic_mapping to true

0 commit comments

Comments
 (0)