regscale-cli 6.16.3.0__py3-none-any.whl → 6.16.4.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 (42) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/internal/control_editor.py +26 -2
  3. regscale/core/app/internal/model_editor.py +39 -26
  4. regscale/integrations/commercial/grype/scanner.py +37 -29
  5. regscale/integrations/commercial/opentext/commands.py +2 -0
  6. regscale/integrations/commercial/opentext/scanner.py +45 -31
  7. regscale/integrations/commercial/qualys.py +3 -1
  8. regscale/integrations/commercial/sicura/commands.py +9 -14
  9. regscale/integrations/commercial/tenablev2/click.py +25 -13
  10. regscale/integrations/commercial/tenablev2/scanner.py +12 -3
  11. regscale/integrations/commercial/trivy/scanner.py +14 -6
  12. regscale/integrations/commercial/wizv2/click.py +15 -37
  13. regscale/integrations/jsonl_scanner_integration.py +120 -16
  14. regscale/integrations/public/fedramp/click.py +8 -8
  15. regscale/integrations/public/fedramp/fedramp_cis_crm.py +499 -106
  16. regscale/integrations/public/fedramp/ssp_logger.py +2 -9
  17. regscale/integrations/scanner_integration.py +14 -9
  18. regscale/models/integration_models/cisa_kev_data.json +39 -8
  19. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  20. regscale/models/integration_models/tenable_models/integration.py +23 -3
  21. regscale/models/regscale_models/control_implementation.py +18 -0
  22. regscale/models/regscale_models/control_objective.py +2 -1
  23. regscale/models/regscale_models/facility.py +10 -26
  24. regscale/models/regscale_models/functional_roles.py +38 -0
  25. regscale/models/regscale_models/issue.py +3 -1
  26. regscale/models/regscale_models/parameter.py +21 -3
  27. regscale/models/regscale_models/profile.py +22 -0
  28. regscale/models/regscale_models/profile_mapping.py +48 -3
  29. regscale/models/regscale_models/regscale_model.py +2 -0
  30. regscale/models/regscale_models/risk.py +38 -30
  31. regscale/models/regscale_models/security_plan.py +1 -0
  32. regscale/models/regscale_models/supply_chain.py +1 -1
  33. regscale/models/regscale_models/user.py +16 -2
  34. regscale/utils/threading/__init__.py +1 -0
  35. regscale/utils/threading/threadsafe_list.py +10 -0
  36. regscale/utils/threading/threadsafe_set.py +116 -0
  37. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/METADATA +1 -1
  38. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/RECORD +42 -40
  39. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/LICENSE +0 -0
  40. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/WHEEL +0 -0
  41. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/entry_points.txt +0 -0
  42. {regscale_cli-6.16.3.0.dist-info → regscale_cli-6.16.4.0.dist-info}/top_level.txt +0 -0
@@ -13,7 +13,7 @@ from pathlib import Path
13
13
 
14
14
  from regscale.core.app.utils.parser_utils import safe_datetime_str
15
15
  from regscale.integrations.jsonl_scanner_integration import JSONLScannerIntegration
16
- from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding
16
+ from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding, issue_due_date
17
17
  from regscale.models import IssueSeverity, AssetStatus, IssueStatus
18
18
 
19
19
  logger = logging.getLogger("regscale")
@@ -50,6 +50,9 @@ class TrivyIntegration(JSONLScannerIntegration):
50
50
  kwargs["read_files_only"] = True
51
51
  kwargs["file_pattern"] = "*.json"
52
52
  self.disable_mapping = kwargs["disable_mapping"] = True
53
+ self.scan_date = kwargs.get("scan_date") if "scan_date" in kwargs else None
54
+ if self.scan_date:
55
+ self.scan_date = self.clean_scan_date(self.scan_date)
53
56
  super().__init__(*args, **kwargs)
54
57
 
55
58
  def is_valid_file(self, data: Any, file_path: Union[Path, str]) -> Tuple[bool, Optional[Dict[str, Any]]]:
@@ -167,8 +170,10 @@ class TrivyIntegration(JSONLScannerIntegration):
167
170
  :return: IntegrationFinding object
168
171
  :rtype: IntegrationFinding
169
172
  """
173
+ created_date = safe_datetime_str(data.get("CreatedAt"))
170
174
  # Get scan date from the finding or use current time
171
- scan_date = safe_datetime_str(data.get("CreatedAt"))
175
+ if self.scan_date is None:
176
+ self.scan_date = created_date
172
177
 
173
178
  # Process severity
174
179
  severity_str = item.get("Severity", "UNKNOWN")
@@ -220,10 +225,10 @@ class TrivyIntegration(JSONLScannerIntegration):
220
225
  plugin_id=plugin_id,
221
226
  asset_identifier=asset_identifier,
222
227
  cve=cve,
223
- first_seen=scan_date,
224
- last_seen=scan_date,
225
- scan_date=scan_date,
226
- date_created=scan_date,
228
+ first_seen=safe_datetime_str(data.get("CreatedAt")),
229
+ last_seen=self.scan_date,
230
+ scan_date=self.scan_date,
231
+ date_created=item.get("CreatedAt"),
227
232
  category="Software",
228
233
  control_labels=[],
229
234
  installed_versions=item.get("InstalledVersion", ""),
@@ -234,6 +239,9 @@ class TrivyIntegration(JSONLScannerIntegration):
234
239
  build_version=build_version,
235
240
  fixed_versions=item.get("FixedVersion", ""),
236
241
  fix_status=item.get("Status", ""),
242
+ due_date=issue_due_date(
243
+ severity=severity, created_date=created_date, title="trivy", config=self.app.config
244
+ ),
237
245
  )
238
246
 
239
247
  @staticmethod
@@ -32,20 +32,17 @@ def authenticate(client_id, client_secret):
32
32
 
33
33
 
34
34
  @wiz.command()
35
- @click.option( # type: ignore
35
+ @click.option(
36
36
  "--wiz_project_id",
37
37
  "-p",
38
38
  required=False,
39
39
  type=str,
40
40
  help="Comma Seperated list of one or more Wiz project ids to pull inventory for.",
41
41
  )
42
- @click.option("--regscale_id", "-i", help="RegScale id to push inventory to in RegScale.") # type: ignore
43
- @click.option( # type: ignore
44
- "--regscale_module",
45
- "-m",
46
- help="Regscale module to push inventory to in RegScale.",
42
+ @regscale_ssp_id(
43
+ help="RegScale SSP ID to push inventory to in RegScale.",
47
44
  )
48
- @click.option( # type: ignore
45
+ @click.option(
49
46
  "--client_id",
50
47
  "-ci",
51
48
  help="Wiz Client ID, or can be set as environment variable wizClientId",
@@ -53,7 +50,7 @@ def authenticate(client_id, client_secret):
53
50
  hide_input=False,
54
51
  required=False,
55
52
  )
56
- @click.option( # type: ignore
53
+ @click.option(
57
54
  "--client_secret",
58
55
  "-cs",
59
56
  help="Wiz Client Secret, or can be set as environment variable wizClientSecret",
@@ -61,7 +58,7 @@ def authenticate(client_id, client_secret):
61
58
  hide_input=False,
62
59
  required=False,
63
60
  )
64
- @click.option( # type: ignore
61
+ @click.option(
65
62
  "--filter_by_override",
66
63
  "-f",
67
64
  default=None,
@@ -71,22 +68,12 @@ def authenticate(client_id, client_secret):
71
68
  IE: --filter_by='{projectId: ["1234"], type: ["VIRTUAL_MACHINE"], subscriptionExternalId: ["1234"],
72
69
  providerUniqueId: ["1234"], updatedAt: {after: "2023-06-14T14:07:06Z"}, search: "test-7"}' """,
73
70
  )
74
- @click.option( # type: ignore
75
- "--full_inventory",
76
- "-fi",
77
- is_flag=True,
78
- help="Pull full inventory list. this disregards the last pull date.",
79
- required=False,
80
- default=False,
81
- )
82
71
  def inventory(
83
72
  wiz_project_id: str,
84
- regscale_id: int,
85
- regscale_module: str,
73
+ regscale_ssp_id: int,
86
74
  client_id: str,
87
75
  client_secret: str,
88
76
  filter_by_override: Optional[str] = None,
89
- full_inventory: bool = False,
90
77
  ) -> None:
91
78
  """Process inventory from Wiz and create assets in RegScale."""
92
79
  from regscale.integrations.commercial.wizv2.scanner import WizVulnerabilityIntegration
@@ -96,9 +83,9 @@ def inventory(
96
83
  if not client_id:
97
84
  client_id = WizVariables.wizClientId
98
85
 
99
- scanner = WizVulnerabilityIntegration(plan_id=regscale_id)
86
+ scanner = WizVulnerabilityIntegration(plan_id=regscale_ssp_id)
100
87
  scanner.sync_assets(
101
- plan_id=regscale_id,
88
+ plan_id=regscale_ssp_id,
102
89
  filter_by_override=filter_by_override or WizVariables.wizInventoryFilterBy, # type: ignore
103
90
  client_id=client_id, # type: ignore
104
91
  client_secret=client_secret, # type: ignore
@@ -106,38 +93,30 @@ def inventory(
106
93
  )
107
94
 
108
95
 
109
- @wiz.command() # type: ignore
110
- @click.option( # type: ignore
96
+ @wiz.command()
97
+ @click.option(
111
98
  "--wiz_project_id",
112
99
  "-p",
113
100
  prompt="Enter the project ID for Wiz",
114
101
  default=None,
115
102
  required=False,
116
103
  )
117
- @regscale_id(help="RegScale will create and update issues as children of this record.")
118
- @click.option( # type: ignore
119
- "--regscale_module",
120
- "-m",
121
- type=click.STRING, # type: ignore
122
- help="Enter the RegScale module name. Default is 'securityplans'",
123
- default="securityplans",
124
- required=False,
125
- )
126
- @click.option( # type: ignore
104
+ @regscale_ssp_id(help="RegScale will create and update issues as children of this record.")
105
+ @click.option(
127
106
  "--client_id",
128
107
  help="Wiz Client ID, or can be set as environment variable wizClientId",
129
108
  default="",
130
109
  hide_input=False,
131
110
  required=False,
132
111
  )
133
- @click.option( # type: ignore
112
+ @click.option(
134
113
  "--client_secret",
135
114
  help="Wiz Client Secret, or can be set as environment variable wizClientSecret",
136
115
  default="",
137
116
  hide_input=True,
138
117
  required=False,
139
118
  )
140
- @click.option( # type: ignore
119
+ @click.option(
141
120
  "--filter_by_override",
142
121
  "-f",
143
122
  default=None,
@@ -152,7 +131,6 @@ def issues(
152
131
  regscale_id: int,
153
132
  client_id: str,
154
133
  client_secret: str,
155
- regscale_module: str = "securityplans",
156
134
  filter_by_override: Optional[str] = None,
157
135
  ) -> None:
158
136
  """
@@ -8,14 +8,18 @@ import logging
8
8
  import os
9
9
  import shutil
10
10
  import tempfile
11
+ import traceback
12
+ from datetime import datetime, time
11
13
  from typing import Any, Dict, Iterator, Optional, Union, Tuple, TypeVar, Type, List
12
14
 
13
15
  import boto3
14
16
  from pathlib import Path
15
17
 
18
+ from regscale.core.app.utils.app_utils import get_current_datetime
16
19
  from regscale.core.app.utils.file_utils import is_s3_path, read_file, find_files, download_from_s3
17
20
  from regscale.exceptions import ValidationException
18
21
  from regscale.integrations.scanner_integration import IntegrationAsset, IntegrationFinding, ScannerIntegration
22
+ from regscale.models import regscale_models
19
23
  from regscale.models.app_models.mapping import Mapping
20
24
 
21
25
  logger = logging.getLogger("regscale")
@@ -43,33 +47,36 @@ class JSONLScannerIntegration(ScannerIntegration):
43
47
  # Constants for file paths - subclasses should override these
44
48
  ASSETS_FILE = "./artifacts/assets.jsonl"
45
49
  FINDINGS_FILE = "./artifacts/findings.jsonl"
50
+ DT_FORMAT = "%Y-%m-%d"
46
51
 
47
52
  def __init__(self, *args, **kwargs):
48
53
  """
49
54
  Initialize the JSONLScannerIntegration.
50
55
  """
56
+ logger.info("Initializing JSONLScannerIntegration")
57
+ self.plan_id = kwargs.get("plan_id", None)
58
+ # plan_id is required for all integrations
59
+ super().__init__(**kwargs)
51
60
  # Extract S3-related kwargs
52
- self.s3_bucket = kwargs.pop("s3_bucket", None)
53
- self.s3_prefix = kwargs.pop("s3_prefix", "")
54
- self.aws_profile = kwargs.pop("aws_profile", "default")
61
+ self.s3_bucket = kwargs.get("s3_bucket", None)
62
+ self.s3_prefix = kwargs.get("s3_prefix", "")
63
+ self.aws_profile = kwargs.get("aws_profile", "default")
55
64
 
56
- self.plan_id = kwargs.pop("plan_id", None)
57
- self.file_path = kwargs.pop("file_path", None)
65
+ self.file_path = kwargs.get("file_path", None)
58
66
  self.empty_files: bool = True
59
- self.scan_date = kwargs.pop("scan_date", None)
60
- self.download_destination = kwargs.pop("destination", None)
61
- self.file_pattern = kwargs.pop("file_pattern", "*.json")
62
- self.read_files_only = kwargs.pop("read_files_only", False)
67
+ self.download_destination = kwargs.get("destination", None)
68
+ self.file_pattern = kwargs.get("file_pattern", "*.json")
69
+ self.read_files_only = kwargs.get("read_files_only", False)
63
70
 
64
71
  # Extract mapping-related kwargs
65
- self.disable_mapping = kwargs.pop("disable_mapping", False)
66
- self.mapping_path = kwargs.pop("mapping_path", f"./mappings/{self.__class__.__name__.lower()}/mapping.json")
67
- self.required_asset_fields = kwargs.pop("required_asset_fields", ["identifier", "name"])
68
- self.required_finding_fields = kwargs.pop("required_finding_fields", ["asset_identifier", "title", "severity"])
72
+ self.disable_mapping = kwargs.get("disable_mapping", False)
73
+ self.mapping_path = kwargs.get("mapping_path", f"./mappings/{self.__class__.__name__.lower()}/mapping.json")
74
+ self.required_asset_fields = kwargs.get("required_asset_fields", ["identifier", "name"])
75
+ self.required_finding_fields = kwargs.get("required_finding_fields", ["asset_identifier", "title", "severity"])
69
76
  self.mapping = self._load_mapping() if not self.disable_mapping else None
70
77
 
78
+ self.set_scan_date(kwargs.get("scan_date", get_current_datetime()))
71
79
  # Initialize parent class
72
- super().__init__(plan_id=self.plan_id, **kwargs)
73
80
 
74
81
  self.s3_client = None
75
82
  if self.s3_bucket and not self.read_files_only:
@@ -80,6 +87,100 @@ class JSONLScannerIntegration(ScannerIntegration):
80
87
  logger.error(f"Failed to initialize S3 client with profile {self.aws_profile}: {str(e)}")
81
88
  raise ValidationException(f"S3 client initialization failed: {str(e)}")
82
89
 
90
+ def set_scan_date(self, scan_date_input: str):
91
+ """
92
+ Set the scan date input.
93
+
94
+ :param str scan_date_input: The scan date input (string or datetime)
95
+ :return: The cleaned scan date in 'YYYY-MM-DD' format
96
+ :rtype: str
97
+ """
98
+
99
+ self.scan_date: str = self.clean_scan_date(scan_date_input)
100
+ logger.info(f"Setting scan date input to {self.scan_date}")
101
+
102
+ def get_scan_date(self) -> str:
103
+ """
104
+ Get the scan date in 'YYYY-MM-DD' format.
105
+
106
+ :return: The scan date as a string
107
+ :rtype: str
108
+ """
109
+ if self.scan_date:
110
+ return self.clean_scan_date(self.scan_date)
111
+ return get_current_datetime()
112
+
113
+ def create_scan_history(self) -> "regscale_models.ScanHistory":
114
+ """
115
+ Creates a new ScanHistory object for the current scan, using self.scan_date if available.
116
+
117
+ :param str scan_date: The date of the scan in 'YYYY-MM-DD' format
118
+ :return: A newly created ScanHistory object
119
+ :rtype: regscale_models.ScanHistory
120
+ """
121
+ scan_date = self.get_scan_date()
122
+ logger.info(f"Creating ScanHistory with scan_date: {scan_date}")
123
+ scan_history = regscale_models.ScanHistory(
124
+ parentId=self.plan_id,
125
+ parentModule=regscale_models.SecurityPlan.get_module_string(),
126
+ scanningTool=self.title,
127
+ scanDate=scan_date,
128
+ createdById=self.assessor_id,
129
+ tenantsId=self.tenant_id,
130
+ vLow=0,
131
+ vMedium=0,
132
+ vHigh=0,
133
+ vCritical=0,
134
+ ).create()
135
+
136
+ count = 0
137
+ regscale_models.ScanHistory.delete_object_cache(scan_history)
138
+ while not regscale_models.ScanHistory.get_object(object_id=scan_history.id) and count < 10:
139
+ logger.info("Waiting for ScanHistory to be created...")
140
+ time.sleep(1)
141
+ count += 1
142
+ regscale_models.ScanHistory.delete_object_cache(scan_history)
143
+ return scan_history
144
+
145
+ @staticmethod
146
+ def clean_scan_date(date_input: Optional[Union[str, datetime]]) -> Optional[str]:
147
+ """
148
+ Convert a date (string or datetime object) to a JSON-serializable string.
149
+
150
+ Args:
151
+ date_input: A date as a string (e.g., '2025-02-01') or datetime object, or None.
152
+
153
+ Returns:
154
+ A string in 'YYYY-MM-DD' format if date_input is valid, otherwise None.
155
+
156
+ Examples:
157
+ >>> to_json_date('2025-02-01')
158
+ '2025-02-01'
159
+ >>> to_json_date(datetime(2025, 2, 1))
160
+ '2025-02-01'
161
+ >>> to_json_date(None)
162
+ None
163
+ """
164
+ if date_input is None:
165
+ return None
166
+ if isinstance(date_input, datetime):
167
+ return date_input.strftime("%Y-%m-%d")
168
+ if isinstance(date_input, str):
169
+ try:
170
+ datetime.strptime(date_input, "%Y-%m-%d")
171
+ return date_input
172
+ except ValueError:
173
+ # Try parsing other common formats if needed
174
+ try:
175
+ try:
176
+ dt = datetime.strptime(date_input, "%Y-%m-%dT%H:%M:%S") # e.g., '2025-01-29T00:43:04'
177
+ except ValueError:
178
+ dt = datetime.strptime(date_input, "%Y-%m-%dT%H:%M:%S%z") # e.g., '2025-01-29T00:43:51+0000'
179
+ return dt.strftime("%Y-%m-%d")
180
+ except ValueError:
181
+ return None
182
+ return None
183
+
83
184
  def _load_mapping(self) -> Optional[Mapping]:
84
185
  """Load the mapping configuration from a JSON file."""
85
186
  try:
@@ -435,8 +536,9 @@ class JSONLScannerIntegration(ScannerIntegration):
435
536
  logger.info(f"Processing file: {file}")
436
537
  self._process_asset(file, data, assets_file, asset_tracker)
437
538
  self._process_findings(file, data, findings_file, asset_tracker.existing, finding_tracker)
438
- except Exception as e:
439
- logger.error(f"Error processing file {file}: {str(e)}")
539
+ except Exception:
540
+ error_message = traceback.format_exc()
541
+ logger.error(f"Error processing file {file}: {str(error_message)}")
440
542
 
441
543
  def _process_asset(
442
544
  self,
@@ -723,6 +825,7 @@ class JSONLScannerIntegration(ScannerIntegration):
723
825
  file_path=file_path,
724
826
  use_jsonl_file=True,
725
827
  asset_count=total_assets,
828
+ scan_date=self.scan_date,
726
829
  )
727
830
 
728
831
  logger.info("Syncing %d findings to RegScale", total_findings)
@@ -731,6 +834,7 @@ class JSONLScannerIntegration(ScannerIntegration):
731
834
  file_path=file_path,
732
835
  use_jsonl_file=True,
733
836
  finding_count=total_findings,
837
+ scan_date=self.scan_date,
734
838
  )
735
839
 
736
840
  logger.info("Assets and findings sync complete")
@@ -3,7 +3,7 @@
3
3
  """standard python imports"""
4
4
  import glob
5
5
  import logging
6
- from datetime import datetime, date
6
+ from datetime import date, datetime
7
7
  from typing import Literal, Optional
8
8
 
9
9
  import click
@@ -432,11 +432,11 @@ def import_drf(file_path: click.Path, regscale_id: int, regscale_module: str) ->
432
432
  required=True,
433
433
  )
434
434
  @click.option(
435
- "--regscale_ssp_id",
436
- "-i",
435
+ "--profile_id",
436
+ "-p",
437
437
  type=click.INT,
438
- help="The ID number from RegScale of the System Security Plan.",
439
- prompt="Enter RegScale System Security Plan ID",
438
+ help="The ID number from RegScale of the Profile. (This will generate the control implementations for a new Security Plan)",
439
+ prompt="Enter RegScale Profile ID",
440
440
  required=True,
441
441
  )
442
442
  @click.option(
@@ -459,11 +459,11 @@ def import_ciscrm(
459
459
  version: str,
460
460
  cis_sheet_name: str,
461
461
  crm_sheet_name: Optional[click.STRING],
462
- regscale_ssp_id: int,
462
+ profile_id: int,
463
463
  leveraged_auth_id: int = 0,
464
464
  ):
465
465
  """
466
- [BETA] Import FedRAMP Rev5 CIS/CRM Workbook into a RegScale System Security Plan.
466
+ [BETA] Import FedRAMP Rev5 CIS/CRM Workbook into a new RegScale System Security Plan.
467
467
  """
468
468
 
469
469
  from regscale.integrations.public.fedramp.fedramp_cis_crm import parse_and_import_ciscrm
@@ -474,6 +474,6 @@ def import_ciscrm(
474
474
  version=version_literal, # type: ignore
475
475
  cis_sheet_name=cis_sheet_name,
476
476
  crm_sheet_name=crm_sheet_name,
477
- regscale_ssp_id=regscale_ssp_id,
477
+ profile_id=profile_id,
478
478
  leveraged_auth_id=leveraged_auth_id,
479
479
  )