regscale-cli 6.25.0.1__py3-none-any.whl → 6.26.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 +18 -3
- regscale/core/app/internal/login.py +0 -1
- regscale/core/app/utils/catalog_utils/common.py +1 -1
- 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/wizv2/click.py +26 -26
- regscale/integrations/commercial/wizv2/compliance_report.py +152 -157
- regscale/integrations/commercial/wizv2/constants.py +20 -71
- regscale/integrations/commercial/wizv2/scanner.py +3 -3
- regscale/integrations/compliance_integration.py +67 -2
- regscale/integrations/control_matcher.py +358 -0
- regscale/integrations/due_date_handler.py +118 -6
- 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/poam_export_v5.py +888 -0
- regscale/integrations/scanner_integration.py +199 -130
- regscale/models/integration_models/cisa_kev_data.json +199 -4
- regscale/models/integration_models/nexpose.py +36 -10
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/locking.py +12 -8
- regscale/models/platform.py +1 -2
- regscale/models/regscale_models/control_implementation.py +46 -21
- regscale/models/regscale_models/issue.py +256 -94
- regscale/models/regscale_models/milestone.py +1 -1
- regscale/models/regscale_models/regscale_model.py +6 -1
- regscale/templates/__init__.py +0 -0
- regscale/utils/threading/threadhandler.py +20 -15
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/RECORD +84 -37
- 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 +1814 -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 +1469 -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/test_compliance_report_normalization.py +138 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1351 -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_policy_compliance.py +750 -0
- tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +264 -0
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +624 -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/test_control_matcher.py +1314 -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_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.25.0.1.dist-info → regscale_cli-6.26.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
Milestone Manager for Issue Tracking
|
|
5
|
+
|
|
6
|
+
Handles creation of milestones for issues based on status transitions (created, reopened, closed).
|
|
7
|
+
Also handles backfilling of missing milestones for existing issues.
|
|
8
|
+
"""
|
|
9
|
+
import logging
|
|
10
|
+
from typing import TYPE_CHECKING, List, Optional
|
|
11
|
+
|
|
12
|
+
from regscale.core.app.utils.app_utils import get_current_datetime
|
|
13
|
+
from regscale.integrations.variables import ScannerVariables
|
|
14
|
+
from regscale.models import regscale_models
|
|
15
|
+
|
|
16
|
+
if TYPE_CHECKING:
|
|
17
|
+
from regscale.integrations.scanner_integration import IntegrationFinding
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger("regscale")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class MilestoneManager:
|
|
23
|
+
"""
|
|
24
|
+
Manages milestone creation for issues based on status transitions.
|
|
25
|
+
|
|
26
|
+
Milestones are created when:
|
|
27
|
+
- A new issue is created
|
|
28
|
+
- An existing issue is reopened (Closed -> Open)
|
|
29
|
+
- An existing issue is closed (Open -> Closed)
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(self, integration_title: str, assessor_id: str, scan_date: str):
|
|
33
|
+
"""
|
|
34
|
+
Initialize the milestone manager.
|
|
35
|
+
|
|
36
|
+
:param str integration_title: Name of the integration (used in milestone titles)
|
|
37
|
+
:param str assessor_id: ID of the assessor/responsible person for milestones
|
|
38
|
+
:param str scan_date: Date of the scan (used for new issue milestones)
|
|
39
|
+
"""
|
|
40
|
+
self.integration_title = integration_title
|
|
41
|
+
self.assessor_id = assessor_id
|
|
42
|
+
self.scan_date = scan_date
|
|
43
|
+
|
|
44
|
+
def create_milestones_for_issue(
|
|
45
|
+
self,
|
|
46
|
+
issue: regscale_models.Issue,
|
|
47
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
48
|
+
existing_issue: Optional[regscale_models.Issue] = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
"""
|
|
51
|
+
Create appropriate milestones for an issue based on status transitions.
|
|
52
|
+
|
|
53
|
+
:param regscale_models.Issue issue: The issue to create milestones for
|
|
54
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging/context)
|
|
55
|
+
:param Optional[regscale_models.Issue] existing_issue: Previous state of issue for comparison
|
|
56
|
+
"""
|
|
57
|
+
if not self._should_create_milestones(issue):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
if self._should_create_reopened_milestone(existing_issue, issue):
|
|
61
|
+
self._create_reopened_milestone(issue, finding)
|
|
62
|
+
elif self._should_create_closed_milestone(existing_issue, issue):
|
|
63
|
+
self._create_closed_milestone(issue, finding)
|
|
64
|
+
elif not existing_issue:
|
|
65
|
+
self._create_new_issue_milestone(issue, finding)
|
|
66
|
+
else:
|
|
67
|
+
logger.debug(
|
|
68
|
+
"No milestone created for issue %s (no status transition detected)",
|
|
69
|
+
issue.id,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _should_create_milestones(self, issue: regscale_models.Issue) -> bool:
|
|
73
|
+
"""
|
|
74
|
+
Check if milestones should be created for this issue.
|
|
75
|
+
|
|
76
|
+
:param regscale_models.Issue issue: The issue to check
|
|
77
|
+
:return: True if milestones should be created
|
|
78
|
+
:rtype: bool
|
|
79
|
+
"""
|
|
80
|
+
if not ScannerVariables.useMilestones:
|
|
81
|
+
logger.debug("Milestone creation disabled (useMilestones=False)")
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
if not issue.id:
|
|
85
|
+
logger.debug("Cannot create milestone - issue has no ID")
|
|
86
|
+
return False
|
|
87
|
+
|
|
88
|
+
return True
|
|
89
|
+
|
|
90
|
+
def _should_create_reopened_milestone(
|
|
91
|
+
self, existing_issue: Optional[regscale_models.Issue], issue: regscale_models.Issue
|
|
92
|
+
) -> bool:
|
|
93
|
+
"""
|
|
94
|
+
Check if a reopened milestone should be created.
|
|
95
|
+
|
|
96
|
+
:param Optional[regscale_models.Issue] existing_issue: The existing issue
|
|
97
|
+
:param regscale_models.Issue issue: The current issue
|
|
98
|
+
:return: True if reopened milestone should be created
|
|
99
|
+
:rtype: bool
|
|
100
|
+
"""
|
|
101
|
+
return (
|
|
102
|
+
existing_issue is not None
|
|
103
|
+
and existing_issue.status == regscale_models.IssueStatus.Closed
|
|
104
|
+
and issue.status == regscale_models.IssueStatus.Open
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
def _should_create_closed_milestone(
|
|
108
|
+
self, existing_issue: Optional[regscale_models.Issue], issue: regscale_models.Issue
|
|
109
|
+
) -> bool:
|
|
110
|
+
"""
|
|
111
|
+
Check if a closed milestone should be created.
|
|
112
|
+
|
|
113
|
+
:param Optional[regscale_models.Issue] existing_issue: The existing issue
|
|
114
|
+
:param regscale_models.Issue issue: The current issue
|
|
115
|
+
:return: True if closed milestone should be created
|
|
116
|
+
:rtype: bool
|
|
117
|
+
"""
|
|
118
|
+
return (
|
|
119
|
+
existing_issue is not None
|
|
120
|
+
and existing_issue.status == regscale_models.IssueStatus.Open
|
|
121
|
+
and issue.status == regscale_models.IssueStatus.Closed
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
def _create_reopened_milestone(
|
|
125
|
+
self,
|
|
126
|
+
issue: regscale_models.Issue,
|
|
127
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""
|
|
130
|
+
Create a milestone for a reopened issue.
|
|
131
|
+
|
|
132
|
+
:param regscale_models.Issue issue: The issue being reopened
|
|
133
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
134
|
+
"""
|
|
135
|
+
milestone_date = get_current_datetime()
|
|
136
|
+
self._create_milestone(
|
|
137
|
+
issue=issue,
|
|
138
|
+
title=f"Issue reopened from {self.integration_title} scan",
|
|
139
|
+
milestone_date=milestone_date,
|
|
140
|
+
milestone_type="reopened",
|
|
141
|
+
finding=finding,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
def _create_closed_milestone(
|
|
145
|
+
self,
|
|
146
|
+
issue: regscale_models.Issue,
|
|
147
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""
|
|
150
|
+
Create a milestone for a closed issue.
|
|
151
|
+
|
|
152
|
+
:param regscale_models.Issue issue: The issue being closed
|
|
153
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
154
|
+
"""
|
|
155
|
+
milestone_date = issue.dateCompleted or get_current_datetime()
|
|
156
|
+
self._create_milestone(
|
|
157
|
+
issue=issue,
|
|
158
|
+
title=f"Issue closed from {self.integration_title} scan",
|
|
159
|
+
milestone_date=milestone_date,
|
|
160
|
+
milestone_type="closed",
|
|
161
|
+
finding=finding,
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
def _create_new_issue_milestone(
|
|
165
|
+
self,
|
|
166
|
+
issue: regscale_models.Issue,
|
|
167
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
168
|
+
) -> None:
|
|
169
|
+
"""
|
|
170
|
+
Create a milestone for a newly created issue.
|
|
171
|
+
|
|
172
|
+
:param regscale_models.Issue issue: The newly created issue
|
|
173
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
174
|
+
"""
|
|
175
|
+
self._create_milestone(
|
|
176
|
+
issue=issue,
|
|
177
|
+
title=f"Issue created from {self.integration_title} scan",
|
|
178
|
+
milestone_date=self.scan_date,
|
|
179
|
+
milestone_type="new",
|
|
180
|
+
finding=finding,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
def _create_milestone(
|
|
184
|
+
self,
|
|
185
|
+
issue: regscale_models.Issue,
|
|
186
|
+
title: str,
|
|
187
|
+
milestone_date: str,
|
|
188
|
+
milestone_type: str,
|
|
189
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
190
|
+
) -> None:
|
|
191
|
+
"""
|
|
192
|
+
Create a milestone with error handling.
|
|
193
|
+
|
|
194
|
+
:param regscale_models.Issue issue: The issue to create milestone for
|
|
195
|
+
:param str title: Title of the milestone
|
|
196
|
+
:param str milestone_date: Date for the milestone
|
|
197
|
+
:param str milestone_type: Type of milestone (for logging: new, reopened, closed)
|
|
198
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
199
|
+
"""
|
|
200
|
+
try:
|
|
201
|
+
regscale_models.Milestone(
|
|
202
|
+
title=title,
|
|
203
|
+
milestoneDate=milestone_date,
|
|
204
|
+
dateCompleted=get_current_datetime(),
|
|
205
|
+
responsiblePersonId=self.assessor_id,
|
|
206
|
+
parentID=issue.id,
|
|
207
|
+
parentModule="issues",
|
|
208
|
+
).create_or_update()
|
|
209
|
+
|
|
210
|
+
logger.debug(f"Created {milestone_type} milestone for issue {issue.id}")
|
|
211
|
+
|
|
212
|
+
except Exception as e:
|
|
213
|
+
logger.warning(f"Failed to create {milestone_type} milestone for issue {issue.id}: {e}")
|
|
214
|
+
|
|
215
|
+
def get_existing_milestones(self, issue: regscale_models.Issue) -> List[regscale_models.Milestone]:
|
|
216
|
+
"""
|
|
217
|
+
Get all existing milestones for an issue.
|
|
218
|
+
|
|
219
|
+
:param regscale_models.Issue issue: The issue to check
|
|
220
|
+
:return: List of existing milestones
|
|
221
|
+
:rtype: List[regscale_models.Milestone]
|
|
222
|
+
"""
|
|
223
|
+
if not issue.id:
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
milestones = regscale_models.Milestone.get_by_parent(parent_id=issue.id, parent_module="issues")
|
|
228
|
+
return milestones if milestones else []
|
|
229
|
+
except Exception as e:
|
|
230
|
+
logger.debug(f"Could not retrieve milestones for issue {issue.id}: {e}")
|
|
231
|
+
return []
|
|
232
|
+
|
|
233
|
+
def has_creation_milestone(self, issue: regscale_models.Issue) -> bool:
|
|
234
|
+
"""
|
|
235
|
+
Check if an issue has a creation milestone.
|
|
236
|
+
|
|
237
|
+
:param regscale_models.Issue issue: The issue to check
|
|
238
|
+
:return: True if creation milestone exists
|
|
239
|
+
:rtype: bool
|
|
240
|
+
"""
|
|
241
|
+
milestones = self.get_existing_milestones(issue)
|
|
242
|
+
|
|
243
|
+
# Check for creation milestone patterns
|
|
244
|
+
creation_patterns = [
|
|
245
|
+
"Issue created from",
|
|
246
|
+
"created from",
|
|
247
|
+
]
|
|
248
|
+
|
|
249
|
+
for milestone in milestones:
|
|
250
|
+
milestone_title = milestone.title.lower() if milestone.title else ""
|
|
251
|
+
if any(pattern.lower() in milestone_title for pattern in creation_patterns):
|
|
252
|
+
logger.debug(f"Found existing creation milestone for issue {issue.id}: {milestone.title}")
|
|
253
|
+
return True
|
|
254
|
+
|
|
255
|
+
return False
|
|
256
|
+
|
|
257
|
+
def ensure_creation_milestone_exists(
|
|
258
|
+
self,
|
|
259
|
+
issue: regscale_models.Issue,
|
|
260
|
+
finding: Optional["IntegrationFinding"] = None,
|
|
261
|
+
) -> None:
|
|
262
|
+
"""
|
|
263
|
+
Ensure an issue has a creation milestone, backfilling if necessary.
|
|
264
|
+
|
|
265
|
+
This method checks if an issue has a creation milestone. If not, it creates one
|
|
266
|
+
based on the issue's dateCreated field to backfill missing milestones.
|
|
267
|
+
|
|
268
|
+
:param regscale_models.Issue issue: The issue to check
|
|
269
|
+
:param Optional[IntegrationFinding] finding: The finding data (for logging)
|
|
270
|
+
"""
|
|
271
|
+
if not self._should_create_milestones(issue):
|
|
272
|
+
return
|
|
273
|
+
|
|
274
|
+
# Check if creation milestone already exists
|
|
275
|
+
if self.has_creation_milestone(issue):
|
|
276
|
+
logger.debug(f"Issue {issue.id} already has creation milestone, skipping backfill")
|
|
277
|
+
return
|
|
278
|
+
|
|
279
|
+
# Backfill missing creation milestone
|
|
280
|
+
logger.debug(f"Backfilling missing creation milestone for issue {issue.id}")
|
|
281
|
+
|
|
282
|
+
# Use issue's dateCreated if available, otherwise use scan_date
|
|
283
|
+
milestone_date = issue.dateCreated if issue.dateCreated else self.scan_date
|
|
284
|
+
|
|
285
|
+
self._create_milestone(
|
|
286
|
+
issue=issue,
|
|
287
|
+
title=f"Issue created from {self.integration_title} scan",
|
|
288
|
+
milestone_date=milestone_date,
|
|
289
|
+
milestone_type="backfilled",
|
|
290
|
+
finding=finding,
|
|
291
|
+
)
|
|
@@ -17,6 +17,7 @@ from regscale.core.lazy_group import LazyGroup
|
|
|
17
17
|
"import_poam": "regscale.integrations.public.fedramp.click.import_fedramp_poam_template",
|
|
18
18
|
"import_drf": "regscale.integrations.public.fedramp.click.import_drf",
|
|
19
19
|
"import_cis_crm": "regscale.integrations.public.fedramp.click.import_ciscrm",
|
|
20
|
+
"export_poam_v5": "regscale.integrations.public.fedramp.click.export_poam_v5",
|
|
20
21
|
},
|
|
21
22
|
name="fedramp",
|
|
22
23
|
)
|
|
@@ -8,7 +8,7 @@ from typing import Dict, List, Optional, Tuple
|
|
|
8
8
|
import click
|
|
9
9
|
|
|
10
10
|
from regscale.core.app.application import Application
|
|
11
|
-
from regscale.core.app.utils.app_utils import error_and_exit
|
|
11
|
+
from regscale.core.app.utils.app_utils import create_progress_object, error_and_exit
|
|
12
12
|
from regscale.models.regscale_models import Catalog, SecurityControl, CCI
|
|
13
13
|
|
|
14
14
|
logger = logging.getLogger("regscale")
|
|
@@ -109,7 +109,7 @@ class CCIImporter:
|
|
|
109
109
|
:rtype: None
|
|
110
110
|
"""
|
|
111
111
|
if self.verbose:
|
|
112
|
-
|
|
112
|
+
logger.info("Parsing CCI items from XML...")
|
|
113
113
|
|
|
114
114
|
for cci_item in self.xml_data.findall(".//{http://iase.disa.mil/cci}cci_item"):
|
|
115
115
|
cci_id, definition = self._extract_cci_data(cci_item)
|
|
@@ -291,7 +291,7 @@ class CCIImporter:
|
|
|
291
291
|
:rtype: Dict[str, int]
|
|
292
292
|
"""
|
|
293
293
|
if self.verbose:
|
|
294
|
-
|
|
294
|
+
logger.info("Mapping CCI data to security controls...")
|
|
295
295
|
|
|
296
296
|
catalog = self._get_catalog(catalog_id)
|
|
297
297
|
security_controls: List[SecurityControl] = SecurityControl.get_all_by_parent(parent_id=catalog.id)
|
|
@@ -303,24 +303,28 @@ class CCIImporter:
|
|
|
303
303
|
updated_count = 0
|
|
304
304
|
skipped_count = 0
|
|
305
305
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
306
|
+
with create_progress_object() as progress:
|
|
307
|
+
logger.info(f"Parsing and mapping {len(self.normalized_cci)} normalized CCI entries...")
|
|
308
|
+
main_task = progress.add_task("Parsing and mapping CCIs...", total=len(self.normalized_cci))
|
|
309
|
+
for main_control, cci_list in self.normalized_cci.items():
|
|
310
|
+
if main_control in control_map:
|
|
311
|
+
control_id = control_map[main_control]
|
|
312
|
+
control_created, control_updated = self._process_cci_for_control(
|
|
313
|
+
control_id, cci_list, user_id, tenant_id
|
|
314
|
+
)
|
|
315
|
+
created_count += control_created
|
|
316
|
+
updated_count += control_updated
|
|
317
|
+
else:
|
|
318
|
+
skipped_count += len(cci_list)
|
|
319
|
+
if self.verbose:
|
|
320
|
+
logger.warning(f"Warning: Control not found for key: {main_control}")
|
|
321
|
+
progress.update(main_task, advance=1)
|
|
318
322
|
|
|
319
323
|
return {
|
|
320
324
|
"created": created_count,
|
|
321
325
|
"updated": updated_count,
|
|
322
326
|
"skipped": skipped_count,
|
|
323
|
-
"total_processed":
|
|
327
|
+
"total_processed": sum(created_count + updated_count + skipped_count),
|
|
324
328
|
}
|
|
325
329
|
|
|
326
330
|
def get_normalized_cci(self) -> Dict[str, List[Dict]]:
|
|
@@ -343,11 +347,10 @@ def _load_xml_file(xml_file: str) -> ET.Element:
|
|
|
343
347
|
:raises click.ClickException: If XML parsing fails
|
|
344
348
|
"""
|
|
345
349
|
try:
|
|
346
|
-
|
|
350
|
+
logger.info(f"Loading XML file: {xml_file}")
|
|
347
351
|
tree = ET.parse(xml_file)
|
|
348
352
|
return tree.getroot()
|
|
349
353
|
except ET.ParseError as e:
|
|
350
|
-
click.echo(click.style(f"Failed to parse XML file: {e}", fg="red"), err=True)
|
|
351
354
|
error_and_exit(f"Failed to parse XML file: {e}")
|
|
352
355
|
|
|
353
356
|
|
|
@@ -358,12 +361,12 @@ def _display_verbose_output(normalized_data: Dict[str, List[Dict]]) -> None:
|
|
|
358
361
|
:param Dict[str, List[Dict]] normalized_data: Dictionary of normalized CCI data
|
|
359
362
|
:rtype: None
|
|
360
363
|
"""
|
|
361
|
-
|
|
364
|
+
logger.info("\nNormalized CCI Data:")
|
|
362
365
|
for key, value in normalized_data.items():
|
|
363
|
-
|
|
366
|
+
logger.info(f" {key}: {len(value)} CCI items")
|
|
364
367
|
for cci in value:
|
|
365
368
|
definition_preview = cci["definition"][:100] + "..." if len(cci["definition"]) > 100 else cci["definition"]
|
|
366
|
-
|
|
369
|
+
logger.info(f" - {cci['cci_id']}: {definition_preview}")
|
|
367
370
|
|
|
368
371
|
|
|
369
372
|
def _display_results(stats: Dict[str, int]) -> None:
|
|
@@ -373,15 +376,12 @@ def _display_results(stats: Dict[str, int]) -> None:
|
|
|
373
376
|
:param Dict[str, int] stats: Dictionary with operation statistics
|
|
374
377
|
:rtype: None
|
|
375
378
|
"""
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
f"\n - Total processed: {stats['total_processed']}",
|
|
383
|
-
fg="green",
|
|
384
|
-
)
|
|
379
|
+
logger.info(
|
|
380
|
+
f"[green]\nDatabase operations completed:"
|
|
381
|
+
f"[green]\n - Created: {stats['created']}"
|
|
382
|
+
f"[green]\n - Updated: {stats['updated']}"
|
|
383
|
+
f"[green]\n - Skipped: {stats['skipped']}"
|
|
384
|
+
f"[green]\n - Total processed: {stats['total_processed']}",
|
|
385
385
|
)
|
|
386
386
|
|
|
387
387
|
|
|
@@ -398,7 +398,7 @@ def _process_cci_import(importer: CCIImporter, dry_run: bool, verbose: bool, cat
|
|
|
398
398
|
importer.parse_cci()
|
|
399
399
|
normalized_data = importer.get_normalized_cci()
|
|
400
400
|
|
|
401
|
-
|
|
401
|
+
logger.info(f"[green]Successfully parsed {len(normalized_data)} normalized CCI entries[/green]")
|
|
402
402
|
|
|
403
403
|
if verbose:
|
|
404
404
|
_display_verbose_output(normalized_data)
|
|
@@ -407,7 +407,7 @@ def _process_cci_import(importer: CCIImporter, dry_run: bool, verbose: bool, cat
|
|
|
407
407
|
stats = importer.map_to_security_controls(catalog_id)
|
|
408
408
|
_display_results(stats)
|
|
409
409
|
else:
|
|
410
|
-
|
|
410
|
+
logger.info("\n[yellow]DRY RUN MODE: No database changes were made[/yellow]")
|
|
411
411
|
|
|
412
412
|
|
|
413
413
|
@click.command(name="cci_importer")
|
|
@@ -431,14 +431,13 @@ def cci_importer(xml_file: str, dry_run: bool, verbose: bool, nist_version: str,
|
|
|
431
431
|
try:
|
|
432
432
|
if not xml_file:
|
|
433
433
|
import importlib.resources as pkg_resources
|
|
434
|
+
from regscale.models import integration_models
|
|
434
435
|
|
|
435
|
-
|
|
436
|
+
files = pkg_resources.files(integration_models)
|
|
437
|
+
cci_path = files / "CCI_List.xml"
|
|
438
|
+
xml_file = str(cci_path)
|
|
436
439
|
root = _load_xml_file(xml_file)
|
|
437
440
|
importer = CCIImporter(root, version=nist_version, verbose=verbose)
|
|
438
441
|
_process_cci_import(importer, dry_run, verbose, catalog_id)
|
|
439
|
-
|
|
440
|
-
except click.ClickException:
|
|
441
|
-
raise
|
|
442
442
|
except Exception as e:
|
|
443
|
-
|
|
444
|
-
raise click.ClickException(f"Unexpected error: {e}")
|
|
443
|
+
error_and_exit(f"Unexpected error: {e}")
|
|
@@ -20,6 +20,59 @@ def fedramp():
|
|
|
20
20
|
"""Performs bulk processing of FedRAMP files (Upload trusted data only)."""
|
|
21
21
|
|
|
22
22
|
|
|
23
|
+
@fedramp.command(context_settings={"show_default": True})
|
|
24
|
+
@click.option(
|
|
25
|
+
"--ssp_id",
|
|
26
|
+
"-s",
|
|
27
|
+
type=click.STRING,
|
|
28
|
+
required=True,
|
|
29
|
+
prompt="Enter the SSP ID to export POAMs from",
|
|
30
|
+
help="The RegScale SSP ID to export POAMs from",
|
|
31
|
+
)
|
|
32
|
+
@click.option(
|
|
33
|
+
"--output_file",
|
|
34
|
+
"-o",
|
|
35
|
+
type=click.STRING,
|
|
36
|
+
required=True,
|
|
37
|
+
prompt="Enter the output file path (xlsx)",
|
|
38
|
+
help="The output file path for the POAM export (xlsx format)",
|
|
39
|
+
)
|
|
40
|
+
@click.option(
|
|
41
|
+
"--template_path",
|
|
42
|
+
"-t",
|
|
43
|
+
type=click.Path(exists=True, dir_okay=False, file_okay=True),
|
|
44
|
+
required=False,
|
|
45
|
+
help="Path to the FedRAMP POAM template Excel file (defaults to ./templates/FedRAMP-POAM-Template.xlsx)",
|
|
46
|
+
)
|
|
47
|
+
@click.option(
|
|
48
|
+
"--point_of_contact",
|
|
49
|
+
"-p",
|
|
50
|
+
type=click.STRING,
|
|
51
|
+
required=False,
|
|
52
|
+
default="",
|
|
53
|
+
help="Point of Contact name for POAMs (defaults to empty string)",
|
|
54
|
+
)
|
|
55
|
+
def export_poam_v5(ssp_id: str, output_file: str, template_path: Optional[click.Path], point_of_contact: str):
|
|
56
|
+
"""
|
|
57
|
+
Export FedRAMP Rev 5 POAM Excel file with advanced formatting.
|
|
58
|
+
|
|
59
|
+
This export includes:
|
|
60
|
+
- Dynamic POAM ID generation based on source file paths
|
|
61
|
+
- KEV date determination from CISA KEV catalog
|
|
62
|
+
- Deviation status mapping (Approved/Pending/Rejected)
|
|
63
|
+
- Custom milestone and comment generation
|
|
64
|
+
- Excel formatting for Rev 5 template
|
|
65
|
+
- Configurable Point of Contact
|
|
66
|
+
"""
|
|
67
|
+
from pathlib import Path
|
|
68
|
+
from regscale.integrations.public.fedramp.poam_export_v5 import export_poam_v5 as export_func
|
|
69
|
+
|
|
70
|
+
logger.info(f"Exporting FedRAMP Rev 5 POAM for SSP {ssp_id}")
|
|
71
|
+
|
|
72
|
+
template = Path(template_path) if template_path else None
|
|
73
|
+
export_func(ssp_id=ssp_id, output_file=output_file, template_path=template, point_of_contact=point_of_contact)
|
|
74
|
+
|
|
75
|
+
|
|
23
76
|
# FedRAMP Docx Support
|
|
24
77
|
@fedramp.command(context_settings={"show_default": True})
|
|
25
78
|
@click.option(
|
|
@@ -108,7 +161,9 @@ def load_fedramp_docx(
|
|
|
108
161
|
"by using the -p flag."
|
|
109
162
|
)
|
|
110
163
|
|
|
111
|
-
process_fedramp_docx_v5(
|
|
164
|
+
process_fedramp_docx_v5(
|
|
165
|
+
file_path, base_fedramp_profile_id, save_data, add_missing, appendix_a_file_path
|
|
166
|
+
) # type: ignore
|
|
112
167
|
|
|
113
168
|
|
|
114
169
|
@fedramp.command()
|
|
@@ -435,7 +490,10 @@ def import_drf(file_path: click.Path, regscale_id: int, regscale_module: str) ->
|
|
|
435
490
|
"--profile_id",
|
|
436
491
|
"-p",
|
|
437
492
|
type=click.INT,
|
|
438
|
-
help=
|
|
493
|
+
help=(
|
|
494
|
+
"The ID number from RegScale of the Profile. (This will generate the control implementations "
|
|
495
|
+
"for a new Security Plan)"
|
|
496
|
+
),
|
|
439
497
|
prompt="Enter RegScale Profile ID",
|
|
440
498
|
required=True,
|
|
441
499
|
)
|