regscale-cli 6.20.1.1__py3-none-any.whl → 6.20.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of regscale-cli might be problematic. Click here for more details.

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