regscale-cli 6.20.1.1__py3-none-any.whl → 6.20.3.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/__init__.py +1 -1
- regscale/core/app/utils/variables.py +5 -3
- regscale/integrations/commercial/__init__.py +15 -0
- regscale/integrations/commercial/axonius/__init__.py +0 -0
- regscale/integrations/commercial/axonius/axonius_integration.py +70 -0
- regscale/integrations/commercial/burp.py +14 -0
- regscale/integrations/commercial/grype/commands.py +8 -1
- regscale/integrations/commercial/grype/scanner.py +2 -1
- regscale/integrations/commercial/jira.py +288 -137
- regscale/integrations/commercial/opentext/commands.py +14 -5
- regscale/integrations/commercial/opentext/scanner.py +3 -2
- regscale/integrations/commercial/qualys/__init__.py +3 -3
- regscale/integrations/commercial/stigv2/click_commands.py +6 -37
- regscale/integrations/commercial/synqly/assets.py +10 -0
- regscale/integrations/commercial/tenablev2/commands.py +12 -4
- regscale/integrations/commercial/tenablev2/sc_scanner.py +21 -1
- regscale/integrations/commercial/tenablev2/sync_compliance.py +3 -0
- regscale/integrations/commercial/trivy/commands.py +11 -4
- regscale/integrations/commercial/trivy/scanner.py +2 -1
- regscale/integrations/commercial/wizv2/constants.py +4 -0
- regscale/integrations/commercial/wizv2/scanner.py +67 -14
- regscale/integrations/commercial/wizv2/utils.py +24 -10
- regscale/integrations/commercial/wizv2/variables.py +7 -0
- regscale/integrations/jsonl_scanner_integration.py +8 -1
- regscale/integrations/public/cisa.py +58 -63
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +153 -104
- regscale/integrations/scanner_integration.py +30 -8
- regscale/integrations/variables.py +1 -0
- regscale/models/app_models/click.py +49 -1
- regscale/models/app_models/import_validater.py +3 -1
- regscale/models/integration_models/axonius_models/__init__.py +0 -0
- regscale/models/integration_models/axonius_models/connectors/__init__.py +3 -0
- regscale/models/integration_models/axonius_models/connectors/assets.py +111 -0
- regscale/models/integration_models/burp.py +11 -8
- regscale/models/integration_models/cisa_kev_data.json +204 -23
- regscale/models/integration_models/flat_file_importer/__init__.py +36 -176
- regscale/models/integration_models/jira_task_sync.py +27 -0
- regscale/models/integration_models/qualys.py +6 -7
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/__init__.py +2 -1
- regscale/models/regscale_models/control_implementation.py +39 -2
- regscale/models/regscale_models/issue.py +1 -0
- regscale/models/regscale_models/regscale_model.py +49 -1
- regscale/models/regscale_models/risk_issue_mapping.py +61 -0
- regscale/models/regscale_models/task.py +1 -0
- regscale/regscale.py +1 -4
- regscale/utils/graphql_client.py +4 -4
- regscale/utils/string.py +13 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/RECORD +54 -48
- regscale/integrations/commercial/synqly_jira.py +0 -840
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.20.1.1.dist-info → regscale_cli-6.20.3.0.dist-info}/top_level.txt +0 -0
|
@@ -7,7 +7,7 @@ import logging
|
|
|
7
7
|
import re
|
|
8
8
|
from concurrent.futures import ALL_COMPLETED, ThreadPoolExecutor, wait
|
|
9
9
|
from datetime import date, datetime
|
|
10
|
-
from typing import List, Optional, Tuple, Any, Dict
|
|
10
|
+
from typing import List, Optional, Tuple, Any, Dict, Union
|
|
11
11
|
from urllib.error import URLError
|
|
12
12
|
from urllib.parse import urlparse
|
|
13
13
|
|
|
@@ -23,6 +23,7 @@ from regscale.core.app.application import Application
|
|
|
23
23
|
from regscale.core.app.internal.login import is_valid
|
|
24
24
|
from regscale.models import Link, Threat
|
|
25
25
|
from regscale.core.app.utils.app_utils import error_and_exit
|
|
26
|
+
from regscale.utils.string import extract_url
|
|
26
27
|
|
|
27
28
|
logger = logging.getLogger("regscale")
|
|
28
29
|
console = Console()
|
|
@@ -71,22 +72,9 @@ def update_regscale_links(threats: List[Threat]) -> None:
|
|
|
71
72
|
:rtype: None
|
|
72
73
|
"""
|
|
73
74
|
|
|
74
|
-
# extract url from html string using regex
|
|
75
|
-
def extract_url(html: str) -> str:
|
|
76
|
-
"""
|
|
77
|
-
Extract URL from HTML string
|
|
78
|
-
|
|
79
|
-
:param str html: HTML string
|
|
80
|
-
:return: URL
|
|
81
|
-
:rtype: str
|
|
82
|
-
"""
|
|
83
|
-
url = re.findall(r"(?P<url>https?://[^\s]+)", html)
|
|
84
|
-
return url[0].replace('"', "") if url else None
|
|
85
|
-
|
|
86
75
|
links = []
|
|
87
76
|
for threat in threats:
|
|
88
|
-
url
|
|
89
|
-
if threat.description:
|
|
77
|
+
if url := extract_url(threat.description):
|
|
90
78
|
link = Link(
|
|
91
79
|
parentID=threat.id,
|
|
92
80
|
parentModule="threats",
|
|
@@ -115,9 +103,8 @@ def process_threats(threats: list[Threat], unique_threats: set[str], reg_threats
|
|
|
115
103
|
update_dict = threat.dict()
|
|
116
104
|
update_dict = merge_old(update_dict, old_dict)
|
|
117
105
|
update_threats.append(update_dict) # Update
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
insert_threats.append(threat.dict()) # Post
|
|
106
|
+
elif threat:
|
|
107
|
+
insert_threats.append(threat.dict()) # Post
|
|
121
108
|
return insert_threats, update_threats
|
|
122
109
|
|
|
123
110
|
|
|
@@ -218,29 +205,29 @@ def build_threat(app: Application, detailed_link: str, short_description: str, t
|
|
|
218
205
|
:rtype: Threat
|
|
219
206
|
"""
|
|
220
207
|
dat = parse_details(detailed_link)
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
208
|
+
if not dat:
|
|
209
|
+
return None
|
|
210
|
+
|
|
211
|
+
date_created = dat[0]
|
|
212
|
+
vulnerability = dat[1]
|
|
213
|
+
mitigation = dat[2]
|
|
214
|
+
notes = dat[3]
|
|
215
|
+
|
|
216
|
+
return Threat(
|
|
217
|
+
uuid=Threat.xstr(None),
|
|
218
|
+
title=title,
|
|
219
|
+
threatType="Specific",
|
|
220
|
+
threatOwnerId=app.config["userId"],
|
|
221
|
+
dateIdentified=date_created,
|
|
222
|
+
targetType="Other",
|
|
223
|
+
source="Open Source",
|
|
224
|
+
description=short_description or f"""<p><a href="{detailed_link}" title="">{detailed_link}</a></p>""",
|
|
225
|
+
vulnerabilityAnalysis="".join(vulnerability),
|
|
226
|
+
mitigations="".join(mitigation),
|
|
227
|
+
notes="".join(notes),
|
|
228
|
+
dateCreated=date_created,
|
|
229
|
+
status="Initial Report/Notification",
|
|
230
|
+
)
|
|
244
231
|
|
|
245
232
|
|
|
246
233
|
def filter_elements(element: Tag) -> Optional[Tag]:
|
|
@@ -332,7 +319,8 @@ def parse_details(link: str) -> Optional[Tuple[str, list, list, list]]:
|
|
|
332
319
|
mitigation = []
|
|
333
320
|
notes = []
|
|
334
321
|
detailed_soup = gen_soup(link)
|
|
335
|
-
date_created
|
|
322
|
+
if not (date_created := fuzzy_find_date(detailed_soup)):
|
|
323
|
+
return None
|
|
336
324
|
last_header = None
|
|
337
325
|
last_h3 = None
|
|
338
326
|
nav_string = ""
|
|
@@ -357,9 +345,8 @@ def parse_details(link: str) -> Optional[Tuple[str, list, list, list]]:
|
|
|
357
345
|
notes.append(DEFAULT_STR)
|
|
358
346
|
if len(mitigation) == 0:
|
|
359
347
|
mitigation.append(DEFAULT_STR)
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
return None
|
|
348
|
+
|
|
349
|
+
return date_created, unique(vulnerability), unique(mitigation), unique(notes)
|
|
363
350
|
|
|
364
351
|
|
|
365
352
|
def fuzzy_find_date(detailed_soup: BeautifulSoup, location: int = 2, attempts: int = 0) -> str:
|
|
@@ -396,7 +383,7 @@ def fuzzy_find_date(detailed_soup: BeautifulSoup, location: int = 2, attempts: i
|
|
|
396
383
|
return fuzzy_dt
|
|
397
384
|
|
|
398
385
|
|
|
399
|
-
def gen_soup(url: str) -> BeautifulSoup:
|
|
386
|
+
def gen_soup(url: Union[str, Tuple[str, ...]]) -> BeautifulSoup:
|
|
400
387
|
"""
|
|
401
388
|
Generate a BeautifulSoup instance for the given URL
|
|
402
389
|
|
|
@@ -404,7 +391,7 @@ def gen_soup(url: str) -> BeautifulSoup:
|
|
|
404
391
|
:raises: URLError if URL is invalid
|
|
405
392
|
:rtype: BeautifulSoup
|
|
406
393
|
"""
|
|
407
|
-
if isinstance(url,
|
|
394
|
+
if isinstance(url, tuple):
|
|
408
395
|
url = url[0]
|
|
409
396
|
if is_url(url):
|
|
410
397
|
req = Api().get(url)
|
|
@@ -445,7 +432,7 @@ def pull_cisa_kev() -> Dict[Any, Any]:
|
|
|
445
432
|
result = []
|
|
446
433
|
|
|
447
434
|
# Get URL from config or use default
|
|
448
|
-
if "
|
|
435
|
+
if "cisaKev" in config:
|
|
449
436
|
cisa_url = config["cisaKev"]
|
|
450
437
|
else:
|
|
451
438
|
cisa_url = CISA_KEV_URL
|
|
@@ -504,7 +491,7 @@ def update_regscale(data: dict) -> None:
|
|
|
504
491
|
threats_updated = []
|
|
505
492
|
new_threats = [dat for dat in data["vulnerabilities"] if dat not in matching_threats]
|
|
506
493
|
console.print(f"Found {len(new_threats)} new threats from CISA")
|
|
507
|
-
if
|
|
494
|
+
if new_threats:
|
|
508
495
|
for rec in new_threats:
|
|
509
496
|
threat = Threat(
|
|
510
497
|
uuid=Threat.xstr(None),
|
|
@@ -558,20 +545,28 @@ def merge_old(update_vuln: dict, old_vuln: dict) -> dict:
|
|
|
558
545
|
:return: A merged vulnerability dictionary
|
|
559
546
|
:rtype: dict
|
|
560
547
|
"""
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
548
|
+
fields_to_preserve = [
|
|
549
|
+
"id",
|
|
550
|
+
"uuid",
|
|
551
|
+
"status",
|
|
552
|
+
"source",
|
|
553
|
+
"threatType",
|
|
554
|
+
"threatOwnerId",
|
|
555
|
+
"notes",
|
|
556
|
+
"targetType",
|
|
557
|
+
"dateCreated",
|
|
558
|
+
"isPublic",
|
|
559
|
+
"investigated",
|
|
560
|
+
"investigationResults",
|
|
561
|
+
]
|
|
562
|
+
merged = update_vuln.copy()
|
|
563
|
+
|
|
564
|
+
# Preserve specified fields from old dictionary if they exist
|
|
565
|
+
for field in fields_to_preserve:
|
|
566
|
+
if field in old_vuln:
|
|
567
|
+
merged[field] = old_vuln[field]
|
|
568
|
+
|
|
569
|
+
return merged
|
|
575
570
|
|
|
576
571
|
|
|
577
572
|
def insert_or_upd_threat(threat: dict, app: Application, threat_id: int = None) -> requests.Response:
|
|
@@ -22,7 +22,7 @@ from regscale.core.app.utils.app_utils import create_progress_object, error_and_
|
|
|
22
22
|
from regscale.core.utils.graphql import GraphQLQuery
|
|
23
23
|
from regscale.integrations.public.fedramp.parts_mapper import PartMapper
|
|
24
24
|
from regscale.integrations.public.fedramp.ssp_logger import SSPLogger
|
|
25
|
-
from regscale.models import ControlObjective, ImplementationObjective, Parameter, Profile
|
|
25
|
+
from regscale.models import ControlObjective, ImplementationObjective, ImportValidater, Parameter, Profile
|
|
26
26
|
from regscale.models.regscale_models import (
|
|
27
27
|
ControlImplementation,
|
|
28
28
|
File,
|
|
@@ -39,6 +39,7 @@ if TYPE_CHECKING:
|
|
|
39
39
|
import pandas as pd
|
|
40
40
|
|
|
41
41
|
from functools import lru_cache
|
|
42
|
+
from tempfile import gettempdir
|
|
42
43
|
|
|
43
44
|
T = TypeVar("T")
|
|
44
45
|
|
|
@@ -49,11 +50,14 @@ progress = create_progress_object()
|
|
|
49
50
|
|
|
50
51
|
SERVICE_PROVIDER_CORPORATE = "Service Provider Corporate"
|
|
51
52
|
SERVICE_PROVIDER_SYSTEM_SPECIFIC = "Service Provider System Specific"
|
|
52
|
-
SERVICE_PROVIDER_HYBRID = "Service Provider Hybrid"
|
|
53
|
+
SERVICE_PROVIDER_HYBRID = "Service Provider Hybrid (Corporate and System Specific)"
|
|
53
54
|
PROVIDER_SYSTEM_SPECIFIC = "Provider (System Specific)"
|
|
54
|
-
CUSTOMER_PROVIDED = "Provided
|
|
55
|
+
CUSTOMER_PROVIDED = "Customer Provided"
|
|
55
56
|
CUSTOMER_CONFIGURED = "Customer Configured"
|
|
56
|
-
|
|
57
|
+
PROVIDED_BY_CUSTOMER = "Provided by Customer (Customer System Specific)"
|
|
58
|
+
CONFIGURED_BY_CUSTOMER = "Configured by Customer (Customer System Specific)"
|
|
59
|
+
INHERITED = "Inherited from pre-existing FedRAMP Authorization"
|
|
60
|
+
SHARED = "Shared (Service Provider and Customer Responsibility)"
|
|
57
61
|
NOT_IMPLEMENTED = ControlImplementationStatus.NotImplemented.value
|
|
58
62
|
PARTIALLY_IMPLEMENTED = ControlImplementationStatus.PartiallyImplemented.value
|
|
59
63
|
CONTROL_ID = "Control ID"
|
|
@@ -77,10 +81,10 @@ STATUS_MAPPING = {
|
|
|
77
81
|
|
|
78
82
|
RESPONSIBILITY_MAP = {
|
|
79
83
|
# Original keys
|
|
80
|
-
|
|
84
|
+
SERVICE_PROVIDER_CORPORATE: SERVICE_PROVIDER_CORPORATE,
|
|
81
85
|
SERVICE_PROVIDER_SYSTEM_SPECIFIC: PROVIDER_SYSTEM_SPECIFIC,
|
|
82
86
|
SERVICE_PROVIDER_HYBRID: "Hybrid",
|
|
83
|
-
|
|
87
|
+
PROVIDED_BY_CUSTOMER: "Customer",
|
|
84
88
|
CONFIGURED_BY_CUSTOMER: CUSTOMER_CONFIGURED,
|
|
85
89
|
"Shared": "Shared",
|
|
86
90
|
"Inherited": "Inherited",
|
|
@@ -241,52 +245,44 @@ def map_implementation_status(control_id: str, cis_data: dict) -> str:
|
|
|
241
245
|
|
|
242
246
|
def map_origination(control_id: str, cis_data: dict) -> dict:
|
|
243
247
|
"""
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
:param
|
|
247
|
-
:param
|
|
248
|
-
:return:
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
"
|
|
253
|
-
"
|
|
254
|
-
"
|
|
255
|
-
"
|
|
256
|
-
"bConfiguredByCustomer"
|
|
257
|
-
"
|
|
258
|
-
"
|
|
259
|
-
"record_text": "",
|
|
248
|
+
Map control implementation responsibility from CRM worksheet data.
|
|
249
|
+
|
|
250
|
+
:param control_id: RegScale control ID
|
|
251
|
+
:param cis_data: Data from the CRM worksheet
|
|
252
|
+
:return: Responsibility information in regscale format
|
|
253
|
+
"""
|
|
254
|
+
# Define mapping of origination strings to boolean keys
|
|
255
|
+
origination_mapping = {
|
|
256
|
+
SERVICE_PROVIDER_CORPORATE: "bServiceProviderCorporate",
|
|
257
|
+
SERVICE_PROVIDER_SYSTEM_SPECIFIC: "bServiceProviderSystemSpecific",
|
|
258
|
+
SERVICE_PROVIDER_HYBRID: "bServiceProviderHybrid",
|
|
259
|
+
PROVIDED_BY_CUSTOMER: "bProvidedByCustomer",
|
|
260
|
+
CONFIGURED_BY_CUSTOMER: "bConfiguredByCustomer",
|
|
261
|
+
SHARED: "bShared",
|
|
262
|
+
INHERITED: "bInherited",
|
|
260
263
|
}
|
|
261
|
-
|
|
262
|
-
|
|
264
|
+
|
|
265
|
+
# Initialize result with all flags set to False
|
|
266
|
+
result = {key: False for key in origination_mapping.values()}
|
|
267
|
+
result["record_text"] = ""
|
|
268
|
+
|
|
269
|
+
# Find matching CIS records
|
|
270
|
+
matching_records = [
|
|
271
|
+
record for record in cis_data.values() if gen_key(record["regscale_control_id"]).lower() == control_id.lower()
|
|
263
272
|
]
|
|
264
|
-
|
|
265
|
-
|
|
273
|
+
|
|
274
|
+
# Process each matching record
|
|
275
|
+
for record in matching_records:
|
|
266
276
|
control_origination = record.get("control_origination", "")
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if CUSTOMER_PROVIDED in control_origination:
|
|
277
|
-
# responsibility = "Customer"
|
|
278
|
-
origination_bools["bProvidedByCustomer"] = True
|
|
279
|
-
if CONFIGURED_BY_CUSTOMER in control_origination:
|
|
280
|
-
# responsibility = "Customer Configured"
|
|
281
|
-
origination_bools["bConfiguredByCustomer"] = True
|
|
282
|
-
if "Shared" in control_origination:
|
|
283
|
-
# responsibility = "Shared"
|
|
284
|
-
origination_bools["bShared"] = True
|
|
285
|
-
if "Inherited" in control_origination:
|
|
286
|
-
# responsibility = "Inherited"
|
|
287
|
-
origination_bools["bInherited"] = True
|
|
288
|
-
origination_bools["record_text"] += control_origination
|
|
289
|
-
return origination_bools
|
|
277
|
+
|
|
278
|
+
# Set flags based on origination string content
|
|
279
|
+
for origination_str, bool_key in origination_mapping.items():
|
|
280
|
+
if origination_str in control_origination:
|
|
281
|
+
result[bool_key] = True
|
|
282
|
+
if control_origination not in result["record_text"]:
|
|
283
|
+
result["record_text"] += control_origination
|
|
284
|
+
|
|
285
|
+
return result
|
|
290
286
|
|
|
291
287
|
|
|
292
288
|
def clean_customer_responsibility(value: str):
|
|
@@ -331,16 +327,6 @@ def update_imp_objective(
|
|
|
331
327
|
NOT_IMPLEMENTED: NOT_IMPLEMENTED,
|
|
332
328
|
}
|
|
333
329
|
|
|
334
|
-
responsibility_map = {
|
|
335
|
-
"Provider": SERVICE_PROVIDER_CORPORATE,
|
|
336
|
-
PROVIDER_SYSTEM_SPECIFIC: SERVICE_PROVIDER_SYSTEM_SPECIFIC,
|
|
337
|
-
"Customer": "Provided by Customer (Customer System Specific)",
|
|
338
|
-
"Hybrid": "Service Provider Hybrid (Corporate and System Specific)",
|
|
339
|
-
CUSTOMER_CONFIGURED: "Configured by Customer (Customer System Specific)",
|
|
340
|
-
"Shared": "Shared (Service Provider and Customer Responsibility)",
|
|
341
|
-
"Inherited": "Inherited from pre-existing FedRAMP Authorization",
|
|
342
|
-
}
|
|
343
|
-
|
|
344
330
|
cis_record = record.get("cis", {})
|
|
345
331
|
crm_record = record.get("crm", {})
|
|
346
332
|
# There could be multiples, take the first one as regscale will not allow multiples at the objective level.
|
|
@@ -349,22 +335,26 @@ def update_imp_objective(
|
|
|
349
335
|
control_originations[ix] = control_origination.strip()
|
|
350
336
|
|
|
351
337
|
try:
|
|
352
|
-
responsibility =
|
|
353
|
-
|
|
354
|
-
)
|
|
338
|
+
responsibility = next(origin for origin in control_originations)
|
|
339
|
+
|
|
355
340
|
except StopIteration:
|
|
356
|
-
responsibility
|
|
341
|
+
if imp.responsibility:
|
|
342
|
+
responsibility = imp.responsibility.split(",")[0] # only one responsiblity allowed here.
|
|
343
|
+
else:
|
|
344
|
+
responsibility = SERVICE_PROVIDER_CORPORATE
|
|
357
345
|
|
|
358
346
|
customer_responsibility = clean_customer_responsibility(
|
|
359
347
|
crm_record.get("specific_inheritance_and_customer_agency_csp_responsibilities")
|
|
360
348
|
)
|
|
361
349
|
existing_pairs = {(obj.objectiveId, obj.implementationId) for obj in existing_imp_obj}
|
|
362
|
-
responsibility = responsibility_map.get(responsibility, responsibility)
|
|
363
350
|
logger.debug(f"CRM Record: {crm_record}")
|
|
364
351
|
can_be_inherited_from_csp: str = crm_record.get("can_be_inherited_from_csp") or ""
|
|
365
352
|
for objective in objectives:
|
|
366
353
|
current_pair = (objective.id, imp.id)
|
|
367
354
|
if current_pair not in existing_pairs:
|
|
355
|
+
if objective.securityControlId != imp.controlID:
|
|
356
|
+
# This is a bad match, do not save.
|
|
357
|
+
continue
|
|
368
358
|
imp_obj = ImplementationObjective(
|
|
369
359
|
id=0,
|
|
370
360
|
uuid="",
|
|
@@ -451,10 +441,16 @@ def parse_control_details(
|
|
|
451
441
|
control_imp.bServiceProviderSystemSpecific = origination_bool["bServiceProviderSystemSpecific"]
|
|
452
442
|
control_imp.bServiceProviderHybrid = origination_bool["bServiceProviderHybrid"]
|
|
453
443
|
control_imp.bConfiguredByCustomer = origination_bool["bConfiguredByCustomer"]
|
|
444
|
+
control_imp.bShared = origination_bool["bShared"]
|
|
454
445
|
control_imp.bProvidedByCustomer = origination_bool["bProvidedByCustomer"]
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
446
|
+
control_imp.responsibility = get_responsibility(origination_bool)
|
|
447
|
+
logger.debug(f"Control Implementation Responsibility: {control_imp.responsibility}")
|
|
448
|
+
logger.debug(f"Control Implementation Status: {control_imp.status}")
|
|
449
|
+
if status == ControlImplementationStatus.Planned:
|
|
450
|
+
control_imp.stepsToImplement = "PLANNED"
|
|
451
|
+
control_imp.plannedImplementationDate = get_current_datetime("%Y-%m-%d")
|
|
452
|
+
if status in [ControlImplementationStatus.Planned, ControlImplementationStatus.NotImplemented]:
|
|
453
|
+
control_imp.exclusionJustification = "Imported from FedRAMP CIS CRM Workbook"
|
|
458
454
|
if updated_control := control_imp.save():
|
|
459
455
|
logger.debug("Control Implementation #%s updated successfully", control_imp.id)
|
|
460
456
|
return updated_control
|
|
@@ -465,29 +461,31 @@ def parse_control_details(
|
|
|
465
461
|
def get_responsibility(origination_bool: dict) -> str:
|
|
466
462
|
"""
|
|
467
463
|
Function to map the responsibility based on origination booleans.
|
|
464
|
+
Returns comma-separated string of all responsibilities for True booleans.
|
|
468
465
|
|
|
469
466
|
:param dict origination_bool: Dictionary containing origination booleans
|
|
470
|
-
:return:
|
|
467
|
+
:return: Comma-separated responsibility string
|
|
471
468
|
:rtype: str
|
|
472
469
|
"""
|
|
473
|
-
|
|
470
|
+
responsibilities = []
|
|
474
471
|
|
|
475
|
-
if origination_bool
|
|
476
|
-
|
|
477
|
-
if origination_bool
|
|
478
|
-
|
|
479
|
-
if origination_bool
|
|
480
|
-
|
|
481
|
-
if origination_bool
|
|
482
|
-
|
|
483
|
-
if origination_bool
|
|
484
|
-
|
|
485
|
-
if origination_bool
|
|
486
|
-
|
|
487
|
-
if origination_bool
|
|
488
|
-
|
|
472
|
+
if origination_bool.get("bServiceProviderCorporate", False):
|
|
473
|
+
responsibilities.append(SERVICE_PROVIDER_CORPORATE)
|
|
474
|
+
if origination_bool.get("bServiceProviderSystemSpecific", False):
|
|
475
|
+
responsibilities.append(SERVICE_PROVIDER_SYSTEM_SPECIFIC)
|
|
476
|
+
if origination_bool.get("bServiceProviderHybrid", False):
|
|
477
|
+
responsibilities.append(SERVICE_PROVIDER_HYBRID)
|
|
478
|
+
if origination_bool.get("bProvidedByCustomer", False):
|
|
479
|
+
responsibilities.append(PROVIDED_BY_CUSTOMER)
|
|
480
|
+
if origination_bool.get("bConfiguredByCustomer", False):
|
|
481
|
+
responsibilities.append(CONFIGURED_BY_CUSTOMER)
|
|
482
|
+
if origination_bool.get("bInherited", False):
|
|
483
|
+
responsibilities.append(INHERITED)
|
|
484
|
+
if origination_bool.get("bShared", False):
|
|
485
|
+
responsibilities.append(SHARED)
|
|
489
486
|
|
|
490
|
-
|
|
487
|
+
# Return comma-separated string, or NA if no responsibilities found
|
|
488
|
+
return ",".join(responsibilities) if responsibilities else ControlImplementationStatus.NA.value
|
|
491
489
|
|
|
492
490
|
|
|
493
491
|
def fetch_and_update_imps(
|
|
@@ -706,6 +704,7 @@ def process_implementation(
|
|
|
706
704
|
errors.extend(method_errors)
|
|
707
705
|
if result:
|
|
708
706
|
processed_objectives.append(result)
|
|
707
|
+
# Update Control Origin at the Implementation Level
|
|
709
708
|
return errors, processed_objectives
|
|
710
709
|
|
|
711
710
|
|
|
@@ -828,7 +827,6 @@ def parse_crm_worksheet(file_path: click.Path, crm_sheet_name: str, version: Lit
|
|
|
828
827
|
:return: Formatted CRM content
|
|
829
828
|
:rtype: dict
|
|
830
829
|
"""
|
|
831
|
-
pd = get_pandas()
|
|
832
830
|
logger.info("Parsing CRM worksheet...")
|
|
833
831
|
formatted_crm = {}
|
|
834
832
|
|
|
@@ -838,13 +836,19 @@ def parse_crm_worksheet(file_path: click.Path, crm_sheet_name: str, version: Lit
|
|
|
838
836
|
# Value for rev4
|
|
839
837
|
skip_rows = 3
|
|
840
838
|
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
839
|
+
validator = ImportValidater(
|
|
840
|
+
file_path=file_path,
|
|
841
|
+
disable_mapping=True,
|
|
842
|
+
required_headers=[],
|
|
843
|
+
mapping_file_path=gettempdir(),
|
|
844
|
+
prompt=False,
|
|
845
|
+
ignore_unnamed=True,
|
|
846
|
+
worksheet_name=crm_sheet_name,
|
|
847
|
+
warn_extra_headers=False,
|
|
844
848
|
)
|
|
845
849
|
|
|
846
850
|
# find index of row where the first column == Control ID
|
|
847
|
-
skip_rows = determine_skip_row(original_df=
|
|
851
|
+
skip_rows = determine_skip_row(original_df=validator.data, text_to_find=CONTROL_ID, original_skip=skip_rows)
|
|
848
852
|
|
|
849
853
|
logger.debug(f"Skipping {skip_rows} rows in CRM worksheet")
|
|
850
854
|
|
|
@@ -856,13 +860,43 @@ def parse_crm_worksheet(file_path: click.Path, crm_sheet_name: str, version: Lit
|
|
|
856
860
|
]
|
|
857
861
|
|
|
858
862
|
try:
|
|
863
|
+
# Verify that the columns are in the dataframe
|
|
864
|
+
header_row = validator.data.iloc[skip_rows - 1 :].iloc[0]
|
|
865
|
+
|
|
866
|
+
# Check if we have enough columns
|
|
867
|
+
if len(header_row) < len(usecols):
|
|
868
|
+
error_and_exit(
|
|
869
|
+
f"Not enough columns found in CRM worksheet. Expected {len(usecols)} columns but found {len(header_row)}."
|
|
870
|
+
)
|
|
871
|
+
|
|
872
|
+
# Verify each required column exists in the correct position
|
|
873
|
+
missing_columns = []
|
|
874
|
+
for i, expected_col in enumerate(usecols):
|
|
875
|
+
if header_row.iloc[i] != expected_col:
|
|
876
|
+
missing_columns.append(
|
|
877
|
+
f"Expected '{expected_col}' at position {i + 1} but found '{header_row.iloc[i]}'"
|
|
878
|
+
)
|
|
879
|
+
|
|
880
|
+
if missing_columns:
|
|
881
|
+
error_msg = "Required columns not found in the CRM worksheet:\n" + "\n".join(missing_columns)
|
|
882
|
+
error_and_exit(error_msg)
|
|
883
|
+
|
|
884
|
+
logger.debug("Verified all required columns exist in CRM worksheet")
|
|
885
|
+
|
|
859
886
|
# Reindex the dataframe and skip some rows
|
|
860
|
-
data =
|
|
887
|
+
data = validator.data.iloc[skip_rows:]
|
|
888
|
+
|
|
889
|
+
# Keep only the first three columns
|
|
890
|
+
data = data.iloc[:, :3]
|
|
891
|
+
|
|
892
|
+
# Rename the columns to match usecols
|
|
861
893
|
data.columns = usecols
|
|
894
|
+
logger.debug(f"Kept only required columns: {', '.join(usecols)}")
|
|
895
|
+
|
|
862
896
|
except KeyError as e:
|
|
863
897
|
error_and_exit(f"KeyError: {e} - One or more columns specified in usecols are not found in the dataframe.")
|
|
864
898
|
except Exception as e:
|
|
865
|
-
error_and_exit(f"An error occurred: {e}")
|
|
899
|
+
error_and_exit(f"An error occurred while processing CRM worksheet: {str(e)}")
|
|
866
900
|
# Filter rows where "Can Be Inherited from CSP" is not equal to "No"
|
|
867
901
|
exclude_no = data[data[CAN_BE_INHERITED_CSP] != "No"]
|
|
868
902
|
|
|
@@ -902,10 +936,21 @@ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
|
902
936
|
pd = get_pandas()
|
|
903
937
|
logger.info("Parsing CIS worksheet...")
|
|
904
938
|
skip_rows = 2
|
|
905
|
-
# Parse the worksheet named 'CIS GovCloud U.S.+DoD (H)', skipping the initial rows
|
|
906
|
-
original_cis = pd.read_excel(file_path, sheet_name=cis_sheet_name)
|
|
907
939
|
|
|
908
|
-
|
|
940
|
+
validator = ImportValidater(
|
|
941
|
+
file_path=file_path,
|
|
942
|
+
disable_mapping=True,
|
|
943
|
+
required_headers=[],
|
|
944
|
+
mapping_file_path=gettempdir(),
|
|
945
|
+
prompt=False,
|
|
946
|
+
ignore_unnamed=True,
|
|
947
|
+
worksheet_name=cis_sheet_name,
|
|
948
|
+
warn_extra_headers=False,
|
|
949
|
+
)
|
|
950
|
+
skip_rows = determine_skip_row(original_df=validator.data, text_to_find=CONTROL_ID, original_skip=skip_rows)
|
|
951
|
+
|
|
952
|
+
# Parse the worksheet named 'CIS GovCloud U.S.+DoD (H)', skipping the initial rows
|
|
953
|
+
original_cis = validator.data
|
|
909
954
|
|
|
910
955
|
cis_df = original_cis.iloc[skip_rows:].reset_index(drop=True)
|
|
911
956
|
|
|
@@ -918,6 +963,9 @@ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
|
918
963
|
# Reset the index
|
|
919
964
|
cis_df.reset_index(drop=True, inplace=True)
|
|
920
965
|
|
|
966
|
+
# Only keep the first 13 columns
|
|
967
|
+
cis_df = cis_df.iloc[:, :13]
|
|
968
|
+
|
|
921
969
|
# Rename columns to standardize names
|
|
922
970
|
cis_df.columns = [
|
|
923
971
|
CONTROL_ID,
|
|
@@ -930,9 +978,9 @@ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
|
930
978
|
SERVICE_PROVIDER_SYSTEM_SPECIFIC,
|
|
931
979
|
SERVICE_PROVIDER_HYBRID,
|
|
932
980
|
CONFIGURED_BY_CUSTOMER,
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
981
|
+
PROVIDED_BY_CUSTOMER,
|
|
982
|
+
SHARED,
|
|
983
|
+
INHERITED,
|
|
936
984
|
]
|
|
937
985
|
|
|
938
986
|
# Fill NaN values with an empty string for processing
|
|
@@ -973,9 +1021,9 @@ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
|
973
1021
|
SERVICE_PROVIDER_SYSTEM_SPECIFIC,
|
|
974
1022
|
SERVICE_PROVIDER_HYBRID,
|
|
975
1023
|
CONFIGURED_BY_CUSTOMER,
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1024
|
+
PROVIDED_BY_CUSTOMER,
|
|
1025
|
+
SHARED,
|
|
1026
|
+
INHERITED,
|
|
979
1027
|
]:
|
|
980
1028
|
if data_row[col]:
|
|
981
1029
|
selected_origination.append(col)
|
|
@@ -1102,15 +1150,15 @@ def parse_instructions_worksheet(
|
|
|
1102
1150
|
return instructions_df.to_dict(orient="records")
|
|
1103
1151
|
|
|
1104
1152
|
|
|
1105
|
-
def
|
|
1153
|
+
def update_customer_text():
|
|
1106
1154
|
"""
|
|
1107
1155
|
Update the implementation responsibility texts from the objective data
|
|
1108
1156
|
"""
|
|
1109
1157
|
with ThreadPoolExecutor() as executor:
|
|
1110
|
-
executor.map(
|
|
1158
|
+
executor.map(_update_imp_customer, EXISTING_IMPLEMENTATIONS.values())
|
|
1111
1159
|
|
|
1112
1160
|
|
|
1113
|
-
def
|
|
1161
|
+
def _update_imp_customer(imp: ControlImplementation):
|
|
1114
1162
|
"""
|
|
1115
1163
|
Update the implementation responsibility text for a given implementation
|
|
1116
1164
|
|
|
@@ -1207,7 +1255,7 @@ def parse_and_map_data(
|
|
|
1207
1255
|
crm_data=crm_data,
|
|
1208
1256
|
version=version,
|
|
1209
1257
|
)
|
|
1210
|
-
|
|
1258
|
+
update_customer_text()
|
|
1211
1259
|
|
|
1212
1260
|
report(error_set)
|
|
1213
1261
|
|
|
@@ -1339,6 +1387,7 @@ def create_new_security_plan(profile_id: int, system_name: str):
|
|
|
1339
1387
|
|
|
1340
1388
|
else:
|
|
1341
1389
|
ret = next((plan for plan in existing_plan), None)
|
|
1390
|
+
logger.info(f"Found existing SSP# {ret.id}")
|
|
1342
1391
|
existing_imps = ControlImplementation.get_list_by_plan(ret.id)
|
|
1343
1392
|
for imp in existing_imps:
|
|
1344
1393
|
EXISTING_IMPLEMENTATIONS[imp.controlID] = imp
|