regscale-cli 6.16.3.0__py3-none-any.whl → 6.16.4.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/internal/control_editor.py +26 -2
- regscale/core/app/internal/model_editor.py +39 -26
- regscale/integrations/commercial/grype/scanner.py +37 -29
- regscale/integrations/commercial/opentext/commands.py +2 -0
- regscale/integrations/commercial/opentext/scanner.py +45 -31
- regscale/integrations/commercial/qualys.py +3 -1
- regscale/integrations/commercial/sicura/commands.py +9 -14
- regscale/integrations/commercial/tenablev2/click.py +25 -13
- regscale/integrations/commercial/tenablev2/scanner.py +12 -3
- regscale/integrations/commercial/trivy/scanner.py +14 -6
- regscale/integrations/commercial/wizv2/click.py +15 -37
- regscale/integrations/jsonl_scanner_integration.py +120 -16
- regscale/integrations/public/fedramp/click.py +8 -8
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +499 -106
- regscale/integrations/public/fedramp/ssp_logger.py +2 -9
- regscale/integrations/scanner_integration.py +14 -9
- regscale/models/integration_models/cisa_kev_data.json +39 -8
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/tenable_models/integration.py +23 -3
- regscale/models/regscale_models/control_implementation.py +18 -0
- regscale/models/regscale_models/control_objective.py +2 -1
- regscale/models/regscale_models/facility.py +10 -26
- regscale/models/regscale_models/functional_roles.py +38 -0
- regscale/models/regscale_models/issue.py +3 -1
- regscale/models/regscale_models/parameter.py +21 -3
- regscale/models/regscale_models/profile.py +22 -0
- regscale/models/regscale_models/profile_mapping.py +48 -3
- regscale/models/regscale_models/regscale_model.py +2 -0
- regscale/models/regscale_models/risk.py +38 -30
- regscale/models/regscale_models/security_plan.py +1 -0
- regscale/models/regscale_models/supply_chain.py +1 -1
- regscale/models/regscale_models/user.py +16 -2
- regscale/utils/threading/__init__.py +1 -0
- regscale/utils/threading/threadsafe_list.py +10 -0
- regscale/utils/threading/threadsafe_set.py +116 -0
- {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/RECORD +42 -40
- {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/top_level.txt +0 -0
|
@@ -2,23 +2,27 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
# pylint: disable=C0415
|
|
4
4
|
"""standard python imports"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
5
7
|
import json
|
|
6
8
|
import math
|
|
7
9
|
import re
|
|
8
10
|
from collections import Counter
|
|
9
11
|
from concurrent.futures import as_completed
|
|
10
12
|
from concurrent.futures.thread import ThreadPoolExecutor
|
|
11
|
-
from
|
|
12
|
-
from
|
|
13
|
+
from threading import Thread
|
|
14
|
+
from types import ModuleType
|
|
15
|
+
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Literal, Optional, Tuple, TypeVar
|
|
13
16
|
|
|
14
17
|
import click
|
|
15
18
|
|
|
16
19
|
from regscale.core.app.api import Api
|
|
20
|
+
from regscale.core.app.utils.api_handler import APIInsertionError, APIUpdateError
|
|
17
21
|
from regscale.core.app.utils.app_utils import create_progress_object, error_and_exit, get_current_datetime
|
|
18
22
|
from regscale.core.utils.graphql import GraphQLQuery
|
|
19
23
|
from regscale.integrations.public.fedramp.parts_mapper import PartMapper
|
|
20
24
|
from regscale.integrations.public.fedramp.ssp_logger import SSPLogger
|
|
21
|
-
from regscale.models import ControlObjective, ImplementationObjective
|
|
25
|
+
from regscale.models import ControlObjective, ImplementationObjective, Parameter, Profile
|
|
22
26
|
from regscale.models.regscale_models import (
|
|
23
27
|
ControlImplementation,
|
|
24
28
|
File,
|
|
@@ -26,7 +30,17 @@ from regscale.models.regscale_models import (
|
|
|
26
30
|
SecurityControl,
|
|
27
31
|
SecurityPlan,
|
|
28
32
|
)
|
|
33
|
+
from regscale.models.regscale_models.compliance_settings import ComplianceSettings
|
|
29
34
|
from regscale.models.regscale_models.control_implementation import ControlImplementationStatus
|
|
35
|
+
from regscale.utils.threading import ThreadSafeDict, ThreadSafeSet
|
|
36
|
+
|
|
37
|
+
# For type annotations only
|
|
38
|
+
if TYPE_CHECKING:
|
|
39
|
+
import pandas as pd
|
|
40
|
+
|
|
41
|
+
from functools import lru_cache
|
|
42
|
+
|
|
43
|
+
T = TypeVar("T")
|
|
30
44
|
|
|
31
45
|
logger = SSPLogger()
|
|
32
46
|
part_mapper_rev5 = PartMapper()
|
|
@@ -49,6 +63,9 @@ IMPACT_LEVEL = "Impact Level"
|
|
|
49
63
|
SYSTEM_NAME = "System Name"
|
|
50
64
|
CSP = "CSP"
|
|
51
65
|
|
|
66
|
+
EXISTING_IMPLEMENTATIONS: ThreadSafeDict[int, ControlImplementation] = ThreadSafeDict()
|
|
67
|
+
UPDATED_IMPLEMENTATION_OBJECTIVES: ThreadSafeSet[ImplementationObjective] = ThreadSafeSet()
|
|
68
|
+
|
|
52
69
|
STATUS_MAPPING = {
|
|
53
70
|
"Implemented": ControlImplementationStatus.Implemented,
|
|
54
71
|
PARTIALLY_IMPLEMENTED: ControlImplementationStatus.PartiallyImplemented,
|
|
@@ -76,6 +93,20 @@ RESPONSIBILITY_MAP = {
|
|
|
76
93
|
"bShared": "Shared",
|
|
77
94
|
"bInherited": "Inherited",
|
|
78
95
|
}
|
|
96
|
+
REGSCALE_SSP_ID: int = 0
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@lru_cache(maxsize=1)
|
|
100
|
+
def get_pandas() -> ModuleType:
|
|
101
|
+
"""
|
|
102
|
+
Lazily import pandas only once when needed
|
|
103
|
+
|
|
104
|
+
:return: The pandas module
|
|
105
|
+
:rtype: ModuleType
|
|
106
|
+
"""
|
|
107
|
+
import pandas as pd
|
|
108
|
+
|
|
109
|
+
return pd
|
|
79
110
|
|
|
80
111
|
|
|
81
112
|
def transform_control(control: str) -> str:
|
|
@@ -280,17 +311,17 @@ def update_imp_objective(
|
|
|
280
311
|
imp: ControlImplementation,
|
|
281
312
|
objectives: List[ControlObjective],
|
|
282
313
|
record: dict,
|
|
283
|
-
) ->
|
|
314
|
+
) -> None:
|
|
284
315
|
"""
|
|
285
|
-
Update the control
|
|
316
|
+
Update the control objectives with the given record data.
|
|
286
317
|
|
|
287
318
|
:param int leverage_auth_id: The leveraged authorization ID
|
|
288
319
|
:param List[ImplementationObjective] existing_imp_obj: The existing implementation objective
|
|
289
320
|
:param ControlImplementation imp: The control implementation to update
|
|
290
321
|
:param List[ControlObjective] objectives: The control objective to update
|
|
291
322
|
:param dict record: The CIS/CRM record data to update the objective with
|
|
292
|
-
:rtype:
|
|
293
|
-
:return:
|
|
323
|
+
:rtype: None
|
|
324
|
+
:return: None
|
|
294
325
|
"""
|
|
295
326
|
status_map = {
|
|
296
327
|
"Implemented": ControlImplementationStatus.Implemented.value,
|
|
@@ -312,32 +343,49 @@ def update_imp_objective(
|
|
|
312
343
|
|
|
313
344
|
cis_record = record.get("cis", {})
|
|
314
345
|
crm_record = record.get("crm", {})
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
)
|
|
346
|
+
# There could be multiples, take the first one as regscale will not allow multiples at the objective level.
|
|
347
|
+
control_originations = cis_record.get("control_origination", "").split(",")
|
|
348
|
+
for ix, control_origination in enumerate(control_originations):
|
|
349
|
+
control_originations[ix] = control_origination.strip()
|
|
350
|
+
|
|
351
|
+
try:
|
|
352
|
+
responsibility = RESPONSIBILITY_MAP.get(
|
|
353
|
+
next(origin for origin in control_originations), SERVICE_PROVIDER_CORPORATE
|
|
354
|
+
)
|
|
355
|
+
except StopIteration:
|
|
356
|
+
responsibility = SERVICE_PROVIDER_CORPORATE
|
|
357
|
+
|
|
318
358
|
customer_responsibility = clean_customer_responsibility(
|
|
319
359
|
crm_record.get("specific_inheritance_and_customer_agency_csp_responsibilities")
|
|
320
360
|
)
|
|
321
|
-
ret_objective = None
|
|
322
361
|
existing_pairs = {(obj.objectiveId, obj.implementationId) for obj in existing_imp_obj}
|
|
362
|
+
responsibility = responsibility_map.get(responsibility, responsibility)
|
|
323
363
|
for objective in objectives:
|
|
324
364
|
current_pair = (objective.id, imp.id)
|
|
325
365
|
if current_pair not in existing_pairs:
|
|
326
366
|
imp_obj = ImplementationObjective(
|
|
327
367
|
id=0,
|
|
328
368
|
uuid="",
|
|
329
|
-
inherited=crm_record.get("can_be_inherited_from_csp")
|
|
369
|
+
inherited=crm_record.get("can_be_inherited_from_csp") in ["Yes", "Partial"],
|
|
330
370
|
implementationId=imp.id,
|
|
331
371
|
status=status_map.get(cis_record.get("implementation_status", NOT_IMPLEMENTED), NOT_IMPLEMENTED),
|
|
332
372
|
objectiveId=objective.id,
|
|
333
373
|
notes=objective.name,
|
|
334
374
|
securityControlId=objective.securityControlId,
|
|
335
|
-
|
|
375
|
+
securityPlanId=REGSCALE_SSP_ID,
|
|
376
|
+
responsibility=responsibility,
|
|
336
377
|
cloudResponsibility=customer_responsibility,
|
|
337
378
|
customerResponsibility=customer_responsibility,
|
|
338
379
|
authorizationId=leverage_auth_id,
|
|
380
|
+
parentObjectiveId=objective.parentObjectiveId,
|
|
381
|
+
)
|
|
382
|
+
logger.debug(
|
|
383
|
+
"Creating new Implementation Objective for Control %s with status: %s responsibility: %s",
|
|
384
|
+
imp_obj.securityControlId,
|
|
385
|
+
imp_obj.status,
|
|
386
|
+
imp_obj.responsibility,
|
|
339
387
|
)
|
|
340
|
-
|
|
388
|
+
UPDATED_IMPLEMENTATION_OBJECTIVES.add(imp_obj)
|
|
341
389
|
else:
|
|
342
390
|
# NOTE: Don't overwrite the responsibility text and only append.
|
|
343
391
|
ex_obj = next((obj for obj in existing_imp_obj if obj.objectiveId == objective.id), None)
|
|
@@ -355,21 +403,19 @@ def update_imp_objective(
|
|
|
355
403
|
)
|
|
356
404
|
if ex_obj.cloudResponsibility:
|
|
357
405
|
ex_obj.cloudResponsibility = (
|
|
358
|
-
seperator.join([ex_obj.cloudResponsibility,
|
|
406
|
+
seperator.join([ex_obj.cloudResponsibility, customer_responsibility])
|
|
359
407
|
if ex_obj.cloudResponsibility != responsibility
|
|
360
408
|
else ex_obj.cloudResponsibility
|
|
361
409
|
)
|
|
362
410
|
if ex_obj.customerResponsibility:
|
|
363
411
|
ex_obj.customerResponsibility = (
|
|
364
|
-
seperator.join([ex_obj.customerResponsibility,
|
|
412
|
+
seperator.join([ex_obj.customerResponsibility, customer_responsibility])
|
|
365
413
|
if ex_obj.cloudResponsibility != responsibility
|
|
366
414
|
else ex_obj.customerResponsibility
|
|
367
415
|
)
|
|
368
416
|
except TypeError:
|
|
369
417
|
logger.warning(f"Failed to update responsibility on Implementation Objective #{ex_obj.id}")
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
return ret_objective
|
|
418
|
+
UPDATED_IMPLEMENTATION_OBJECTIVES.add(ex_obj)
|
|
373
419
|
|
|
374
420
|
|
|
375
421
|
def parse_control_details(
|
|
@@ -456,62 +502,62 @@ def fetch_and_update_imps(
|
|
|
456
502
|
:rtype: Optional[ControlImplementation]
|
|
457
503
|
"""
|
|
458
504
|
# get the control and control implementation objects
|
|
459
|
-
regscale_control = SecurityControl.get_object(control
|
|
460
|
-
|
|
505
|
+
regscale_control = SecurityControl.get_object(control.controlID)
|
|
506
|
+
if not regscale_control:
|
|
507
|
+
api.logger.error(f"Failed to fetch control with ID {control['scId']}")
|
|
508
|
+
return None
|
|
461
509
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
510
|
+
control_id = regscale_control.controlId if regscale_control else ""
|
|
511
|
+
security_control_id = regscale_control.id if regscale_control else 0
|
|
512
|
+
regscale_control_imp = EXISTING_IMPLEMENTATIONS.get(security_control_id)
|
|
513
|
+
|
|
514
|
+
if not regscale_control_imp:
|
|
515
|
+
api.logger.error(f"Failed to find control implementation for control ID {control_id}")
|
|
516
|
+
return None
|
|
465
517
|
|
|
466
518
|
updated_control = parse_control_details(
|
|
467
519
|
version=version, control_imp=regscale_control_imp, control=regscale_control, cis_data=cis_data
|
|
468
520
|
)
|
|
521
|
+
|
|
522
|
+
# Find the index of the old implementation and replace it with the updated one
|
|
523
|
+
EXISTING_IMPLEMENTATIONS[updated_control.controlID] = updated_control
|
|
524
|
+
|
|
469
525
|
return updated_control
|
|
470
526
|
|
|
471
527
|
|
|
472
|
-
def get_all_imps(api: Api,
|
|
528
|
+
def get_all_imps(api: Api, cis_data: dict, version: Literal["rev4", "rev5"]) -> None:
|
|
473
529
|
"""
|
|
474
530
|
Function to retrieve control implementations and their objectives from RegScale
|
|
475
531
|
|
|
476
532
|
:param Api api: The RegScale API object
|
|
477
|
-
:param int ssp_id: The SSP ID
|
|
478
533
|
:param dict cis_data: The data from the CIS worksheet
|
|
479
534
|
:param Literal["rev4", "rev5"] version: The version of the workbook
|
|
480
|
-
:return:
|
|
481
|
-
:rtype:
|
|
535
|
+
:return: None
|
|
536
|
+
:rtype: None
|
|
482
537
|
"""
|
|
483
538
|
from requests import RequestException
|
|
484
539
|
|
|
485
|
-
updated_controls = []
|
|
486
|
-
url = urljoin(api.config["domain"], f"/api/controlImplementation/getSCListByPlan/{ssp_id}")
|
|
487
|
-
response = api.get(url)
|
|
488
|
-
|
|
489
|
-
if response.status_code == 404:
|
|
490
|
-
api.logger.warning(f"SSP with ID {ssp_id} has no controls.")
|
|
491
|
-
return updated_controls
|
|
492
|
-
|
|
493
540
|
# Check if the response is successful
|
|
494
|
-
if
|
|
495
|
-
ssp_controls = response.json()
|
|
541
|
+
if EXISTING_IMPLEMENTATIONS:
|
|
496
542
|
# Get Control Implementations For SSP
|
|
497
543
|
fetching_imps = progress.add_task(
|
|
498
|
-
f"[magenta]
|
|
544
|
+
f"[magenta]Updating {len(EXISTING_IMPLEMENTATIONS)} implementation(s)...",
|
|
545
|
+
total=len(EXISTING_IMPLEMENTATIONS),
|
|
499
546
|
)
|
|
500
547
|
with ThreadPoolExecutor(max_workers=50) as executor:
|
|
501
548
|
futures = [
|
|
502
|
-
executor.submit(fetch_and_update_imps, control, api, cis_data, version)
|
|
549
|
+
executor.submit(fetch_and_update_imps, control, api, cis_data, version)
|
|
550
|
+
for control in EXISTING_IMPLEMENTATIONS.values()
|
|
503
551
|
]
|
|
552
|
+
|
|
553
|
+
# Just wait for all tasks to complete
|
|
504
554
|
for future in as_completed(futures):
|
|
505
|
-
progress.update(fetching_imps, advance=1)
|
|
506
555
|
try:
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
api.logger.error(f"Failed to fetch controls: {response.status_code}: {response.reason}")
|
|
513
|
-
|
|
514
|
-
return updated_controls
|
|
556
|
+
# Only call result() to propagate any exceptions
|
|
557
|
+
future.result()
|
|
558
|
+
progress.update(fetching_imps, advance=1)
|
|
559
|
+
except Exception as e:
|
|
560
|
+
logger.error(f"Error updating implementation: {e}")
|
|
515
561
|
|
|
516
562
|
|
|
517
563
|
def get_all_control_objectives(imps: List[ControlImplementation]) -> List[ControlObjective]:
|
|
@@ -543,7 +589,6 @@ def update_all_objectives(
|
|
|
543
589
|
leveraged_auth_id: int,
|
|
544
590
|
cis_data: Dict[str, Dict[str, str]],
|
|
545
591
|
crm_data: Dict[str, Dict[str, str]],
|
|
546
|
-
control_implementations: List[ControlImplementation],
|
|
547
592
|
version: Literal["rev4", "rev5"],
|
|
548
593
|
) -> set:
|
|
549
594
|
"""
|
|
@@ -553,25 +598,27 @@ def update_all_objectives(
|
|
|
553
598
|
:param int leveraged_auth_id: The leveraged authorization ID
|
|
554
599
|
:param Dict[str, Dict[str, str]] cis_data: The CIS data to update from
|
|
555
600
|
:param Dict[str, Dict[str, str]] crm_data: The CRM data to update from
|
|
556
|
-
:param List[ControlImplementation] control_implementations: The control implementations to update
|
|
557
601
|
:param Literal["rev4", "rev5"] version: The version of the workbook
|
|
558
602
|
:return: A set of errors, if any
|
|
559
603
|
:rtype: set
|
|
560
604
|
"""
|
|
561
|
-
|
|
605
|
+
|
|
606
|
+
all_control_objectives = get_all_control_objectives(imps=EXISTING_IMPLEMENTATIONS.values())
|
|
562
607
|
error_set = set()
|
|
563
|
-
|
|
608
|
+
process_task = progress.add_task(
|
|
609
|
+
"[cyan]Processing control objectives...", total=len(EXISTING_IMPLEMENTATIONS.values())
|
|
610
|
+
)
|
|
564
611
|
# Create a combined dataset for easier access
|
|
565
612
|
combined_data = {key: {"cis": cis_data[key], "crm": crm_data.get(key, {})} for key in cis_data}
|
|
566
613
|
|
|
567
614
|
# Process implementations in parallel
|
|
568
|
-
with ThreadPoolExecutor(max_workers=
|
|
615
|
+
with ThreadPoolExecutor(max_workers=30) as executor:
|
|
569
616
|
# Submit all tasks
|
|
570
617
|
future_to_control = {
|
|
571
618
|
executor.submit(
|
|
572
619
|
process_implementation, leveraged_auth_id, imp, combined_data, version, all_control_objectives
|
|
573
620
|
): imp
|
|
574
|
-
for imp in
|
|
621
|
+
for imp in EXISTING_IMPLEMENTATIONS.values()
|
|
575
622
|
}
|
|
576
623
|
|
|
577
624
|
# Process results as they complete
|
|
@@ -581,8 +628,18 @@ def update_all_objectives(
|
|
|
581
628
|
error_lst = result[0]
|
|
582
629
|
for inf in error_lst:
|
|
583
630
|
error_set.add(inf)
|
|
584
|
-
progress.update(
|
|
585
|
-
|
|
631
|
+
progress.update(process_task, advance=1)
|
|
632
|
+
save_task = progress.add_task("[yellow]Saving control objectives...", total=len(UPDATED_IMPLEMENTATION_OBJECTIVES))
|
|
633
|
+
# Process implementations in parallel
|
|
634
|
+
# Note, not using threadpool executor here due to phantom 500 errors. This is a workaround
|
|
635
|
+
for obj in UPDATED_IMPLEMENTATION_OBJECTIVES:
|
|
636
|
+
try:
|
|
637
|
+
obj.create_or_update()
|
|
638
|
+
progress.update(save_task, advance=1)
|
|
639
|
+
except APIInsertionError as e:
|
|
640
|
+
error_set.add(f"Failed to create Implementation Objective: {e}")
|
|
641
|
+
except APIUpdateError as e:
|
|
642
|
+
error_set.add(f"Failed to update Implementation Objective: {e}")
|
|
586
643
|
return error_set
|
|
587
644
|
|
|
588
645
|
|
|
@@ -673,7 +730,9 @@ def gen_filtered_records(
|
|
|
673
730
|
else:
|
|
674
731
|
try:
|
|
675
732
|
control_label = next(
|
|
676
|
-
dat
|
|
733
|
+
dat
|
|
734
|
+
for dat in part_mapper_rev4.data
|
|
735
|
+
if dat.get("Oscal Control ID") == security_control.controlId.lower()
|
|
677
736
|
).get("CONTROLLABEL")
|
|
678
737
|
except StopIteration:
|
|
679
738
|
control_label = None
|
|
@@ -714,15 +773,20 @@ def process_single_record(**kwargs) -> Tuple[List[str], Optional[ImplementationO
|
|
|
714
773
|
mapped_objectives: List[ControlObjective] = []
|
|
715
774
|
result = None
|
|
716
775
|
parts = []
|
|
717
|
-
|
|
776
|
+
# Note: The Control ID from the CIS/CRM can be in non-standard formats, as compared to the example sheet on fedramp.
|
|
718
777
|
if version == "rev5":
|
|
778
|
+
key = record["cis"]["control_id"].replace(" ", "")
|
|
719
779
|
source = part_mapper_rev5.find_by_source(key)
|
|
720
780
|
else:
|
|
781
|
+
key = record["cis"]["control_id"]
|
|
721
782
|
source = part_mapper_rev4.find_by_source(key)
|
|
722
783
|
if parts := part_mapper_rev4.find_sub_parts(key):
|
|
723
784
|
for part in parts:
|
|
724
785
|
try:
|
|
725
|
-
|
|
786
|
+
if version == "rev5":
|
|
787
|
+
mapped_objectives.append(next(obj for obj in control_objectives if obj.name == part))
|
|
788
|
+
else:
|
|
789
|
+
mapped_objectives.append(next(obj for obj in control_objectives if obj.otherId == part))
|
|
726
790
|
except StopIteration:
|
|
727
791
|
errors.append(f"Unable to find part {part} for control {key}")
|
|
728
792
|
if not source and not parts:
|
|
@@ -733,7 +797,7 @@ def process_single_record(**kwargs) -> Tuple[List[str], Optional[ImplementationO
|
|
|
733
797
|
objective = next(
|
|
734
798
|
obj
|
|
735
799
|
for obj in control_objectives
|
|
736
|
-
if obj.otherId == source and version
|
|
800
|
+
if (obj.otherId == source and version in ["rev5", "rev4"]) or (obj.name == source and version == "rev4")
|
|
737
801
|
)
|
|
738
802
|
mapped_objectives.append(objective)
|
|
739
803
|
except StopIteration:
|
|
@@ -741,7 +805,7 @@ def process_single_record(**kwargs) -> Tuple[List[str], Optional[ImplementationO
|
|
|
741
805
|
errors.append(f"Unable to find objective for control {key} ({source})")
|
|
742
806
|
|
|
743
807
|
if mapped_objectives:
|
|
744
|
-
|
|
808
|
+
update_imp_objective(
|
|
745
809
|
leverage_auth_id=leveraged_auth_id,
|
|
746
810
|
existing_imp_obj=existing_objectives,
|
|
747
811
|
imp=implementation,
|
|
@@ -762,43 +826,58 @@ def parse_crm_worksheet(file_path: click.Path, crm_sheet_name: str, version: Lit
|
|
|
762
826
|
:return: Formatted CRM content
|
|
763
827
|
:rtype: dict
|
|
764
828
|
"""
|
|
829
|
+
pd = get_pandas()
|
|
830
|
+
logger.info("Parsing CRM worksheet...")
|
|
765
831
|
formatted_crm = {}
|
|
766
832
|
|
|
767
833
|
if not crm_sheet_name:
|
|
768
834
|
return formatted_crm
|
|
769
|
-
import pandas as pd # Optimize import performance
|
|
770
835
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
else:
|
|
774
|
-
skip_rows = 3
|
|
836
|
+
# Value for rev4
|
|
837
|
+
skip_rows = 3
|
|
775
838
|
|
|
776
|
-
|
|
839
|
+
original_data = pd.read_excel(
|
|
777
840
|
str(file_path),
|
|
778
841
|
sheet_name=crm_sheet_name,
|
|
779
|
-
skiprows=skip_rows,
|
|
780
|
-
usecols=[
|
|
781
|
-
CONTROL_ID,
|
|
782
|
-
"Can Be Inherited from CSP",
|
|
783
|
-
"Specific Inheritance and Customer Agency/CSP Responsibilities",
|
|
784
|
-
],
|
|
785
842
|
)
|
|
786
843
|
|
|
844
|
+
# find index of row where the first column == Control ID
|
|
845
|
+
skip_rows = determine_skip_row(original_df=original_data, text_to_find=CONTROL_ID, original_skip=skip_rows)
|
|
846
|
+
|
|
847
|
+
logger.debug(f"Skipping {skip_rows} rows in CRM worksheet")
|
|
848
|
+
|
|
849
|
+
# only use thse coloumns
|
|
850
|
+
usecols = [
|
|
851
|
+
CONTROL_ID,
|
|
852
|
+
"Can Be Inherited from CSP",
|
|
853
|
+
"Specific Inheritance and Customer Agency/CSP Responsibilities",
|
|
854
|
+
]
|
|
855
|
+
|
|
856
|
+
try:
|
|
857
|
+
# Reindex the dataframe and skip some rows
|
|
858
|
+
data = original_data.iloc[skip_rows:].reset_index(drop=True)
|
|
859
|
+
data.columns = usecols
|
|
860
|
+
except KeyError as e:
|
|
861
|
+
error_and_exit(f"KeyError: {e} - One or more columns specified in usecols are not found in the dataframe.")
|
|
862
|
+
except Exception as e:
|
|
863
|
+
error_and_exit(f"An error occurred: {e}")
|
|
787
864
|
# Filter rows where "Can Be Inherited from CSP" is not equal to "No"
|
|
788
865
|
exclude_no = data[data[CAN_BE_INHERITED_CSP] != "No"]
|
|
789
866
|
|
|
790
867
|
# Iterate through each row and add to the dictionary
|
|
791
868
|
for _, row in exclude_no.iterrows():
|
|
792
869
|
control_id = row[CONTROL_ID]
|
|
870
|
+
if version == "rev5":
|
|
871
|
+
control_id = row[CONTROL_ID].replace(" ", "")
|
|
793
872
|
|
|
794
873
|
# Convert camel case to snake case, remove special characters, and convert to lowercase
|
|
795
874
|
clean_control_id = re.sub(r"\W+", "", control_id)
|
|
796
875
|
clean_control_id = re.sub("([a-z0-9])([A-Z])", r"\1_\2", clean_control_id).lower()
|
|
797
876
|
|
|
798
877
|
# Use clean_control_id as the key to avoid overwriting
|
|
799
|
-
formatted_crm[
|
|
800
|
-
"control_id":
|
|
801
|
-
"
|
|
878
|
+
formatted_crm[control_id] = {
|
|
879
|
+
"control_id": control_id,
|
|
880
|
+
"clean_control_id": clean_control_id,
|
|
802
881
|
"regscale_control_id": transform_control(control_id),
|
|
803
882
|
"can_be_inherited_from_csp": row[CAN_BE_INHERITED_CSP],
|
|
804
883
|
"specific_inheritance_and_customer_agency_csp_responsibilities": row[
|
|
@@ -818,10 +897,15 @@ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
|
818
897
|
:return: Formatted CIS content
|
|
819
898
|
:rtype: dict
|
|
820
899
|
"""
|
|
821
|
-
|
|
822
|
-
|
|
900
|
+
pd = get_pandas()
|
|
901
|
+
logger.info("Parsing CIS worksheet...")
|
|
902
|
+
skip_rows = 2
|
|
823
903
|
# Parse the worksheet named 'CIS GovCloud U.S.+DoD (H)', skipping the initial rows
|
|
824
|
-
|
|
904
|
+
original_cis = pd.read_excel(file_path, sheet_name=cis_sheet_name)
|
|
905
|
+
|
|
906
|
+
skip_rows = determine_skip_row(original_df=original_cis, text_to_find=CONTROL_ID, original_skip=skip_rows)
|
|
907
|
+
|
|
908
|
+
cis_df = original_cis.iloc[skip_rows:].reset_index(drop=True)
|
|
825
909
|
|
|
826
910
|
# Set the appropriate headers
|
|
827
911
|
cis_df.columns = cis_df.iloc[0]
|
|
@@ -918,27 +1002,81 @@ def parse_cis_worksheet(file_path: click.Path, cis_sheet_name: str) -> dict:
|
|
|
918
1002
|
return {result["control_id"]: result for result in results}
|
|
919
1003
|
|
|
920
1004
|
|
|
1005
|
+
def determine_skip_row(original_df: "pd.DataFrame", text_to_find: str, original_skip: int):
|
|
1006
|
+
"""
|
|
1007
|
+
Function to determine the row to skip when parsing a worksheet
|
|
1008
|
+
|
|
1009
|
+
:param pd.DataFrame original_df: The original dataframe to search
|
|
1010
|
+
:param str text_to_find: The text to find
|
|
1011
|
+
:param int original_skip: The original row to skip
|
|
1012
|
+
:return: The row to skip
|
|
1013
|
+
:rtype: int
|
|
1014
|
+
"""
|
|
1015
|
+
skip_rows = original_skip
|
|
1016
|
+
for idx, row in original_df.iterrows():
|
|
1017
|
+
if row.iloc[0] == text_to_find:
|
|
1018
|
+
skip_rows = idx + 1
|
|
1019
|
+
break
|
|
1020
|
+
return skip_rows
|
|
1021
|
+
|
|
1022
|
+
|
|
1023
|
+
def _drop_rows_nan(instructions_df: "pd.DataFrame") -> "pd.DataFrame":
|
|
1024
|
+
"""
|
|
1025
|
+
Drop any row with nan and every row after it
|
|
1026
|
+
|
|
1027
|
+
:param pd.DataFrame instructions_df: The instructions dataframe to process
|
|
1028
|
+
:return: The processed dataframe
|
|
1029
|
+
:rtype: pd.DataFrame
|
|
1030
|
+
"""
|
|
1031
|
+
# Find the first row containing any NaN value
|
|
1032
|
+
first_nan_index = None
|
|
1033
|
+
for i in range(len(instructions_df)):
|
|
1034
|
+
if instructions_df.iloc[i].isna().any():
|
|
1035
|
+
first_nan_index = i
|
|
1036
|
+
break
|
|
1037
|
+
|
|
1038
|
+
# If a row with NaN is found, keep only rows before it
|
|
1039
|
+
if first_nan_index is not None:
|
|
1040
|
+
instructions_df = instructions_df.iloc[:first_nan_index]
|
|
1041
|
+
else:
|
|
1042
|
+
# Otherwise, just drop any rows with NaN values as before
|
|
1043
|
+
instructions_df = instructions_df.dropna()
|
|
1044
|
+
return instructions_df
|
|
1045
|
+
|
|
1046
|
+
|
|
921
1047
|
def parse_instructions_worksheet(
|
|
922
|
-
|
|
1048
|
+
df: "pd.DataFrame",
|
|
1049
|
+
version: Literal["rev4", "rev5"],
|
|
1050
|
+
instructions_sheet_name: str = "Instructions",
|
|
923
1051
|
) -> list[dict]:
|
|
924
1052
|
"""
|
|
925
1053
|
Function to parse the instructions sheet from the FedRAMP Rev5 CIS/CRM workbook
|
|
926
1054
|
|
|
927
|
-
:param
|
|
1055
|
+
:param pd.DataFrame df: The dataframe to parse
|
|
928
1056
|
:param Literal["rev4", "rev5"] version: The version of the FedRAMP CIS CRM workbook
|
|
929
1057
|
:param str instructions_sheet_name: The name of the instructions sheet to parse, defaults to "Instructions"
|
|
930
1058
|
:return: List of formatted instructions content as a dictionary
|
|
931
1059
|
:rtype: list[dict]
|
|
932
1060
|
"""
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
instructions_df =
|
|
1061
|
+
pd = get_pandas()
|
|
1062
|
+
df = df[instructions_sheet_name].iloc[2:]
|
|
1063
|
+
instructions_df = df.dropna(axis=1, how="all")
|
|
936
1064
|
|
|
937
1065
|
if version == "rev5":
|
|
1066
|
+
relevant_columns = [CSP, SYSTEM_NAME, "System Identifier", IMPACT_LEVEL]
|
|
938
1067
|
# Set the appropriate headers
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
1068
|
+
# Find the row with the CSP column e.g. "System Name (CSP to complete all cells)"
|
|
1069
|
+
for index in range(len(instructions_df)):
|
|
1070
|
+
# Check if CSP is in the non-NaN values of this row
|
|
1071
|
+
row_values = [val for val in instructions_df.iloc[index].values if not pd.isna(val)]
|
|
1072
|
+
if CSP in row_values:
|
|
1073
|
+
# Keep only columns that have non-NaN values in this row
|
|
1074
|
+
non_nan_cols = instructions_df.columns[~instructions_df.iloc[index].isna()]
|
|
1075
|
+
instructions_df = instructions_df.loc[:, non_nan_cols]
|
|
1076
|
+
instructions_df.columns = relevant_columns
|
|
1077
|
+
instructions_df = instructions_df[index + 1 :]
|
|
1078
|
+
break
|
|
1079
|
+
|
|
942
1080
|
else:
|
|
943
1081
|
for index in range(len(instructions_df)):
|
|
944
1082
|
if CSP in instructions_df.iloc[index].values:
|
|
@@ -949,6 +1087,9 @@ def parse_instructions_worksheet(
|
|
|
949
1087
|
relevant_columns = [SYSTEM_NAME, CSP, IMPACT_LEVEL]
|
|
950
1088
|
try:
|
|
951
1089
|
instructions_df = instructions_df[relevant_columns]
|
|
1090
|
+
# drop any row with nan
|
|
1091
|
+
instructions_df = _drop_rows_nan(instructions_df)
|
|
1092
|
+
|
|
952
1093
|
except KeyError:
|
|
953
1094
|
error_and_exit(
|
|
954
1095
|
f"Unable to find the relevant columns in the Instructions worksheet. Do you have the correct "
|
|
@@ -959,8 +1100,91 @@ def parse_instructions_worksheet(
|
|
|
959
1100
|
return instructions_df.to_dict(orient="records")
|
|
960
1101
|
|
|
961
1102
|
|
|
1103
|
+
def update_responsiblity_text():
|
|
1104
|
+
"""
|
|
1105
|
+
Update the implementation responsibility texts from the objective data
|
|
1106
|
+
"""
|
|
1107
|
+
with ThreadPoolExecutor() as executor:
|
|
1108
|
+
executor.map(_update_imp_responsibility, EXISTING_IMPLEMENTATIONS.values())
|
|
1109
|
+
|
|
1110
|
+
|
|
1111
|
+
def _update_imp_responsibility(imp: ControlImplementation):
|
|
1112
|
+
"""
|
|
1113
|
+
Update the implementation responsibility text for a given implementation
|
|
1114
|
+
|
|
1115
|
+
:param ControlImplementation imp: The implementation to update
|
|
1116
|
+
:rtype: None
|
|
1117
|
+
:return: None
|
|
1118
|
+
"""
|
|
1119
|
+
# Get relevant objectives and sort them
|
|
1120
|
+
objs = _get_sorted_objectives(imp.id)
|
|
1121
|
+
|
|
1122
|
+
# Generate formatted responsibility texts
|
|
1123
|
+
customer_text = _format_responsibility_text(objs, "customerResponsibility")
|
|
1124
|
+
cloud_text = _format_responsibility_text(objs, "cloudResponsibility")
|
|
1125
|
+
|
|
1126
|
+
# Update implementation if we have content
|
|
1127
|
+
if customer_text or cloud_text:
|
|
1128
|
+
_save_implementation_text(imp, customer_text, cloud_text)
|
|
1129
|
+
|
|
1130
|
+
|
|
1131
|
+
def _get_sorted_objectives(imp_id: int) -> List[ImplementationObjective]:
|
|
1132
|
+
"""
|
|
1133
|
+
Get relevant objectives sorted by notes field
|
|
1134
|
+
|
|
1135
|
+
:param int imp_id: The implementation ID to filter objectives by
|
|
1136
|
+
:rtype: list
|
|
1137
|
+
:return: Sorted list of objectives
|
|
1138
|
+
:rtype: List[ImplementationObjective]
|
|
1139
|
+
"""
|
|
1140
|
+
objs = [obj for obj in UPDATED_IMPLEMENTATION_OBJECTIVES if obj.implementationId == imp_id]
|
|
1141
|
+
return sorted(objs, key=lambda x: x.notes)
|
|
1142
|
+
|
|
1143
|
+
|
|
1144
|
+
def _format_responsibility_text(objs: list, resp_attr: str) -> str:
|
|
1145
|
+
"""
|
|
1146
|
+
Format responsibility text for the given objects and attribute
|
|
1147
|
+
|
|
1148
|
+
:param list objs: The list of objects to format
|
|
1149
|
+
:param str resp_attr: The attribute to format
|
|
1150
|
+
:rtype: str
|
|
1151
|
+
:return: Formatted text
|
|
1152
|
+
"""
|
|
1153
|
+
text = ""
|
|
1154
|
+
multi_part = len(objs) > 1
|
|
1155
|
+
|
|
1156
|
+
for obj in objs:
|
|
1157
|
+
resp_text = getattr(obj, resp_attr, "")
|
|
1158
|
+
if resp_text:
|
|
1159
|
+
if multi_part:
|
|
1160
|
+
text += f"<p>part: {obj.notes}</p>"
|
|
1161
|
+
text += f"<p>{resp_text}</p>"
|
|
1162
|
+
|
|
1163
|
+
return text
|
|
1164
|
+
|
|
1165
|
+
|
|
1166
|
+
def _save_implementation_text(imp: ControlImplementation, customer_text: str, cloud_text: str):
|
|
1167
|
+
"""
|
|
1168
|
+
Save the implementation texts and update parameters
|
|
1169
|
+
|
|
1170
|
+
:param ControlImplementation imp: The implementation to save
|
|
1171
|
+
:param str customer_text: The customer responsibility text
|
|
1172
|
+
:param str cloud_text: The cloud responsibility text
|
|
1173
|
+
:rtype: None
|
|
1174
|
+
:return: None
|
|
1175
|
+
"""
|
|
1176
|
+
imp.customerImplementation = customer_text
|
|
1177
|
+
imp.cloudImplementation = cloud_text
|
|
1178
|
+
|
|
1179
|
+
# Update parameters in background thread
|
|
1180
|
+
_spin_off_thread(parameter_merge, imp.id, imp.controlID)
|
|
1181
|
+
|
|
1182
|
+
# Save implementation changes
|
|
1183
|
+
imp.save()
|
|
1184
|
+
|
|
1185
|
+
|
|
962
1186
|
def parse_and_map_data(
|
|
963
|
-
leveraged_auth_id: int, api: Api,
|
|
1187
|
+
leveraged_auth_id: int, api: Api, cis_data: dict, crm_data: dict, version: Literal["rev5", "rev4"]
|
|
964
1188
|
) -> None:
|
|
965
1189
|
"""
|
|
966
1190
|
Function to parse and map data from RegScale and the workbook.
|
|
@@ -974,14 +1198,14 @@ def parse_and_map_data(
|
|
|
974
1198
|
:rtype: None
|
|
975
1199
|
"""
|
|
976
1200
|
with progress:
|
|
977
|
-
|
|
1201
|
+
get_all_imps(api=api, cis_data=cis_data, version=version)
|
|
978
1202
|
error_set = update_all_objectives(
|
|
979
1203
|
leveraged_auth_id=leveraged_auth_id,
|
|
980
1204
|
cis_data=cis_data,
|
|
981
1205
|
crm_data=crm_data,
|
|
982
|
-
control_implementations=implementations,
|
|
983
1206
|
version=version,
|
|
984
1207
|
)
|
|
1208
|
+
update_responsiblity_text()
|
|
985
1209
|
|
|
986
1210
|
report(error_set)
|
|
987
1211
|
|
|
@@ -1032,12 +1256,171 @@ def rev_4_map(control_id: str) -> Optional[str]:
|
|
|
1032
1256
|
return f"{base_id}{f'.{letter}' if letter else ''}"
|
|
1033
1257
|
|
|
1034
1258
|
|
|
1259
|
+
def build_implementations_dict(security_plan_id) -> None:
|
|
1260
|
+
"""
|
|
1261
|
+
Save the implementations to a dictionary
|
|
1262
|
+
|
|
1263
|
+
:param int security_plan_id: The security plan id
|
|
1264
|
+
:rtype: None
|
|
1265
|
+
:return: None
|
|
1266
|
+
"""
|
|
1267
|
+
logger.info("Saving to an implementation dictionary ..")
|
|
1268
|
+
imps = ControlImplementation.get_list_by_plan(security_plan_id)
|
|
1269
|
+
for imp in imps:
|
|
1270
|
+
EXISTING_IMPLEMENTATIONS[imp.controlID] = imp
|
|
1271
|
+
logger.debug("Built %s implementations", len(imps))
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
def create_new_security_plan(profile_id: int, system_name: str):
|
|
1275
|
+
"""
|
|
1276
|
+
Create a new FedRamp security plan and map controls based on the profile id.
|
|
1277
|
+
|
|
1278
|
+
:param int profile_id: The profile id to map controls from
|
|
1279
|
+
:param str system_name: The system name to create the security plan for
|
|
1280
|
+
:rtype: SecurityPlan
|
|
1281
|
+
:return: The created security plan
|
|
1282
|
+
"""
|
|
1283
|
+
compliance_settings = ComplianceSettings.get_by_current_tenant()
|
|
1284
|
+
try:
|
|
1285
|
+
compliance_setting = next(
|
|
1286
|
+
(
|
|
1287
|
+
setting.id
|
|
1288
|
+
for setting in compliance_settings
|
|
1289
|
+
if setting and setting.title == "FedRAMP Compliance Setting"
|
|
1290
|
+
),
|
|
1291
|
+
2,
|
|
1292
|
+
)
|
|
1293
|
+
except TypeError:
|
|
1294
|
+
compliance_setting = 2
|
|
1295
|
+
existing_plans = SecurityPlan.get_list()
|
|
1296
|
+
existing_plan = [plan for plan in existing_plans if plan and plan.systemName == system_name]
|
|
1297
|
+
if not existing_plan:
|
|
1298
|
+
profile = Profile.get_object(profile_id)
|
|
1299
|
+
if not profile:
|
|
1300
|
+
error_and_exit("Unable to find the profile with the given ID, please try again")
|
|
1301
|
+
logger.info(f"Loading Profile Mappings from profile #{profile.id} - {profile.name}..")
|
|
1302
|
+
ret = SecurityPlan(
|
|
1303
|
+
**{
|
|
1304
|
+
"status": "Under Development",
|
|
1305
|
+
"systemType": "Major Application",
|
|
1306
|
+
"systemName": system_name,
|
|
1307
|
+
"users": 0,
|
|
1308
|
+
"privilegedUsers": 0,
|
|
1309
|
+
"usersMFA": 0,
|
|
1310
|
+
"privilegedUsersMFA": 0,
|
|
1311
|
+
"internalUsers": 0,
|
|
1312
|
+
"externalUsers": 0,
|
|
1313
|
+
"internalUsersFuture": 0,
|
|
1314
|
+
"externalUsersFuture": 0,
|
|
1315
|
+
"hva": False,
|
|
1316
|
+
"isPublic": True,
|
|
1317
|
+
"bModelSaaS": False,
|
|
1318
|
+
"bModelPaaS": False,
|
|
1319
|
+
"bModelIaaS": False,
|
|
1320
|
+
"bModelOther": False,
|
|
1321
|
+
"otherModelRemarks": "",
|
|
1322
|
+
"bDeployPrivate": False,
|
|
1323
|
+
"bDeployPublic": False,
|
|
1324
|
+
"bDeployGov": False,
|
|
1325
|
+
"bDeployHybrid": False,
|
|
1326
|
+
"bDeployOther": False,
|
|
1327
|
+
"fedrampDateSubmitted": "",
|
|
1328
|
+
"fedrampDateAuthorized": "",
|
|
1329
|
+
"defaultAssessmentDays": 0,
|
|
1330
|
+
"complianceSettingsId": compliance_setting,
|
|
1331
|
+
}
|
|
1332
|
+
).create()
|
|
1333
|
+
logger.info(f"Created the new Security Plan as ID# {ret.id}")
|
|
1334
|
+
logger.info("Building the implementations from the profile mappings ..")
|
|
1335
|
+
Profile.apply_profile(ret.id, "securityplans", profile_id, True)
|
|
1336
|
+
build_implementations_dict(security_plan_id=ret.id)
|
|
1337
|
+
|
|
1338
|
+
else:
|
|
1339
|
+
ret = next((plan for plan in existing_plan), None)
|
|
1340
|
+
existing_imps = ControlImplementation.get_list_by_plan(ret.id)
|
|
1341
|
+
for imp in existing_imps:
|
|
1342
|
+
EXISTING_IMPLEMENTATIONS[imp.controlID] = imp
|
|
1343
|
+
|
|
1344
|
+
if ret is None:
|
|
1345
|
+
raise ValueError("Unable to create a new security plan.")
|
|
1346
|
+
|
|
1347
|
+
if not EXISTING_IMPLEMENTATIONS:
|
|
1348
|
+
# We must have some implementations, build them if empty.
|
|
1349
|
+
Profile.apply_profile(ret.id, "securityplans", profile_id, True)
|
|
1350
|
+
build_implementations_dict(security_plan_id=ret.id)
|
|
1351
|
+
|
|
1352
|
+
return ret
|
|
1353
|
+
|
|
1354
|
+
|
|
1355
|
+
def parameter_merge(implementation_id: int, security_control_id: int):
|
|
1356
|
+
"""
|
|
1357
|
+
Merge parameters for a given implementation ID and security control ID.
|
|
1358
|
+
|
|
1359
|
+
:param int implementation_id: The implementation ID
|
|
1360
|
+
:param int security_control_id: The security control ID
|
|
1361
|
+
:rtype: None
|
|
1362
|
+
"""
|
|
1363
|
+
parameters = Parameter.merge_parameters(implementation_id, security_control_id)
|
|
1364
|
+
for param in parameters:
|
|
1365
|
+
param.create()
|
|
1366
|
+
|
|
1367
|
+
|
|
1368
|
+
def objective_merge(implementation_id: int, security_control_id: int):
|
|
1369
|
+
"""
|
|
1370
|
+
Merge objectives for a given implementation ID and security control ID.
|
|
1371
|
+
|
|
1372
|
+
:param int implementation_id: The implementation ID
|
|
1373
|
+
:param int security_control_id: The security control ID
|
|
1374
|
+
:rtype: None
|
|
1375
|
+
"""
|
|
1376
|
+
imp_objectives = ImplementationObjective.merge_objectives(implementation_id, security_control_id)
|
|
1377
|
+
for obj in imp_objectives:
|
|
1378
|
+
obj.create()
|
|
1379
|
+
|
|
1380
|
+
|
|
1381
|
+
def _spin_off_thread(function: Callable[..., T], *args: Any) -> Thread:
|
|
1382
|
+
"""
|
|
1383
|
+
Spin off a thread to run the function with the given arguments.
|
|
1384
|
+
|
|
1385
|
+
:param function: The function to run
|
|
1386
|
+
:param args: The arguments to pass to the function
|
|
1387
|
+
:return: The thread object
|
|
1388
|
+
"""
|
|
1389
|
+
thread = Thread(target=function, args=args)
|
|
1390
|
+
thread.start()
|
|
1391
|
+
return thread
|
|
1392
|
+
|
|
1393
|
+
|
|
1394
|
+
def _check_sheet_names_exist(
|
|
1395
|
+
file_path: click.Path, cis_sheet_name: str, crm_sheet_name: str
|
|
1396
|
+
) -> dict[str, "pd.DataFrame"]:
|
|
1397
|
+
"""
|
|
1398
|
+
Check if the sheet names exist in the workbook.
|
|
1399
|
+
|
|
1400
|
+
:param click.Path file_path: The file path to the FedRAMP CIS CRM workbook
|
|
1401
|
+
:param str cis_sheet_name: The name of the CIS sheet to check
|
|
1402
|
+
:param str crm_sheet_name: The name of the CRM sheet to check
|
|
1403
|
+
:raises SystemExit: If the sheet names do not exist
|
|
1404
|
+
:rtype: dict[str, pd.DataFrame]
|
|
1405
|
+
:return: A dictionary of dataframes for each sheet
|
|
1406
|
+
"""
|
|
1407
|
+
pd = get_pandas()
|
|
1408
|
+
|
|
1409
|
+
df = pd.read_excel(file_path, sheet_name=None)
|
|
1410
|
+
sheet_names = df.keys()
|
|
1411
|
+
if cis_sheet_name not in sheet_names:
|
|
1412
|
+
error_and_exit(f"The CIS sheet name '{cis_sheet_name}' does not exist in the workbook.")
|
|
1413
|
+
if crm_sheet_name and crm_sheet_name not in sheet_names:
|
|
1414
|
+
error_and_exit(f"The CRM sheet name '{crm_sheet_name}' does not exist in the workbook.")
|
|
1415
|
+
return df
|
|
1416
|
+
|
|
1417
|
+
|
|
1035
1418
|
def parse_and_import_ciscrm(
|
|
1036
1419
|
file_path: click.Path,
|
|
1037
1420
|
version: Literal["rev4", "rev5", "4", "5"],
|
|
1038
1421
|
cis_sheet_name: str,
|
|
1039
|
-
crm_sheet_name: str,
|
|
1040
|
-
|
|
1422
|
+
crm_sheet_name: Optional[str],
|
|
1423
|
+
profile_id: int,
|
|
1041
1424
|
leveraged_auth_id: int = 0,
|
|
1042
1425
|
) -> None:
|
|
1043
1426
|
"""
|
|
@@ -1046,17 +1429,17 @@ def parse_and_import_ciscrm(
|
|
|
1046
1429
|
:param click.Path file_path: The file path to the FedRAMP CIS CRM .xlsx file
|
|
1047
1430
|
:param Literal["rev4", "rev5"] version: FedRAMP revision version
|
|
1048
1431
|
:param str cis_sheet_name: CIS sheet name in the FedRAMP CIS CRM .xlsx to parse
|
|
1049
|
-
:param str crm_sheet_name: CRM sheet name in the FedRAMP CIS CRM .xlsx to parse
|
|
1050
|
-
:param int
|
|
1432
|
+
:param Optional[str] crm_sheet_name: CRM sheet name in the FedRAMP CIS CRM .xlsx to parse
|
|
1433
|
+
:param int profile_id: The ID number from RegScale of the RegScale Profile to generate the control mapping
|
|
1051
1434
|
:param int leveraged_auth_id: RegScale Leveraged Authorization ID #, if none provided, one will be created
|
|
1052
1435
|
:raises ValueError: If the SSP with the given ID is not found in RegScale
|
|
1053
1436
|
:rtype: None
|
|
1054
1437
|
"""
|
|
1438
|
+
global REGSCALE_SSP_ID # Declare that you're modifying the global variable
|
|
1055
1439
|
sys_name_key = "System Name"
|
|
1056
1440
|
api = Api()
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
raise ValueError(f"SSP with ID {regscale_ssp_id} not found in RegScale.")
|
|
1441
|
+
|
|
1442
|
+
df = _check_sheet_names_exist(file_path, cis_sheet_name, crm_sheet_name)
|
|
1060
1443
|
|
|
1061
1444
|
if "5" in version:
|
|
1062
1445
|
version = "rev5"
|
|
@@ -1065,12 +1448,25 @@ def parse_and_import_ciscrm(
|
|
|
1065
1448
|
version = "rev4"
|
|
1066
1449
|
part_mapper_rev4.load_fedramp_version_4_mapping()
|
|
1067
1450
|
# parse the instructions worksheet to get the csp name, system name, and other data
|
|
1068
|
-
instructions_data = parse_instructions_worksheet(
|
|
1451
|
+
instructions_data = parse_instructions_worksheet(df=df, version=version) # type: ignore
|
|
1069
1452
|
|
|
1070
1453
|
# get the system names from the instructions data by dropping any non-string values
|
|
1071
|
-
|
|
1454
|
+
|
|
1455
|
+
system_names = [
|
|
1456
|
+
entry[sys_name_key]
|
|
1457
|
+
for entry in instructions_data
|
|
1458
|
+
if isinstance(entry[sys_name_key], str) and cis_sheet_name in entry[sys_name_key].lower()
|
|
1459
|
+
]
|
|
1460
|
+
if not system_names:
|
|
1461
|
+
system_names = [entry[sys_name_key] for entry in instructions_data if isinstance(entry[sys_name_key], str)]
|
|
1072
1462
|
name_match: str = system_names[0]
|
|
1073
1463
|
|
|
1464
|
+
# create the new security plan
|
|
1465
|
+
ssp: SecurityPlan = create_new_security_plan(profile_id=profile_id, system_name=name_match)
|
|
1466
|
+
REGSCALE_SSP_ID = ssp.id
|
|
1467
|
+
|
|
1468
|
+
if not ssp:
|
|
1469
|
+
raise ValueError("Unable to create a new SSP.")
|
|
1074
1470
|
# update the instructions data to the matched system names
|
|
1075
1471
|
instructions_data = [
|
|
1076
1472
|
(
|
|
@@ -1091,9 +1487,7 @@ def parse_and_import_ciscrm(
|
|
|
1091
1487
|
cis_data = parse_cis_worksheet(file_path=file_path, cis_sheet_name=cis_sheet_name)
|
|
1092
1488
|
crm_data = {}
|
|
1093
1489
|
if crm_sheet_name:
|
|
1094
|
-
crm_data = parse_crm_worksheet(
|
|
1095
|
-
file_path=file_path, crm_sheet_name=crm_sheet_name, version=version # type: ignore
|
|
1096
|
-
)
|
|
1490
|
+
crm_data = parse_crm_worksheet(file_path=file_path, crm_sheet_name=crm_sheet_name, version=version) # type: ignore
|
|
1097
1491
|
if leveraged_auth_id == 0:
|
|
1098
1492
|
auths = LeveragedAuthorization.get_all_by_parent(ssp.id)
|
|
1099
1493
|
if auths:
|
|
@@ -1101,7 +1495,7 @@ def parse_and_import_ciscrm(
|
|
|
1101
1495
|
else:
|
|
1102
1496
|
leveraged_auth_id = new_leveraged_auth(
|
|
1103
1497
|
ssp=ssp,
|
|
1104
|
-
user_id=api.config["userId"],
|
|
1498
|
+
user_id=api.app.config["userId"],
|
|
1105
1499
|
instructions_data=instructions_data,
|
|
1106
1500
|
version=version, # type: ignore
|
|
1107
1501
|
)
|
|
@@ -1109,7 +1503,6 @@ def parse_and_import_ciscrm(
|
|
|
1109
1503
|
parse_and_map_data(
|
|
1110
1504
|
leveraged_auth_id=leveraged_auth_id,
|
|
1111
1505
|
api=api,
|
|
1112
|
-
ssp_id=regscale_ssp_id,
|
|
1113
1506
|
cis_data=cis_data,
|
|
1114
1507
|
crm_data=crm_data,
|
|
1115
1508
|
version=version, # type: ignore
|
|
@@ -1118,7 +1511,7 @@ def parse_and_import_ciscrm(
|
|
|
1118
1511
|
# upload workbook to the SSP
|
|
1119
1512
|
File.upload_file_to_regscale(
|
|
1120
1513
|
file_name=str(file_path),
|
|
1121
|
-
parent_id=
|
|
1514
|
+
parent_id=ssp.id,
|
|
1122
1515
|
parent_module="securityplans",
|
|
1123
1516
|
api=api,
|
|
1124
1517
|
)
|