regscale-cli 6.22.0.0__py3-none-any.whl → 6.22.0.1__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.

@@ -0,0 +1,1129 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """Integrates CSAM into RegScale"""
4
+
5
+ # standard python imports
6
+ import logging
7
+ from typing import List, Optional, Tuple, Any, Dict
8
+ from urllib.parse import urljoin
9
+ from rich.progress import track
10
+ import click
11
+ from rich.console import Console
12
+ from regscale.core.app.api import Api
13
+ from regscale.core.app.application import Application
14
+ from regscale.core.utils.date import format_to_regscale_iso, date_obj
15
+ from regscale.core.app.utils.app_utils import error_and_exit, filter_list
16
+ from regscale.core.app.utils.parser_utils import safe_date_str
17
+ from regscale.models.regscale_models import (
18
+ Catalog,
19
+ ControlImplementation,
20
+ InheritedControl,
21
+ Inheritance,
22
+ Issue,
23
+ Organization,
24
+ SecurityControl,
25
+ SecurityPlan,
26
+ User,
27
+ )
28
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
29
+ from regscale.models.regscale_models.module import Module
30
+ from regscale.models.regscale_models.form_field_value import FormFieldValue
31
+ from regscale.core.app.utils.regscale_utils import normalize_controlid
32
+
33
+ logger = logging.getLogger("regscale")
34
+ console = Console()
35
+
36
+ ####################################################################################################
37
+ #
38
+ # IMPORT SSP / POAM FROM DoJ's CSAM GRC
39
+ # CSAM API Docs: https://csam.dhs.gov/CSAM/api/docs/index.html (required PIV)
40
+ #
41
+ ####################################################################################################
42
+
43
+ SSP_BASIC_TAB = "Basic Info"
44
+ SSP_SYSTEM_TAB = "System Information"
45
+ SSP_FINANCIAL_TAB = "Financial Info"
46
+ SSP_PRIVACY_TAB = "Privacy-Details"
47
+ CSAM_FIELD_NAME = "CSAM Id"
48
+ FISMA_FIELD_NAME = "FISMA Id"
49
+ POAM_ID = "POAM Id"
50
+ SYSTEM_ID = "System ID"
51
+
52
+
53
+ @click.group()
54
+ def csam():
55
+ """Integrate CSAM."""
56
+
57
+
58
+ @csam.command(name="import_ssp")
59
+ def import_ssp():
60
+ """
61
+ Import SSP from CSAM
62
+ Into RegScale
63
+ """
64
+
65
+ import_csam_ssp()
66
+
67
+
68
+ @csam.command(name="import_poam")
69
+ def import_poam():
70
+ """
71
+ Import POAMS from CSAM
72
+ Into RegScale
73
+ """
74
+
75
+ import_csam_poams()
76
+
77
+
78
+ def import_csam_ssp():
79
+ """
80
+ Import an SSP from CSAM
81
+ Into RegScale
82
+ """
83
+ custom_fields_basic_list = [
84
+ "acronym",
85
+ "Financial System",
86
+ "Classification",
87
+ "FISMA Reportable",
88
+ "Contractor System",
89
+ "Authorization Process",
90
+ "ATO Date",
91
+ "Critical Infrastructure",
92
+ "Mission Essential",
93
+ "uui Code",
94
+ "HVA Identifier",
95
+ "External Web Interface",
96
+ "CFO Designation",
97
+ "AI/ML Components",
98
+ "Law Enforcement Sensitive",
99
+ CSAM_FIELD_NAME,
100
+ FISMA_FIELD_NAME,
101
+ ]
102
+ custom_fields_financial_list = [
103
+ "omb Exhibit",
104
+ "Investment Name",
105
+ "Portfolio",
106
+ "Prior Fy Funding",
107
+ "Current Fy Funding",
108
+ "Next Fy Funding",
109
+ "Funding Import Status",
110
+ ]
111
+
112
+ # Check Custom Fields exist
113
+ custom_fields_basic_map = FormFieldValue.check_custom_fields(
114
+ custom_fields_basic_list, "securityplans", SSP_BASIC_TAB
115
+ )
116
+ custom_fields_financial_map = FormFieldValue.check_custom_fields(
117
+ custom_fields_financial_list, "securityplans", SSP_FINANCIAL_TAB
118
+ )
119
+
120
+ # Get a map of existing custom forms
121
+ ssp_map = retrieve_ssps_custom_form_map(
122
+ tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[FISMA_FIELD_NAME]
123
+ )
124
+
125
+ # Get a list of orgs and create a map to id
126
+ orgs = Organization.get_list()
127
+ org_map = {org.name: org.id for org in orgs}
128
+
129
+ # Grab the data from CSAM
130
+ app = Application()
131
+ csam_token = app.config.get("csamToken")
132
+ csam_url = app.config.get("csamURL")
133
+ csam_filter = app.config.get("csamFilter", None)
134
+
135
+ results = retrieve_from_csam(
136
+ csam_token=csam_token,
137
+ csam_url=csam_url,
138
+ csam_endpoint="/CSAM/api/v1/systems",
139
+ )
140
+
141
+ results = filter_list(results, csam_filter)
142
+ if not results:
143
+ error_and_exit(
144
+ "No results match filter in CSAM. \
145
+ Please check your CSAM configuration."
146
+ )
147
+
148
+ # Parse the results
149
+ updated_ssps = []
150
+ updated_ssps = save_ssp_front_matter(
151
+ results=results,
152
+ ssp_map=ssp_map,
153
+ custom_fields_basic_map=custom_fields_basic_map,
154
+ custom_fields_financial_map=custom_fields_financial_map,
155
+ org_map=org_map,
156
+ )
157
+
158
+ # Now have to get the system details for each system
159
+ update_ssp_agency_details(updated_ssps, csam_token, csam_url, custom_fields_basic_map)
160
+
161
+ # Import the Privacy date
162
+ import_csam_privacy_info(import_ids=[ssp.id for ssp in updated_ssps])
163
+
164
+ # Import the controls
165
+ import_csam_controls(import_ids=[ssp.id for ssp in updated_ssps])
166
+
167
+ # Set inheritance if system type = program
168
+ for result in results:
169
+ if result.get("systemType") == "Program":
170
+ # Get the RegScale SSP Id
171
+ program_id = ssp_map.get(result["externalId"])
172
+ if not program_id:
173
+ logger.error(
174
+ f"Could not find RegScale SSP for CSAM id: {result['externalId']}. \
175
+ Please create or import the Security Plan prior to importing inheritance."
176
+ )
177
+ continue
178
+
179
+ # Set the inheritable flag
180
+ set_inheritable(regscale_id=result.get("id"))
181
+
182
+ # Import the Inheritance
183
+ import_csam_inheritance(import_ids=[ssp.id for ssp in updated_ssps])
184
+
185
+ # Import the POCs
186
+ import_csam_pocs(import_ids=[ssp.id for ssp in updated_ssps])
187
+
188
+
189
+ def import_csam_controls(import_ids: Optional[List[int]] = None):
190
+ """
191
+ Import Controls from CSAM
192
+
193
+ :param list import_ids: Filtered list of SSPs
194
+ :return: None
195
+ """
196
+
197
+ # Grab the data from CSAM
198
+ app = Application()
199
+ csam_token = app.config.get("csamToken")
200
+ csam_url = app.config.get("csamURL")
201
+
202
+ # Get existing ssps by CSAM Id
203
+ custom_fields_basic_map = FormFieldValue.check_custom_fields([CSAM_FIELD_NAME], "securityplans", SSP_BASIC_TAB)
204
+ ssp_map = retrieve_custom_form_ssp_map(
205
+ tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
206
+ )
207
+
208
+ plans = import_ids if import_ids else list(ssp_map.keys())
209
+
210
+ # Find the Catalogs
211
+ rev5_catalog_id, rev4_catalog_id = get_catalogs()
212
+
213
+ # Get the list of controls for each catalog
214
+ rev5_controls = SecurityControl.get_list_by_catalog(catalog_id=rev5_catalog_id)
215
+ rev4_controls = SecurityControl.get_list_by_catalog(catalog_id=rev4_catalog_id)
216
+
217
+ control_implementations = []
218
+ for regscale_ssp_id in plans:
219
+ results = []
220
+ system_id = ssp_map.get(regscale_ssp_id)
221
+
222
+ # Get the Implementation for AC-1
223
+ # Check the controlSet
224
+ # Match the catalog
225
+ imp = retrieve_from_csam(
226
+ csam_token=csam_token,
227
+ csam_url=csam_url,
228
+ csam_endpoint=f"/CSAM/api/v1/systems/{system_id}/controls/AC-1",
229
+ )
230
+
231
+ # Get the controls
232
+ if imp[0].get("controlSet") in ["NIST 800-53 Rev4", "NIST 800-53 Rev5"]:
233
+ results = retrieve_controls(
234
+ csam_token=csam_token,
235
+ csam_url=csam_url,
236
+ csam_id=system_id,
237
+ controls=rev4_controls if imp[0].get("controlSet") == "NIST 800-53 Rev4" else rev5_controls,
238
+ regscale_id=regscale_ssp_id,
239
+ )
240
+ else:
241
+ logger.warning(
242
+ f"System framework {imp.get('controlSet')} \
243
+ for system {system_id} is not supported"
244
+ )
245
+ continue
246
+
247
+ if not results:
248
+ logger.warning(f"No controls found for system id: {system_id}")
249
+ continue
250
+
251
+ # Build the controls
252
+ control_implementations = build_implementations(results=results, csam_id=system_id, regscale_id=regscale_ssp_id)
253
+
254
+ # Save the control implementations
255
+ for index in track(
256
+ range(len(control_implementations)),
257
+ description=f"Saving {len(control_implementations)} control implementations...",
258
+ ):
259
+ control_implementation = control_implementations[index]
260
+ control_implementation.create() if control_implementation.id == 0 else control_implementation.save()
261
+
262
+
263
+ def import_csam_poams():
264
+ # Check Custom Fields
265
+ custom_fields_basic_map = FormFieldValue.check_custom_fields(
266
+ fields_list=[FISMA_FIELD_NAME, CSAM_FIELD_NAME], module_name="securityplans", tab_name=SSP_BASIC_TAB
267
+ )
268
+
269
+ # Get the SSPs
270
+ ssp_map = retrieve_ssps_custom_form_map(
271
+ tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
272
+ )
273
+
274
+ # Get a list of users and create a map to id
275
+ users = User.get_all()
276
+ user_map = {user.userName: user.id for user in users}
277
+
278
+ # Grab the data from CSAM
279
+ app = Application()
280
+ csam_token = app.config.get("csamToken")
281
+ csam_url = app.config.get("csamURL")
282
+ results = retrieve_from_csam(
283
+ csam_token=csam_token, csam_url=csam_url, csam_endpoint="/CSAM/api/v1/reports/POAM_Details_Report_CBP"
284
+ )
285
+
286
+ # Parse the results
287
+ poam_list = []
288
+ for index in track(
289
+ range(len(results)),
290
+ description=f"Importing {len(results)} POA&Ms...",
291
+ ):
292
+ result = results[index]
293
+
294
+ # Get the existing SSP:
295
+ ssp_id = ssp_map.get(str(result[SYSTEM_ID]))
296
+ if not ssp_id:
297
+ logger.error(
298
+ f"A RegScale Security Plan does not exist for CSAM id: {result[SYSTEM_ID]}\
299
+ create or import the Security Plan prior to importing POA&Ms"
300
+ )
301
+ continue
302
+
303
+ # Check if the POAM exists:
304
+ existing_issue = Issue.find_by_other_identifier(result[POAM_ID])
305
+ if existing_issue:
306
+ new_issue = existing_issue
307
+ else:
308
+ new_issue = Issue()
309
+
310
+ # Update the issue
311
+ new_issue.isPoam = True
312
+ new_issue.parentId = ssp_id
313
+ new_issue.parentModule = "securityplans"
314
+ new_issue.otherIdentifier = result[POAM_ID]
315
+ new_issue.title = result["POAM Title"]
316
+ new_issue.affectedControls = result["Controls"]
317
+ new_issue.securityPlanId = ssp_id
318
+ new_issue.identification = "Vulnerability Assessment"
319
+ new_issue.description = result["Detailed Weakness Description"]
320
+ new_issue.poamComments = f"{result['Weakness Comments']}\n \
321
+ {result['POA&M Delayed Comments']}\n \
322
+ {result['POA&M Comments']}"
323
+ new_issue.dateFirstDetected = safe_date_str(result["Create Date"])
324
+ new_issue.dueDate = safe_date_str(result["Planned Finish Date"])
325
+ # Need to convert cost to a int
326
+ # new_issue.costEstimate = result['Cost']
327
+ new_issue.issueOwnerId = (
328
+ user_map.get(result["Email"]) if user_map.get(result["Email"]) else RegScaleModel.get_user_id()
329
+ )
330
+ # Update with IssueSeverity String
331
+ new_issue.severityLevel = result["Severity"]
332
+ # Update with IssueStatus String
333
+ new_issue.status = result["Status"]
334
+
335
+ poam_list.append(new_issue)
336
+
337
+ for index in track(
338
+ range(len(poam_list)),
339
+ description=f"Updating RegScale with {len(poam_list)} POA&Ms...",
340
+ ):
341
+ poam = poam_list[index]
342
+ if poam.id == 0:
343
+ poam.create()
344
+ else:
345
+ poam.save()
346
+ logger.info(f"Added or updated {len(poam_list)} POA&Ms in RegScale")
347
+
348
+
349
+ def import_csam_pocs(import_ids: Optional[List[int]] = None):
350
+ """
351
+ Import the Points of Contact from CSAM
352
+ Into RegScale
353
+ """
354
+ custom_fields_system_list = [
355
+ "Certifying Official",
356
+ "Alternate Information System Security Manager",
357
+ "Alternate Information System Security Officer",
358
+ ]
359
+ # Check Custom Fields exist
360
+ custom_fields_system_map = FormFieldValue.check_custom_fields(
361
+ custom_fields_system_list, "securityplans", SSP_SYSTEM_TAB
362
+ )
363
+
364
+ # Get existing ssps by CSAM Id
365
+ custom_fields_basic_map = FormFieldValue.check_custom_fields([CSAM_FIELD_NAME], "securityplans", SSP_BASIC_TAB)
366
+ ssp_map = retrieve_custom_form_ssp_map(
367
+ tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
368
+ )
369
+
370
+ plans = import_ids if import_ids else list(ssp_map.keys())
371
+
372
+ # Get a list of users and create a map to id
373
+ users = User.get_all()
374
+ user_map = {user.userName: user.id for user in users}
375
+
376
+ # TO DO... Add the rest of the logic
377
+ # Delete these lines: Added to shut up sonarqube
378
+ logger.debug(f"Custom Fields Map: {custom_fields_system_map}, User Map: {user_map}")
379
+ logger.debug(f"SSP Map: {ssp_map}, Plans: {plans}")
380
+
381
+
382
+ def import_csam_privacy_info(import_ids: Optional[List[int]] = None):
383
+ """
384
+ Import the Privacy Info from CSAM
385
+ Into RegScale
386
+ """
387
+ custom_fields_privacy_list = ["PIA Date", "PTA Date", "SORN Date", "SORN Id"]
388
+
389
+ # Check for custom fields
390
+ custom_fields_privacy_map = FormFieldValue.check_custom_fields(
391
+ custom_fields_privacy_list, "securityplans", SSP_PRIVACY_TAB
392
+ )
393
+
394
+ # Grab the data from CSAM
395
+ app = Application()
396
+ csam_token = app.config.get("csamToken")
397
+ csam_url = app.config.get("csamURL")
398
+
399
+ # Get existing ssps by CSAM Id
400
+ custom_fields_basic_map = FormFieldValue.check_custom_fields([CSAM_FIELD_NAME], "securityplans", SSP_BASIC_TAB)
401
+ ssp_map = retrieve_custom_form_ssp_map(
402
+ tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
403
+ )
404
+
405
+ plans = import_ids if import_ids else list(ssp_map.keys())
406
+
407
+ for regscale_ssp_id in plans:
408
+ system_id = ssp_map.get(regscale_ssp_id)
409
+
410
+ # Get Privacy Status
411
+ privacy_status = retrieve_from_csam(
412
+ csam_token=csam_token,
413
+ csam_url=csam_url,
414
+ csam_endpoint=f"/CSAM/api/v1/systems/{system_id}/privacy",
415
+ )
416
+ pia_date = privacy_status.get("privacyImpactAssessmentDateCompleted")
417
+ pta_date = privacy_status.get("privacyThresholdAnalysisDateCompleted")
418
+
419
+ # Get SORN Status
420
+ sorn_statuses = retrieve_from_csam(
421
+ csam_token=csam_token,
422
+ csam_url=csam_url,
423
+ csam_endpoint=f"/CSAM/api/v1/systems/{system_id}/sorn",
424
+ )
425
+ sorn_date = 0
426
+ sorn_id = ""
427
+ for sorn_status in sorn_statuses:
428
+ if date_obj(sorn_status.get("publishedDate")) > date_obj(sorn_date):
429
+ sorn_date = sorn_status.get("publishedDate")
430
+ sorn_id = sorn_status.get("systemOfRecordsNoticeId").strip()
431
+
432
+ # Set the records
433
+ record = {"pia_date": pia_date, "pta_date": pta_date, "sorn_date": sorn_date, "sorn_id": sorn_id}
434
+ save_privacy_records(regscale_id=regscale_ssp_id, custom_fields_map=custom_fields_privacy_map, record=record)
435
+
436
+
437
+ def save_privacy_records(regscale_id: int, custom_fields_map: dict, record: dict):
438
+ privacy_fields = []
439
+ if record.get("pia_date"):
440
+ privacy_fields.append(
441
+ {
442
+ "record_id": regscale_id,
443
+ "form_field_id": custom_fields_map["PIA Date"],
444
+ "field_value": format_to_regscale_iso(record.get("pia_date")),
445
+ }
446
+ )
447
+ if record.get("pta_date"):
448
+ privacy_fields.append(
449
+ {
450
+ "record_id": regscale_id,
451
+ "form_field_id": custom_fields_map["PTA Date"],
452
+ "field_value": format_to_regscale_iso(record.get("pta_date")),
453
+ }
454
+ )
455
+ if record.get("sorn_date"):
456
+ privacy_fields.append(
457
+ {
458
+ "record_id": regscale_id,
459
+ "form_field_id": custom_fields_map["SORN Date"],
460
+ "field_value": format_to_regscale_iso(record.get("sorn_date")),
461
+ }
462
+ )
463
+ if record.get("sorn_id"):
464
+ privacy_fields.append(
465
+ {
466
+ "record_id": regscale_id,
467
+ "form_field_id": custom_fields_map["SORN Id"],
468
+ "field_value": record.get("sorn_id"),
469
+ }
470
+ )
471
+ if len(privacy_fields) > 0:
472
+ FormFieldValue.save_custom_fields(privacy_fields)
473
+
474
+
475
+ def import_csam_status():
476
+ """
477
+ Import the Status Info from CSAM
478
+ Into RegScale
479
+ """
480
+ # TO DO... Add the rest of the logic
481
+ pass
482
+
483
+
484
+ def import_csam_inheritance(import_ids: Optional[List[int]] = None):
485
+ """
486
+ Import control inheritance from CSAM
487
+
488
+ :param list import_ids: List of SSPs to import
489
+ :return: None
490
+ """
491
+
492
+ # Get list of existing SSPs in RegScale
493
+ existing_ssps = SecurityPlan.get_ssp_list()
494
+ ssp_map = {ssp["title"]: ssp["id"] for ssp in existing_ssps}
495
+
496
+ if not import_ids:
497
+ import_ids = [ssp["id"] for ssp in existing_ssps]
498
+
499
+ # Get Inheritance data from CSAM
500
+ app = Application()
501
+ for index in track(
502
+ range(len(import_ids)),
503
+ description=f"Importing inheritance for {len(import_ids)} Systems...",
504
+ ):
505
+ ssp = SecurityPlan.get_object(object_id=import_ids[index])
506
+ linked_ssps = []
507
+ # Get the inheritance data from CSAM
508
+
509
+ inheritances = retrieve_from_csam(
510
+ csam_url=app.config.get("csamURL"),
511
+ csam_token=app.config.get("csamToken"),
512
+ csam_endpoint=f"/CSAM/api/v1/systems/{ssp.otherIdentifier}/inheritedcontrols",
513
+ )
514
+ if not inheritances:
515
+ logger.debug(f"No inheritance data found for SSP {ssp.systemName} (ID: {ssp.id})")
516
+ continue
517
+ # Process each inheritance record
518
+ imp_map = ControlImplementation.get_control_label_map_by_plan(plan_id=ssp.id)
519
+
520
+ process_inheritances(
521
+ inheritances=inheritances, ssp=ssp, ssp_map=ssp_map, imp_map=imp_map, linked_ssps=linked_ssps
522
+ )
523
+
524
+
525
+ def retrieve_from_csam(csam_url: str, csam_token: str, csam_endpoint: str) -> list:
526
+ """
527
+ Connect to CSAM and retrieve data
528
+
529
+ :param str csam_url: URL of CSAM System
530
+ :param str csam_token: Bearer Token
531
+ :param str csam_endpoint: API Endpoint
532
+ :return: List of dict objects
533
+ :return_type: list
534
+ """
535
+ logger.debug("Retrieving data from CSAM")
536
+ reg_api = Api()
537
+ if "Bearer" not in csam_token:
538
+ csam_token = f"Bearer {csam_token}"
539
+
540
+ url = urljoin(csam_url, csam_endpoint)
541
+ headers = {
542
+ "Content-Type": "application/json",
543
+ "Accept": "application/json",
544
+ "Authorization": csam_token,
545
+ }
546
+
547
+ issue_response = reg_api.get(url=url, headers=headers)
548
+ if not issue_response or issue_response.status_code in [204, 404]:
549
+ logger.warning(f"Call to {url} Returned error: {issue_response.text}")
550
+ return []
551
+ if issue_response and issue_response.ok:
552
+ return issue_response.json()
553
+
554
+ return []
555
+
556
+
557
+ def retrieve_ssps_custom_form_map(tab_name: str, field_form_id: int) -> dict:
558
+ """
559
+ Retreives a list of the SSPs in RegScale
560
+ Returns a map of Custom Field Value: RegScale Id
561
+
562
+ :param str tab_name: The RegScale tab name where the custom field is located
563
+ :param int field_form_id: The RegScale Form Id of custom field
564
+ :param int tab_id: The RegScale tab id
565
+ :return: dictionary of FieldForm Id: regscale_ssp_id
566
+ :return_type: dict
567
+ """
568
+ tab = Module.get_tab_by_name(regscale_module_name="securityplans", regscale_tab_name=tab_name)
569
+
570
+ field_form_map = {}
571
+ ssps = SecurityPlan.get_ssp_list()
572
+ form_values = []
573
+ for ssp in ssps:
574
+ form_values = FormFieldValue.get_field_values(
575
+ record_id=ssp["id"], module_name=SecurityPlan.get_module_slug(), form_id=tab.id
576
+ )
577
+
578
+ for form in form_values:
579
+ if form.formFieldId == field_form_id and form.data:
580
+ field_form_map[form.data] = ssp["id"]
581
+ form_values = []
582
+ return field_form_map
583
+
584
+
585
+ def retrieve_custom_form_ssp_map(tab_name: str, field_form_id: int) -> dict:
586
+ """
587
+ Retreives a list of the SSPs in RegScale
588
+ Returns a map of RegScale ID: Custom Field Value
589
+
590
+ :param str tab_name: The RegScale tab name where the custom field is located
591
+ :param int field_form_id: The RegScale Form Id of custom field
592
+ :param int tab_id: The RegScale tab id
593
+ :return: dictionary of FieldForm Id: regscale_ssp_id
594
+ :return_type: dict
595
+ """
596
+ tab = Module.get_tab_by_name(regscale_module_name="securityplans", regscale_tab_name=tab_name)
597
+
598
+ field_form_map = {}
599
+ ssps = SecurityPlan.get_ssp_list()
600
+ form_values = []
601
+ for ssp in ssps:
602
+ form_values = FormFieldValue.get_field_values(
603
+ record_id=ssp["id"], module_name=SecurityPlan.get_module_slug(), form_id=tab.id
604
+ )
605
+ for form in form_values:
606
+ if form.formFieldId == field_form_id and form.data:
607
+ field_form_map[ssp["id"]] = form.data
608
+ form_values = []
609
+ return field_form_map
610
+
611
+
612
+ def update_ssp_general(ssp: SecurityPlan, record: dict, org_map: dict) -> SecurityPlan:
613
+ """
614
+ Update or Create the SSP Record
615
+ Based upon the values in Record
616
+
617
+ :param SecurityPlan ssp: RegScale Security Plan
618
+ :param dict record: record of values
619
+ :param dict org_map: map of org names to orgId
620
+ :return: SecurityPlan Object
621
+ :return_type: SecurityPlan
622
+ """
623
+
624
+ ssp.otherIdentifier = record["id"]
625
+ ssp.overallCategorization = record["categorization"]
626
+ ssp.confidentiality = record["categorization"]
627
+ ssp.integrity = record["categorization"]
628
+ ssp.availability = record["categorization"]
629
+ ssp.status = record["operationalStatus"]
630
+ ssp.systemType = record["systemType"]
631
+ ssp.description = record["purpose"]
632
+ if record["organization"] and org_map.get(record["organization"]):
633
+ ssp.orgId = org_map.get(record["organization"])
634
+
635
+ if ssp.id == 0:
636
+ new_ssp = ssp.create()
637
+ else:
638
+ new_ssp = ssp.save()
639
+
640
+ return new_ssp
641
+
642
+
643
+ def save_ssp_front_matter(
644
+ results: list, ssp_map: dict, custom_fields_basic_map: dict, custom_fields_financial_map: dict, org_map: dict
645
+ ) -> list:
646
+ """
647
+ Save the SSP data from the /systems endpoint
648
+
649
+ :param list results: list of results from CSAM
650
+ :param dict ssp_map: map of existing SSPs in RegScale
651
+ :param dict custom_fields_basic_map: map of custom fields in RegScale
652
+ :param dict custom_fields_financial_map: map of custom fields in RegScale
653
+ :param dict org_map: map of existing orgs in RegScale
654
+ :return: list of updated SSPs
655
+ :return_type: List[SecurityPlan]
656
+ """
657
+ updated_ssps = []
658
+ for index in track(
659
+ range(len(results)),
660
+ description=f"Importing {len(results)} SSP front matter...",
661
+ ):
662
+ result = results[index]
663
+
664
+ # Get the existing SSP:
665
+ ssp_id = ssp_map.get(result["externalId"])
666
+ if ssp_id:
667
+ ssp = SecurityPlan.get_object(ssp_id)
668
+ else:
669
+ ssp = SecurityPlan(systemName=result["name"])
670
+ # Update the SSP
671
+ ssp = update_ssp_general(ssp, result, org_map)
672
+
673
+ # Grab the Custom Fields
674
+ field_values = set_front_matter_fields(
675
+ ssp=ssp,
676
+ result=result,
677
+ custom_fields_basic_map=custom_fields_basic_map,
678
+ custom_fields_financial_map=custom_fields_financial_map,
679
+ )
680
+
681
+ # System Custom Fields
682
+ FormFieldValue.save_custom_fields(field_values)
683
+ updated_ssps.append(ssp)
684
+ logger.info(f"Updated {len(results)} Security Plans Front Matter")
685
+ return updated_ssps
686
+
687
+
688
+ def update_ssp_agency_details(ssps: list, csam_token: str, csam_url: str, custom_fields_basic_map: dict) -> list:
689
+ """
690
+ Update the Agency Details of the SSPs
691
+ This requires a call to the /system/{id}/agencydefineddataitems
692
+ endpoint
693
+
694
+ :param list ssps: list of RegScale SSPs
695
+ :param str csam_token: CSAM Bearer Token
696
+ :param str csam_url: CSAM URL
697
+ :param dict custom_fields_basic_map: map of custom fields in RegScale
698
+ :return: list of updated SSPs
699
+ :return_type: List[SecurityPlan]
700
+ """
701
+ updated_ssps = []
702
+ if len(ssps) == 0:
703
+ return updated_ssps
704
+ for index in track(
705
+ range(len(ssps)),
706
+ description=f"Importing {len(ssps)} SSP agency details...",
707
+ ):
708
+ ssp = ssps[index]
709
+ csam_id = ssp.otherIdentifier
710
+ if not csam_id:
711
+ logger.error(f"Could not find CSAM ID for SSP {ssp.systemName} id: {ssp.id}")
712
+ continue
713
+ else:
714
+ updated_ssps.append(ssp)
715
+
716
+ result = retrieve_from_csam(
717
+ csam_token=csam_token,
718
+ csam_url=csam_url,
719
+ csam_endpoint=f"/CSAM/api/v1/systems/{csam_id}/agencydefineddataitems",
720
+ )
721
+ if len(result) == 0:
722
+ logger.error(
723
+ f"Could not retrieve details for CSAM ID {csam_id}. RegScale SSP: Name: {ssp.systemName} id: {ssp.id}"
724
+ )
725
+ continue
726
+ # Get the custom fields
727
+ set_agency_details(result, ssp, custom_fields_basic_map)
728
+
729
+ logger.info(f"Updated {len(updated_ssps)} Security Plans with Agency Details")
730
+ return updated_ssps
731
+
732
+
733
+ def set_agency_details(result: list, ssp: SecurityPlan, custom_fields_basic_map: dict):
734
+ """
735
+ Loop through results of agencydefineddataitems
736
+ and set the custom fields in RegScale
737
+
738
+ :param list result: list of dict objects from CSAM
739
+ :param SecurityPlan ssp: RegScale Security Plan
740
+ :param dict custom_fields_basic_map: map of custom field names to ids
741
+ """
742
+ field_values = []
743
+ # Update the fields we need
744
+ for item in result:
745
+ if item.get("attributeName") == "High Value Asset":
746
+ ssp.hva = True if item.get("value") == "1" else False
747
+
748
+ # Binary Values
749
+ if item.get("attributeName") in [
750
+ "External Web Interface",
751
+ "CFO Designation",
752
+ "Law Enforcement Sensitive",
753
+ "AI/ML Components",
754
+ ]:
755
+ field_values.append(set_binary_fields(item, ssp, custom_fields_basic_map))
756
+
757
+ if item.get("attributeName") == "Cloud System":
758
+ ssp = set_cloud_system(ssp, item)
759
+
760
+ if item.get("attributeName") == "Cloud Service Model":
761
+ ssp = set_cloud_service(ssp, item)
762
+
763
+ if item.get("attributeName") == "HVA Identifier":
764
+ field_values.append(set_custom_fields(item, ssp, custom_fields_basic_map))
765
+
766
+ # Save the SSP & Custom Fields
767
+ ssp.save()
768
+ if len(field_values) > 0:
769
+ FormFieldValue.save_custom_fields(field_values)
770
+
771
+
772
+ def set_front_matter_fields(
773
+ ssp: SecurityPlan, result: dict, custom_fields_basic_map: dict, custom_fields_financial_map: dict
774
+ ) -> list:
775
+ """
776
+ parse the front matter custom fields
777
+ and return a list of field values to be saved
778
+
779
+ :param SecurityPlan ssp: RegScale Security Plan object
780
+ :param dict result: response from CSAM
781
+ :param dict custom_fields_basic_map: map of basic custom fields
782
+ :param dict custom_fields_financial_map: map of financial custom fields
783
+ :return: list of dictionaries with field values
784
+ :return_type: list
785
+ """
786
+ field_values = []
787
+ # FISMA ID
788
+ field_values.append(
789
+ {
790
+ "record_id": ssp.id,
791
+ "form_field_id": custom_fields_basic_map[FISMA_FIELD_NAME],
792
+ "field_value": str(result["externalId"]),
793
+ }
794
+ )
795
+ # CSAM ID
796
+ field_values.append(
797
+ {
798
+ "record_id": ssp.id,
799
+ "form_field_id": custom_fields_basic_map[CSAM_FIELD_NAME],
800
+ "field_value": str(result["id"]),
801
+ }
802
+ )
803
+ # Basic Tab
804
+ for key in result.keys():
805
+ if key in custom_fields_basic_map.keys() and key not in [CSAM_FIELD_NAME, FISMA_FIELD_NAME]:
806
+ if isinstance(result.get(key), bool):
807
+ field_values.append(
808
+ {
809
+ "record_id": ssp.id,
810
+ "form_field_id": custom_fields_basic_map[key],
811
+ "field_value": "Yes" if result.get(key) else "No",
812
+ }
813
+ )
814
+ else:
815
+ field_values.append(
816
+ {
817
+ "record_id": ssp.id,
818
+ "form_field_id": custom_fields_basic_map[key],
819
+ "field_value": str(result.get(key)),
820
+ }
821
+ )
822
+
823
+ # Financial Info Tab
824
+ # custom fields and csam values match
825
+ for key in result.keys():
826
+ if key in custom_fields_financial_map.keys():
827
+ field_values.append(
828
+ {
829
+ "record_id": ssp.id,
830
+ "form_field_id": custom_fields_financial_map[key],
831
+ "field_value": str(result.get(key)),
832
+ }
833
+ )
834
+ return field_values
835
+
836
+
837
+ def set_cloud_system(ssp: SecurityPlan, item: dict) -> SecurityPlan:
838
+ """
839
+ Set the cloud system values in the SSP
840
+ :param SeucrityPlan ssp: RegScale Security Plan
841
+ :param dict item: record from CSAM
842
+ :return: SecurityPlan object with updated cloud system values
843
+ :return_type: SecurityPlan
844
+ """
845
+ ssp.bDeployPublic = True if item.get("value") == "Public" else False
846
+ ssp.bDeployPrivate = True if item.get("value") == "Private" else False
847
+ ssp.bDeployHybrid = True if item.get("value") == "Hybrid" else False
848
+ ssp.bDeployGov = True if item.get("value") == "GovCloud" else False
849
+ ssp.bDeployOther = True if item.get("value") == "Community" else False
850
+ if ssp.bDeployHybrid or ssp.bDeployOther:
851
+ ssp.deployOtherRemarks = "Hybrid or Community"
852
+
853
+ return ssp
854
+
855
+
856
+ def set_cloud_service(ssp: SecurityPlan, item: dict) -> SecurityPlan:
857
+ """
858
+ Set the cloud service model values in the SSP
859
+
860
+ :param SecurityPlan ssp: RegScale Security Plan
861
+ :param dict item: record from CSAM
862
+ :return: Updated SecurityPlan object
863
+ :return_type: SecurityPlan
864
+ """
865
+ ssp.bModelIaaS = True if "IaaS" in item.get("value") else False
866
+ ssp.bModelPaaS = True if "PaaS" in item.get("value") else False
867
+ ssp.bModelSaaS = True if "SaaS" in item.get("value") else False
868
+ return ssp
869
+
870
+
871
+ def set_binary_fields(item: dict, ssp: SecurityPlan, custom_fields_map: dict) -> dict:
872
+ return {
873
+ "record_id": ssp.id,
874
+ "form_field_id": custom_fields_map[item.get("attributeName")],
875
+ "field_value": "Yes" if (item.get("value")) == "1" else "No",
876
+ }
877
+
878
+
879
+ def set_custom_fields(item: dict, ssp: SecurityPlan, custom_fields_map: dict) -> dict:
880
+ """
881
+ Set the custom fields for the SSP
882
+
883
+ :param dict item: record from CSAM
884
+ :param SecurityPlan ssp: RegScale Security Plan
885
+ :param dict custom_fields_map: map of custom fields in RegScale
886
+ :return: dictionary of field values to be saved
887
+ :return_type: dict
888
+ """
889
+ return {
890
+ "record_id": ssp.id,
891
+ "form_field_id": custom_fields_map[item.get("attributeName")],
892
+ "field_value": str(item.get("value")),
893
+ }
894
+
895
+
896
+ def get_catalogs() -> Tuple[Optional[int], Optional[int]]:
897
+ """
898
+ Get the catalog ids for NIST SP 800-53 Rev 5 and Rev 4
899
+
900
+ :return: tuple of catalog ids
901
+ :return_type: Tuple[Optional[int], Optional[int]]
902
+ """
903
+ # Find the Catalogs
904
+ rev5_catalog = Catalog.find_by_guid("b0c40faa-fda4-4ed3-83df-368908d9e9b2") # NIST SP 800-53 Rev 5
905
+ rev5_catalog_id = rev5_catalog.id if rev5_catalog else None
906
+ rev4_catalog = Catalog.find_by_guid("02158108-e491-49de-b9a8-3cb1cb8197dd") # NIST SP 800-53 Rev 4
907
+ rev4_catalog_id = rev4_catalog.id if rev4_catalog else None
908
+
909
+ return rev5_catalog_id, rev4_catalog_id
910
+
911
+
912
+ def build_implementations(results: list, csam_id: str, regscale_id: int) -> list:
913
+ """
914
+ Build out the control implementations
915
+ from the results returned from CSAM
916
+
917
+ :param list results: records from CSAM
918
+ :param int csam_id: CSAM System Id
919
+ :param int regscale_id: RegScale SSP Id
920
+ :return: list of ControlImplementation objects
921
+ :return_type: list
922
+ """
923
+ existing_implementations = ControlImplementation.get_list_by_parent(
924
+ regscale_id=regscale_id, regscale_module="securityplans"
925
+ )
926
+ implementations_map = {normalize_controlid(impl["controlId"]): impl["id"] for impl in existing_implementations}
927
+ control_implementations = []
928
+ # Loop through the results and create or update the controls
929
+ for index in track(
930
+ range(len(results)),
931
+ description=f"Importing {len(results)} controls for system id: {csam_id}...",
932
+ ):
933
+ result = results[index]
934
+ # Debug
935
+ imp_id = (
936
+ implementations_map.get(normalize_controlid(result["controlId"]))
937
+ if normalize_controlid(result["controlId"]) in implementations_map
938
+ else 0
939
+ )
940
+
941
+ control_implementations.append(
942
+ ControlImplementation(
943
+ id=imp_id,
944
+ status=(
945
+ "Fully Implemented" if result["statedImplementationStatus"] == "Implemented" else "Not Implemented"
946
+ ), # Implemented
947
+ responsibility=(
948
+ result["applicability"]
949
+ if result["applicability"] in ["Hybrid", "Inherited"]
950
+ else "Provider (System Specific)"
951
+ ), # Hybrid, Applicable
952
+ controlSource="Baseline",
953
+ implementation=result["implementationStatement"],
954
+ controlID=result["controlID"],
955
+ parentId=result["securityPlanId"],
956
+ parentModule="securityplans",
957
+ )
958
+ )
959
+ return control_implementations
960
+
961
+
962
+ def retrieve_controls(csam_token: str, csam_url: str, csam_id: int, controls: list, regscale_id: int) -> list:
963
+ """
964
+ Takes a system id and list of controls
965
+ returns a list of implmentations for
966
+ that system id and framework
967
+
968
+ :param str csam_token: access token for CSAM
969
+ :param str csam_url: url for CSAM API
970
+ :param int system_id: CSAM system id
971
+ :param str framework: Framework name
972
+ :param list controls: list of possible controls
973
+ :param int regscale_id: RegScale SSP Id
974
+ :return: list of control implementations
975
+ :return_type: list
976
+ """
977
+ imps = []
978
+ # Loop through the controls and get the implementations
979
+ for index in track(
980
+ range(len(controls)),
981
+ description=f"Retrieving implementations for system id: {csam_id}...",
982
+ ):
983
+ control = controls[index]
984
+ implementations = retrieve_from_csam(
985
+ csam_token=csam_token,
986
+ csam_url=csam_url,
987
+ csam_endpoint=f"/CSAM/api/v1/systems/{csam_id}/controls/{control.controlId}",
988
+ )
989
+
990
+ if len(implementations) == 0:
991
+ logger.debug(f"No implementations found for control {control.controlId} in system id: {csam_id}")
992
+ continue
993
+
994
+ # Add the RegScale SSP Id and controlID to the implementation
995
+ for impl in implementations:
996
+ if "NotApplicable" in impl["applicability"]:
997
+ continue
998
+
999
+ impl["securityPlanId"] = regscale_id
1000
+ impl["controlID"] = control.id
1001
+ imps.append(impl)
1002
+ return imps
1003
+
1004
+
1005
+ def set_inheritable(regscale_id: int):
1006
+ """
1007
+ Given a RegScale SSP Id
1008
+ Sets the inheritable flag on all control implementations
1009
+
1010
+ :param int regscale_id: id of Security Plan
1011
+ :return: None
1012
+ """
1013
+
1014
+ # Get list of existing controlimplementations
1015
+ implementations = ControlImplementation.get_list_by_parent(regscale_id=regscale_id, regscale_module="securityplans")
1016
+
1017
+ for index in track(
1018
+ range(len(implementations)),
1019
+ description="Setting controls Inheritable...",
1020
+ ):
1021
+ implementation = implementations[index]
1022
+ imp = ControlImplementation.get_object(object_id=implementation["id"])
1023
+ imp.inheritable = True
1024
+ imp.save()
1025
+
1026
+
1027
+ def process_inheritances(
1028
+ inheritances: List[Dict[str, Any]],
1029
+ ssp: SecurityPlan,
1030
+ ssp_map: Dict[str, int],
1031
+ imp_map: Dict[str, int],
1032
+ linked_ssps: List[SecurityPlan],
1033
+ ):
1034
+ for inheritance in inheritances:
1035
+ # Check if the control exists in plan
1036
+ control_id = normalize_controlid(inheritance.get("controlId"))
1037
+ if control_id not in imp_map:
1038
+ logger.debug(f"Control {control_id} not found in RegScale for SSP {ssp.systemName} (ID: {ssp.id})")
1039
+ continue
1040
+
1041
+ # Find the baseControl in RegScale
1042
+ # Find the SSP
1043
+ base_ssp = ssp_map.get(inheritance.get("offeringSystemName"))
1044
+ if not base_ssp:
1045
+ logger.debug(f"Base SSP {inheritance.get('offeringSystemName')} not found in RegScale, skipping")
1046
+ continue
1047
+
1048
+ base_control_map = ControlImplementation.get_control_label_map_by_plan(plan_id=base_ssp)
1049
+ base_control_id = base_control_map.get(normalize_controlid(inheritance.get("controlId")))
1050
+
1051
+ # Create or update the inheritance record
1052
+ if inheritance.get("isInherited") is False:
1053
+ continue
1054
+
1055
+ # Add the parent if not already linked
1056
+ if base_ssp not in linked_ssps:
1057
+ linked_ssps.append(base_ssp)
1058
+
1059
+ # Create the records
1060
+ create_inheritance(
1061
+ parent_id=ssp.id,
1062
+ parent_module="securityplans",
1063
+ hybrid=inheritance.get("isHybrid", True),
1064
+ base_id=base_ssp,
1065
+ control_id=imp_map[control_id],
1066
+ base_control_id=base_control_id,
1067
+ )
1068
+
1069
+ # Create the Inheritance Record(s)
1070
+ for inheritance_ssp in linked_ssps:
1071
+ create_inheritance_linage(
1072
+ parent_id=ssp.id,
1073
+ parent_module="securityplans",
1074
+ base_id=inheritance_ssp,
1075
+ )
1076
+
1077
+
1078
+ def create_inheritance(
1079
+ parent_id: int, parent_module: str, base_id: int, hybrid: bool, control_id: int, base_control_id: int
1080
+ ):
1081
+ """
1082
+ Creates the records for inheritance
1083
+
1084
+ :param int parent_id: Id of inheriting record
1085
+ :param str parent_module: Module of inheriting record
1086
+ :param int base_id: Id of inherited record
1087
+ :param bool hybrid: Is the control hybrid
1088
+ :param int control_id: Id of inheriting control
1089
+ :param int base_control_id: Id of inherited control
1090
+ :return: None
1091
+ """
1092
+
1093
+ # Update the control implementation
1094
+ control_impl = ControlImplementation.get_object(object_id=control_id)
1095
+ if control_impl:
1096
+ control_impl.bInherited = True
1097
+ control_impl.responsibility = "Hybrid" if hybrid else "Inherited"
1098
+ control_impl.inheritedControlId = base_control_id
1099
+ control_impl.inheritedSecurityPlanId = base_id
1100
+ control_impl.save()
1101
+
1102
+ # Check if the Inherited Control already exists
1103
+ existing = InheritedControl.get_all_by_control(control_id=control_id)
1104
+ for exists in existing:
1105
+ if exists["inheritedControlId"] == base_control_id:
1106
+ return
1107
+
1108
+ InheritedControl(
1109
+ parentId=parent_id, parentModule=parent_module, baseControlId=control_id, inheritedControlId=base_control_id
1110
+ ).create()
1111
+
1112
+
1113
+ def create_inheritance_linage(parent_id: int, parent_module: str, base_id: int):
1114
+ """
1115
+ Creates a RegScale Inheritance Record
1116
+
1117
+ :param int parent_id: Id of inheriting record
1118
+ :param str parent_module: Module of inheriting record
1119
+ :param int base_control_id: Id of inherited control
1120
+ :return: None
1121
+ """
1122
+ # Check if the Inheritance already exists
1123
+ existing = Inheritance.get_all_by_parent(parent_id=parent_id, parent_module=parent_module)
1124
+ for exists in existing:
1125
+ if exists.planId == base_id:
1126
+ return
1127
+
1128
+ # Update Lineage (no way to update.. only create)
1129
+ Inheritance(recordId=parent_id, recordModule=parent_module, planId=base_id).create()