regscale-cli 6.25.1.0__py3-none-any.whl → 6.27.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of regscale-cli might be problematic. Click here for more details.
- regscale/_version.py +1 -1
- regscale/airflow/hierarchy.py +2 -2
- regscale/core/app/application.py +19 -4
- regscale/core/app/internal/evidence.py +419 -2
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- regscale/dev/code_gen.py +24 -20
- regscale/integrations/commercial/jira.py +367 -126
- regscale/integrations/commercial/qualys/__init__.py +7 -8
- regscale/integrations/commercial/qualys/scanner.py +8 -3
- regscale/integrations/commercial/sicura/api.py +14 -13
- regscale/integrations/commercial/sicura/commands.py +8 -2
- regscale/integrations/commercial/sicura/scanner.py +49 -39
- regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
- regscale/integrations/commercial/synqly/assets.py +17 -0
- regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
- regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
- regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
- regscale/integrations/commercial/tenablev2/commands.py +142 -1
- regscale/integrations/commercial/tenablev2/scanner.py +0 -1
- regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
- regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
- regscale/integrations/commercial/wizv2/click.py +64 -79
- regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
- regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
- regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
- regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
- regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
- regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
- regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
- regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
- regscale/integrations/commercial/wizv2/issue.py +1 -1
- regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
- regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
- regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
- regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
- regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
- regscale/integrations/commercial/wizv2/reports.py +1 -1
- regscale/integrations/commercial/wizv2/sbom.py +1 -1
- regscale/integrations/commercial/wizv2/scanner.py +39 -99
- regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
- regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
- regscale/integrations/commercial/wizv2/variables.py +89 -3
- regscale/integrations/compliance_integration.py +60 -41
- regscale/integrations/control_matcher.py +377 -0
- regscale/integrations/due_date_handler.py +14 -8
- regscale/integrations/milestone_manager.py +291 -0
- regscale/integrations/public/__init__.py +1 -0
- regscale/integrations/public/cci_importer.py +37 -38
- regscale/integrations/public/fedramp/click.py +60 -2
- regscale/integrations/public/fedramp/docx_parser.py +10 -1
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
- regscale/integrations/public/fedramp/fedramp_five.py +1 -1
- regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +277 -153
- regscale/models/integration_models/cisa_kev_data.json +282 -9
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/qualys.py +3 -4
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
- regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +47 -22
- regscale/models/regscale_models/issue.py +256 -95
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
- tests/regscale/integrations/commercial/__init__.py +0 -0
- tests/regscale/integrations/commercial/conftest.py +28 -0
- tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
- tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
- tests/regscale/integrations/commercial/test_aws.py +3731 -0
- tests/regscale/integrations/commercial/test_burp.py +48 -0
- tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
- tests/regscale/integrations/commercial/test_dependabot.py +341 -0
- tests/regscale/integrations/commercial/test_gcp.py +1543 -0
- tests/regscale/integrations/commercial/test_gitlab.py +549 -0
- tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
- tests/regscale/integrations/commercial/test_jira.py +2204 -0
- tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
- tests/regscale/integrations/commercial/test_okta.py +1228 -0
- tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
- tests/regscale/integrations/commercial/test_sicura.py +350 -0
- tests/regscale/integrations/commercial/test_snow.py +423 -0
- tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
- tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
- tests/regscale/integrations/commercial/test_stig.py +33 -0
- tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
- tests/regscale/integrations/commercial/test_stigv2.py +406 -0
- tests/regscale/integrations/commercial/test_wiz.py +1365 -0
- tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
- tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
- tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
- tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
- tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
- tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
- tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
- tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
- tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
- tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
- tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
- tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
- tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
- tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
- tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
- tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
- tests/regscale/integrations/public/fedramp/__init__.py +1 -0
- tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
- tests/regscale/integrations/public/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +1397 -0
- tests/regscale/integrations/test_control_matching.py +155 -0
- tests/regscale/integrations/test_milestone_manager.py +408 -0
- tests/regscale/models/test_issue.py +378 -1
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Test for GitLab integration in RegScale CLI
|
|
5
|
+
"""
|
|
6
|
+
import unittest
|
|
7
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
8
|
+
|
|
9
|
+
from regscale.integrations.commercial.gitlab import (
|
|
10
|
+
# run_sync_issues,
|
|
11
|
+
get_issues_from_gitlab,
|
|
12
|
+
get_regscale_issues,
|
|
13
|
+
save_or_update_issues,
|
|
14
|
+
extract_links_with_labels,
|
|
15
|
+
convert_issues,
|
|
16
|
+
)
|
|
17
|
+
from regscale.models.regscale_models.issue import Issue
|
|
18
|
+
from regscale.models.regscale_models.link import Link
|
|
19
|
+
from tests import CLITestFixture
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class TestGitLabIntegration(CLITestFixture, unittest.TestCase):
|
|
23
|
+
@patch("regscale.integrations.commercial.gitlab.check_license")
|
|
24
|
+
@patch("regscale.integrations.commercial.gitlab.Issue")
|
|
25
|
+
@patch("regscale.integrations.commercial.gitlab.logger")
|
|
26
|
+
def test_get_regscale_issues_securityplans(self, mock_logger, mock_issue, mock_check_license):
|
|
27
|
+
# Mocking the check_license function
|
|
28
|
+
mock_app = MagicMock()
|
|
29
|
+
mock_check_license.return_value = mock_app
|
|
30
|
+
|
|
31
|
+
# Mocking the fetch_issues_by_ssp method and setting return value
|
|
32
|
+
mock_issue.fetch_issues_by_ssp.return_value = ["issue1", "issue2"]
|
|
33
|
+
|
|
34
|
+
# Mocking the job_progress object
|
|
35
|
+
mock_job_progress = MagicMock()
|
|
36
|
+
mock_task = MagicMock()
|
|
37
|
+
mock_job_progress.add_task.return_value = mock_task
|
|
38
|
+
|
|
39
|
+
# Call the method to test
|
|
40
|
+
issues = get_regscale_issues(1, "securityplans", mock_job_progress)
|
|
41
|
+
|
|
42
|
+
# Assertions to check if the results are as expected
|
|
43
|
+
self.assertEqual(len(issues), 2)
|
|
44
|
+
mock_logger.info.assert_called_with("Fetched 2 issues from RegScale by SSP.")
|
|
45
|
+
mock_job_progress.add_task.assert_called_once()
|
|
46
|
+
mock_job_progress.update.assert_called_with(mock_task, advance=1)
|
|
47
|
+
|
|
48
|
+
@patch("regscale.integrations.commercial.gitlab.check_license")
|
|
49
|
+
@patch("regscale.integrations.commercial.gitlab.Issue")
|
|
50
|
+
@patch("regscale.integrations.commercial.gitlab.logger")
|
|
51
|
+
def test_get_regscale_issues_not_securityplan(self, mock_logger, mock_issue, mock_check_license):
|
|
52
|
+
# Mocking the check_license function
|
|
53
|
+
mock_app = MagicMock()
|
|
54
|
+
mock_check_license.return_value = mock_app
|
|
55
|
+
|
|
56
|
+
# Mocking the fetch_issues_by_parent method and setting return value
|
|
57
|
+
mock_issue.fetch_issues_by_parent.return_value = ["issue1", "issue2"]
|
|
58
|
+
|
|
59
|
+
# Mocking the job_progress object
|
|
60
|
+
mock_job_progress = MagicMock()
|
|
61
|
+
mock_task = MagicMock()
|
|
62
|
+
mock_job_progress.add_task.return_value = mock_task
|
|
63
|
+
|
|
64
|
+
# Call the method to test
|
|
65
|
+
issues = get_regscale_issues(1, "not_securityplans", mock_job_progress)
|
|
66
|
+
|
|
67
|
+
# Assertions to check if the results are as expected
|
|
68
|
+
self.assertEqual(len(issues), 2)
|
|
69
|
+
mock_logger.info.assert_called_with("Fetched 2 issues from RegScale by issue parent.")
|
|
70
|
+
mock_job_progress.add_task.assert_called_once()
|
|
71
|
+
mock_job_progress.update.assert_called_with(mock_task, advance=1)
|
|
72
|
+
|
|
73
|
+
@patch("regscale.integrations.commercial.gitlab.check_license")
|
|
74
|
+
@patch("regscale.integrations.commercial.gitlab.Issue")
|
|
75
|
+
def test_save_issues(self, mock_issue, mock_check_license):
|
|
76
|
+
"""Test save_or_update_issues function intended to save"""
|
|
77
|
+
mock_app = MagicMock()
|
|
78
|
+
mock_check_license.return_value = mock_app
|
|
79
|
+
|
|
80
|
+
# Mocking the job_progress object
|
|
81
|
+
mock_job_progress = MagicMock()
|
|
82
|
+
mock_task = MagicMock()
|
|
83
|
+
mock_job_progress.add_task.return_value = mock_task
|
|
84
|
+
|
|
85
|
+
gitlab_issues = [
|
|
86
|
+
{
|
|
87
|
+
"issue": Issue(
|
|
88
|
+
id=1,
|
|
89
|
+
title="Issue 1",
|
|
90
|
+
description="Description 1",
|
|
91
|
+
status="Open",
|
|
92
|
+
severityLevel="Low",
|
|
93
|
+
dependabotId="1",
|
|
94
|
+
),
|
|
95
|
+
"links": [],
|
|
96
|
+
}
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
regscale_issues = [
|
|
100
|
+
Issue(
|
|
101
|
+
id=1,
|
|
102
|
+
title="Issue 2",
|
|
103
|
+
description="Description 2",
|
|
104
|
+
status="Open",
|
|
105
|
+
severityLevel="Low",
|
|
106
|
+
dependabotId="2",
|
|
107
|
+
)
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# mock the api call
|
|
111
|
+
mock_issue.insert_issue.return_value = gitlab_issues[0]["issue"]
|
|
112
|
+
|
|
113
|
+
# gitlab dependabot id not in regscale, will save new issue
|
|
114
|
+
save_or_update_issues(gitlab_issues, regscale_issues, mock_job_progress)
|
|
115
|
+
|
|
116
|
+
# Verify the issue was inserted with correct parameters
|
|
117
|
+
mock_issue.insert_issue.assert_called_once_with(app=mock_app, issue=gitlab_issues[0]["issue"])
|
|
118
|
+
|
|
119
|
+
# Verify job progress was updated
|
|
120
|
+
mock_job_progress.update.assert_called_with(mock_task, advance=1)
|
|
121
|
+
|
|
122
|
+
@patch("regscale.integrations.commercial.gitlab.check_license")
|
|
123
|
+
@patch("regscale.integrations.commercial.gitlab.Issue")
|
|
124
|
+
@patch("regscale.integrations.commercial.gitlab.Link")
|
|
125
|
+
def test_save_issues_with_links(self, mock_link, mock_issue, mock_check_license):
|
|
126
|
+
"""Test save_or_update_issues function intended to save with links in gitlab issue"""
|
|
127
|
+
mock_app = MagicMock()
|
|
128
|
+
mock_check_license.return_value = mock_app
|
|
129
|
+
|
|
130
|
+
# Mocking the job_progress object
|
|
131
|
+
mock_job_progress = MagicMock()
|
|
132
|
+
mock_task = MagicMock()
|
|
133
|
+
mock_job_progress.add_task.return_value = mock_task
|
|
134
|
+
|
|
135
|
+
gitlab_issues = [
|
|
136
|
+
{
|
|
137
|
+
"issue": Issue(
|
|
138
|
+
id=1,
|
|
139
|
+
title="Issue 1",
|
|
140
|
+
description="Description 1",
|
|
141
|
+
status="Open",
|
|
142
|
+
severityLevel="Low",
|
|
143
|
+
dependabotId="1",
|
|
144
|
+
),
|
|
145
|
+
"links": [
|
|
146
|
+
Link(
|
|
147
|
+
id=None,
|
|
148
|
+
title="Link to GitLab repo",
|
|
149
|
+
url="https://www.gitlab.com",
|
|
150
|
+
createdById=None,
|
|
151
|
+
lastUpdatedById=None,
|
|
152
|
+
dateLastUpdated=None,
|
|
153
|
+
isPublic=True,
|
|
154
|
+
),
|
|
155
|
+
],
|
|
156
|
+
}
|
|
157
|
+
]
|
|
158
|
+
|
|
159
|
+
regscale_issues = [
|
|
160
|
+
Issue(
|
|
161
|
+
id=1,
|
|
162
|
+
title="Issue 2",
|
|
163
|
+
description="Description 2",
|
|
164
|
+
status="Open",
|
|
165
|
+
severityLevel="Low",
|
|
166
|
+
dependabotId="2",
|
|
167
|
+
)
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
# mock the api calls
|
|
171
|
+
mock_issue.insert_issue.return_value = gitlab_issues[0]["issue"]
|
|
172
|
+
mock_link.insert_link.return_value = gitlab_issues[0]["links"][0]
|
|
173
|
+
|
|
174
|
+
# gitlab dependabot id not in regscale, will save new issue
|
|
175
|
+
save_or_update_issues(gitlab_issues, regscale_issues, mock_job_progress)
|
|
176
|
+
|
|
177
|
+
# Verify the issue was inserted with correct parameters
|
|
178
|
+
mock_issue.insert_issue.assert_called_once_with(app=mock_app, issue=gitlab_issues[0]["issue"])
|
|
179
|
+
|
|
180
|
+
# verify the link was inserted with correct parameters
|
|
181
|
+
mock_link.insert_link.assert_called_once_with(app=mock_app, link=gitlab_issues[0]["links"][0])
|
|
182
|
+
|
|
183
|
+
# Verify job progress was updated
|
|
184
|
+
mock_job_progress.update.assert_called_with(mock_task, advance=1)
|
|
185
|
+
|
|
186
|
+
@patch("regscale.integrations.commercial.gitlab.check_license")
|
|
187
|
+
@patch("regscale.integrations.commercial.gitlab.Issue")
|
|
188
|
+
def test_update_issues(self, mock_issue, mock_check_license):
|
|
189
|
+
"""Test save_or_update_issues function intended to update"""
|
|
190
|
+
mock_app = MagicMock()
|
|
191
|
+
mock_check_license.return_value = mock_app
|
|
192
|
+
|
|
193
|
+
# Mocking the job_progress object
|
|
194
|
+
mock_job_progress = MagicMock()
|
|
195
|
+
mock_task = MagicMock()
|
|
196
|
+
mock_job_progress.add_task.return_value = mock_task
|
|
197
|
+
|
|
198
|
+
gitlab_issues = [
|
|
199
|
+
{
|
|
200
|
+
"issue": Issue(
|
|
201
|
+
id=1,
|
|
202
|
+
title="Issue 1",
|
|
203
|
+
description="Description 1",
|
|
204
|
+
status="Closed",
|
|
205
|
+
severityLevel="High",
|
|
206
|
+
dependabotId="1",
|
|
207
|
+
),
|
|
208
|
+
"links": [],
|
|
209
|
+
}
|
|
210
|
+
]
|
|
211
|
+
|
|
212
|
+
regscale_issues = [
|
|
213
|
+
Issue(
|
|
214
|
+
id=2,
|
|
215
|
+
title="Issue 2",
|
|
216
|
+
description="Description 2",
|
|
217
|
+
status="Open",
|
|
218
|
+
severityLevel="Low",
|
|
219
|
+
dependabotId="1",
|
|
220
|
+
)
|
|
221
|
+
]
|
|
222
|
+
|
|
223
|
+
# Verify the issues are different
|
|
224
|
+
self.assertNotEqual(
|
|
225
|
+
gitlab_issues[0]["issue"], regscale_issues[0], "Issues should be different but __eq__ is returning True"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
# mock api call
|
|
229
|
+
mock_issue.update_issue.return_value = gitlab_issues[0]["issue"]
|
|
230
|
+
|
|
231
|
+
save_or_update_issues(gitlab_issues, regscale_issues, mock_job_progress)
|
|
232
|
+
|
|
233
|
+
# Verify the issue was updated with correct parameters
|
|
234
|
+
mock_issue.update_issue.assert_called_once_with(app=mock_app, issue=gitlab_issues[0]["issue"])
|
|
235
|
+
|
|
236
|
+
# Verify job progress was updated
|
|
237
|
+
mock_job_progress.update.assert_called_with(mock_task, advance=1)
|
|
238
|
+
|
|
239
|
+
@patch("regscale.integrations.commercial.gitlab.check_license")
|
|
240
|
+
@patch("regscale.integrations.commercial.gitlab.Issue")
|
|
241
|
+
@patch("regscale.integrations.commercial.gitlab.Link")
|
|
242
|
+
def test_update_issues_with_links(self, mock_link, mock_issue, mock_check_license):
|
|
243
|
+
"""Test save_or_update_issues function intended to update with links in gitlab issue"""
|
|
244
|
+
mock_app = MagicMock()
|
|
245
|
+
mock_check_license.return_value = mock_app
|
|
246
|
+
|
|
247
|
+
# Mocking the job_progress object
|
|
248
|
+
mock_job_progress = MagicMock()
|
|
249
|
+
mock_task = MagicMock()
|
|
250
|
+
mock_job_progress.add_task.return_value = mock_task
|
|
251
|
+
|
|
252
|
+
gitlab_issues = [
|
|
253
|
+
{
|
|
254
|
+
"issue": Issue(
|
|
255
|
+
id=1,
|
|
256
|
+
title="Issue 1",
|
|
257
|
+
description="Description 1",
|
|
258
|
+
status="Closed",
|
|
259
|
+
severityLevel="High",
|
|
260
|
+
dependabotId="1",
|
|
261
|
+
),
|
|
262
|
+
"links": [
|
|
263
|
+
Link(
|
|
264
|
+
id=None,
|
|
265
|
+
title="Link to GitLab repo",
|
|
266
|
+
url="https://www.gitlab.com",
|
|
267
|
+
createdById=None,
|
|
268
|
+
lastUpdatedById=None,
|
|
269
|
+
dateLastUpdated=None,
|
|
270
|
+
isPublic=True,
|
|
271
|
+
),
|
|
272
|
+
],
|
|
273
|
+
}
|
|
274
|
+
]
|
|
275
|
+
|
|
276
|
+
regscale_issues = [
|
|
277
|
+
Issue(
|
|
278
|
+
id=1,
|
|
279
|
+
title="Issue 2",
|
|
280
|
+
description="Description 2",
|
|
281
|
+
status="Open",
|
|
282
|
+
severityLevel="Low",
|
|
283
|
+
dependabotId="1",
|
|
284
|
+
)
|
|
285
|
+
]
|
|
286
|
+
|
|
287
|
+
# mock api calls
|
|
288
|
+
mock_issue.update_issue.return_value = gitlab_issues[0]["issue"]
|
|
289
|
+
mock_link.insert_link.return_value = gitlab_issues[0]["links"][0]
|
|
290
|
+
|
|
291
|
+
save_or_update_issues(gitlab_issues, regscale_issues, mock_job_progress)
|
|
292
|
+
|
|
293
|
+
# Verify the issue was inserted with correct parameters
|
|
294
|
+
mock_issue.update_issue.assert_called_once_with(app=mock_app, issue=gitlab_issues[0]["issue"])
|
|
295
|
+
mock_link.insert_link.assert_called_once_with(app=mock_app, link=gitlab_issues[0]["links"][0])
|
|
296
|
+
|
|
297
|
+
# Verify job progress was updated
|
|
298
|
+
mock_job_progress.update.assert_called_with(mock_task, advance=1)
|
|
299
|
+
|
|
300
|
+
def test_extract_links_with_labels(self):
|
|
301
|
+
# Test data
|
|
302
|
+
text = "Link to GitLab repo: https: https://www.gitlab.com <br>\nAnother link: https: https://example.com"
|
|
303
|
+
parent_id = 1
|
|
304
|
+
parent_module = "issues"
|
|
305
|
+
# Expected result
|
|
306
|
+
expected_links = [
|
|
307
|
+
Link(
|
|
308
|
+
id=None,
|
|
309
|
+
title="Link to GitLab repo",
|
|
310
|
+
url="https://www.gitlab.com",
|
|
311
|
+
parentID=1,
|
|
312
|
+
parentModule=parent_module,
|
|
313
|
+
createdById=None,
|
|
314
|
+
lastUpdatedById=None,
|
|
315
|
+
dateLastUpdated=None,
|
|
316
|
+
isPublic=True,
|
|
317
|
+
),
|
|
318
|
+
Link(
|
|
319
|
+
id=None,
|
|
320
|
+
title="Another link",
|
|
321
|
+
url="https://example.com",
|
|
322
|
+
parentID=1,
|
|
323
|
+
parentModule=parent_module,
|
|
324
|
+
createdById=None,
|
|
325
|
+
lastUpdatedById=None,
|
|
326
|
+
dateLastUpdated=None,
|
|
327
|
+
isPublic=True,
|
|
328
|
+
),
|
|
329
|
+
]
|
|
330
|
+
|
|
331
|
+
# Call the function
|
|
332
|
+
result = extract_links_with_labels(text, parent_id, parent_module)
|
|
333
|
+
print(result)
|
|
334
|
+
# Assert
|
|
335
|
+
self.assertEqual(result, expected_links)
|
|
336
|
+
|
|
337
|
+
def test_convert_issues_basic_properties(self):
|
|
338
|
+
"""Test basic issue property conversion"""
|
|
339
|
+
gitlab_issues = [
|
|
340
|
+
{
|
|
341
|
+
"title": "Test issue",
|
|
342
|
+
"description": "Test description",
|
|
343
|
+
"state": "open",
|
|
344
|
+
"weight": 3,
|
|
345
|
+
"due_date": "2023-06-25",
|
|
346
|
+
"id": 1,
|
|
347
|
+
"created_at": "2023-06-24",
|
|
348
|
+
}
|
|
349
|
+
]
|
|
350
|
+
|
|
351
|
+
job_progress = Mock()
|
|
352
|
+
job_progress.add_task.return_value = "task"
|
|
353
|
+
|
|
354
|
+
result = convert_issues(gitlab_issues, 1, "securityplans", True, job_progress)
|
|
355
|
+
|
|
356
|
+
self.assertIsInstance(result, list)
|
|
357
|
+
self.assertEqual(len(result), 1)
|
|
358
|
+
|
|
359
|
+
issue = result[0]["issue"]
|
|
360
|
+
self.assertIsInstance(issue, Issue)
|
|
361
|
+
self.assertEqual(issue.title, "Test issue")
|
|
362
|
+
self.assertEqual(issue.severityLevel, Issue.assign_severity(gitlab_issues[0]["weight"]))
|
|
363
|
+
self.assertEqual(issue.securityPlanId, 1)
|
|
364
|
+
self.assertIsNone(issue.componentId)
|
|
365
|
+
|
|
366
|
+
def test_convert_issues_status_handling(self):
|
|
367
|
+
"""Test issue status conversion for different states"""
|
|
368
|
+
gitlab_issues = [
|
|
369
|
+
{
|
|
370
|
+
"title": "Open issue",
|
|
371
|
+
"description": "Test description",
|
|
372
|
+
"state": "open",
|
|
373
|
+
"weight": 3,
|
|
374
|
+
"due_date": "2023-06-25",
|
|
375
|
+
"id": 1,
|
|
376
|
+
"created_at": "2023-06-24",
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
"title": "Closed issue",
|
|
380
|
+
"description": "Test description",
|
|
381
|
+
"state": "closed",
|
|
382
|
+
"weight": 3,
|
|
383
|
+
"due_date": "2023-06-25",
|
|
384
|
+
"id": 2,
|
|
385
|
+
"created_at": "2023-06-24",
|
|
386
|
+
"closed_at": "2023-06-25",
|
|
387
|
+
},
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
job_progress = Mock()
|
|
391
|
+
job_progress.add_task.return_value = "task"
|
|
392
|
+
|
|
393
|
+
result = convert_issues(gitlab_issues, 1, "securityplans", True, job_progress)
|
|
394
|
+
|
|
395
|
+
self.assertEqual(result[0]["issue"].status, "Open")
|
|
396
|
+
self.assertEqual(result[1]["issue"].status, "Closed")
|
|
397
|
+
|
|
398
|
+
@patch("regscale.integrations.commercial.gitlab.get_current_datetime", return_value="2024-06-25")
|
|
399
|
+
def test_convert_issues_date_handling(self, mock_get_current_datetime):
|
|
400
|
+
"""Test dateCompleted handling for closed issues"""
|
|
401
|
+
gitlab_issues = [
|
|
402
|
+
{
|
|
403
|
+
"title": "Closed with date",
|
|
404
|
+
"description": "Test description",
|
|
405
|
+
"state": "closed",
|
|
406
|
+
"weight": 3,
|
|
407
|
+
"due_date": "2023-06-25",
|
|
408
|
+
"id": 1,
|
|
409
|
+
"created_at": "2023-06-24",
|
|
410
|
+
"closed_at": "2023-06-25",
|
|
411
|
+
},
|
|
412
|
+
{
|
|
413
|
+
"title": "Closed without date",
|
|
414
|
+
"description": "Test description",
|
|
415
|
+
"state": "closed",
|
|
416
|
+
"weight": 3,
|
|
417
|
+
"due_date": "2023-06-25",
|
|
418
|
+
"id": 2,
|
|
419
|
+
"created_at": "2023-06-24",
|
|
420
|
+
},
|
|
421
|
+
]
|
|
422
|
+
|
|
423
|
+
job_progress = Mock()
|
|
424
|
+
job_progress.add_task.return_value = "task"
|
|
425
|
+
|
|
426
|
+
result = convert_issues(gitlab_issues, 1, "securityplans", True, job_progress)
|
|
427
|
+
|
|
428
|
+
self.assertEqual(result[0]["issue"].dateCompleted, "2023-06-25") # Uses closed_at
|
|
429
|
+
self.assertEqual(result[1]["issue"].dateCompleted, "2024-06-25") # Uses current datetime
|
|
430
|
+
|
|
431
|
+
def test_convert_issues_with_links(self):
|
|
432
|
+
"""Test link extraction and inclusion"""
|
|
433
|
+
gitlab_issues = [
|
|
434
|
+
{
|
|
435
|
+
"title": "Test issue",
|
|
436
|
+
"description": "This is a test issue with a link: https: https://example.com",
|
|
437
|
+
"state": "open",
|
|
438
|
+
"weight": 3,
|
|
439
|
+
"due_date": "2023-06-25",
|
|
440
|
+
"id": 1,
|
|
441
|
+
"created_at": "2023-06-24",
|
|
442
|
+
}
|
|
443
|
+
]
|
|
444
|
+
|
|
445
|
+
job_progress = Mock()
|
|
446
|
+
job_progress.add_task.return_value = "task"
|
|
447
|
+
|
|
448
|
+
result = convert_issues(gitlab_issues, 1, "securityplans", True, job_progress)
|
|
449
|
+
|
|
450
|
+
self.assertEqual(len(result[0]["links"]), 1)
|
|
451
|
+
self.assertEqual(result[0]["links"][0].url, "https://example.com")
|
|
452
|
+
job_progress.update.assert_called_with("task", advance=1)
|
|
453
|
+
|
|
454
|
+
def test_convert_issues_no_links(self):
|
|
455
|
+
"""Test convert issues with include_links set to False"""
|
|
456
|
+
gitlab_issues = [
|
|
457
|
+
{
|
|
458
|
+
"title": "Test issue",
|
|
459
|
+
"description": "This is a test issue with a link: https: https://example.com",
|
|
460
|
+
"state": "closed",
|
|
461
|
+
"weight": 3,
|
|
462
|
+
"due_date": "2023-06-25",
|
|
463
|
+
"id": 1,
|
|
464
|
+
"created_at": "2023-06-24",
|
|
465
|
+
"closed_at": "2023-06-25",
|
|
466
|
+
},
|
|
467
|
+
]
|
|
468
|
+
|
|
469
|
+
job_progress = Mock()
|
|
470
|
+
job_progress.add_task.return_value = "task"
|
|
471
|
+
|
|
472
|
+
result = convert_issues(gitlab_issues, 1, "securityplans", False, job_progress)
|
|
473
|
+
|
|
474
|
+
# Assertions
|
|
475
|
+
self.assertIsInstance(result, list)
|
|
476
|
+
self.assertEqual(len(result), 1)
|
|
477
|
+
self.assertEqual(result[0]["links"], [])
|
|
478
|
+
|
|
479
|
+
def test_convert_issues_empty(self):
|
|
480
|
+
"""Test with empty issue list from gitlab"""
|
|
481
|
+
job_progress = Mock()
|
|
482
|
+
job_progress.add_task.return_value = "task"
|
|
483
|
+
|
|
484
|
+
result = convert_issues([], 1, "securityplans", True, job_progress)
|
|
485
|
+
assert result == []
|
|
486
|
+
|
|
487
|
+
@patch("regscale.integrations.commercial.gitlab.requests.get")
|
|
488
|
+
@patch("regscale.integrations.commercial.gitlab.job_progress.add_task")
|
|
489
|
+
@patch("regscale.integrations.commercial.gitlab.job_progress.update")
|
|
490
|
+
def test_get_issues_from_gitlab_success(self, mock_update, mock_add_task, mock_get):
|
|
491
|
+
# Mock the response from the GitLab API
|
|
492
|
+
mock_response = MagicMock()
|
|
493
|
+
mock_response.ok = True
|
|
494
|
+
mock_response.json.return_value = [
|
|
495
|
+
{"id": 1, "title": "Issue 1"},
|
|
496
|
+
{"id": 2, "title": "Issue 2"},
|
|
497
|
+
]
|
|
498
|
+
mock_get.return_value = mock_response
|
|
499
|
+
|
|
500
|
+
# Mock job_progress
|
|
501
|
+
mock_job_progress = MagicMock()
|
|
502
|
+
mock_add_task.return_value = "fetching_issues"
|
|
503
|
+
|
|
504
|
+
# Call the function
|
|
505
|
+
issues = get_issues_from_gitlab("https://gitlab.com", 1, "api_token", mock_job_progress)
|
|
506
|
+
|
|
507
|
+
# Assertions
|
|
508
|
+
self.assertEqual(len(issues), 2)
|
|
509
|
+
self.assertEqual(issues[0]["id"], 1)
|
|
510
|
+
self.assertEqual(issues[0]["title"], "Issue 1")
|
|
511
|
+
self.assertEqual(issues[1]["id"], 2)
|
|
512
|
+
self.assertEqual(issues[1]["title"], "Issue 2")
|
|
513
|
+
|
|
514
|
+
# Assert that the mock methods were called
|
|
515
|
+
# mock_add_task.assert_called_once()
|
|
516
|
+
# mock_update.assert_called_once_with("fetching_issues", advance=1)
|
|
517
|
+
mock_get.assert_called_once_with(
|
|
518
|
+
"https://gitlab.com/api/v4/projects/1/issues",
|
|
519
|
+
headers={"Private-Token": "api_token"},
|
|
520
|
+
)
|
|
521
|
+
|
|
522
|
+
@patch("regscale.integrations.commercial.gitlab.requests.get")
|
|
523
|
+
@patch("regscale.integrations.commercial.gitlab.job_progress.add_task")
|
|
524
|
+
@patch("regscale.integrations.commercial.gitlab.job_progress.update")
|
|
525
|
+
def test_get_issues_from_gitlab_failure(self, mock_update, mock_add_task, mock_get):
|
|
526
|
+
# Mock the response from the GitLab API
|
|
527
|
+
mock_response = MagicMock()
|
|
528
|
+
mock_response.ok = False
|
|
529
|
+
mock_response.status_code = 404
|
|
530
|
+
mock_response.text = "Not Found"
|
|
531
|
+
|
|
532
|
+
mock_get.return_value = mock_response
|
|
533
|
+
|
|
534
|
+
# Mock job_progress
|
|
535
|
+
mock_job_progress = MagicMock()
|
|
536
|
+
mock_add_task.return_value = "fetching_issues"
|
|
537
|
+
|
|
538
|
+
# Call the function and verify it raises SystemExit
|
|
539
|
+
with self.assertRaises(SystemExit) as context:
|
|
540
|
+
get_issues_from_gitlab("https://gitlab.com", 1, "api_token", mock_job_progress)
|
|
541
|
+
|
|
542
|
+
# Verify the exit code
|
|
543
|
+
self.assertEqual(context.exception.code, 1)
|
|
544
|
+
|
|
545
|
+
# Verify the API call was made correctly
|
|
546
|
+
mock_get.assert_called_once_with(
|
|
547
|
+
"https://gitlab.com/api/v4/projects/1/issues",
|
|
548
|
+
headers={"Private-Token": "api_token"},
|
|
549
|
+
)
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_build_limited_string():
|
|
6
|
+
items = {"item1", "item2", "item3"}
|
|
7
|
+
result = TenableIntegration._build_limited_string(items, char_limit=10)
|
|
8
|
+
assert len(result) <= 10
|
|
9
|
+
assert isinstance(result, str)
|
|
10
|
+
|
|
11
|
+
# Test empty set
|
|
12
|
+
assert TenableIntegration._build_limited_string(set()) == ""
|
|
13
|
+
|
|
14
|
+
# Test with exact limit
|
|
15
|
+
items = {"a", "b"}
|
|
16
|
+
assert "a" in TenableIntegration._build_limited_string(
|
|
17
|
+
items, char_limit=4
|
|
18
|
+
) and "b" in TenableIntegration._build_limited_string(items, char_limit=4)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_get_all_mac_addresses():
|
|
22
|
+
# Test case 1: Normal case with multiple MAC addresses
|
|
23
|
+
node = {
|
|
24
|
+
"network_interfaces": [
|
|
25
|
+
{"mac_addresses": ["00:11:22:33:44:55", "66:77:88:99:AA:BB"]},
|
|
26
|
+
{"mac_addresses": ["CC:DD:EE:FF:00:11"]},
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
result = TenableIntegration.get_all_mac_addresses(node)
|
|
30
|
+
assert "00:11:22:33:44:55" in result
|
|
31
|
+
assert "66:77:88:99:AA:BB" in result
|
|
32
|
+
assert "CC:DD:EE:FF:00:11" in result
|
|
33
|
+
|
|
34
|
+
# Test case 2: Empty node
|
|
35
|
+
empty_node = {}
|
|
36
|
+
assert TenableIntegration.get_all_mac_addresses(empty_node) == ""
|
|
37
|
+
|
|
38
|
+
# Test case 3: Node with empty network interfaces
|
|
39
|
+
node_empty_interfaces = {"network_interfaces": []}
|
|
40
|
+
assert TenableIntegration.get_all_mac_addresses(node_empty_interfaces) == ""
|
|
41
|
+
|
|
42
|
+
# Test case 4: Test character limit (450 chars)
|
|
43
|
+
long_mac = "00:11:22:33:44:55"
|
|
44
|
+
print(f"testing {len(long_mac) * 30} chars")
|
|
45
|
+
node_many_macs = {"network_interfaces": [{"mac_addresses": [long_mac] * 30}]} # Should exceed 450 chars with commas
|
|
46
|
+
result = TenableIntegration.get_all_mac_addresses(node_many_macs)
|
|
47
|
+
assert len(result) <= 450
|
|
48
|
+
assert long_mac in result
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_get_all_ip_addresses():
|
|
52
|
+
ipv_node = {"ipv4s": ["192.168.1.1", "192.168.1.2"], "ipv6s": ["fe80::1", "fe80::2"]}
|
|
53
|
+
|
|
54
|
+
expected_result = "192.168.1.1, 192.168.1.2, fe80::1, fe80::2"
|
|
55
|
+
result = TenableIntegration.get_all_ip_addresses(ipv_node)
|
|
56
|
+
|
|
57
|
+
for r in result.split(","):
|
|
58
|
+
assert r.strip() in expected_result
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_get_all_ip_addresses_length():
|
|
62
|
+
ipv4s = [f"192.168.1.{i}" for i in range(1, 1001)]
|
|
63
|
+
ipv6s = [f"fe80::{i}" for i in range(1, 1001)]
|
|
64
|
+
|
|
65
|
+
ipv_node = {"ipv4s": ipv4s, "ipv6s": ipv6s}
|
|
66
|
+
|
|
67
|
+
result = TenableIntegration.get_all_ip_addresses(ipv_node)
|
|
68
|
+
# we need to stay under 450 and warn the user that all the data may not have fit in the field.
|
|
69
|
+
assert len(result) <= 450
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_get_all_mac_address_length():
|
|
73
|
+
mac_addresses = [f"00:11:22:33:44:{i}" for i in range(1, 1001)]
|
|
74
|
+
node = {"network_interfaces": [{"mac_addresses": mac_addresses}]}
|
|
75
|
+
result = TenableIntegration.get_all_mac_addresses(node)
|
|
76
|
+
assert len(result) <= 450
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
test_build_limited_string()
|
|
81
|
+
test_get_all_mac_addresses()
|
|
82
|
+
test_get_all_ip_addresses()
|
|
83
|
+
test_get_all_mac_address_length()
|
|
84
|
+
test_get_all_ip_addresses_length()
|