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.

Files changed (146) hide show
  1. regscale/_version.py +1 -1
  2. regscale/airflow/hierarchy.py +2 -2
  3. regscale/core/app/application.py +19 -4
  4. regscale/core/app/internal/evidence.py +419 -2
  5. regscale/core/app/internal/login.py +0 -1
  6. regscale/core/app/utils/catalog_utils/common.py +1 -1
  7. regscale/dev/code_gen.py +24 -20
  8. regscale/integrations/commercial/jira.py +367 -126
  9. regscale/integrations/commercial/qualys/__init__.py +7 -8
  10. regscale/integrations/commercial/qualys/scanner.py +8 -3
  11. regscale/integrations/commercial/sicura/api.py +14 -13
  12. regscale/integrations/commercial/sicura/commands.py +8 -2
  13. regscale/integrations/commercial/sicura/scanner.py +49 -39
  14. regscale/integrations/commercial/stigv2/ckl_parser.py +5 -5
  15. regscale/integrations/commercial/synqly/assets.py +17 -0
  16. regscale/integrations/commercial/synqly/vulnerabilities.py +45 -28
  17. regscale/integrations/commercial/tenablev2/cis_parsers.py +453 -0
  18. regscale/integrations/commercial/tenablev2/cis_scanner.py +447 -0
  19. regscale/integrations/commercial/tenablev2/commands.py +142 -1
  20. regscale/integrations/commercial/tenablev2/scanner.py +0 -1
  21. regscale/integrations/commercial/tenablev2/stig_parsers.py +113 -57
  22. regscale/integrations/commercial/wizv2/WizDataMixin.py +1 -1
  23. regscale/integrations/commercial/wizv2/click.py +64 -79
  24. regscale/integrations/commercial/wizv2/compliance/__init__.py +15 -0
  25. regscale/integrations/commercial/wizv2/{policy_compliance_helpers.py → compliance/helpers.py} +78 -60
  26. regscale/integrations/commercial/wizv2/compliance_report.py +161 -165
  27. regscale/integrations/commercial/wizv2/core/__init__.py +133 -0
  28. regscale/integrations/commercial/wizv2/{async_client.py → core/client.py} +3 -3
  29. regscale/integrations/commercial/wizv2/{constants.py → core/constants.py} +1 -17
  30. regscale/integrations/commercial/wizv2/core/file_operations.py +237 -0
  31. regscale/integrations/commercial/wizv2/fetchers/__init__.py +11 -0
  32. regscale/integrations/commercial/wizv2/{data_fetcher.py → fetchers/policy_assessment.py} +5 -9
  33. regscale/integrations/commercial/wizv2/issue.py +1 -1
  34. regscale/integrations/commercial/wizv2/models/__init__.py +0 -0
  35. regscale/integrations/commercial/wizv2/parsers/__init__.py +34 -0
  36. regscale/integrations/commercial/wizv2/{parsers.py → parsers/main.py} +1 -1
  37. regscale/integrations/commercial/wizv2/processors/__init__.py +11 -0
  38. regscale/integrations/commercial/wizv2/{finding_processor.py → processors/finding.py} +1 -1
  39. regscale/integrations/commercial/wizv2/reports.py +1 -1
  40. regscale/integrations/commercial/wizv2/sbom.py +1 -1
  41. regscale/integrations/commercial/wizv2/scanner.py +39 -99
  42. regscale/integrations/commercial/wizv2/utils/__init__.py +48 -0
  43. regscale/integrations/commercial/wizv2/{utils.py → utils/main.py} +116 -61
  44. regscale/integrations/commercial/wizv2/variables.py +89 -3
  45. regscale/integrations/compliance_integration.py +60 -41
  46. regscale/integrations/control_matcher.py +377 -0
  47. regscale/integrations/due_date_handler.py +14 -8
  48. regscale/integrations/milestone_manager.py +291 -0
  49. regscale/integrations/public/__init__.py +1 -0
  50. regscale/integrations/public/cci_importer.py +37 -38
  51. regscale/integrations/public/fedramp/click.py +60 -2
  52. regscale/integrations/public/fedramp/docx_parser.py +10 -1
  53. regscale/integrations/public/fedramp/fedramp_cis_crm.py +393 -340
  54. regscale/integrations/public/fedramp/fedramp_five.py +1 -1
  55. regscale/integrations/public/fedramp/poam_export_v5.py +888 -0
  56. regscale/integrations/scanner_integration.py +277 -153
  57. regscale/models/integration_models/cisa_kev_data.json +282 -9
  58. regscale/models/integration_models/nexpose.py +36 -10
  59. regscale/models/integration_models/qualys.py +3 -4
  60. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  61. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +24 -7
  62. regscale/models/integration_models/synqly_models/synqly_model.py +8 -1
  63. regscale/models/locking.py +12 -8
  64. regscale/models/platform.py +1 -2
  65. regscale/models/regscale_models/control_implementation.py +47 -22
  66. regscale/models/regscale_models/issue.py +256 -95
  67. regscale/models/regscale_models/milestone.py +1 -1
  68. regscale/models/regscale_models/regscale_model.py +6 -1
  69. regscale/templates/__init__.py +0 -0
  70. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
  71. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +145 -65
  72. tests/regscale/integrations/commercial/__init__.py +0 -0
  73. tests/regscale/integrations/commercial/conftest.py +28 -0
  74. tests/regscale/integrations/commercial/microsoft_defender/__init__.py +1 -0
  75. tests/regscale/integrations/commercial/microsoft_defender/test_defender.py +1517 -0
  76. tests/regscale/integrations/commercial/microsoft_defender/test_defender_api.py +1748 -0
  77. tests/regscale/integrations/commercial/microsoft_defender/test_defender_constants.py +327 -0
  78. tests/regscale/integrations/commercial/microsoft_defender/test_defender_scanner.py +487 -0
  79. tests/regscale/integrations/commercial/test_aws.py +3731 -0
  80. tests/regscale/integrations/commercial/test_burp.py +48 -0
  81. tests/regscale/integrations/commercial/test_crowdstrike.py +49 -0
  82. tests/regscale/integrations/commercial/test_dependabot.py +341 -0
  83. tests/regscale/integrations/commercial/test_gcp.py +1543 -0
  84. tests/regscale/integrations/commercial/test_gitlab.py +549 -0
  85. tests/regscale/integrations/commercial/test_ip_mac_address_length.py +84 -0
  86. tests/regscale/integrations/commercial/test_jira.py +2204 -0
  87. tests/regscale/integrations/commercial/test_npm_audit.py +42 -0
  88. tests/regscale/integrations/commercial/test_okta.py +1228 -0
  89. tests/regscale/integrations/commercial/test_sarif_converter.py +251 -0
  90. tests/regscale/integrations/commercial/test_sicura.py +350 -0
  91. tests/regscale/integrations/commercial/test_snow.py +423 -0
  92. tests/regscale/integrations/commercial/test_sonarcloud.py +394 -0
  93. tests/regscale/integrations/commercial/test_sqlserver.py +186 -0
  94. tests/regscale/integrations/commercial/test_stig.py +33 -0
  95. tests/regscale/integrations/commercial/test_stig_mapper.py +153 -0
  96. tests/regscale/integrations/commercial/test_stigv2.py +406 -0
  97. tests/regscale/integrations/commercial/test_wiz.py +1365 -0
  98. tests/regscale/integrations/commercial/test_wiz_inventory.py +256 -0
  99. tests/regscale/integrations/commercial/wizv2/__init__.py +339 -0
  100. tests/regscale/integrations/commercial/wizv2/compliance/__init__.py +1 -0
  101. tests/regscale/integrations/commercial/wizv2/compliance/test_helpers.py +903 -0
  102. tests/regscale/integrations/commercial/wizv2/core/__init__.py +1 -0
  103. tests/regscale/integrations/commercial/wizv2/core/test_auth.py +701 -0
  104. tests/regscale/integrations/commercial/wizv2/core/test_client.py +1037 -0
  105. tests/regscale/integrations/commercial/wizv2/core/test_file_operations.py +989 -0
  106. tests/regscale/integrations/commercial/wizv2/fetchers/__init__.py +1 -0
  107. tests/regscale/integrations/commercial/wizv2/fetchers/test_policy_assessment.py +805 -0
  108. tests/regscale/integrations/commercial/wizv2/parsers/__init__.py +1 -0
  109. tests/regscale/integrations/commercial/wizv2/parsers/test_main.py +1153 -0
  110. tests/regscale/integrations/commercial/wizv2/processors/__init__.py +1 -0
  111. tests/regscale/integrations/commercial/wizv2/processors/test_finding.py +671 -0
  112. tests/regscale/integrations/commercial/wizv2/test_WizDataMixin.py +537 -0
  113. tests/regscale/integrations/commercial/wizv2/test_click_comprehensive.py +851 -0
  114. tests/regscale/integrations/commercial/wizv2/test_compliance_report_comprehensive.py +910 -0
  115. tests/regscale/integrations/commercial/wizv2/test_compliance_report_normalization.py +138 -0
  116. tests/regscale/integrations/commercial/wizv2/test_file_cleanup.py +283 -0
  117. tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
  118. tests/regscale/integrations/commercial/wizv2/test_issue.py +343 -0
  119. tests/regscale/integrations/commercial/wizv2/test_issue_comprehensive.py +1203 -0
  120. tests/regscale/integrations/commercial/wizv2/test_reports.py +497 -0
  121. tests/regscale/integrations/commercial/wizv2/test_sbom.py +643 -0
  122. tests/regscale/integrations/commercial/wizv2/test_scanner_comprehensive.py +805 -0
  123. tests/regscale/integrations/commercial/wizv2/test_wiz_click_client_id.py +165 -0
  124. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +1394 -0
  125. tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_unit.py +341 -0
  126. tests/regscale/integrations/commercial/wizv2/test_wiz_control_normalization.py +138 -0
  127. tests/regscale/integrations/commercial/wizv2/test_wiz_findings_comprehensive.py +364 -0
  128. tests/regscale/integrations/commercial/wizv2/test_wiz_inventory_comprehensive.py +644 -0
  129. tests/regscale/integrations/commercial/wizv2/test_wiz_status_mapping.py +149 -0
  130. tests/regscale/integrations/commercial/wizv2/test_wizv2.py +1132 -0
  131. tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +519 -0
  132. tests/regscale/integrations/commercial/wizv2/utils/__init__.py +1 -0
  133. tests/regscale/integrations/commercial/wizv2/utils/test_main.py +1523 -0
  134. tests/regscale/integrations/public/fedramp/__init__.py +1 -0
  135. tests/regscale/integrations/public/fedramp/test_poam_export_v5.py +1293 -0
  136. tests/regscale/integrations/public/test_fedramp.py +301 -0
  137. tests/regscale/integrations/test_control_matcher.py +1397 -0
  138. tests/regscale/integrations/test_control_matching.py +155 -0
  139. tests/regscale/integrations/test_milestone_manager.py +408 -0
  140. tests/regscale/models/test_issue.py +378 -1
  141. regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
  142. /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
  143. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
  144. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
  145. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
  146. {regscale_cli-6.25.1.0.dist-info → regscale_cli-6.27.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
- click.echo("Parsing CCI items from XML...")
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
- click.echo("Mapping CCI data to security controls...")
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
- for main_control, cci_list in self.normalized_cci.items():
307
- if main_control in control_map:
308
- control_id = control_map[main_control]
309
- control_created, control_updated = self._process_cci_for_control(
310
- control_id, cci_list, user_id, tenant_id
311
- )
312
- created_count += control_created
313
- updated_count += control_updated
314
- else:
315
- skipped_count += len(cci_list)
316
- if self.verbose:
317
- click.echo(f"Warning: Control not found for key: {main_control}", err=True)
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": len(self.normalized_cci),
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
- click.echo(f"Loading XML file: {xml_file}")
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
- click.echo("\nNormalized CCI Data:")
364
+ logger.info("\nNormalized CCI Data:")
362
365
  for key, value in normalized_data.items():
363
- click.echo(f" {key}: {len(value)} CCI items")
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
- click.echo(f" - {cci['cci_id']}: {definition_preview}")
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
- click.echo(
377
- click.style(
378
- f"\nDatabase operations completed:"
379
- f"\n - Created: {stats['created']}"
380
- f"\n - Updated: {stats['updated']}"
381
- f"\n - Skipped: {stats['skipped']}"
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
- click.echo(click.style(f"Successfully parsed {len(normalized_data)} normalized CCI entries", fg="green"))
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
- click.echo(click.style("\nDRY RUN MODE: No database changes were made", fg="yellow"))
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
- xml_file = pkg_resources.path("regscale.models.integration_models", "CCI_List.xml")
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
- click.echo(click.style(f"Unexpected error: {e}", fg="red"), err=True)
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(file_path, base_fedramp_profile_id, save_data, add_missing, appendix_a_file_path) # type: ignore
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="The ID number from RegScale of the Profile. (This will generate the control implementations for a new Security Plan)",
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
  )
@@ -246,7 +246,16 @@ class SSPDocParser:
246
246
  headers = self.extract_table_headers(table, namespaces)
247
247
  table_data = []
248
248
  tables_dicts_list["preceding_text"] = preceding_text[i] if i < len(preceding_text) else ""
249
- if len(headers) == 1 and headers[0].lower() in vertical_tables:
249
+ # Check if this is a vertical table:
250
+ is_vertical = False
251
+ # - Single header that matches vertical_tables list
252
+ first_option = len(headers) == 1 and headers[0].lower() in vertical_tables
253
+ # - OR two headers where first matches and second is empty/whitespace
254
+ second_option = len(headers) == 2 and headers[0].lower() in vertical_tables and not headers[1].strip()
255
+ if first_option or second_option:
256
+ is_vertical = True
257
+
258
+ if is_vertical:
250
259
  table_data = self.extract_vertical_row_data(table, namespaces)
251
260
  tables_dicts_list["table_data"] = table_data
252
261
  # tables_dicts_list.append({headers[0].lower(): table_data})