regscale-cli 6.27.2.0__py3-none-any.whl → 6.27.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.

Files changed (40) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/application.py +1 -0
  3. regscale/core/app/internal/control_editor.py +73 -21
  4. regscale/core/app/internal/login.py +4 -1
  5. regscale/core/app/internal/model_editor.py +219 -64
  6. regscale/core/login.py +21 -4
  7. regscale/core/utils/date.py +77 -1
  8. regscale/integrations/commercial/aws/scanner.py +4 -1
  9. regscale/integrations/commercial/synqly/query_builder.py +4 -1
  10. regscale/integrations/control_matcher.py +78 -23
  11. regscale/integrations/public/csam/csam.py +572 -763
  12. regscale/integrations/public/csam/csam_agency_defined.py +179 -0
  13. regscale/integrations/public/csam/csam_common.py +154 -0
  14. regscale/integrations/public/csam/csam_controls.py +432 -0
  15. regscale/integrations/public/csam/csam_poam.py +124 -0
  16. regscale/integrations/public/fedramp/click.py +17 -4
  17. regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
  18. regscale/integrations/public/fedramp/poam/scanner.py +74 -7
  19. regscale/integrations/scanner_integration.py +16 -1
  20. regscale/models/integration_models/cisa_kev_data.json +49 -19
  21. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  22. regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
  23. regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
  24. regscale/models/platform.py +3 -0
  25. regscale/models/regscale_models/__init__.py +5 -0
  26. regscale/models/regscale_models/component.py +1 -1
  27. regscale/models/regscale_models/control_implementation.py +55 -24
  28. regscale/models/regscale_models/organization.py +3 -0
  29. regscale/models/regscale_models/regscale_model.py +17 -5
  30. regscale/models/regscale_models/security_plan.py +1 -0
  31. regscale/regscale.py +11 -1
  32. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
  33. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +40 -36
  34. tests/regscale/core/test_login.py +171 -4
  35. tests/regscale/integrations/test_control_matcher.py +24 -0
  36. tests/regscale/models/test_control_implementation.py +118 -3
  37. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
  38. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
  39. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
  40. {regscale_cli-6.27.2.0.dist-info → regscale_cli-6.27.3.0.dist-info}/top_level.txt +0 -0
@@ -4,8 +4,7 @@
4
4
 
5
5
  # standard python imports
6
6
  import logging
7
- from typing import List, Optional, Tuple, Any, Dict
8
- from urllib.parse import urljoin
7
+ from typing import List, Optional
9
8
  from rich.progress import track
10
9
  import click
11
10
  from rich.console import Console
@@ -13,22 +12,31 @@ from regscale.core.app.api import Api
13
12
  from regscale.core.app.application import Application
14
13
  from regscale.core.utils.date import format_to_regscale_iso, date_obj
15
14
  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
15
  from regscale.models.regscale_models import (
18
- Catalog,
19
- ControlImplementation,
20
- InheritedControl,
21
- Inheritance,
22
- Issue,
23
16
  Organization,
24
- SecurityControl,
25
17
  SecurityPlan,
26
18
  User,
27
19
  )
28
- from regscale.models.regscale_models.regscale_model import RegScaleModel
29
20
  from regscale.models.regscale_models.module import Module
30
21
  from regscale.models.regscale_models.form_field_value import FormFieldValue
31
- from regscale.core.app.utils.regscale_utils import normalize_controlid
22
+ from regscale.integrations.public.csam.csam_poam import import_csam_poams
23
+ from regscale.integrations.public.csam.csam_controls import (
24
+ import_csam_controls,
25
+ set_inheritable,
26
+ import_csam_inheritance,
27
+ )
28
+ from regscale.integrations.public.csam.csam_agency_defined import update_ssp_agency_details
29
+ from regscale.integrations.public.csam.csam_common import (
30
+ retrieve_ssps_custom_form_map,
31
+ retrieve_custom_form_ssps_map,
32
+ retrieve_from_csam,
33
+ set_custom_fields,
34
+ fix_form_field_value,
35
+ CSAM_FIELD_NAME,
36
+ FISMA_FIELD_NAME,
37
+ SSP_BASIC_TAB,
38
+ )
39
+
32
40
 
33
41
  logger = logging.getLogger("regscale")
34
42
  console = Console()
@@ -40,14 +48,30 @@ console = Console()
40
48
  #
41
49
  ####################################################################################################
42
50
 
43
- SSP_BASIC_TAB = "Basic Info"
51
+
44
52
  SSP_SYSTEM_TAB = "System Information"
45
53
  SSP_FINANCIAL_TAB = "Financial Info"
46
54
  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"
55
+ SSP_CONTINGENCY_TAB = "Continuity and Incident Response"
56
+
57
+ CUSTOM_FIELDS_BASIC_LIST = [
58
+ "acronym",
59
+ "Classification",
60
+ "FISMA Reportable",
61
+ "Contractor System",
62
+ "Authorization Process",
63
+ "ATO Date",
64
+ "ATO Status",
65
+ "Critical Infrastructure",
66
+ "Mission Essential",
67
+ "uiiCode",
68
+ "HVA Identifier",
69
+ "External Web Interface",
70
+ "CFO Designation",
71
+ "Law Enforcement Sensitive",
72
+ CSAM_FIELD_NAME,
73
+ FISMA_FIELD_NAME,
74
+ ]
51
75
 
52
76
 
53
77
  @click.group()
@@ -77,48 +101,19 @@ def import_poam():
77
101
 
78
102
  def import_csam_ssp():
79
103
  """
80
- Import an SSP from CSAM
104
+ Import SSPs from CSAM
81
105
  Into RegScale
106
+ According to a filter in init.yaml
82
107
  """
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
108
 
109
+ logger.info("Gathering reference info...")
112
110
  # Check Custom Fields exist
113
111
  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
112
+ CUSTOM_FIELDS_BASIC_LIST, "securityplans", SSP_BASIC_TAB
118
113
  )
119
114
 
120
115
  # Get a map of existing custom forms
121
- ssp_map = retrieve_ssps_custom_form_map(
116
+ ssp_map = retrieve_custom_form_ssps_map(
122
117
  tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[FISMA_FIELD_NAME]
123
118
  )
124
119
 
@@ -128,16 +123,18 @@ def import_csam_ssp():
128
123
 
129
124
  # Grab the data from CSAM
130
125
  app = Application()
131
- csam_token = app.config.get("csamToken")
132
- csam_url = app.config.get("csamURL")
133
126
  csam_filter = app.config.get("csamFilter", None)
134
127
 
128
+ logger.info("Retrieving systems from CSAM...")
135
129
  results = retrieve_from_csam(
136
- csam_token=csam_token,
137
- csam_url=csam_url,
138
130
  csam_endpoint="/CSAM/api/v1/systems",
139
131
  )
140
132
 
133
+ if not results:
134
+ error_and_exit("Failure to retrieve plans from CSAM")
135
+ else:
136
+ logger.info("Retrieved plans from CSAM, parsing results...")
137
+
141
138
  results = filter_list(results, csam_filter)
142
139
  if not results:
143
140
  error_and_exit(
@@ -145,22 +142,28 @@ def import_csam_ssp():
145
142
  Please check your CSAM configuration."
146
143
  )
147
144
 
145
+ logger.info("Importing systems... ")
148
146
  # Parse the results
149
147
  updated_ssps = []
150
148
  updated_ssps = save_ssp_front_matter(
151
149
  results=results,
152
150
  ssp_map=ssp_map,
153
151
  custom_fields_basic_map=custom_fields_basic_map,
154
- custom_fields_financial_map=custom_fields_financial_map,
155
152
  org_map=org_map,
156
153
  )
157
154
 
158
155
  # 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)
156
+ update_ssp_agency_details(updated_ssps, custom_fields_basic_map)
157
+
158
+ # Import the authorization process and status
159
+ import_csam_authorization(import_ids=[ssp.id for ssp in updated_ssps])
160
160
 
161
161
  # Import the Privacy date
162
162
  import_csam_privacy_info(import_ids=[ssp.id for ssp in updated_ssps])
163
163
 
164
+ # Import the Contingency & IR data
165
+ import_csam_contingency(import_ids=[ssp.id for ssp in updated_ssps])
166
+
164
167
  # Import the controls
165
168
  import_csam_controls(import_ids=[ssp.id for ssp in updated_ssps])
166
169
 
@@ -177,7 +180,7 @@ def import_csam_ssp():
177
180
  continue
178
181
 
179
182
  # Set the inheritable flag
180
- set_inheritable(regscale_id=result.get("id"))
183
+ set_inheritable(regscale_id=program_id)
181
184
 
182
185
  # Import the Inheritance
183
186
  import_csam_inheritance(import_ids=[ssp.id for ssp in updated_ssps])
@@ -186,164 +189,59 @@ def import_csam_ssp():
186
189
  import_csam_pocs(import_ids=[ssp.id for ssp in updated_ssps])
187
190
 
188
191
 
189
- def import_csam_controls(import_ids: Optional[List[int]] = None):
190
- """
191
- Import Controls from CSAM
192
+ def sync_csam_ssps():
192
193
 
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
194
+ logger.info("Gathering reference info...")
195
+ # Check Custom Fields exist
265
196
  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
197
+ CUSTOM_FIELDS_BASIC_LIST, "securityplans", SSP_BASIC_TAB
267
198
  )
268
199
 
269
- # Get the SSPs
270
- ssp_map = retrieve_ssps_custom_form_map(
200
+ # Get a map of existing custom forms
201
+ ssp_map = retrieve_custom_form_ssps_map(
202
+ tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[FISMA_FIELD_NAME]
203
+ )
204
+ csam_map = retrieve_custom_form_ssps_map(
271
205
  tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
272
206
  )
273
207
 
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}
208
+ # Get a list of orgs and create a map to id
209
+ orgs = Organization.get_list()
210
+ org_map = {org.name: org.id for org in orgs}
277
211
 
278
- # Grab the data from CSAM
279
- app = Application()
280
- csam_token = app.config.get("csamToken")
281
- csam_url = app.config.get("csamURL")
212
+ csam_list = []
213
+ for id in csam_map.keys():
214
+ csam_list.append(int(id))
215
+ csam_filter = {"id": csam_list}
216
+
217
+ logger.info("Retrieving systems from CSAM...")
282
218
  results = retrieve_from_csam(
283
- csam_token=csam_token, csam_url=csam_url, csam_endpoint="/CSAM/api/v1/reports/POAM_Details_Report_CBP"
219
+ csam_endpoint="/CSAM/api/v1/systems",
284
220
  )
285
221
 
222
+ results = filter_list(results, csam_filter)
223
+
224
+ logger.info("Syncing systems... ")
286
225
  # 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]
226
+ updated_ssps = []
227
+ updated_ssps = save_ssp_front_matter(
228
+ results=results,
229
+ ssp_map=ssp_map,
230
+ custom_fields_basic_map=custom_fields_basic_map,
231
+ org_map=org_map,
232
+ )
293
233
 
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
234
+ # Now have to get the system details for each system
235
+ update_ssp_agency_details(updated_ssps, custom_fields_basic_map)
302
236
 
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"]
237
+ # Import the authorization process and status
238
+ import_csam_authorization(import_ids=[ssp.id for ssp in updated_ssps])
334
239
 
335
- poam_list.append(new_issue)
240
+ # Import the Privacy date
241
+ import_csam_privacy_info(import_ids=[ssp.id for ssp in updated_ssps])
336
242
 
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")
243
+ # Import the Contingency & IR data
244
+ import_csam_contingency(import_ids=[ssp.id for ssp in updated_ssps])
347
245
 
348
246
 
349
247
  def import_csam_pocs(import_ids: Optional[List[int]] = None):
@@ -351,19 +249,19 @@ def import_csam_pocs(import_ids: Optional[List[int]] = None):
351
249
  Import the Points of Contact from CSAM
352
250
  Into RegScale
353
251
  """
354
- custom_fields_system_list = [
252
+ custom_fields_pocs_list = [
355
253
  "Certifying Official",
356
254
  "Alternate Information System Security Manager",
357
255
  "Alternate Information System Security Officer",
358
256
  ]
359
257
  # Check Custom Fields exist
360
- custom_fields_system_map = FormFieldValue.check_custom_fields(
361
- custom_fields_system_list, "securityplans", SSP_SYSTEM_TAB
258
+ custom_fields_pocs_map = FormFieldValue.check_custom_fields(
259
+ custom_fields_pocs_list, "securityplans", "Points of Contact"
362
260
  )
363
261
 
364
262
  # Get existing ssps by CSAM Id
365
263
  custom_fields_basic_map = FormFieldValue.check_custom_fields([CSAM_FIELD_NAME], "securityplans", SSP_BASIC_TAB)
366
- ssp_map = retrieve_custom_form_ssp_map(
264
+ ssp_map = retrieve_ssps_custom_form_map(
367
265
  tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
368
266
  )
369
267
 
@@ -375,7 +273,7 @@ def import_csam_pocs(import_ids: Optional[List[int]] = None):
375
273
 
376
274
  # TO DO... Add the rest of the logic
377
275
  # Delete these lines: Added to shut up sonarqube
378
- logger.debug(f"Custom Fields Map: {custom_fields_system_map}, User Map: {user_map}")
276
+ logger.debug(f"Custom Fields Map: {custom_fields_pocs_map}, User Map: {user_map}")
379
277
  logger.debug(f"SSP Map: {ssp_map}, Plans: {plans}")
380
278
 
381
279
 
@@ -384,62 +282,72 @@ def import_csam_privacy_info(import_ids: Optional[List[int]] = None):
384
282
  Import the Privacy Info from CSAM
385
283
  Into RegScale
386
284
  """
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
285
 
399
286
  # Get existing ssps by CSAM Id
400
287
  custom_fields_basic_map = FormFieldValue.check_custom_fields([CSAM_FIELD_NAME], "securityplans", SSP_BASIC_TAB)
401
- ssp_map = retrieve_custom_form_ssp_map(
288
+ ssp_map = retrieve_ssps_custom_form_map(
402
289
  tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
403
290
  )
404
291
 
405
- plans = import_ids if import_ids else list(ssp_map.keys())
292
+ ssps = import_ids if import_ids else list(ssp_map.keys())
406
293
 
407
- for regscale_ssp_id in plans:
408
- system_id = ssp_map.get(regscale_ssp_id)
294
+ updated_ssps = []
295
+ if len(ssps) == 0:
296
+ return
297
+ for index in track(
298
+ range(len(ssps)),
299
+ description=f"Importing {len(ssps)} SSP privacy...",
300
+ ):
301
+ ssp = ssps[index]
302
+ system_id = ssp_map.get(ssp)
303
+ if not system_id:
304
+ logger.error(f"Could not find CSAM ID for SSP id: {ssp}")
305
+ continue
306
+ else:
307
+ updated_ssps.append(ssp)
409
308
 
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")
309
+ result = retrieve_from_csam(csam_endpoint=f"/CSAM/api/v1/systems/{system_id}/privacy")
310
+ if len(result) == 0:
311
+ logger.error(f"Could not retrieve privacy for CSAM ID {system_id}. RegScale SSP id: {ssp}")
312
+ continue
313
+
314
+ pia_date = result.get("privacyImpactAssessmentDateCompleted")
315
+ pta_date = result.get("privacyThresholdAnalysisDateCompleted")
418
316
 
419
317
  # Get SORN Status
420
- sorn_statuses = retrieve_from_csam(
421
- csam_token=csam_token,
422
- csam_url=csam_url,
318
+ result = retrieve_from_csam(
423
319
  csam_endpoint=f"/CSAM/api/v1/systems/{system_id}/sorn",
424
320
  )
321
+ if len(result) == 0:
322
+ logger.debug(f"Could not retrieve SORN for CSAM ID {system_id}. RegScale SSP id: {ssp}")
323
+ continue
425
324
  sorn_date = 0
426
325
  sorn_id = ""
427
- for sorn_status in sorn_statuses:
326
+ for sorn_status in result:
428
327
  if date_obj(sorn_status.get("publishedDate")) > date_obj(sorn_date):
429
328
  sorn_date = sorn_status.get("publishedDate")
430
329
  sorn_id = sorn_status.get("systemOfRecordsNoticeId").strip()
431
330
 
432
331
  # Set the records
433
332
  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)
333
+ save_privacy_records(regscale_id=ssp, record=record)
334
+
335
+ logger.info(f"Updated {len(updated_ssps)} Security Plans with privacy data")
435
336
 
436
337
 
437
- def save_privacy_records(regscale_id: int, custom_fields_map: dict, record: dict):
338
+ def save_privacy_records(regscale_id: int, record: dict):
339
+
340
+ custom_fields_privacy_list = ["PIA Date", "PTA Date", "SORN Date", "SORN Id"]
341
+
342
+ # Check for custom fields
343
+ custom_fields_map = FormFieldValue.check_custom_fields(custom_fields_privacy_list, "securityplans", SSP_PRIVACY_TAB)
344
+
438
345
  privacy_fields = []
439
346
  if record.get("pia_date"):
440
347
  privacy_fields.append(
441
348
  {
442
349
  "record_id": regscale_id,
350
+ "record_module": "securityplans",
443
351
  "form_field_id": custom_fields_map["PIA Date"],
444
352
  "field_value": format_to_regscale_iso(record.get("pia_date")),
445
353
  }
@@ -448,6 +356,7 @@ def save_privacy_records(regscale_id: int, custom_fields_map: dict, record: dict
448
356
  privacy_fields.append(
449
357
  {
450
358
  "record_id": regscale_id,
359
+ "record_module": "securityplans",
451
360
  "form_field_id": custom_fields_map["PTA Date"],
452
361
  "field_value": format_to_regscale_iso(record.get("pta_date")),
453
362
  }
@@ -456,6 +365,7 @@ def save_privacy_records(regscale_id: int, custom_fields_map: dict, record: dict
456
365
  privacy_fields.append(
457
366
  {
458
367
  "record_id": regscale_id,
368
+ "record_module": "securityplans",
459
369
  "form_field_id": custom_fields_map["SORN Date"],
460
370
  "field_value": format_to_regscale_iso(record.get("sorn_date")),
461
371
  }
@@ -464,149 +374,368 @@ def save_privacy_records(regscale_id: int, custom_fields_map: dict, record: dict
464
374
  privacy_fields.append(
465
375
  {
466
376
  "record_id": regscale_id,
377
+ "record_module": "securityplans",
467
378
  "form_field_id": custom_fields_map["SORN Id"],
468
- "field_value": record.get("sorn_id"),
379
+ "field_value": str(record.get("sorn_id")),
469
380
  }
470
381
  )
471
382
  if len(privacy_fields) > 0:
383
+ privacy_fields = fix_form_field_value(privacy_fields)
472
384
  FormFieldValue.save_custom_fields(privacy_fields)
473
385
 
474
386
 
475
- def import_csam_status():
387
+ def import_csam_authorization(import_ids: Optional[List[int]] = None):
476
388
  """
477
- Import the Status Info from CSAM
478
- Into RegScale
389
+ Update the Authorization of the SSPs
390
+ This requires a call to the /system/{id}/securityauthorization
391
+ endpoint
392
+
393
+ :param list import_ids: Filtered list of SSPs
394
+ :return: None
479
395
  """
480
- # TO DO... Add the rest of the logic
481
- pass
396
+ # Get existing ssps by CSAM Id
397
+ custom_fields_basic_map = FormFieldValue.check_custom_fields([CSAM_FIELD_NAME], "securityplans", SSP_BASIC_TAB)
398
+ ssp_map = retrieve_ssps_custom_form_map(
399
+ tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
400
+ )
482
401
 
402
+ ssps = import_ids if import_ids else list(ssp_map.keys())
483
403
 
484
- def import_csam_inheritance(import_ids: Optional[List[int]] = None):
404
+ updated_ssps = []
405
+ field_values = []
406
+ if len(ssps) == 0:
407
+ return
408
+ for index in track(
409
+ range(len(ssps)),
410
+ description=f"Importing {len(ssps)} SSP authorization...",
411
+ ):
412
+ ssp = ssps[index]
413
+ csam_id = ssp_map.get(ssp)
414
+ if not csam_id:
415
+ logger.error(f"Could not find CSAM ID for SSP id: {ssp}")
416
+ continue
417
+ else:
418
+ updated_ssps.append(ssp)
419
+
420
+ result = retrieve_from_csam(
421
+ csam_endpoint=f"/CSAM/api/v1/systems/{csam_id}/securityauthorization",
422
+ )
423
+ if len(result) == 0:
424
+ logger.error(f"Could not retrieve details for CSAM ID {csam_id}. RegScale SSP id: {ssp}")
425
+ continue
426
+ # Set the authorization expiration date
427
+ ssp_obj = SecurityPlan.get_object(object_id=ssp)
428
+ if ssp_obj:
429
+ ssp_obj.authorizationTerminationDate = result.get("authorizationExpirationDate")
430
+ ssp_obj.save()
431
+ else:
432
+ logger.debug(f"Failed to retrieve Security Plan id: {ssp}")
433
+ # Get the custom fields
434
+ field_values.append(
435
+ {
436
+ "record_id": ssp,
437
+ "record_module": "securityplans",
438
+ "form_field_id": custom_fields_basic_map["Authorization Process"],
439
+ "field_value": str(result.get("authorizationProcess")),
440
+ }
441
+ )
442
+ field_values.append(
443
+ {
444
+ "record_id": ssp,
445
+ "record_module": "securityplans",
446
+ "form_field_id": custom_fields_basic_map["ATO Date"],
447
+ "field_value": str(result.get("lastAuthorizationDate")),
448
+ }
449
+ )
450
+ field_values.append(
451
+ {
452
+ "record_id": ssp,
453
+ "record_module": "securityplans",
454
+ "form_field_id": custom_fields_basic_map["ATO Status"],
455
+ "field_value": str(result.get("authorizationStatus")),
456
+ }
457
+ )
458
+ # Save the Custom Fields
459
+ if len(field_values) > 0:
460
+ field_values = fix_form_field_value(field_values)
461
+ FormFieldValue.save_custom_fields(field_values)
462
+
463
+ logger.info(f"Updated {len(updated_ssps)} Security Plans with authorization data")
464
+
465
+
466
+ def import_csam_contingency(import_ids: Optional[List[int]] = None):
485
467
  """
486
- Import control inheritance from CSAM
468
+ Update the Contingency & IR of the SSPs
469
+ This requires a call to the /systems/<system_id>/continuityresponse
470
+ endpoint
487
471
 
488
- :param list import_ids: List of SSPs to import
472
+ :param list import_ids: Filtered list of SSPs
489
473
  :return: None
490
474
  """
475
+ # Continuity & IR Fields
476
+ # Goes to "Continuity and Incident Response" Tab
477
+ # /CSAM/api/v1/systems/<system_id>/continuityresponse
478
+ continuity_map = {
479
+ "maximumTolerableDowntime": "MTD",
480
+ "recoveryTimeObjective": "RTO",
481
+ "recoveryPointObjective": "RPO",
482
+ "businessImpactAnalysisDateCompleted": "BIA Completed",
483
+ "businessImpactAnalysisNextDueDate": "BIA Next Due Date",
484
+ "contingencyPlanDateCompleted": "CP Completed",
485
+ "contingencyPlanNextDueDate": "CP Next Due Date",
486
+ "contingencyPlanTrainingDateCompleted": "CP Training Completed",
487
+ "contingencyPlanTrainingNextDueDate": "CP Training Next Due Date",
488
+ "contingencyPlanTestNextDueDate": "CP Test Next Due Date",
489
+ "incidentResponsePlanDateCompleted": "IRP Completed",
490
+ "incidentResponsePlanNextDueDate": "IRP Next Due Date",
491
+ "incidentResponsePlanTrainingDateCompleted": "IRP Training Completed",
492
+ "incidentResponsePlanTrainingNextDueDate": "IRP Training Next Due Date",
493
+ "incidentResponsePlanTestNextDueDate": "IRP Test Next Due Date",
494
+ }
491
495
 
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}
496
+ # /CSAM/api/v1/systems/<system_id>/continuitytest
497
+ # testItem == "Contingency Plan (CP)""
498
+ continuity_test_map = {"testType": "CP Test Type", "dateTested": "CP Date Tested", "outcome": "CP Test Outcome"}
499
+ # testItem == "Incident Response Plan (IRP)"
500
+ irp_test_map = {"testType": "IRP Test Type", "dateTested": "IRP Date Tested", "outcome": "IRP Test Outcome"}
501
+
502
+ # /CSAM/api/v1/systems/{system_id}/additionalstatus
503
+ # name == "Contingency Plan Review"
504
+ contingency_plan_map = {
505
+ "dateCompleted": "CPR Completed",
506
+ "nextDueDate": "CPR Next Due Date",
507
+ "expirationDate": "CPR Expiration Date",
508
+ }
509
+ continuity_ir_fields = []
510
+ continuity_ir_fields = continuity_ir_fields + list(continuity_map.values())
511
+ continuity_ir_fields = continuity_ir_fields + list(continuity_test_map.values())
512
+ continuity_ir_fields = continuity_ir_fields + list(irp_test_map.values())
513
+ continuity_ir_fields = continuity_ir_fields + list(contingency_plan_map.values())
514
+ continuity_ir_fields_map = FormFieldValue.check_custom_fields(
515
+ continuity_ir_fields, "securityplans", "Continuity and Incident Response"
516
+ )
495
517
 
496
- if not import_ids:
497
- import_ids = [ssp["id"] for ssp in existing_ssps]
518
+ # name == "Document Review Approval"
519
+ # goes to "Document Review" Tab
520
+ doc_approv_map = {
521
+ "dateCompleted": "Doc Review Completed",
522
+ "nextDueDate": "Doc Review Next Due Date",
523
+ "expirationDate": "Doc Review Expiration Date",
524
+ }
525
+ doc_approv_fields = list(doc_approv_map.values())
526
+ doc_approv_fields_map = FormFieldValue.check_custom_fields(doc_approv_fields, "securityplans", "Document Review")
498
527
 
499
- # Get Inheritance data from CSAM
500
- app = Application()
528
+ # Get existing ssps by CSAM Id
529
+ custom_fields_basic_map = FormFieldValue.check_custom_fields([CSAM_FIELD_NAME], "securityplans", SSP_BASIC_TAB)
530
+ ssp_map = retrieve_ssps_custom_form_map(
531
+ tab_name=SSP_BASIC_TAB, field_form_id=custom_fields_basic_map[CSAM_FIELD_NAME]
532
+ )
533
+
534
+ ssps = import_ids if import_ids else list(ssp_map.keys())
535
+
536
+ updated_ssps = []
537
+ field_values = []
538
+ if len(ssps) == 0:
539
+ return
501
540
  for index in track(
502
- range(len(import_ids)),
503
- description=f"Importing inheritance for {len(import_ids)} Systems...",
541
+ range(len(ssps)),
542
+ description=f"Importing {len(ssps)} SSP contingency data...",
504
543
  ):
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})")
544
+ ssp = ssps[index]
545
+ csam_id = ssp_map.get(ssp)
546
+ if not csam_id:
547
+ logger.error(f"Could not find CSAM ID for SSP id: {ssp}")
516
548
  continue
517
- # Process each inheritance record
518
- imp_map = ControlImplementation.get_control_label_map_by_plan(plan_id=ssp.id)
549
+ else:
550
+ updated_ssps.append(ssp)
519
551
 
520
- process_inheritances(
521
- inheritances=inheritances, ssp=ssp, ssp_map=ssp_map, imp_map=imp_map, linked_ssps=linked_ssps
552
+ continuity_response = get_continuity_response_fields(
553
+ ssp=ssp, csam_id=csam_id, continuity_ir_fields_map=continuity_ir_fields_map, continuity_map=continuity_map
522
554
  )
555
+ field_values = field_values + continuity_response
523
556
 
557
+ continuity_test = get_continuity_test_fields(
558
+ ssp=ssp,
559
+ csam_id=csam_id,
560
+ continuity_ir_fields_map=continuity_ir_fields_map,
561
+ continuity_test_map=continuity_test_map,
562
+ irp_test_map=irp_test_map,
563
+ )
564
+ field_values = field_values + continuity_test
524
565
 
525
- def retrieve_from_csam(csam_url: str, csam_token: str, csam_endpoint: str) -> list:
526
- """
527
- Connect to CSAM and retrieve data
566
+ additional_status = get_additional_status_fields(
567
+ ssp=ssp,
568
+ csam_id=csam_id,
569
+ continuity_ir_fields_map=continuity_ir_fields_map,
570
+ contingency_plan_map=contingency_plan_map,
571
+ doc_approv_fields_map=doc_approv_fields_map,
572
+ doc_approv_map=doc_approv_map,
573
+ )
574
+ field_values = field_values + additional_status
528
575
 
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,
576
+ # Save the Custom Fields
577
+ if len(field_values) > 0:
578
+ field_values = fix_form_field_value(field_values)
579
+ FormFieldValue.save_custom_fields(field_values)
580
+
581
+ logger.info(f"Updated {len(updated_ssps)} Security Plans with contingency data")
582
+
583
+
584
+ def get_continuity_response_fields(ssp: int, csam_id: int, continuity_map: dict, continuity_ir_fields_map: dict):
585
+ # Get the data from /continuityresponse
586
+ result = retrieve_from_csam(
587
+ csam_endpoint=f"/CSAM/api/v1/systems/{csam_id}/continuityresponse",
588
+ )
589
+
590
+ if not result:
591
+ logger.error(f"Could not retrieve details for CSAM ID {csam_id}. RegScale SSP id: {ssp}")
592
+ return []
593
+
594
+ response_data = result[0]
595
+
596
+ # Pre-compute the field mapping to avoid nested lookups
597
+ field_mapping = {
598
+ field: continuity_ir_fields_map[mapped_name]
599
+ for field, mapped_name in continuity_map.items()
600
+ if mapped_name in continuity_ir_fields_map
601
+ }
602
+
603
+ # Build field values with single-level lookup
604
+ return [
605
+ {
606
+ "record_id": ssp,
607
+ "record_module": "securityplans",
608
+ "form_field_id": field_id,
609
+ "field_value": str(response_data[field]),
610
+ }
611
+ for field, field_id in field_mapping.items()
612
+ if field in response_data
613
+ ]
614
+
615
+
616
+ def get_continuity_test_fields(
617
+ ssp: int, csam_id: int, continuity_ir_fields_map: dict, continuity_test_map: dict, irp_test_map: dict
618
+ ):
619
+ # Get the data from continuitytest
620
+ results = retrieve_from_csam(csam_endpoint=f"/CSAM/api/v1/systems/{csam_id}/continuitytest")
621
+
622
+ if not results:
623
+ logger.error(f"Could not retrieve details for CSAM ID {csam_id}. RegScale SSP id: {ssp}")
624
+ return []
625
+
626
+ # Pre-compute field mappings to avoid nested lookups
627
+ cp_field_mapping = {
628
+ field: continuity_ir_fields_map[mapped_name]
629
+ for field, mapped_name in continuity_test_map.items()
630
+ if mapped_name in continuity_ir_fields_map
631
+ }
632
+
633
+ irp_field_mapping = {
634
+ field: continuity_ir_fields_map[mapped_name]
635
+ for field, mapped_name in irp_test_map.items()
636
+ if mapped_name in continuity_ir_fields_map
545
637
  }
546
638
 
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}")
639
+ field_values = []
640
+ for result in results:
641
+ test_item = result.get("testItem")
642
+
643
+ # Process Contingency Plan (CP) fields
644
+ if test_item == "Contingency Plan (CP)":
645
+ field_values.extend(
646
+ [
647
+ {
648
+ "record_id": ssp,
649
+ "record_module": "securityplans",
650
+ "form_field_id": field_id,
651
+ "field_value": str(result[field]),
652
+ }
653
+ for field, field_id in cp_field_mapping.items()
654
+ if field in result
655
+ ]
656
+ )
657
+
658
+ # Process Incident Response Plan (IRP) fields
659
+ elif test_item == "Incident Response Plan (IRP)":
660
+ field_values.extend(
661
+ [
662
+ {
663
+ "record_id": ssp,
664
+ "record_module": "securityplans",
665
+ "form_field_id": field_id,
666
+ "field_value": str(result[field]),
667
+ }
668
+ for field, field_id in irp_field_mapping.items()
669
+ if field in result
670
+ ]
671
+ )
672
+
673
+ return field_values
674
+
675
+
676
+ def get_additional_status_fields(
677
+ ssp: int,
678
+ csam_id: int,
679
+ continuity_ir_fields_map: dict,
680
+ contingency_plan_map: dict,
681
+ doc_approv_fields_map: dict,
682
+ doc_approv_map: dict,
683
+ ):
684
+ # Get the data from additional status
685
+ results = retrieve_from_csam(csam_endpoint=f"/CSAM/api/v1/systems/{csam_id}/additionalstatus")
686
+
687
+ if not results:
688
+ logger.error(f"Could not retrieve details for CSAM ID {csam_id}. RegScale SSP id: {ssp}")
550
689
  return []
551
- if issue_response and issue_response.ok:
552
- return issue_response.json()
553
690
 
554
- return []
691
+ # Pre-compute field mappings to avoid nested lookups
692
+ cp_field_mapping = {
693
+ field: continuity_ir_fields_map[mapped_name]
694
+ for field, mapped_name in contingency_plan_map.items()
695
+ if mapped_name in continuity_ir_fields_map
696
+ }
555
697
 
698
+ doc_field_mapping = {
699
+ field: doc_approv_fields_map[mapped_name]
700
+ for field, mapped_name in doc_approv_map.items()
701
+ if mapped_name in doc_approv_fields_map
702
+ }
556
703
 
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
- )
704
+ field_values = []
705
+ for result in results:
706
+ result_name = result.get("name")
577
707
 
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
708
+ # Process Contingency Plan Review fields
709
+ if result_name == "Contingency Plan Review":
710
+ field_values.extend(
711
+ [
712
+ {
713
+ "record_id": ssp,
714
+ "record_module": "securityplans",
715
+ "form_field_id": field_id,
716
+ "field_value": str(result[field]),
717
+ }
718
+ for field, field_id in cp_field_mapping.items()
719
+ if field in result
720
+ ]
721
+ )
583
722
 
723
+ # Process Document Review Approval fields
724
+ elif result_name == "Document Review Approval":
725
+ field_values.extend(
726
+ [
727
+ {
728
+ "record_id": ssp,
729
+ "record_module": "securityplans",
730
+ "form_field_id": field_id,
731
+ "field_value": str(result[field]),
732
+ }
733
+ for field, field_id in doc_field_mapping.items()
734
+ if field in result
735
+ ]
736
+ )
584
737
 
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
738
+ return field_values
610
739
 
611
740
 
612
741
  def update_ssp_general(ssp: SecurityPlan, record: dict, org_map: dict) -> SecurityPlan:
@@ -629,6 +758,7 @@ def update_ssp_general(ssp: SecurityPlan, record: dict, org_map: dict) -> Securi
629
758
  ssp.status = record["operationalStatus"]
630
759
  ssp.systemType = record["systemType"]
631
760
  ssp.description = record["purpose"]
761
+ ssp.defaultAssessmentDays = 0
632
762
  if record["organization"] and org_map.get(record["organization"]):
633
763
  ssp.orgId = org_map.get(record["organization"])
634
764
 
@@ -640,20 +770,18 @@ def update_ssp_general(ssp: SecurityPlan, record: dict, org_map: dict) -> Securi
640
770
  return new_ssp
641
771
 
642
772
 
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:
773
+ def save_ssp_front_matter(results: list, ssp_map: dict, custom_fields_basic_map: dict, org_map: dict) -> list:
646
774
  """
647
775
  Save the SSP data from the /systems endpoint
648
776
 
649
777
  :param list results: list of results from CSAM
650
778
  :param dict ssp_map: map of existing SSPs in RegScale
651
779
  :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
780
  :param dict org_map: map of existing orgs in RegScale
654
781
  :return: list of updated SSPs
655
782
  :return_type: List[SecurityPlan]
656
783
  """
784
+
657
785
  updated_ssps = []
658
786
  for index in track(
659
787
  range(len(results)),
@@ -671,107 +799,17 @@ def save_ssp_front_matter(
671
799
  ssp = update_ssp_general(ssp, result, org_map)
672
800
 
673
801
  # 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
- )
802
+ field_values = set_front_matter_fields(ssp=ssp, result=result, custom_fields_basic_map=custom_fields_basic_map)
680
803
 
681
804
  # System Custom Fields
805
+ field_values = fix_form_field_value(field_values)
682
806
  FormFieldValue.save_custom_fields(field_values)
683
807
  updated_ssps.append(ssp)
684
808
  logger.info(f"Updated {len(results)} Security Plans Front Matter")
685
809
  return updated_ssps
686
810
 
687
811
 
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:
812
+ def set_front_matter_fields(ssp: SecurityPlan, result: dict, custom_fields_basic_map: dict) -> list:
775
813
  """
776
814
  parse the front matter custom fields
777
815
  and return a list of field values to be saved
@@ -779,351 +817,122 @@ def set_front_matter_fields(
779
817
  :param SecurityPlan ssp: RegScale Security Plan object
780
818
  :param dict result: response from CSAM
781
819
  :param dict custom_fields_basic_map: map of basic custom fields
782
- :param dict custom_fields_financial_map: map of financial custom fields
783
820
  :return: list of dictionaries with field values
784
821
  :return_type: list
785
822
  """
786
- field_values = []
787
- # FISMA ID
788
- field_values.append(
823
+ custom_fields_financial_list = [
824
+ "Financial System",
825
+ "omb Exhibit",
826
+ "Investment Name",
827
+ "Portfolio",
828
+ "Prior Fy Funding",
829
+ "Current Fy Funding",
830
+ "Next Fy Funding",
831
+ "Funding Import Status",
832
+ ]
833
+
834
+ custom_fields_financial_map = FormFieldValue.check_custom_fields(
835
+ custom_fields_financial_list, "securityplans", SSP_FINANCIAL_TAB
836
+ )
837
+
838
+ custom_fields_map = {
839
+ "acronym": "acronym",
840
+ "classification": "Classification",
841
+ "fismaReportable": "FISMA Reportable",
842
+ "contractorSystem": "Contractor System",
843
+ "criticalInfrastructure": "Critical Infrastructure",
844
+ "missionCritical": "Mission Essential",
845
+ "uiiCode": "uiiCode",
846
+ }
847
+ custom_fields_fin_map = {
848
+ "financialSystem": "Financial System",
849
+ "ombExhibit": "omb Exhibit",
850
+ "investmentName": "Investment Name",
851
+ "portfolio": "Portfolio",
852
+ "priorFyFunding": "Prior Fy Funding",
853
+ "currentFyFunding": "Current Fy Funding",
854
+ "nextFyFunding": "Next Fy Funding",
855
+ "fundingImportStatus": "Funding Import Status",
856
+ }
857
+
858
+ # Pre-compute field mappings to avoid nested lookups
859
+ basic_field_mapping = {
860
+ field: custom_fields_basic_map[mapped_name]
861
+ for field, mapped_name in custom_fields_map.items()
862
+ if mapped_name in custom_fields_basic_map and field in result
863
+ }
864
+
865
+ financial_field_mapping = {
866
+ field: custom_fields_financial_map[mapped_name]
867
+ for field, mapped_name in custom_fields_fin_map.items()
868
+ if mapped_name in custom_fields_financial_map and field in result
869
+ }
870
+
871
+ # Start with required ID fields
872
+ field_values = [
789
873
  {
790
874
  "record_id": ssp.id,
875
+ "record_module": "securityplans",
791
876
  "form_field_id": custom_fields_basic_map[FISMA_FIELD_NAME],
792
877
  "field_value": str(result["externalId"]),
793
- }
794
- )
795
- # CSAM ID
796
- field_values.append(
878
+ },
797
879
  {
798
880
  "record_id": ssp.id,
881
+ "record_module": "securityplans",
799
882
  "form_field_id": custom_fields_basic_map[CSAM_FIELD_NAME],
800
883
  "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
884
+ },
885
+ ]
908
886
 
909
- return rev5_catalog_id, rev4_catalog_id
887
+ # Process basic tab fields
888
+ field_values.extend(_create_basic_field_values(ssp.id, result, basic_field_mapping))
910
889
 
890
+ # Process financial tab fields
891
+ field_values.extend(_create_financial_field_values(ssp.id, result, financial_field_mapping))
911
892
 
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
893
+ return field_values
916
894
 
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
895
 
896
+ def _create_basic_field_values(record_id: int, result: dict, field_mapping: dict) -> list:
897
+ """Helper function to create basic field values with proper type handling"""
898
+ field_values = []
899
+ for field, field_id in field_mapping.items():
900
+ value = result.get(field)
901
+ if isinstance(value, bool):
902
+ field_value = "Yes" if value else "No"
903
+ else:
904
+ field_value = str(value)
961
905
 
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}",
906
+ field_values.append(
907
+ {
908
+ "record_id": record_id,
909
+ "record_module": "securityplans",
910
+ "form_field_id": field_id,
911
+ "field_value": field_value,
912
+ }
988
913
  )
914
+ return field_values
989
915
 
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
916
 
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")))
917
+ def _create_financial_field_values(record_id: int, result: dict, field_mapping: dict) -> list:
918
+ """Helper function to create financial field values with proper handling of funding fields"""
919
+ funding_fields = ["priorFyFunding", "currentFyFunding", "nextFyFunding"]
1050
920
 
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
- )
921
+ field_values = []
922
+ for field, field_id in field_mapping.items():
923
+ value = result.get(field)
924
+ # Handle blank dollar values
925
+ if field in funding_fields:
926
+ field_value = str(value) if value else "0"
927
+ else:
928
+ field_value = str(value)
1068
929
 
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,
930
+ field_values.append(
931
+ {
932
+ "record_id": record_id,
933
+ "record_module": "securityplans",
934
+ "form_field_id": field_id,
935
+ "field_value": field_value,
936
+ }
1075
937
  )
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()
938
+ return field_values