regscale-cli 6.26.0.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/core/app/application.py +1 -1
- regscale/core/app/internal/evidence.py +419 -2
- 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/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 +44 -59
- 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 +10 -9
- 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 +40 -100
- 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 +0 -46
- regscale/integrations/control_matcher.py +22 -3
- regscale/integrations/due_date_handler.py +14 -8
- 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/scanner_integration.py +127 -57
- regscale/models/integration_models/cisa_kev_data.json +132 -9
- 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/regscale_models/control_implementation.py +1 -1
- regscale/models/regscale_models/issue.py +0 -1
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/METADATA +1 -17
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/RECORD +93 -60
- tests/regscale/integrations/commercial/test_jira.py +481 -91
- tests/regscale/integrations/commercial/test_wiz.py +96 -200
- tests/regscale/integrations/commercial/wizv2/__init__.py +1 -1
- 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_file_cleanup.py +283 -0
- tests/regscale/integrations/commercial/wizv2/test_file_operations.py +260 -0
- tests/regscale/integrations/commercial/wizv2/test_issue.py +1 -1
- 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 +1 -1
- tests/regscale/integrations/commercial/wizv2/test_wiz_compliance_report.py +72 -29
- 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_wizv2.py +946 -78
- tests/regscale/integrations/commercial/wizv2/test_wizv2_utils.py +97 -202
- 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/test_fedramp.py +301 -0
- tests/regscale/integrations/test_control_matcher.py +83 -0
- regscale/integrations/commercial/wizv2/policy_compliance.py +0 -3543
- tests/regscale/integrations/commercial/wizv2/test_wiz_policy_compliance.py +0 -750
- /regscale/integrations/commercial/wizv2/{wiz_auth.py → core/auth.py} +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.26.0.0.dist-info → regscale_cli-6.27.0.0.dist-info}/top_level.txt +0 -0
|
@@ -12,7 +12,9 @@ from collections import Counter
|
|
|
12
12
|
from concurrent.futures import as_completed
|
|
13
13
|
from concurrent.futures.thread import ThreadPoolExecutor
|
|
14
14
|
from datetime import datetime
|
|
15
|
+
from functools import lru_cache
|
|
15
16
|
from pathlib import Path
|
|
17
|
+
from tempfile import gettempdir
|
|
16
18
|
from threading import Thread
|
|
17
19
|
from types import ModuleType
|
|
18
20
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Tuple, TypeVar
|
|
@@ -23,6 +25,7 @@ from regscale.core.app.api import Api
|
|
|
23
25
|
from regscale.core.app.utils.api_handler import APIInsertionError, APIUpdateError
|
|
24
26
|
from regscale.core.app.utils.app_utils import compute_hash, create_progress_object, error_and_exit, get_current_datetime
|
|
25
27
|
from regscale.core.utils.graphql import GraphQLQuery
|
|
28
|
+
from regscale.integrations.control_matcher import ControlMatcher
|
|
26
29
|
from regscale.integrations.public.fedramp.ssp_logger import SSPLogger
|
|
27
30
|
from regscale.models import ControlObjective, ImplementationObjective, ImportValidater, Parameter, Profile
|
|
28
31
|
from regscale.models.regscale_models import (
|
|
@@ -41,9 +44,6 @@ from regscale.utils.version import RegscaleVersion
|
|
|
41
44
|
if TYPE_CHECKING:
|
|
42
45
|
import pandas as pd
|
|
43
46
|
|
|
44
|
-
from functools import lru_cache
|
|
45
|
-
from tempfile import gettempdir
|
|
46
|
-
|
|
47
47
|
T = TypeVar("T")
|
|
48
48
|
|
|
49
49
|
logger = SSPLogger()
|
|
@@ -116,131 +116,91 @@ def get_pandas() -> ModuleType:
|
|
|
116
116
|
return pd
|
|
117
117
|
|
|
118
118
|
|
|
119
|
-
def
|
|
120
|
-
source: str, control_objectives: List[ControlObjective]
|
|
121
|
-
) -> Tuple[Optional[str], List[str], str]:
|
|
122
|
-
"""
|
|
123
|
-
Smart algorithm to find mapping by source, checking ControlObjective table only.
|
|
124
|
-
|
|
125
|
-
:param str source: The source control ID (e.g., "AC-1(a)", "AC-01 (a)")
|
|
126
|
-
:param List[ControlObjective] control_objectives: List of ControlObjective objects to search
|
|
127
|
-
:return: Tuple of (primary_oscal_id, sub_parts, status_message)
|
|
128
|
-
:rtype: Tuple[Optional[str], List[str], str]
|
|
119
|
+
def _build_potential_oscal_ids(variation: str) -> List[str]:
|
|
129
120
|
"""
|
|
130
|
-
|
|
131
|
-
expected_oscal = _convert_to_oscal_identifier(source)
|
|
132
|
-
|
|
133
|
-
if not expected_oscal:
|
|
134
|
-
return None, [], f"Unable to convert control {source} to OSCAL format"
|
|
135
|
-
|
|
136
|
-
# Step 2: Search otherId field in ControlObjective table for exact match
|
|
137
|
-
if _find_exact_objective_by_other_id(expected_oscal, control_objectives):
|
|
138
|
-
return expected_oscal, [], f"Found exact match: {expected_oscal}"
|
|
139
|
-
|
|
140
|
-
# Step 3: Search for subparts (pattern: expected_oscal + ".*")
|
|
141
|
-
if sub_parts := _find_subpart_objectives_by_other_id(expected_oscal, control_objectives):
|
|
142
|
-
return None, sub_parts, f"Control exists with {len(sub_parts)} sub-parts. Update import file."
|
|
121
|
+
Build potential OSCAL ID formats from a control ID variation.
|
|
143
122
|
|
|
144
|
-
|
|
145
|
-
return
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
def _convert_to_oscal_identifier(source: str) -> Optional[str]:
|
|
149
|
-
"""
|
|
150
|
-
Convert control name to OSCAL identifier using algorithmic patterns.
|
|
151
|
-
|
|
152
|
-
:param str source: The source control ID (e.g., "AC-1(a)", "AC-01 (a)", "AC-6(1)")
|
|
153
|
-
:return: Generated OSCAL identifier or None
|
|
154
|
-
:rtype: Optional[str]
|
|
123
|
+
:param str variation: Control ID variation (e.g., "AC-1", "AC-01")
|
|
124
|
+
:return: List of potential OSCAL IDs
|
|
125
|
+
:rtype: List[str]
|
|
155
126
|
"""
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
# Pattern 2: Control part - AC-1(a), AC-01 (a), AC-1 ( a )
|
|
165
|
-
if match := re.match(r"^([A-Z]{2})-(\d{1,2})\s*\(\s*([a-z])\s*\)$", source, re.IGNORECASE):
|
|
166
|
-
family, number, part = match.groups()
|
|
167
|
-
return f"{family.lower()}-{int(number)}_smt.{part.lower()}"
|
|
168
|
-
|
|
169
|
-
# Pattern 3: Control enhancement part - AC-6(1)(a), AC-02 (07) (a), AC-6 ( 1 ) ( a )
|
|
170
|
-
if match := re.match(r"^([A-Z]{2})-(\d{1,2})\s*\(\s*(\d{1,2})\s*\)\s*\(\s*([a-z])\s*\)$", source, re.IGNORECASE):
|
|
171
|
-
family, number, enhancement, part = match.groups()
|
|
172
|
-
return f"{family.lower()}-{int(number)}.{int(enhancement)}_smt.{part.lower()}"
|
|
173
|
-
|
|
174
|
-
# Pattern 4: Base control - AC-1, AC-01
|
|
175
|
-
if match := re.match(r"^([A-Z]{2})-(\d{1,2})$", source, re.IGNORECASE):
|
|
176
|
-
family, number = match.groups()
|
|
177
|
-
return f"{family.lower()}-{int(number)}_smt"
|
|
178
|
-
|
|
179
|
-
return None
|
|
127
|
+
variation_lower = variation.lower()
|
|
128
|
+
return [
|
|
129
|
+
f"{variation_lower}_smt",
|
|
130
|
+
f"{variation_lower}_smt.a",
|
|
131
|
+
f"{variation_lower}_smt.b",
|
|
132
|
+
f"{variation_lower}_smt.c",
|
|
133
|
+
]
|
|
180
134
|
|
|
181
135
|
|
|
182
|
-
def
|
|
136
|
+
def _matches_oscal_id(obj_id: str, variation: str) -> bool:
|
|
183
137
|
"""
|
|
184
|
-
Check if
|
|
138
|
+
Check if an objective's otherId matches any OSCAL ID format for the given variation.
|
|
185
139
|
|
|
186
|
-
:param str
|
|
187
|
-
:param
|
|
188
|
-
:return: True if
|
|
140
|
+
:param str obj_id: The objective's otherId
|
|
141
|
+
:param str variation: Control ID variation
|
|
142
|
+
:return: True if matches, False otherwise
|
|
189
143
|
:rtype: bool
|
|
190
144
|
"""
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
return True
|
|
194
|
-
return False
|
|
145
|
+
potential_ids = _build_potential_oscal_ids(variation)
|
|
146
|
+
return obj_id in potential_ids or obj_id.startswith(f"{variation.lower()}_smt")
|
|
195
147
|
|
|
196
148
|
|
|
197
|
-
def
|
|
149
|
+
def _find_matching_objectives(control_objectives: List[ControlObjective], variations: set) -> List[ControlObjective]:
|
|
198
150
|
"""
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
Examples:
|
|
202
|
-
- "ac-1" -> "ac-01"
|
|
203
|
-
- "ac-10" -> "ac-10"
|
|
204
|
-
- "ac-2.7" -> "ac-02"
|
|
151
|
+
Find objectives that match any of the control ID variations.
|
|
205
152
|
|
|
206
|
-
:param
|
|
207
|
-
:
|
|
208
|
-
:
|
|
153
|
+
:param List[ControlObjective] control_objectives: List of objectives to search
|
|
154
|
+
:param set variations: Set of control ID variations
|
|
155
|
+
:return: List of matched objectives
|
|
156
|
+
:rtype: List[ControlObjective]
|
|
209
157
|
"""
|
|
210
|
-
|
|
211
|
-
base_control = oscal_control_id.split(".")[0] # "ac-2.7" -> "ac-2"
|
|
212
|
-
|
|
213
|
-
# Split into family and number
|
|
214
|
-
parts = base_control.split("-")
|
|
215
|
-
if len(parts) != 2:
|
|
216
|
-
return oscal_control_id # Return as-is if not in expected format
|
|
158
|
+
matched_objectives = []
|
|
217
159
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
160
|
+
for obj in control_objectives:
|
|
161
|
+
if not hasattr(obj, "otherId") or not obj.otherId:
|
|
162
|
+
continue
|
|
163
|
+
|
|
164
|
+
obj_id = obj.otherId
|
|
165
|
+
for variation in variations:
|
|
166
|
+
if _matches_oscal_id(obj_id, variation):
|
|
167
|
+
if obj not in matched_objectives:
|
|
168
|
+
matched_objectives.append(obj)
|
|
169
|
+
break
|
|
223
170
|
|
|
224
|
-
return
|
|
171
|
+
return matched_objectives
|
|
225
172
|
|
|
226
173
|
|
|
227
|
-
def
|
|
174
|
+
def find_objectives_using_control_matcher(
|
|
175
|
+
source: str, control_objectives: List[ControlObjective], control_matcher: ControlMatcher
|
|
176
|
+
) -> Tuple[List[ControlObjective], str]:
|
|
228
177
|
"""
|
|
229
|
-
Find
|
|
178
|
+
Find control objectives using ControlMatcher for consistent control ID parsing and matching.
|
|
230
179
|
|
|
231
|
-
:param str
|
|
232
|
-
:param List[ControlObjective] control_objectives: List of ControlObjective objects
|
|
233
|
-
:
|
|
234
|
-
:
|
|
180
|
+
:param str source: The source control ID (e.g., "AC-1(a)", "AC-01 (a)")
|
|
181
|
+
:param List[ControlObjective] control_objectives: List of ControlObjective objects to search
|
|
182
|
+
:param ControlMatcher control_matcher: Instance of ControlMatcher for parsing and variations
|
|
183
|
+
:return: Tuple of (matched objectives list, status_message)
|
|
184
|
+
:rtype: Tuple[List[ControlObjective], str]
|
|
235
185
|
"""
|
|
236
|
-
|
|
237
|
-
|
|
186
|
+
# Parse the control ID using ControlMatcher
|
|
187
|
+
parsed_id = control_matcher.parse_control_id(source)
|
|
188
|
+
if not parsed_id:
|
|
189
|
+
return [], f"Unable to parse control {source}"
|
|
238
190
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
191
|
+
# Get all variations of this control ID
|
|
192
|
+
# pylint: disable=protected-access # Using internal method for control ID variation matching
|
|
193
|
+
variations = control_matcher._get_control_id_variations(parsed_id)
|
|
194
|
+
if not variations:
|
|
195
|
+
return [], f"Unable to generate variations for {source}"
|
|
196
|
+
|
|
197
|
+
# Find matching objectives
|
|
198
|
+
matched_objectives = _find_matching_objectives(control_objectives, variations)
|
|
242
199
|
|
|
243
|
-
|
|
200
|
+
if matched_objectives:
|
|
201
|
+
return matched_objectives, f"Found {len(matched_objectives)} objective(s) for {source}"
|
|
202
|
+
|
|
203
|
+
return [], f"No database match found for {source} (parsed: {parsed_id})"
|
|
244
204
|
|
|
245
205
|
|
|
246
206
|
def transform_control(control: str) -> str:
|
|
@@ -314,7 +274,7 @@ def gen_key(control_id: str):
|
|
|
314
274
|
# 1. The last (number) if it exists
|
|
315
275
|
# 2. The main control number if no enhancement exists
|
|
316
276
|
# And excludes any trailing (letter) - handles extra spaces like AC-6 ( 1 ) ( a )
|
|
317
|
-
pattern = r"^(
|
|
277
|
+
pattern = r"^(\w+-\d+(?:\s*\(\s*\d+\s*\))?)(?:\s*\(\s*[a-zA-Z]\s*\))?$"
|
|
318
278
|
|
|
319
279
|
match = re.match(pattern, control_id)
|
|
320
280
|
if match:
|
|
@@ -392,7 +352,7 @@ def map_origination(control_id: str, cis_data: dict) -> dict:
|
|
|
392
352
|
}
|
|
393
353
|
|
|
394
354
|
# Initialize result with all flags set to False
|
|
395
|
-
result =
|
|
355
|
+
result = dict.fromkeys(origination_mapping.values(), False)
|
|
396
356
|
result["record_text"] = ""
|
|
397
357
|
|
|
398
358
|
# Find matching CIS records
|
|
@@ -469,6 +429,103 @@ def get_multi_status(record: dict) -> str:
|
|
|
469
429
|
return status_map.get(implementation_status, NOT_IMPLEMENTED)
|
|
470
430
|
|
|
471
431
|
|
|
432
|
+
def _calculate_responsibility(control_originations: List[str], imp: ControlImplementation) -> str:
|
|
433
|
+
"""
|
|
434
|
+
Calculate responsibility from control originations.
|
|
435
|
+
|
|
436
|
+
:param List[str] control_originations: List of control origination values
|
|
437
|
+
:param ControlImplementation imp: Control implementation
|
|
438
|
+
:return: Calculated responsibility value
|
|
439
|
+
:rtype: str
|
|
440
|
+
"""
|
|
441
|
+
try:
|
|
442
|
+
if RegscaleVersion.meets_minimum_version("6.20.17.0"):
|
|
443
|
+
return ",".join(control_originations)
|
|
444
|
+
return next(iter(control_originations))
|
|
445
|
+
except StopIteration:
|
|
446
|
+
if imp.responsibility:
|
|
447
|
+
return imp.responsibility.split(",")[0]
|
|
448
|
+
return SERVICE_PROVIDER_CORPORATE
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
def _create_new_implementation_objective(
|
|
452
|
+
leverage_auth_id: int,
|
|
453
|
+
imp: ControlImplementation,
|
|
454
|
+
objective: ControlObjective,
|
|
455
|
+
cis_record: dict,
|
|
456
|
+
responsibility: str,
|
|
457
|
+
cloud_responsibility: str,
|
|
458
|
+
customer_responsibility: str,
|
|
459
|
+
can_be_inherited_from_csp: str,
|
|
460
|
+
) -> ImplementationObjective:
|
|
461
|
+
"""
|
|
462
|
+
Create a new implementation objective.
|
|
463
|
+
|
|
464
|
+
:param int leverage_auth_id: Leveraged authorization ID
|
|
465
|
+
:param ControlImplementation imp: Control implementation
|
|
466
|
+
:param ControlObjective objective: Control objective
|
|
467
|
+
:param dict cis_record: CIS record data
|
|
468
|
+
:param str responsibility: Responsibility value
|
|
469
|
+
:param str cloud_responsibility: Cloud responsibility value
|
|
470
|
+
:param str customer_responsibility: Customer responsibility value
|
|
471
|
+
:param str can_be_inherited_from_csp: Can be inherited flag
|
|
472
|
+
:return: New implementation objective
|
|
473
|
+
:rtype: ImplementationObjective
|
|
474
|
+
"""
|
|
475
|
+
imp_obj = ImplementationObjective(
|
|
476
|
+
id=0,
|
|
477
|
+
uuid="",
|
|
478
|
+
inherited=can_be_inherited_from_csp in ["Yes", "Partial"],
|
|
479
|
+
implementationId=imp.id,
|
|
480
|
+
status=get_multi_status(cis_record),
|
|
481
|
+
objectiveId=objective.id,
|
|
482
|
+
notes=objective.name,
|
|
483
|
+
securityControlId=objective.securityControlId,
|
|
484
|
+
securityPlanId=REGSCALE_SSP_ID,
|
|
485
|
+
responsibility=responsibility,
|
|
486
|
+
cloudResponsibility=cloud_responsibility,
|
|
487
|
+
customerResponsibility=customer_responsibility,
|
|
488
|
+
authorizationId=leverage_auth_id,
|
|
489
|
+
parentObjectiveId=objective.parentObjectiveId,
|
|
490
|
+
)
|
|
491
|
+
logger.debug(
|
|
492
|
+
"Creating new Implementation Objective for Control %s with status: %s responsibility: %s",
|
|
493
|
+
imp_obj.securityControlId,
|
|
494
|
+
imp_obj.status,
|
|
495
|
+
imp_obj.responsibility,
|
|
496
|
+
)
|
|
497
|
+
return imp_obj
|
|
498
|
+
|
|
499
|
+
|
|
500
|
+
def _update_existing_implementation_objective(
|
|
501
|
+
ex_obj: ImplementationObjective,
|
|
502
|
+
cis_record: dict,
|
|
503
|
+
responsibility: str,
|
|
504
|
+
cloud_responsibility: str,
|
|
505
|
+
customer_responsibility: str,
|
|
506
|
+
) -> None:
|
|
507
|
+
"""
|
|
508
|
+
Update an existing implementation objective.
|
|
509
|
+
|
|
510
|
+
:param ImplementationObjective ex_obj: Existing implementation objective
|
|
511
|
+
:param dict cis_record: CIS record data
|
|
512
|
+
:param str responsibility: Responsibility value
|
|
513
|
+
:param str cloud_responsibility: Cloud responsibility value
|
|
514
|
+
:param str customer_responsibility: Customer responsibility value
|
|
515
|
+
:rtype: None
|
|
516
|
+
"""
|
|
517
|
+
ex_obj.status = get_multi_status(cis_record)
|
|
518
|
+
ex_obj.responsibility = responsibility
|
|
519
|
+
if cloud_responsibility.strip():
|
|
520
|
+
logger.debug(f"Updating Implementation Objective #{ex_obj.id} with responsibility: {responsibility}")
|
|
521
|
+
ex_obj.cloudResponsibility = cloud_responsibility
|
|
522
|
+
if customer_responsibility.strip():
|
|
523
|
+
logger.debug(
|
|
524
|
+
f"Updating Implementation Objective #{ex_obj.id} with cloud responsibility: {cloud_responsibility}"
|
|
525
|
+
)
|
|
526
|
+
ex_obj.customerResponsibility = customer_responsibility
|
|
527
|
+
|
|
528
|
+
|
|
472
529
|
def update_imp_objective(
|
|
473
530
|
leverage_auth_id: int,
|
|
474
531
|
existing_imp_obj: List[ImplementationObjective],
|
|
@@ -487,80 +544,49 @@ def update_imp_objective(
|
|
|
487
544
|
:rtype: None
|
|
488
545
|
:return: None
|
|
489
546
|
"""
|
|
490
|
-
|
|
491
547
|
cis_record = record.get("cis", {})
|
|
492
548
|
crm_record = record.get("crm", {})
|
|
493
|
-
# There could be multiples, take the first one as regscale will not allow multiples at the objective level.
|
|
494
|
-
control_originations = cis_record.get("control_origination", "").split(",")
|
|
495
|
-
for ix, control_origination in enumerate(control_originations):
|
|
496
|
-
control_originations[ix] = control_origination.strip()
|
|
497
549
|
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
responsibility = ",".join(control_originations)
|
|
501
|
-
else:
|
|
502
|
-
responsibility = next(origin for origin in control_originations)
|
|
550
|
+
# Parse and clean control originations
|
|
551
|
+
control_originations = [orig.strip() for orig in cis_record.get("control_origination", "").split(",")]
|
|
503
552
|
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
responsibility = imp.responsibility.split(",")[0] # only one responsiblity allowed here.
|
|
507
|
-
else:
|
|
508
|
-
responsibility = SERVICE_PROVIDER_CORPORATE
|
|
553
|
+
# Calculate responsibility
|
|
554
|
+
responsibility = _calculate_responsibility(control_originations, imp)
|
|
509
555
|
|
|
556
|
+
# Parse responsibility fields
|
|
510
557
|
customer_responsibility = clean_customer_responsibility(
|
|
511
558
|
crm_record.get("specific_inheritance_and_customer_agency_csp_responsibilities")
|
|
512
559
|
)
|
|
513
|
-
existing_pairs = {(obj.objectiveId, obj.implementationId) for obj in existing_imp_obj}
|
|
514
|
-
logger.debug(f"CRM Record: {crm_record}")
|
|
515
560
|
can_be_inherited_from_csp: str = crm_record.get("can_be_inherited_from_csp") or ""
|
|
516
561
|
cloud_responsibility = customer_responsibility if can_be_inherited_from_csp.lower() == "yes" else ""
|
|
517
562
|
customer_responsibility = customer_responsibility if can_be_inherited_from_csp.lower() != "yes" else ""
|
|
563
|
+
|
|
564
|
+
existing_pairs = {(obj.objectiveId, obj.implementationId) for obj in existing_imp_obj}
|
|
565
|
+
logger.debug(f"CRM Record: {crm_record}")
|
|
566
|
+
|
|
518
567
|
for objective in objectives:
|
|
568
|
+
if objective.securityControlId != imp.controlID:
|
|
569
|
+
continue
|
|
570
|
+
|
|
519
571
|
current_pair = (objective.id, imp.id)
|
|
520
572
|
if current_pair not in existing_pairs:
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
status=get_multi_status(cis_record),
|
|
531
|
-
objectiveId=objective.id,
|
|
532
|
-
notes=objective.name,
|
|
533
|
-
securityControlId=objective.securityControlId,
|
|
534
|
-
securityPlanId=REGSCALE_SSP_ID,
|
|
535
|
-
responsibility=responsibility,
|
|
536
|
-
cloudResponsibility=cloud_responsibility,
|
|
537
|
-
customerResponsibility=customer_responsibility,
|
|
538
|
-
authorizationId=leverage_auth_id,
|
|
539
|
-
parentObjectiveId=objective.parentObjectiveId,
|
|
540
|
-
)
|
|
541
|
-
logger.debug(
|
|
542
|
-
"Creating new Implementation Objective for Control %s with status: %s responsibility: %s",
|
|
543
|
-
imp_obj.securityControlId,
|
|
544
|
-
imp_obj.status,
|
|
545
|
-
imp_obj.responsibility,
|
|
573
|
+
imp_obj = _create_new_implementation_objective(
|
|
574
|
+
leverage_auth_id,
|
|
575
|
+
imp,
|
|
576
|
+
objective,
|
|
577
|
+
cis_record,
|
|
578
|
+
responsibility,
|
|
579
|
+
cloud_responsibility,
|
|
580
|
+
customer_responsibility,
|
|
581
|
+
can_be_inherited_from_csp,
|
|
546
582
|
)
|
|
547
583
|
UPDATED_IMPLEMENTATION_OBJECTIVES.add(imp_obj)
|
|
548
584
|
else:
|
|
549
585
|
ex_obj = next((obj for obj in existing_imp_obj if obj.objectiveId == objective.id), None)
|
|
550
586
|
if ex_obj:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
logger.debug(
|
|
555
|
-
f"Updating Implementation Objective #{ex_obj.id} with responsibility: {responsibility}"
|
|
556
|
-
)
|
|
557
|
-
ex_obj.cloudResponsibility = cloud_responsibility
|
|
558
|
-
if customer_responsibility.strip():
|
|
559
|
-
logger.debug(
|
|
560
|
-
f"Updating Implementation Objective #{ex_obj.id} with cloud responsibility: {cloud_responsibility}"
|
|
561
|
-
)
|
|
562
|
-
ex_obj.customerResponsibility = customer_responsibility
|
|
563
|
-
|
|
587
|
+
_update_existing_implementation_objective(
|
|
588
|
+
ex_obj, cis_record, responsibility, cloud_responsibility, customer_responsibility
|
|
589
|
+
)
|
|
564
590
|
UPDATED_IMPLEMENTATION_OBJECTIVES.add(ex_obj)
|
|
565
591
|
|
|
566
592
|
|
|
@@ -689,8 +715,6 @@ def get_all_imps(api: Api, cis_data: dict, version: Literal["rev4", "rev5"]) ->
|
|
|
689
715
|
:return: None
|
|
690
716
|
:rtype: None
|
|
691
717
|
"""
|
|
692
|
-
from requests import RequestException
|
|
693
|
-
|
|
694
718
|
# Check if the response is successful
|
|
695
719
|
if EXISTING_IMPLEMENTATIONS:
|
|
696
720
|
# Get Control Implementations For SSP
|
|
@@ -765,6 +789,9 @@ def update_all_objectives(
|
|
|
765
789
|
"""
|
|
766
790
|
|
|
767
791
|
all_control_objectives = get_all_control_objectives(imps=EXISTING_IMPLEMENTATIONS.values())
|
|
792
|
+
# Create ControlMatcher instance for consistent control ID parsing
|
|
793
|
+
control_matcher = ControlMatcher()
|
|
794
|
+
|
|
768
795
|
error_set = set()
|
|
769
796
|
process_task = progress.add_task(
|
|
770
797
|
"[cyan]Processing control objectives...", total=len(EXISTING_IMPLEMENTATIONS.values())
|
|
@@ -777,7 +804,13 @@ def update_all_objectives(
|
|
|
777
804
|
# Submit all tasks
|
|
778
805
|
future_to_control = {
|
|
779
806
|
executor.submit(
|
|
780
|
-
process_implementation,
|
|
807
|
+
process_implementation,
|
|
808
|
+
leveraged_auth_id,
|
|
809
|
+
imp,
|
|
810
|
+
combined_data,
|
|
811
|
+
version,
|
|
812
|
+
all_control_objectives,
|
|
813
|
+
control_matcher,
|
|
781
814
|
): imp
|
|
782
815
|
for imp in EXISTING_IMPLEMENTATIONS.values()
|
|
783
816
|
}
|
|
@@ -833,6 +866,7 @@ def process_implementation(
|
|
|
833
866
|
sheet_data: dict,
|
|
834
867
|
version: Literal["rev4", "rev5"],
|
|
835
868
|
all_objectives: List[ControlObjective],
|
|
869
|
+
control_matcher: ControlMatcher,
|
|
836
870
|
) -> Tuple[List[str], List[ImplementationObjective]]:
|
|
837
871
|
"""
|
|
838
872
|
Processes a single implementation and its associated records.
|
|
@@ -842,6 +876,7 @@ def process_implementation(
|
|
|
842
876
|
:param dict sheet_data: The CIS/CRM data to process
|
|
843
877
|
:param Literal["rev4", "rev5"] version: The version of the workbook
|
|
844
878
|
:param List[ControlObjective] all_objectives: all the control objectives
|
|
879
|
+
:param ControlMatcher control_matcher: ControlMatcher instance for control ID parsing
|
|
845
880
|
:rtype Tuple[List[str], List[ImplementationObjective]]
|
|
846
881
|
:returns A list of updated implementation objectives
|
|
847
882
|
"""
|
|
@@ -849,7 +884,7 @@ def process_implementation(
|
|
|
849
884
|
errors = []
|
|
850
885
|
processed_objectives = []
|
|
851
886
|
|
|
852
|
-
existing_objectives, filtered_records = gen_filtered_records(implementation, sheet_data,
|
|
887
|
+
existing_objectives, filtered_records = gen_filtered_records(implementation, sheet_data, control_matcher)
|
|
853
888
|
result = None
|
|
854
889
|
for record in filtered_records:
|
|
855
890
|
res = process_single_record(
|
|
@@ -859,6 +894,7 @@ def process_implementation(
|
|
|
859
894
|
control_objectives=all_objectives,
|
|
860
895
|
existing_objectives=existing_objectives,
|
|
861
896
|
version=version,
|
|
897
|
+
control_matcher=control_matcher,
|
|
862
898
|
)
|
|
863
899
|
if isinstance(res, tuple):
|
|
864
900
|
method_errors, result = res
|
|
@@ -870,32 +906,35 @@ def process_implementation(
|
|
|
870
906
|
|
|
871
907
|
|
|
872
908
|
def gen_filtered_records(
|
|
873
|
-
implementation: ControlImplementation, sheet_data: dict,
|
|
909
|
+
implementation: ControlImplementation, sheet_data: dict, control_matcher: ControlMatcher
|
|
874
910
|
) -> Tuple[List[ImplementationObjective], List[Dict[str, str]]]:
|
|
875
911
|
"""
|
|
876
|
-
Generates filtered records for a given implementation.
|
|
912
|
+
Generates filtered records for a given implementation using ControlMatcher.
|
|
877
913
|
|
|
878
914
|
:param ControlImplementation implementation: The control implementation to filter records for
|
|
879
915
|
:param dict sheet_data: The CIS/CRM data to filter
|
|
880
|
-
:param
|
|
916
|
+
:param ControlMatcher control_matcher: ControlMatcher instance for control ID matching
|
|
881
917
|
:returns A tuple of existing objectives, and filtered records
|
|
882
918
|
:rtype Tuple[List[ImplementationObjective], List[Dict[str, str]]]
|
|
883
919
|
"""
|
|
884
920
|
security_control = SecurityControl.get_object(implementation.controlID)
|
|
885
921
|
existing_objectives = ImplementationObjective.get_by_control(implementation.id)
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
922
|
+
|
|
923
|
+
# Get all variations of the control ID using ControlMatcher
|
|
924
|
+
# pylint: disable=protected-access # Using internal method for control ID variation matching
|
|
925
|
+
control_variations = control_matcher._get_control_id_variations(security_control.controlId)
|
|
926
|
+
|
|
927
|
+
# Filter records that match any variation of the control ID
|
|
928
|
+
filtered_records = []
|
|
929
|
+
for record in sheet_data.values():
|
|
930
|
+
record_control_id = record["cis"].get("regscale_control_id", "")
|
|
931
|
+
# Parse the record's control ID
|
|
932
|
+
parsed_record_id = control_matcher.parse_control_id(record_control_id)
|
|
933
|
+
if parsed_record_id:
|
|
934
|
+
# Check if the parsed record control ID matches any variation
|
|
935
|
+
# pylint: disable=protected-access # Using internal method for control ID variation matching
|
|
936
|
+
if control_variations & control_matcher._get_control_id_variations(parsed_record_id):
|
|
937
|
+
filtered_records.append(record)
|
|
899
938
|
|
|
900
939
|
return existing_objectives, filtered_records
|
|
901
940
|
|
|
@@ -919,61 +958,28 @@ def process_single_record(**kwargs) -> Tuple[List[str], Optional[ImplementationO
|
|
|
919
958
|
:rtype Tuple[List[str], Optional[ImplementationObjective]]
|
|
920
959
|
:returns A list of errors and the Implementation Objective if successful, otherwise None
|
|
921
960
|
"""
|
|
922
|
-
# No longer need to load JSON mappings - using smart algorithm only
|
|
923
|
-
|
|
924
961
|
errors = []
|
|
925
|
-
version = kwargs.get("version")
|
|
926
962
|
leveraged_auth_id: int = kwargs.get("leveraged_auth_id")
|
|
927
963
|
implementation: ControlImplementation = kwargs.get("implementation")
|
|
928
964
|
record: dict = kwargs.get("record")
|
|
929
965
|
control_objectives: List[ControlObjective] = kwargs.get("control_objectives")
|
|
930
966
|
existing_objectives: List[ImplementationObjective] = kwargs.get("existing_objectives")
|
|
931
|
-
|
|
967
|
+
control_matcher: ControlMatcher = kwargs.get("control_matcher")
|
|
932
968
|
result = None
|
|
933
969
|
|
|
934
970
|
# Get the control ID from the CIS/CRM record
|
|
935
971
|
key = record["cis"]["control_id"]
|
|
936
972
|
|
|
937
|
-
# Use
|
|
938
|
-
|
|
973
|
+
# Use ControlMatcher to find matching objectives
|
|
974
|
+
mapped_objectives, status = find_objectives_using_control_matcher(key, control_objectives, control_matcher)
|
|
939
975
|
|
|
940
|
-
logger.debug(f"
|
|
976
|
+
logger.debug(f"Control matching result for {key}: {status}")
|
|
941
977
|
|
|
942
|
-
# Add to errors list if
|
|
943
|
-
if not
|
|
978
|
+
# Add to errors list if no objectives found
|
|
979
|
+
if not mapped_objectives:
|
|
944
980
|
errors.append(f"{key}: {status}")
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
if source:
|
|
948
|
-
try:
|
|
949
|
-
objective = next(
|
|
950
|
-
obj
|
|
951
|
-
for obj in control_objectives
|
|
952
|
-
if (obj.otherId == source and version in ["rev5", "rev4"]) or (obj.name == source and version == "rev4")
|
|
953
|
-
)
|
|
954
|
-
mapped_objectives.append(objective)
|
|
955
|
-
except StopIteration:
|
|
956
|
-
logger.debug(f"Missing Source: {source}")
|
|
957
|
-
errors.append(f"Unable to find objective for control {key} ({source})")
|
|
958
|
-
|
|
959
|
-
# Process sub-parts if found
|
|
960
|
-
if parts:
|
|
961
|
-
for part in parts:
|
|
962
|
-
try:
|
|
963
|
-
if version == "rev5":
|
|
964
|
-
mapped_objectives.append(next(obj for obj in control_objectives if obj.otherId == part))
|
|
965
|
-
else:
|
|
966
|
-
mapped_objectives.append(
|
|
967
|
-
next(obj for obj in control_objectives if obj.otherId == part or obj.name == part)
|
|
968
|
-
)
|
|
969
|
-
except StopIteration:
|
|
970
|
-
errors.append(f"Unable to find part {part} for control {key}")
|
|
971
|
-
|
|
972
|
-
# Report if no mapping found at all
|
|
973
|
-
if not source and not parts:
|
|
974
|
-
errors.append(f"Unable to find source and part for control {key}")
|
|
975
|
-
|
|
976
|
-
if mapped_objectives:
|
|
981
|
+
else:
|
|
982
|
+
# Update implementation objectives with the matched control objectives
|
|
977
983
|
update_imp_objective(
|
|
978
984
|
leverage_auth_id=leveraged_auth_id,
|
|
979
985
|
existing_imp_obj=existing_objectives,
|
|
@@ -1023,46 +1029,53 @@ def parse_crm_worksheet(file_path: click.Path, crm_sheet_name: str, version: Lit
|
|
|
1023
1029
|
|
|
1024
1030
|
logger.debug(f"Skipping {skip_rows} rows in CRM worksheet")
|
|
1025
1031
|
|
|
1026
|
-
#
|
|
1027
|
-
|
|
1032
|
+
# Define required columns
|
|
1033
|
+
required_columns = [
|
|
1028
1034
|
CONTROL_ID,
|
|
1029
1035
|
"Can Be Inherited from CSP",
|
|
1030
1036
|
"Specific Inheritance and Customer Agency/CSP Responsibilities",
|
|
1031
1037
|
]
|
|
1032
1038
|
|
|
1033
1039
|
try:
|
|
1034
|
-
#
|
|
1035
|
-
header_row = validator.data.iloc[skip_rows - 1
|
|
1040
|
+
# Get the header row (which is at skip_rows - 1)
|
|
1041
|
+
header_row = validator.data.iloc[skip_rows - 1]
|
|
1036
1042
|
|
|
1037
|
-
#
|
|
1038
|
-
|
|
1039
|
-
error_and_exit(
|
|
1040
|
-
f"Not enough columns found in CRM worksheet. Expected {len(usecols)} columns but found {len(header_row)}."
|
|
1041
|
-
)
|
|
1043
|
+
# Get data rows starting from skip_rows
|
|
1044
|
+
data = validator.data.iloc[skip_rows:].reset_index(drop=True)
|
|
1042
1045
|
|
|
1043
|
-
#
|
|
1046
|
+
# Set column names from header row
|
|
1047
|
+
data.columns = header_row
|
|
1048
|
+
|
|
1049
|
+
# Find required columns by name (case-insensitive, handle extra columns in AWS CIS/CRM)
|
|
1050
|
+
available_columns = data.columns.tolist()
|
|
1051
|
+
columns_to_use = []
|
|
1044
1052
|
missing_columns = []
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
)
|
|
1053
|
+
|
|
1054
|
+
for required_col in required_columns:
|
|
1055
|
+
# Find column that matches (case-insensitive, strip whitespace)
|
|
1056
|
+
matching_col = next(
|
|
1057
|
+
(col for col in available_columns if str(col).strip().lower() == required_col.lower()), None
|
|
1058
|
+
)
|
|
1059
|
+
if matching_col is not None:
|
|
1060
|
+
columns_to_use.append(matching_col)
|
|
1061
|
+
else:
|
|
1062
|
+
missing_columns.append(required_col)
|
|
1050
1063
|
|
|
1051
1064
|
if missing_columns:
|
|
1052
|
-
error_msg =
|
|
1065
|
+
error_msg = (
|
|
1066
|
+
f"Required columns not found in the CRM worksheet: {', '.join(missing_columns)}\n"
|
|
1067
|
+
f"Available columns: {', '.join(str(col) for col in available_columns)}"
|
|
1068
|
+
)
|
|
1053
1069
|
error_and_exit(error_msg)
|
|
1054
1070
|
|
|
1055
|
-
logger.debug("
|
|
1071
|
+
logger.debug(f"Found all required columns in CRM worksheet: {', '.join(required_columns)}")
|
|
1056
1072
|
|
|
1057
|
-
#
|
|
1058
|
-
data =
|
|
1073
|
+
# Keep only the required columns
|
|
1074
|
+
data = data[columns_to_use]
|
|
1059
1075
|
|
|
1060
|
-
#
|
|
1061
|
-
data =
|
|
1062
|
-
|
|
1063
|
-
# Rename the columns to match usecols
|
|
1064
|
-
data.columns = usecols
|
|
1065
|
-
logger.debug(f"Kept only required columns: {', '.join(usecols)}")
|
|
1076
|
+
# Rename the columns to standardize names
|
|
1077
|
+
data.columns = required_columns
|
|
1078
|
+
logger.debug(f"Using columns: {', '.join(required_columns)}")
|
|
1066
1079
|
|
|
1067
1080
|
except KeyError as e:
|
|
1068
1081
|
error_and_exit(f"KeyError: {e} - One or more columns specified in usecols are not found in the dataframe.")
|
|
@@ -1098,19 +1111,66 @@ def parse_crm_worksheet(file_path: click.Path, crm_sheet_name: str, version: Lit
|
|
|
1098
1111
|
return formatted_crm
|
|
1099
1112
|
|
|
1100
1113
|
|
|
1101
|
-
def
|
|
1114
|
+
def _get_expected_cis_columns() -> List[str]:
|
|
1102
1115
|
"""
|
|
1103
|
-
|
|
1116
|
+
Get the expected column names for CIS worksheet in order.
|
|
1104
1117
|
|
|
1105
|
-
:
|
|
1106
|
-
:
|
|
1107
|
-
:return: Formatted CIS content
|
|
1108
|
-
:rtype: dict
|
|
1118
|
+
:return: List of expected column names
|
|
1119
|
+
:rtype: List[str]
|
|
1109
1120
|
"""
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1121
|
+
return [
|
|
1122
|
+
CONTROL_ID,
|
|
1123
|
+
"Implemented",
|
|
1124
|
+
ControlImplementationStatus.PartiallyImplemented,
|
|
1125
|
+
"Planned",
|
|
1126
|
+
ALT_IMPLEMENTATION,
|
|
1127
|
+
ControlImplementationStatus.NA,
|
|
1128
|
+
SERVICE_PROVIDER_CORPORATE,
|
|
1129
|
+
SERVICE_PROVIDER_SYSTEM_SPECIFIC,
|
|
1130
|
+
SERVICE_PROVIDER_HYBRID,
|
|
1131
|
+
CONFIGURED_BY_CUSTOMER,
|
|
1132
|
+
PROVIDED_BY_CUSTOMER,
|
|
1133
|
+
SHARED,
|
|
1134
|
+
INHERITED,
|
|
1135
|
+
]
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
def _normalize_cis_columns(cis_df, expected_columns: List[str]):
|
|
1139
|
+
"""
|
|
1140
|
+
Normalize CIS dataframe columns by matching expected columns and handling missing ones.
|
|
1141
|
+
|
|
1142
|
+
:param cis_df: The CIS dataframe
|
|
1143
|
+
:param List[str] expected_columns: List of expected column names
|
|
1144
|
+
:return: Normalized dataframe with standardized column names
|
|
1145
|
+
"""
|
|
1146
|
+
available_columns = cis_df.columns.tolist()
|
|
1147
|
+
columns_to_keep = []
|
|
1148
|
+
|
|
1149
|
+
for expected_col in expected_columns:
|
|
1150
|
+
matching_col = next(
|
|
1151
|
+
(col for col in available_columns if str(col).strip().lower() == expected_col.lower()), None
|
|
1152
|
+
)
|
|
1153
|
+
if matching_col is not None:
|
|
1154
|
+
columns_to_keep.append(matching_col)
|
|
1155
|
+
else:
|
|
1156
|
+
logger.warning(f"Expected column '{expected_col}' not found in CIS worksheet. Using empty values.")
|
|
1157
|
+
cis_df[expected_col] = ""
|
|
1158
|
+
columns_to_keep.append(expected_col)
|
|
1159
|
+
|
|
1160
|
+
cis_df = cis_df[columns_to_keep]
|
|
1161
|
+
cis_df.columns = expected_columns
|
|
1162
|
+
return cis_df.fillna("")
|
|
1113
1163
|
|
|
1164
|
+
|
|
1165
|
+
def _load_and_prepare_cis_dataframe(file_path: click.Path, cis_sheet_name: str, skip_rows: int):
|
|
1166
|
+
"""
|
|
1167
|
+
Load and prepare the CIS dataframe from the workbook.
|
|
1168
|
+
|
|
1169
|
+
:param click.Path file_path: The file path to the workbook
|
|
1170
|
+
:param str cis_sheet_name: The sheet name to parse
|
|
1171
|
+
:param int skip_rows: Number of rows to skip
|
|
1172
|
+
:return: Tuple of (prepared dataframe, updated skip_rows) or (None, skip_rows) if empty
|
|
1173
|
+
"""
|
|
1114
1174
|
validator = ImportValidater(
|
|
1115
1175
|
file_path=file_path,
|
|
1116
1176
|
disable_mapping=True,
|
|
@@ -1122,35 +1182,49 @@ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
|
1122
1182
|
warn_extra_headers=False,
|
|
1123
1183
|
)
|
|
1124
1184
|
if validator.data.empty:
|
|
1125
|
-
return
|
|
1185
|
+
return None, skip_rows
|
|
1126
1186
|
|
|
1127
1187
|
skip_rows = determine_skip_row(original_df=validator.data, text_to_find=CONTROL_ID, original_skip=skip_rows)
|
|
1128
1188
|
|
|
1129
|
-
|
|
1130
|
-
original_cis = validator.data
|
|
1131
|
-
|
|
1132
|
-
cis_df = original_cis.iloc[skip_rows:].reset_index(drop=True)
|
|
1133
|
-
|
|
1134
|
-
# Set the appropriate headers
|
|
1189
|
+
cis_df = validator.data.iloc[skip_rows:].reset_index(drop=True)
|
|
1135
1190
|
cis_df.columns = cis_df.iloc[0]
|
|
1136
|
-
|
|
1137
|
-
# Drop any fully empty rows
|
|
1138
1191
|
cis_df.dropna(how="all", inplace=True)
|
|
1139
|
-
|
|
1140
|
-
# Reset the index
|
|
1141
1192
|
cis_df.reset_index(drop=True, inplace=True)
|
|
1142
1193
|
|
|
1143
|
-
|
|
1144
|
-
cis_df = cis_df.iloc[:, :13]
|
|
1194
|
+
return cis_df, skip_rows
|
|
1145
1195
|
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1196
|
+
|
|
1197
|
+
def _extract_status(data_row) -> str:
|
|
1198
|
+
"""
|
|
1199
|
+
Extract the first non-empty implementation status from the CIS worksheet.
|
|
1200
|
+
|
|
1201
|
+
:param data_row: The data row to extract the status from
|
|
1202
|
+
:return: The implementation status
|
|
1203
|
+
:rtype: str
|
|
1204
|
+
"""
|
|
1205
|
+
selected_status = []
|
|
1206
|
+
for col in [
|
|
1149
1207
|
"Implemented",
|
|
1150
1208
|
ControlImplementationStatus.PartiallyImplemented,
|
|
1151
1209
|
"Planned",
|
|
1152
1210
|
ALT_IMPLEMENTATION,
|
|
1153
1211
|
ControlImplementationStatus.NA,
|
|
1212
|
+
]:
|
|
1213
|
+
if data_row[col]:
|
|
1214
|
+
selected_status.append(col)
|
|
1215
|
+
return ", ".join(selected_status) if selected_status else ""
|
|
1216
|
+
|
|
1217
|
+
|
|
1218
|
+
def _extract_origination(data_row) -> str:
|
|
1219
|
+
"""
|
|
1220
|
+
Extract the first non-empty control origination from the CIS worksheet.
|
|
1221
|
+
|
|
1222
|
+
:param data_row: The data row to extract the origination from
|
|
1223
|
+
:return: The control origination
|
|
1224
|
+
:rtype: str
|
|
1225
|
+
"""
|
|
1226
|
+
selected_origination = []
|
|
1227
|
+
for col in [
|
|
1154
1228
|
SERVICE_PROVIDER_CORPORATE,
|
|
1155
1229
|
SERVICE_PROVIDER_SYSTEM_SPECIFIC,
|
|
1156
1230
|
SERVICE_PROVIDER_HYBRID,
|
|
@@ -1158,75 +1232,53 @@ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
|
1158
1232
|
PROVIDED_BY_CUSTOMER,
|
|
1159
1233
|
SHARED,
|
|
1160
1234
|
INHERITED,
|
|
1161
|
-
]
|
|
1235
|
+
]:
|
|
1236
|
+
if data_row[col]:
|
|
1237
|
+
selected_origination.append(col)
|
|
1238
|
+
return ", ".join(selected_origination) if selected_origination else ""
|
|
1162
1239
|
|
|
1163
|
-
# Fill NaN values with an empty string for processing
|
|
1164
|
-
cis_df = cis_df.fillna("")
|
|
1165
|
-
|
|
1166
|
-
# Function to extract the first non-empty implementation status
|
|
1167
|
-
def _extract_status(data_row: pd.Series) -> str:
|
|
1168
|
-
"""
|
|
1169
|
-
Function to extract the first non-empty implementation status from the CIS worksheet
|
|
1170
|
-
|
|
1171
|
-
:param pd.Series data_row: The data row to extract the status from
|
|
1172
|
-
:return: The implementation status
|
|
1173
|
-
:rtype: str
|
|
1174
|
-
"""
|
|
1175
|
-
selected_status = []
|
|
1176
|
-
for col in [
|
|
1177
|
-
"Implemented",
|
|
1178
|
-
ControlImplementationStatus.PartiallyImplemented,
|
|
1179
|
-
"Planned",
|
|
1180
|
-
ALT_IMPLEMENTATION,
|
|
1181
|
-
ControlImplementationStatus.NA,
|
|
1182
|
-
]:
|
|
1183
|
-
if data_row[col]:
|
|
1184
|
-
selected_status.append(col)
|
|
1185
|
-
return ", ".join(selected_status) if selected_status else ""
|
|
1186
|
-
|
|
1187
|
-
# Function to extract the first non-empty control origination
|
|
1188
|
-
def _extract_origination(data_row: pd.Series) -> str:
|
|
1189
|
-
"""
|
|
1190
|
-
Function to extract the first non-empty control origination from the CIS worksheet
|
|
1191
|
-
|
|
1192
|
-
:param pd.Series data_row: The data row to extract the origination from
|
|
1193
|
-
:return: The control origination
|
|
1194
|
-
:rtype: str
|
|
1195
|
-
"""
|
|
1196
|
-
selected_origination = []
|
|
1197
|
-
for col in [
|
|
1198
|
-
SERVICE_PROVIDER_CORPORATE,
|
|
1199
|
-
SERVICE_PROVIDER_SYSTEM_SPECIFIC,
|
|
1200
|
-
SERVICE_PROVIDER_HYBRID,
|
|
1201
|
-
CONFIGURED_BY_CUSTOMER,
|
|
1202
|
-
PROVIDED_BY_CUSTOMER,
|
|
1203
|
-
SHARED,
|
|
1204
|
-
INHERITED,
|
|
1205
|
-
]:
|
|
1206
|
-
if data_row[col]:
|
|
1207
|
-
selected_origination.append(col)
|
|
1208
|
-
return ", ".join(selected_origination) if selected_origination else ""
|
|
1209
|
-
|
|
1210
|
-
def _process_row(row: pd.Series) -> dict:
|
|
1211
|
-
"""
|
|
1212
|
-
Function to process a row from the CIS worksheet
|
|
1213
|
-
|
|
1214
|
-
:param pd.Series row: The row to process
|
|
1215
|
-
:return: The processed row
|
|
1216
|
-
:rtype: dict
|
|
1217
|
-
"""
|
|
1218
|
-
return {
|
|
1219
|
-
"control_id": row[CONTROL_ID],
|
|
1220
|
-
"regscale_control_id": transform_control(row[CONTROL_ID]),
|
|
1221
|
-
"implementation_status": _extract_status(row),
|
|
1222
|
-
"control_origination": _extract_origination(row),
|
|
1223
|
-
}
|
|
1224
1240
|
|
|
1225
|
-
|
|
1241
|
+
def _process_cis_row(row) -> dict:
|
|
1242
|
+
"""
|
|
1243
|
+
Process a row from the CIS worksheet.
|
|
1244
|
+
|
|
1245
|
+
:param row: The row to process
|
|
1246
|
+
:return: The processed row
|
|
1247
|
+
:rtype: dict
|
|
1248
|
+
"""
|
|
1249
|
+
return {
|
|
1250
|
+
"control_id": row[CONTROL_ID],
|
|
1251
|
+
"regscale_control_id": transform_control(row[CONTROL_ID]),
|
|
1252
|
+
"implementation_status": _extract_status(row),
|
|
1253
|
+
"control_origination": _extract_origination(row),
|
|
1254
|
+
}
|
|
1255
|
+
|
|
1256
|
+
|
|
1257
|
+
def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
1258
|
+
"""
|
|
1259
|
+
Function to parse and format the CIS worksheet content
|
|
1260
|
+
|
|
1261
|
+
:param click.Path file_path: The file path to the FedRAMP CIS CRM workbook
|
|
1262
|
+
:param str cis_sheet_name: The name of the CIS sheet to parse
|
|
1263
|
+
:return: Formatted CIS content
|
|
1264
|
+
:rtype: dict
|
|
1265
|
+
"""
|
|
1266
|
+
logger.info("Parsing CIS worksheet...")
|
|
1267
|
+
|
|
1268
|
+
# Load and prepare the dataframe
|
|
1269
|
+
cis_df, _ = _load_and_prepare_cis_dataframe(file_path, cis_sheet_name, skip_rows=2)
|
|
1270
|
+
if cis_df is None:
|
|
1271
|
+
return {}
|
|
1272
|
+
|
|
1273
|
+
# Get expected columns and normalize the dataframe
|
|
1274
|
+
expected_columns = _get_expected_cis_columns()
|
|
1275
|
+
cis_df = _normalize_cis_columns(cis_df, expected_columns)
|
|
1276
|
+
|
|
1277
|
+
# Process rows in parallel
|
|
1226
1278
|
with ThreadPoolExecutor() as executor:
|
|
1227
|
-
results = list(executor.map(
|
|
1279
|
+
results = list(executor.map(_process_cis_row, [row for _, row in cis_df.iterrows()]))
|
|
1228
1280
|
|
|
1229
|
-
#
|
|
1281
|
+
# Index by control_id
|
|
1230
1282
|
return {clean_key(result["control_id"]): result for result in results}
|
|
1231
1283
|
|
|
1232
1284
|
|
|
@@ -1589,7 +1641,7 @@ def create_new_security_plan(profile_id: int, system_name: str):
|
|
|
1589
1641
|
|
|
1590
1642
|
else:
|
|
1591
1643
|
INITIAL_IMPORT = False
|
|
1592
|
-
ret = next((
|
|
1644
|
+
ret = next(iter(existing_plan), None)
|
|
1593
1645
|
logger.info(f"Found existing SSP# {ret.id}")
|
|
1594
1646
|
create_backup_file(ret.id)
|
|
1595
1647
|
existing_imps = ControlImplementation.get_list_by_plan(ret.id)
|
|
@@ -1778,7 +1830,8 @@ def parse_and_import_ciscrm(
|
|
|
1778
1830
|
cis_data = parse_cis_worksheet(file_path=file_path, cis_sheet_name=cis_sheet_name)
|
|
1779
1831
|
crm_data = {}
|
|
1780
1832
|
if crm_sheet_name:
|
|
1781
|
-
|
|
1833
|
+
# type: ignore
|
|
1834
|
+
crm_data = parse_crm_worksheet(file_path=file_path, crm_sheet_name=crm_sheet_name, version=version)
|
|
1782
1835
|
if leveraged_auth_id == 0:
|
|
1783
1836
|
auths = LeveragedAuthorization.get_all_by_parent(ssp.id)
|
|
1784
1837
|
if auths:
|