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.
- regscale/_version.py +1 -1
- regscale/core/app/api.py +8 -1
- regscale/core/app/application.py +130 -20
- regscale/core/utils/date.py +16 -16
- regscale/integrations/commercial/aqua/aqua.py +1 -1
- regscale/integrations/commercial/aws/cli.py +1 -1
- regscale/integrations/commercial/defender.py +1 -1
- regscale/integrations/commercial/ecr.py +1 -1
- regscale/integrations/commercial/ibm.py +1 -1
- regscale/integrations/commercial/nexpose.py +1 -1
- regscale/integrations/commercial/prisma.py +1 -1
- regscale/integrations/commercial/qualys/__init__.py +150 -77
- regscale/integrations/commercial/qualys/containers.py +2 -1
- regscale/integrations/commercial/qualys/scanner.py +5 -3
- regscale/integrations/commercial/snyk.py +14 -4
- regscale/integrations/commercial/synqly/ticketing.py +23 -11
- regscale/integrations/commercial/veracode.py +15 -4
- regscale/integrations/commercial/xray.py +1 -1
- regscale/integrations/public/cisa.py +7 -1
- regscale/integrations/public/nist_catalog.py +8 -2
- regscale/integrations/scanner_integration.py +18 -36
- regscale/models/integration_models/cisa_kev_data.json +51 -6
- regscale/models/integration_models/flat_file_importer/__init__.py +34 -19
- regscale/models/integration_models/send_reminders.py +8 -2
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/control_implementation.py +40 -0
- regscale/models/regscale_models/issue.py +7 -4
- regscale/models/regscale_models/parameter.py +3 -2
- regscale/models/regscale_models/ports_protocol.py +15 -5
- regscale/models/regscale_models/vulnerability.py +1 -1
- regscale/utils/graphql_client.py +3 -6
- regscale/utils/threading/threadhandler.py +12 -2
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/METADATA +13 -13
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/RECORD +41 -40
- tests/regscale/core/test_app.py +402 -16
- tests/regscale/core/test_version_regscale.py +62 -0
- tests/regscale/test_init.py +2 -0
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.20.7.0.dist-info → regscale_cli-6.20.9.0.dist-info}/entry_points.txt +0 -0
- {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
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
|
-
|
|
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(
|
regscale/core/app/application.py
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
391
|
-
domain = config.get("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()
|
|
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
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
parsed_dict
|
|
416
|
-
|
|
417
|
-
self.
|
|
418
|
-
|
|
419
|
-
|
|
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
|
|
658
|
-
if config_value is None or config_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
|
regscale/core/utils/date.py
CHANGED
|
@@ -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
|
-
|
|
28
|
-
|
|
27
|
+
try:
|
|
28
|
+
if isinstance(date_object, str):
|
|
29
|
+
date_object = date_obj(date_object)
|
|
29
30
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
31
|
+
# Handles passed None and date_obj returning None
|
|
32
|
+
if not date_object:
|
|
33
|
+
return ""
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
if isinstance(date_object, (datetime.datetime, Timestamp)):
|
|
36
|
+
date_object = date_object.date()
|
|
36
37
|
|
|
37
|
-
|
|
38
|
-
|
|
38
|
+
if date_format:
|
|
39
|
+
return date_object.strftime(date_format)
|
|
39
40
|
|
|
40
|
-
|
|
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
|
-
|
|
60
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
82
|
+
object_id=regscale_ssp_id,
|
|
83
83
|
scan_date=scan_date,
|
|
84
84
|
mappings_path=mappings_path,
|
|
85
85
|
disable_mapping=disable_mapping,
|