regscale-cli 6.20.7.0__py3-none-any.whl → 6.20.9.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 (41) hide show
  1. regscale/_version.py +1 -1
  2. regscale/core/app/api.py +8 -1
  3. regscale/core/app/application.py +130 -20
  4. regscale/core/utils/date.py +16 -16
  5. regscale/integrations/commercial/aqua/aqua.py +1 -1
  6. regscale/integrations/commercial/aws/cli.py +1 -1
  7. regscale/integrations/commercial/defender.py +1 -1
  8. regscale/integrations/commercial/ecr.py +1 -1
  9. regscale/integrations/commercial/ibm.py +1 -1
  10. regscale/integrations/commercial/nexpose.py +1 -1
  11. regscale/integrations/commercial/prisma.py +1 -1
  12. regscale/integrations/commercial/qualys/__init__.py +150 -77
  13. regscale/integrations/commercial/qualys/containers.py +2 -1
  14. regscale/integrations/commercial/qualys/scanner.py +5 -3
  15. regscale/integrations/commercial/snyk.py +14 -4
  16. regscale/integrations/commercial/synqly/ticketing.py +23 -11
  17. regscale/integrations/commercial/veracode.py +15 -4
  18. regscale/integrations/commercial/xray.py +1 -1
  19. regscale/integrations/public/cisa.py +7 -1
  20. regscale/integrations/public/nist_catalog.py +8 -2
  21. regscale/integrations/scanner_integration.py +18 -36
  22. regscale/models/integration_models/cisa_kev_data.json +51 -6
  23. regscale/models/integration_models/flat_file_importer/__init__.py +34 -19
  24. regscale/models/integration_models/send_reminders.py +8 -2
  25. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  26. regscale/models/regscale_models/control_implementation.py +40 -0
  27. regscale/models/regscale_models/issue.py +7 -4
  28. regscale/models/regscale_models/parameter.py +3 -2
  29. regscale/models/regscale_models/ports_protocol.py +15 -5
  30. regscale/models/regscale_models/vulnerability.py +1 -1
  31. regscale/utils/graphql_client.py +3 -6
  32. regscale/utils/threading/threadhandler.py +12 -2
  33. {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/METADATA +13 -13
  34. {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/RECORD +41 -40
  35. tests/regscale/core/test_app.py +402 -16
  36. tests/regscale/core/test_version_regscale.py +62 -0
  37. tests/regscale/test_init.py +2 -0
  38. {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/LICENSE +0 -0
  39. {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/WHEEL +0 -0
  40. {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/entry_points.txt +0 -0
  41. {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/top_level.txt +0 -0
regscale/_version.py CHANGED
@@ -33,7 +33,7 @@ def get_version_from_pyproject() -> str:
33
33
  return match.group(1)
34
34
  except Exception:
35
35
  pass
36
- return "6.20.7.0" # fallback version
36
+ return "6.20.9.0" # fallback version
37
37
 
38
38
 
39
39
  __version__ = get_version_from_pyproject()
regscale/core/app/api.py CHANGED
@@ -432,7 +432,14 @@ class Api:
432
432
  if json_list and len(json_list) > 0:
433
433
  with Progress(transient=False) as progress:
434
434
  task = progress.add_task(message, total=len(json_list))
435
- with concurrent.futures.ThreadPoolExecutor(max_workers=self.config["maxThreads"]) as executor:
435
+ # Ensure maxThreads is an integer for ThreadPoolExecutor
436
+ max_workers = self.config.get("maxThreads", 100)
437
+ if not isinstance(max_workers, int):
438
+ try:
439
+ max_workers = int(max_workers)
440
+ except (ValueError, TypeError):
441
+ max_workers = 100
442
+ with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as executor:
436
443
  if method.lower() == "post":
437
444
  result_futures = list(
438
445
  map(
@@ -298,7 +298,19 @@ class Application(metaclass=Singleton):
298
298
  self.config = self._gen_config(config)
299
299
  self.os = platform.system()
300
300
  self.input_host = ""
301
- self.thread_manager = ThreadManager(self.config.get("maxThreads", 100))
301
+ # Ensure maxThreads is an integer for ThreadManager
302
+ max_threads = self.config.get("maxThreads", 100)
303
+ if not isinstance(max_threads, int):
304
+ logger.debug(f"maxThreads is not an integer: {max_threads} (type: {type(max_threads)})")
305
+ try:
306
+ max_threads = int(max_threads)
307
+ logger.debug(f"Converted maxThreads to integer: {max_threads}")
308
+ except (ValueError, TypeError) as e:
309
+ logger.warning(
310
+ f"Failed to convert maxThreads '{max_threads}' to integer: {e}. Using default value 100."
311
+ )
312
+ max_threads = 100
313
+ self.thread_manager = ThreadManager(max_threads)
302
314
  logger.debug("Finished Initializing Application")
303
315
  logger.debug("*" * 80)
304
316
 
@@ -387,10 +399,10 @@ class Application(metaclass=Singleton):
387
399
  if config is None:
388
400
  config = {}
389
401
  self.logger.debug(f"Provided config in _fetch_config_from_regscale is: {type(config)}")
390
- token = config.get("token") or os.getenv("REGSCALE_TOKEN")
391
- domain = config.get("domain") or os.getenv("REGSCALE_DOMAIN")
402
+ token = config.get("token", os.getenv("REGSCALE_TOKEN"))
403
+ domain = config.get("domain", os.getenv("REGSCALE_DOMAIN"))
392
404
  if domain is None or "http" not in domain or domain == self.template["domain"]:
393
- domain = self.retrieve_domain()[:-1] if self.retrieve_domain().endswith("/") else self.retrieve_domain()
405
+ domain = self.retrieve_domain().rstrip("/")
394
406
  self.logger.debug(f"domain: {domain}, token: {token}")
395
407
  if domain is not None and token is not None:
396
408
  self.logger.info(f"Fetching config from {domain}...")
@@ -404,23 +416,87 @@ class Application(metaclass=Singleton):
404
416
  },
405
417
  )
406
418
  self.logger.debug(f"status_code: {response.status_code} text: {response.text}")
407
- res_data = response.json()
408
- if config := res_data.get("cliConfig"):
409
- parsed_dict = yaml.safe_load(config)
410
- self.logger.debug(f"parsed_dict: {parsed_dict}")
411
- parsed_dict["token"] = token
412
- parsed_dict["domain"] = domain
413
- from regscale.core.app.internal.login import parse_user_id_from_jwt
414
-
415
- parsed_dict["userId"] = res_data.get("userId") or parse_user_id_from_jwt(self, token)
416
- self.logger.debug(f"Updated domain, token and userId: {parsed_dict}")
417
- self.logger.info("Successfully fetched config from RegScale.")
418
- # fill in any missing keys with the template
419
- return {**self.template, **parsed_dict}
419
+
420
+ # Get the encrypted config from the response
421
+ fetched_config = response.json()
422
+ if not fetched_config or response.text == "":
423
+ self.logger.warning("No secrets found in %s", domain)
424
+ return {}
425
+ # see if it's just a dictionary
426
+ if isinstance(fetched_config, dict):
427
+ parsed_dict = fetched_config
428
+ else:
429
+ decrypted_config = self._decrypt_config(fetched_config, token)
430
+ parsed_dict = json.loads(decrypted_config)
431
+
432
+ parsed_dict["token"] = token
433
+ parsed_dict["domain"] = domain
434
+ from regscale.core.app.internal.login import parse_user_id_from_jwt
435
+
436
+ parsed_dict["userId"] = parsed_dict.get("userId") or parse_user_id_from_jwt(self, token)
437
+ self.logger.info("Successfully fetched config from RegScale.")
438
+ # fill in any missing keys with the template
439
+ return {**self.template, **parsed_dict}
420
440
  except Exception as ex:
421
- self.logger.error("Unable to fetch config from RegScale.\n%s", ex)
441
+ self.logger.error("Unable to fetch config from RegScale.\n%s", str(ex))
422
442
  return {}
423
443
 
444
+ def _decrypt_config(self, encrypted_text: str, bearer_token: str) -> str:
445
+ """
446
+ Decrypt the configuration using AES encryption with the bearer token as key
447
+
448
+ :param str encrypted_text: Base64 encoded encrypted text
449
+ :param str bearer_token: Bearer token used as encryption key
450
+ :return: Decrypted configuration string
451
+ :rtype: str
452
+ """
453
+ import base64
454
+ import hashlib
455
+ from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
456
+ from cryptography.hazmat.backends import default_backend
457
+
458
+ try:
459
+ # Convert from base64
460
+ combined = base64.b64decode(encrypted_text)
461
+
462
+ # Extract IV (first 16 bytes) and cipher text
463
+ iv = combined[:16]
464
+ cipher_text = combined[16:]
465
+
466
+ # Generate key from bearer token using SHA256
467
+ key = hashlib.sha256(bearer_token.encode()).digest()
468
+
469
+ # Create cipher
470
+ cipher = Cipher(algorithms.AES(key), modes.GCM(iv), backend=default_backend())
471
+
472
+ # Decrypt
473
+ decryptor = cipher.decryptor()
474
+ decrypted = decryptor.update(cipher_text) + decryptor.finalize()
475
+
476
+ # Remove padding and convert to string
477
+ decoded = decrypted.decode("utf-8")
478
+ # Remove all trailing whitespace and control characters
479
+ cleaned = decoded.rstrip()
480
+ # Also remove any trailing null bytes that might remain
481
+ while cleaned.endswith("\0"):
482
+ cleaned = cleaned[:-1]
483
+ # Use regex to remove any ending backslash-like pattern and characters after it
484
+ import re
485
+
486
+ # Remove any trailing backslash followed by any characters until the end
487
+ # This handles both literal backslashes and control characters like \x0e
488
+ cleaned = re.sub(r"\\[^\\]*$", "", cleaned)
489
+ # Also remove any trailing control characters that might remain
490
+ # Avoid regex for trailing control character removal to prevent potential catastrophic backtracking.
491
+ # Instead, use rstrip with a string of control characters.
492
+ cleaned = cleaned.rstrip(
493
+ "".join([chr(i) for i in range(0x00, 0x20)]) + "".join([chr(i) for i in range(0x7F, 0xA0)])
494
+ )
495
+ return cleaned
496
+ except Exception as err:
497
+ self.logger.error("Unable to decrypt config: %s", err)
498
+ return "{}"
499
+
424
500
  def _load_config_from_click_context(self) -> Optional[dict]:
425
501
  """
426
502
  Load configuration from Click context
@@ -654,12 +730,46 @@ class Application(metaclass=Singleton):
654
730
  for key, template_value in template.items():
655
731
  config_value = config.get(key)
656
732
 
657
- # If key missing or value type mismatch, use template value
658
- if config_value is None or config_value == "" or not isinstance(config_value, type(template_value)):
733
+ # If key missing or empty, use template value
734
+ if config_value is None or config_value == "":
659
735
  updated_config[key] = template_value
660
736
  # If value is a dict, recurse
661
737
  elif isinstance(template_value, dict):
662
738
  updated_config[key] = self.verify_config(template_value, config.get(key, {}))
739
+ # If type mismatch, try to convert the value to the expected type
740
+ elif not isinstance(config_value, type(template_value)):
741
+ self.logger.debug(
742
+ f"Type mismatch for key '{key}': expected {type(template_value).__name__}, got {type(config_value).__name__} with value '{config_value}'"
743
+ )
744
+ try:
745
+ if isinstance(template_value, int):
746
+ updated_config[key] = int(config_value)
747
+ self.logger.debug(
748
+ f"Converted '{key}' from {type(config_value).__name__} to int: {config_value} -> {updated_config[key]}"
749
+ )
750
+ elif isinstance(template_value, float):
751
+ updated_config[key] = float(config_value)
752
+ self.logger.debug(
753
+ f"Converted '{key}' from {type(config_value).__name__} to float: {config_value} -> {updated_config[key]}"
754
+ )
755
+ elif isinstance(template_value, bool):
756
+ if isinstance(config_value, str):
757
+ updated_config[key] = config_value.lower() in ("true", "1", "yes", "on")
758
+ else:
759
+ updated_config[key] = bool(config_value)
760
+ self.logger.debug(
761
+ f"Converted '{key}' from {type(config_value).__name__} to bool: {config_value} -> {updated_config[key]}"
762
+ )
763
+ else:
764
+ # For other types, use template value as fallback
765
+ updated_config[key] = template_value
766
+ self.logger.debug(f"Using template value for '{key}': {template_value}")
767
+ except (ValueError, TypeError) as e:
768
+ # If conversion fails, use template value
769
+ self.logger.warning(
770
+ f"Failed to convert '{key}' from '{config_value}' to {type(template_value).__name__}: {e}. Using template value: {template_value}"
771
+ )
772
+ updated_config[key] = template_value
663
773
  # Else, retain the config value
664
774
  else:
665
775
  updated_config[key] = config_value
@@ -24,20 +24,23 @@ def date_str(date_object: Union[str, datetime.datetime, datetime.date, None], da
24
24
  :param Optional[str] date_format: The format to use for the date string.
25
25
  :return: The date as a string.
26
26
  """
27
- if isinstance(date_object, str):
28
- date_object = date_obj(date_object)
27
+ try:
28
+ if isinstance(date_object, str):
29
+ date_object = date_obj(date_object)
29
30
 
30
- # Handles passed None and date_obj returning None
31
- if not date_object:
32
- return ""
31
+ # Handles passed None and date_obj returning None
32
+ if not date_object:
33
+ return ""
33
34
 
34
- if isinstance(date_object, (datetime.datetime, Timestamp)):
35
- date_object = date_object.date()
35
+ if isinstance(date_object, (datetime.datetime, Timestamp)):
36
+ date_object = date_object.date()
36
37
 
37
- if date_format:
38
- return date_object.strftime(date_format)
38
+ if date_format:
39
+ return date_object.strftime(date_format)
39
40
 
40
- return date_object.isoformat()
41
+ return date_object.isoformat()
42
+ except Exception:
43
+ return ""
41
44
 
42
45
 
43
46
  def datetime_str(
@@ -52,14 +55,11 @@ def datetime_str(
52
55
  """
53
56
  if not date_format:
54
57
  date_format = default_date_format
55
-
56
58
  if isinstance(date_object, str):
57
59
  date_object = datetime_obj(date_object)
58
-
59
- if not date_object:
60
- return ""
61
-
62
- return date_object.strftime(date_format)
60
+ if isinstance(date_object, datetime.date):
61
+ return date_object.strftime(date_format)
62
+ return ""
63
63
 
64
64
 
65
65
  def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.date]:
@@ -80,7 +80,7 @@ def import_aqua_scan(
80
80
  import_name="Aqua",
81
81
  file_types=[".csv", ".xlsx"],
82
82
  folder_path=folder_path,
83
- regscale_ssp_id=regscale_ssp_id,
83
+ object_id=regscale_ssp_id,
84
84
  scan_date=scan_date,
85
85
  mappings_path=mappings_path,
86
86
  disable_mapping=disable_mapping,
@@ -249,7 +249,7 @@ def import_aws_scans(
249
249
  import_name="AWS Inspector",
250
250
  file_types=[".csv", ".json"],
251
251
  folder_path=folder_path,
252
- regscale_ssp_id=regscale_ssp_id,
252
+ object_id=regscale_ssp_id,
253
253
  scan_date=scan_date,
254
254
  mappings_path=mappings_path,
255
255
  disable_mapping=disable_mapping,
@@ -251,7 +251,7 @@ def import_defender_alerts(
251
251
  import_name="Defender",
252
252
  file_types=".csv",
253
253
  folder_path=folder_path,
254
- regscale_ssp_id=regscale_ssp_id,
254
+ object_id=regscale_ssp_id,
255
255
  scan_date=scan_date,
256
256
  mappings_path=mappings_path,
257
257
  disable_mapping=disable_mapping,
@@ -79,7 +79,7 @@ def import_ecr_scans(
79
79
  import_name="ECR",
80
80
  file_types=[".csv", ".json"],
81
81
  folder_path=folder_path,
82
- regscale_ssp_id=regscale_ssp_id,
82
+ object_id=regscale_ssp_id,
83
83
  scan_date=scan_date,
84
84
  mappings_path=mappings_path,
85
85
  disable_mapping=disable_mapping,
@@ -79,7 +79,7 @@ def import_appscan_files(
79
79
  import_name="IBM AppScan",
80
80
  file_types=".csv",
81
81
  folder_path=folder_path,
82
- regscale_ssp_id=regscale_ssp_id,
82
+ object_id=regscale_ssp_id,
83
83
  scan_date=scan_date,
84
84
  mappings_path=mappings_path,
85
85
  disable_mapping=disable_mapping,
@@ -79,7 +79,7 @@ def import_nexpose_files(
79
79
  import_name="Nexpose",
80
80
  file_types=".csv",
81
81
  folder_path=folder_path,
82
- regscale_ssp_id=regscale_ssp_id,
82
+ object_id=regscale_ssp_id,
83
83
  scan_date=scan_date,
84
84
  mappings_path=mappings_path,
85
85
  disable_mapping=disable_mapping,
@@ -80,7 +80,7 @@ def import_prisma_data(
80
80
  import_name="Prisma",
81
81
  file_types=".csv",
82
82
  folder_path=folder_path,
83
- regscale_ssp_id=regscale_ssp_id,
83
+ object_id=regscale_ssp_id,
84
84
  scan_date=scan_date,
85
85
  mappings_path=mappings_path,
86
86
  disable_mapping=disable_mapping,