regscale-cli 6.27.1.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.
- regscale/_version.py +1 -1
- regscale/core/app/application.py +1 -0
- regscale/core/app/internal/control_editor.py +73 -21
- regscale/core/app/internal/login.py +4 -1
- regscale/core/app/internal/model_editor.py +219 -64
- regscale/core/app/utils/app_utils.py +41 -7
- regscale/core/login.py +21 -4
- regscale/core/utils/date.py +77 -1
- regscale/integrations/commercial/aws/scanner.py +7 -3
- regscale/integrations/commercial/microsoft_defender/defender_api.py +1 -1
- regscale/integrations/commercial/sicura/api.py +65 -29
- regscale/integrations/commercial/sicura/scanner.py +36 -7
- regscale/integrations/commercial/synqly/query_builder.py +4 -1
- regscale/integrations/commercial/tenablev2/commands.py +4 -4
- regscale/integrations/commercial/tenablev2/scanner.py +1 -2
- regscale/integrations/commercial/wizv2/scanner.py +40 -16
- regscale/integrations/control_matcher.py +78 -23
- regscale/integrations/public/cci_importer.py +400 -9
- regscale/integrations/public/csam/csam.py +572 -763
- regscale/integrations/public/csam/csam_agency_defined.py +179 -0
- regscale/integrations/public/csam/csam_common.py +154 -0
- regscale/integrations/public/csam/csam_controls.py +432 -0
- regscale/integrations/public/csam/csam_poam.py +124 -0
- regscale/integrations/public/fedramp/click.py +17 -4
- regscale/integrations/public/fedramp/fedramp_cis_crm.py +271 -62
- regscale/integrations/public/fedramp/poam/scanner.py +74 -7
- regscale/integrations/scanner_integration.py +16 -1
- regscale/models/integration_models/aqua.py +2 -2
- regscale/models/integration_models/cisa_kev_data.json +121 -18
- regscale/models/integration_models/flat_file_importer/__init__.py +4 -6
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/integration_models/synqly_models/connectors/vulnerabilities.py +35 -2
- regscale/models/integration_models/synqly_models/ocsf_mapper.py +41 -12
- regscale/models/platform.py +3 -0
- regscale/models/regscale_models/__init__.py +5 -0
- regscale/models/regscale_models/component.py +1 -1
- regscale/models/regscale_models/control_implementation.py +55 -24
- regscale/models/regscale_models/organization.py +3 -0
- regscale/models/regscale_models/regscale_model.py +17 -5
- regscale/models/regscale_models/security_plan.py +1 -0
- regscale/regscale.py +11 -1
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/RECORD +53 -49
- tests/regscale/core/test_login.py +171 -4
- tests/regscale/integrations/commercial/test_sicura.py +0 -1
- tests/regscale/integrations/commercial/wizv2/test_wizv2.py +86 -0
- tests/regscale/integrations/public/test_cci.py +596 -1
- tests/regscale/integrations/test_control_matcher.py +24 -0
- tests/regscale/models/test_control_implementation.py +118 -3
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.27.1.0.dist-info → regscale_cli-6.27.3.0.dist-info}/top_level.txt +0 -0
|
@@ -82,6 +82,30 @@ class Vulnerabilities(SynqlyModel):
|
|
|
82
82
|
severity_filter = f"severity[in]{','.join(mapped_severities)}"
|
|
83
83
|
return severity_filter
|
|
84
84
|
|
|
85
|
+
@staticmethod
|
|
86
|
+
def _translate_asset_filter(replace: str, replace_with: str, asset_filters: Optional[list[str]]) -> list[str]:
|
|
87
|
+
"""
|
|
88
|
+
Translate asset filters to the correct format for the integration
|
|
89
|
+
|
|
90
|
+
:param str replace: The string to replace
|
|
91
|
+
:param str replace_with: The string to replace with
|
|
92
|
+
:param list[str] asset_filters: The asset filters to translate
|
|
93
|
+
:return: The translated asset filters
|
|
94
|
+
:rtype: list[str]
|
|
95
|
+
"""
|
|
96
|
+
translated_asset_filters = []
|
|
97
|
+
for asset_filter in asset_filters:
|
|
98
|
+
# Remove outer double quotes if present
|
|
99
|
+
cleaned_filter = asset_filter
|
|
100
|
+
if cleaned_filter.startswith('"') and cleaned_filter.endswith('"') and len(cleaned_filter) > 1:
|
|
101
|
+
cleaned_filter = cleaned_filter[1:-1]
|
|
102
|
+
|
|
103
|
+
if replace_with in cleaned_filter:
|
|
104
|
+
translated_asset_filters.append(cleaned_filter)
|
|
105
|
+
elif replace in cleaned_filter:
|
|
106
|
+
translated_asset_filters.append(cleaned_filter.replace(replace, replace_with))
|
|
107
|
+
return translated_asset_filters
|
|
108
|
+
|
|
85
109
|
def _handle_scan_date_options(self, regscale_ssp_id: int, **kwargs) -> list[str]:
|
|
86
110
|
"""
|
|
87
111
|
Handle scan date options for the integration sync process
|
|
@@ -94,6 +118,13 @@ class Vulnerabilities(SynqlyModel):
|
|
|
94
118
|
|
|
95
119
|
vuln_filter = [self._build_severity_filter(kwargs.get("minimum_severity_filter"))]
|
|
96
120
|
|
|
121
|
+
if asset_filters := kwargs.get("filter", []):
|
|
122
|
+
vuln_filter.extend(
|
|
123
|
+
self._translate_asset_filter(
|
|
124
|
+
replace="device.", replace_with="resources.data.", asset_filters=asset_filters
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
|
|
97
128
|
if kwargs.get("all_scans"):
|
|
98
129
|
vuln_filter.append("finding.last_seen_time[gte]915148800") # Friday, January 1, 1999 12:00:00 AM UTC
|
|
99
130
|
elif scan_date := kwargs.get("scan_date"):
|
|
@@ -118,8 +149,10 @@ class Vulnerabilities(SynqlyModel):
|
|
|
118
149
|
self.logger.debug(f"Vulnerability filter: {vuln_filter}")
|
|
119
150
|
|
|
120
151
|
# Pop the filter from kwargs so it doesn't get passed to query_findings
|
|
121
|
-
asset_filter
|
|
122
|
-
|
|
152
|
+
if asset_filter := kwargs.pop("filter", []):
|
|
153
|
+
asset_filter = self._translate_asset_filter(
|
|
154
|
+
replace="resources.data.", replace_with="device.", asset_filters=asset_filter
|
|
155
|
+
)
|
|
123
156
|
self.logger.debug(f"Asset filter: {asset_filter}")
|
|
124
157
|
|
|
125
158
|
self.logger.info(f"Fetching asset data from {self.integration_name}...")
|
|
@@ -106,9 +106,8 @@ class Mapper:
|
|
|
106
106
|
regscale_issue.manualDetectionId = ticket.id
|
|
107
107
|
return regscale_issue
|
|
108
108
|
|
|
109
|
-
@staticmethod
|
|
110
109
|
def _ocsf_asset_to_regscale(
|
|
111
|
-
connector: Union["Edr", "Vulnerabilities"], asset: Union[InventoryInfo, OCSFAsset, SoftwareInfo], **kwargs
|
|
110
|
+
self, connector: Union["Edr", "Vulnerabilities"], asset: Union[InventoryInfo, OCSFAsset, SoftwareInfo], **kwargs
|
|
112
111
|
) -> IntegrationAsset:
|
|
113
112
|
"""
|
|
114
113
|
Convert OCSF Asset to RegScale Asset
|
|
@@ -134,6 +133,9 @@ class Mapper:
|
|
|
134
133
|
else:
|
|
135
134
|
name = device_data.name or device_data.hostname or f"{connector.provider} Asset: {device_data.uid}"
|
|
136
135
|
category = AssetCategory.Software
|
|
136
|
+
ip_v4s, ip_v6s = self._determine_ip_addresses(
|
|
137
|
+
device_data.ip_addresses if device_data.ip_addresses else [device_data.ip]
|
|
138
|
+
)
|
|
137
139
|
return IntegrationAsset(
|
|
138
140
|
name=name,
|
|
139
141
|
identifier=device_data.uid,
|
|
@@ -143,7 +145,8 @@ class Mapper:
|
|
|
143
145
|
parent_module=SecurityPlan.get_module_string(),
|
|
144
146
|
mac_address=device_data.mac,
|
|
145
147
|
fqdn=device_data.hostname,
|
|
146
|
-
ip_address=", ".join(
|
|
148
|
+
ip_address=", ".join(ip_v4s),
|
|
149
|
+
ipv6_address=", ".join(ip_v6s),
|
|
147
150
|
location=device_data.location or device_data.zone,
|
|
148
151
|
vlan_id=device_data.vlan_uid,
|
|
149
152
|
other_tracking_number=device_data.uid,
|
|
@@ -155,6 +158,31 @@ class Mapper:
|
|
|
155
158
|
software_inventory=software_inventory,
|
|
156
159
|
)
|
|
157
160
|
|
|
161
|
+
@staticmethod
|
|
162
|
+
def _determine_ip_addresses(ips: list[str]) -> tuple[list[str], list[str]]:
|
|
163
|
+
"""
|
|
164
|
+
Parse the list of ips and return a tuple of two lists: list of IP v4 and IP v6 addresses
|
|
165
|
+
|
|
166
|
+
:param list[str] ips: List of IP addresses
|
|
167
|
+
:return: Tuple containing two lists, list of IP v4 addresses and list of IP v6 addresses
|
|
168
|
+
:rtype: tuple[list[str], list[str]]
|
|
169
|
+
"""
|
|
170
|
+
import ipaddress
|
|
171
|
+
|
|
172
|
+
ip_v4s = []
|
|
173
|
+
ip_v6s = []
|
|
174
|
+
for ip in ips:
|
|
175
|
+
try:
|
|
176
|
+
ipaddress.IPv4Address(ip)
|
|
177
|
+
ip_v4s.append(ip)
|
|
178
|
+
except ipaddress.AddressValueError:
|
|
179
|
+
try:
|
|
180
|
+
ipaddress.IPv6Address(ip)
|
|
181
|
+
ip_v6s.append(ip)
|
|
182
|
+
except ipaddress.AddressValueError:
|
|
183
|
+
continue
|
|
184
|
+
return ip_v4s, ip_v6s
|
|
185
|
+
|
|
158
186
|
@staticmethod
|
|
159
187
|
def _determine_date(
|
|
160
188
|
attribute: str, finding: Optional["Finding"] = None, vuln: Optional["Vulnerability"] = None
|
|
@@ -205,6 +233,7 @@ class Mapper:
|
|
|
205
233
|
"plugin_id": vuln.cve.uid if vuln else finding.finding.product_uid,
|
|
206
234
|
"severity": vuln.severity if vuln and getattr(vuln, "severity") else finding.severity_id,
|
|
207
235
|
"remediation": getattr(vuln, "remediation") if vuln else finding.finding.remediation.desc,
|
|
236
|
+
"title": getattr(vuln, "title") or finding.finding.title,
|
|
208
237
|
}
|
|
209
238
|
if vuln:
|
|
210
239
|
finding_data["cve"] = vuln.cve.uid
|
|
@@ -267,28 +296,28 @@ class Mapper:
|
|
|
267
296
|
"""
|
|
268
297
|
base = vuln if vuln else finding.finding
|
|
269
298
|
finding_data = self._parse_finding_data(finding, vuln)
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
299
|
+
resource_data = getattr(resource, "data", {})
|
|
300
|
+
dns = resource_data.get("hostname") or resource.uid if vuln else None
|
|
301
|
+
ip_v4s, ip_v6s = self._determine_ip_addresses(
|
|
302
|
+
resource_data["ipAddresses"] if resource_data.get("ipAddresses") else [resource_data.get("ip")]
|
|
303
|
+
)
|
|
304
|
+
ips = ip_v4s + ip_v6s
|
|
276
305
|
|
|
277
306
|
finding_obj = IntegrationFinding(
|
|
278
307
|
control_labels=[],
|
|
279
308
|
category=f"{connector.integration_name} Vulnerability",
|
|
280
|
-
title=
|
|
309
|
+
title=finding_data["title"],
|
|
281
310
|
plugin_name=connector.integration_name,
|
|
282
311
|
severity=Issue.assign_severity(finding.severity), # type: ignore
|
|
283
312
|
description=base.desc,
|
|
284
313
|
status=finding.status or "Open",
|
|
285
314
|
first_seen=self._datetime_to_str(finding_data["first_seen"]),
|
|
286
315
|
last_seen=self._datetime_to_str(finding_data["last_seen"]),
|
|
287
|
-
ip_address=
|
|
316
|
+
ip_address=", ".join(ips),
|
|
288
317
|
plugin_id=finding_data["plugin_id"],
|
|
289
318
|
dns=dns,
|
|
290
319
|
severity_int=finding_data["severity"],
|
|
291
|
-
issue_title=
|
|
320
|
+
issue_title=finding_data["title"],
|
|
292
321
|
cve=finding_data["cve"],
|
|
293
322
|
evidence=finding.evidence,
|
|
294
323
|
impact=finding.impact,
|
regscale/models/platform.py
CHANGED
|
@@ -44,6 +44,7 @@ class RegScaleAuth(BaseModel):
|
|
|
44
44
|
password: Optional[Union[str, SecretStr]] = None,
|
|
45
45
|
domain: Optional[str] = None,
|
|
46
46
|
mfa_token: Optional[str] = None,
|
|
47
|
+
app_id: Optional[int] = 1,
|
|
47
48
|
) -> "RegScaleAuth":
|
|
48
49
|
"""
|
|
49
50
|
Authenticate with RegScale and return a token and a user_id
|
|
@@ -52,6 +53,7 @@ class RegScaleAuth(BaseModel):
|
|
|
52
53
|
:param Optional[Union[str, SecretStr]] password: Password to log in with, defaults to None
|
|
53
54
|
:param Optional[str] domain: Domain to log into, defaults to None
|
|
54
55
|
:param Optional[str] mfa_token: mfa_token to verify, defaults to None
|
|
56
|
+
:param Optional[int] app_id: The app ID to login with
|
|
55
57
|
:return: RegScaleAuth object
|
|
56
58
|
:rtype: RegScaleAuth
|
|
57
59
|
"""
|
|
@@ -88,6 +90,7 @@ class RegScaleAuth(BaseModel):
|
|
|
88
90
|
password=password.get_secret_value(),
|
|
89
91
|
domain=domain,
|
|
90
92
|
mfa_token=mfa_token,
|
|
93
|
+
app_id=app_id,
|
|
91
94
|
)
|
|
92
95
|
return cls(
|
|
93
96
|
user_id=uid,
|
|
@@ -51,6 +51,8 @@ from .link import *
|
|
|
51
51
|
from .master_assessment import *
|
|
52
52
|
from .milestone import *
|
|
53
53
|
from .meta_data import *
|
|
54
|
+
from .module import *
|
|
55
|
+
from .modules import *
|
|
54
56
|
from .objective import *
|
|
55
57
|
from .organization import *
|
|
56
58
|
from .parameter import *
|
|
@@ -81,10 +83,13 @@ from .stig import *
|
|
|
81
83
|
from .supply_chain import *
|
|
82
84
|
from .system_role import *
|
|
83
85
|
from .system_role_external_assignment import *
|
|
86
|
+
from .tag import *
|
|
87
|
+
from .tag_mapping import *
|
|
84
88
|
from .task import *
|
|
85
89
|
from .threat import *
|
|
86
90
|
from .team import *
|
|
87
91
|
from .user import *
|
|
92
|
+
from .user_group import *
|
|
88
93
|
from .vulnerability import *
|
|
89
94
|
from .vulnerability_mapping import *
|
|
90
95
|
from .workflow import *
|
|
@@ -76,7 +76,7 @@ class Component(RegScaleModel):
|
|
|
76
76
|
externalId: Optional[str] = None
|
|
77
77
|
isPublic: bool = True
|
|
78
78
|
riskCategorization: Optional[str] = None
|
|
79
|
-
complianceSettingsId: Optional[int] =
|
|
79
|
+
complianceSettingsId: Optional[int] = 1
|
|
80
80
|
|
|
81
81
|
@staticmethod
|
|
82
82
|
def _get_additional_endpoints() -> ConfigDict:
|
|
@@ -489,15 +489,15 @@ class ControlImplementation(RegScaleModel):
|
|
|
489
489
|
:return: A dictionary mapping control IDs to implementation IDs
|
|
490
490
|
:rtype: Dict[str, int]
|
|
491
491
|
"""
|
|
492
|
-
logger.
|
|
492
|
+
logger.debug("Getting control label map by parent...")
|
|
493
493
|
response = cls._get_api_handler().get(
|
|
494
494
|
endpoint=cls.get_endpoint("get_all_by_parent").format(intParentID=parent_id, strModule=parent_module)
|
|
495
495
|
)
|
|
496
496
|
|
|
497
497
|
if response and response.ok:
|
|
498
|
-
logger.
|
|
498
|
+
logger.debug("Fetched control label map by parent successfully.")
|
|
499
499
|
return {parentheses_to_dot(ci["controlName"]): ci["id"] for ci in response.json()}
|
|
500
|
-
logger.
|
|
500
|
+
logger.debug("Unable to get control label map by parent.")
|
|
501
501
|
return {}
|
|
502
502
|
|
|
503
503
|
@classmethod
|
|
@@ -509,14 +509,14 @@ class ControlImplementation(RegScaleModel):
|
|
|
509
509
|
:return: A dictionary mapping control IDs to implementation IDs
|
|
510
510
|
:rtype: Dict[int, int]
|
|
511
511
|
"""
|
|
512
|
-
logger.
|
|
512
|
+
logger.debug("Getting control id map by parent...")
|
|
513
513
|
response = cls._get_api_handler().get(
|
|
514
514
|
endpoint=cls.get_endpoint("get_all_by_parent").format(intParentID=parent_id, strModule=parent_module)
|
|
515
515
|
)
|
|
516
516
|
if response and response.ok:
|
|
517
|
-
logger.
|
|
517
|
+
logger.debug("Fetched control id map by parent successfully.")
|
|
518
518
|
return {ci["controlID"]: ci["id"] for ci in response.json()}
|
|
519
|
-
logger.
|
|
519
|
+
logger.debug("Unable to get control id map by parent.")
|
|
520
520
|
return {}
|
|
521
521
|
|
|
522
522
|
@classmethod
|
|
@@ -1218,6 +1218,8 @@ class ControlImplementation(RegScaleModel):
|
|
|
1218
1218
|
:return: list GraphQL response from RegScale
|
|
1219
1219
|
:rtype: list
|
|
1220
1220
|
"""
|
|
1221
|
+
from regscale.core.app.internal.control_editor import _extract_control_owner_display
|
|
1222
|
+
|
|
1221
1223
|
body = """
|
|
1222
1224
|
query{
|
|
1223
1225
|
controlImplementations (skip: 0, take: 50, where: {parentId: {eq: parent_id} parentModule: {eq: "parent_module"}}) {
|
|
@@ -1263,22 +1265,25 @@ class ControlImplementation(RegScaleModel):
|
|
|
1263
1265
|
moded_item = {}
|
|
1264
1266
|
moded_item["id"] = item["id"]
|
|
1265
1267
|
moded_item["controlID"] = item["controlID"]
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
moded_item["
|
|
1281
|
-
moded_item["
|
|
1268
|
+
|
|
1269
|
+
# Extract control owner display using centralized method
|
|
1270
|
+
moded_item["controlOwnerId"] = _extract_control_owner_display(item)
|
|
1271
|
+
|
|
1272
|
+
# Handle case where control or its fields might be None
|
|
1273
|
+
if item.get("control") and item["control"] is not None:
|
|
1274
|
+
moded_item["controlName"] = item["control"].get("controlId", "")
|
|
1275
|
+
moded_item["controlTitle"] = item["control"].get("title", "")
|
|
1276
|
+
moded_item["description"] = item["control"].get("description", "")
|
|
1277
|
+
else:
|
|
1278
|
+
moded_item["controlName"] = ""
|
|
1279
|
+
moded_item["controlTitle"] = ""
|
|
1280
|
+
moded_item["description"] = ""
|
|
1281
|
+
|
|
1282
|
+
moded_item["status"] = item.get("status", "")
|
|
1283
|
+
moded_item["policy"] = item.get("policy", "")
|
|
1284
|
+
moded_item["implementation"] = item.get("implementation", "")
|
|
1285
|
+
moded_item["responsibility"] = item.get("responsibility", "")
|
|
1286
|
+
moded_item["inheritable"] = item.get("inheritable", False)
|
|
1282
1287
|
moded_data.append(moded_item)
|
|
1283
1288
|
return moded_data
|
|
1284
1289
|
return []
|
|
@@ -1351,8 +1356,34 @@ class ControlImplementation(RegScaleModel):
|
|
|
1351
1356
|
:return: list of control implementations, or None if not found
|
|
1352
1357
|
:rtype: Optional[list[dict]]
|
|
1353
1358
|
"""
|
|
1354
|
-
|
|
1355
|
-
|
|
1359
|
+
query = {
|
|
1360
|
+
"parentID": 0,
|
|
1361
|
+
"module": "",
|
|
1362
|
+
"friendlyName": "",
|
|
1363
|
+
"workbench": "",
|
|
1364
|
+
"base": "",
|
|
1365
|
+
"sort": "sortId",
|
|
1366
|
+
"direction": "Ascending",
|
|
1367
|
+
"simpleSearch": "",
|
|
1368
|
+
"page": 1,
|
|
1369
|
+
"pageSize": 1000,
|
|
1370
|
+
"query": {
|
|
1371
|
+
"id": 0,
|
|
1372
|
+
"viewName": "",
|
|
1373
|
+
"module": "",
|
|
1374
|
+
"scope": "",
|
|
1375
|
+
"createdById": "",
|
|
1376
|
+
"dateCreated": None,
|
|
1377
|
+
"parameters": [],
|
|
1378
|
+
},
|
|
1379
|
+
"groupBy": "",
|
|
1380
|
+
"intDays": 0,
|
|
1381
|
+
"subTab": True,
|
|
1382
|
+
}
|
|
1383
|
+
query["parentId"] = regscale_id
|
|
1384
|
+
query["module"] = regscale_module
|
|
1385
|
+
endpoint = cls.get_endpoint("filter_control_implementations")
|
|
1386
|
+
response = cls._get_api_handler().post(endpoint=endpoint, data=query)
|
|
1356
1387
|
if response and response.ok:
|
|
1357
1388
|
return response.json()
|
|
1358
1389
|
return None
|
|
@@ -15,7 +15,10 @@ class Organization(RegScaleModel):
|
|
|
15
15
|
id: int = 0
|
|
16
16
|
name: Optional[str] = ""
|
|
17
17
|
description: Optional[str] = ""
|
|
18
|
+
orgCode: Optional[str] = ""
|
|
19
|
+
orgUrl: Optional[str] = ""
|
|
18
20
|
status: Optional[str] = "Active"
|
|
21
|
+
externalId: Optional[str] = ""
|
|
19
22
|
|
|
20
23
|
@staticmethod
|
|
21
24
|
def _get_additional_endpoints() -> dict:
|
|
@@ -1258,17 +1258,18 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1258
1258
|
return ConfigDict()
|
|
1259
1259
|
|
|
1260
1260
|
@classmethod
|
|
1261
|
-
def get_endpoint(cls, endpoint_type: str) -> str:
|
|
1261
|
+
def get_endpoint(cls, endpoint_type: str, suppress_error: bool = False) -> str:
|
|
1262
1262
|
"""
|
|
1263
1263
|
Get the endpoint for a specific type.
|
|
1264
1264
|
|
|
1265
1265
|
:param str endpoint_type: The type of endpoint
|
|
1266
|
+
:param bool suppress_error: Whether to suppress the error if the endpoint is not found, defaults to False
|
|
1266
1267
|
:raises ValueError: If the endpoint type is not found
|
|
1267
1268
|
:return: The endpoint
|
|
1268
1269
|
:rtype: str
|
|
1269
1270
|
"""
|
|
1270
1271
|
endpoint = cls._get_endpoints().get(endpoint_type, "na") # noqa
|
|
1271
|
-
if not endpoint or endpoint == "na":
|
|
1272
|
+
if not endpoint or endpoint == "na" and not suppress_error:
|
|
1272
1273
|
logger.error(f"{cls.__name__} does not have endpoint {endpoint_type}")
|
|
1273
1274
|
raise ValueError(f"Endpoint {endpoint_type} not found")
|
|
1274
1275
|
endpoint = str(endpoint).replace("{model_slug}", cls.get_module_slug())
|
|
@@ -1566,11 +1567,22 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1566
1567
|
f"[#f68d1f]Updating {total_items} RegScale {cls.__name__}s...",
|
|
1567
1568
|
total=total_items,
|
|
1568
1569
|
)
|
|
1570
|
+
endpoint = cls.get_endpoint("batch_update", suppress_error=True)
|
|
1571
|
+
if not endpoint or endpoint == "na":
|
|
1572
|
+
logger.debug(f"No batch_update endpoint found for {cls.__name__}, using save method instead")
|
|
1573
|
+
for item in items:
|
|
1574
|
+
updated_item = item.save()
|
|
1575
|
+
cls.cache_object(updated_item)
|
|
1576
|
+
results.append(updated_item)
|
|
1577
|
+
if progress and update_job is not None:
|
|
1578
|
+
progress.advance(update_job, advance=1)
|
|
1579
|
+
cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
|
|
1580
|
+
return results
|
|
1569
1581
|
for i in range(0, total_items, batch_size):
|
|
1570
1582
|
batch = items[i : i + batch_size]
|
|
1571
1583
|
batch_results = cls._handle_list_response(
|
|
1572
1584
|
cls._get_api_handler().put(
|
|
1573
|
-
endpoint=
|
|
1585
|
+
endpoint=endpoint,
|
|
1574
1586
|
data=[item.model_dump() for item in batch if item],
|
|
1575
1587
|
)
|
|
1576
1588
|
)
|
|
@@ -1583,10 +1595,10 @@ class RegScaleModel(BaseModel, ABC):
|
|
|
1583
1595
|
cls._check_and_remove_progress_object(progress_context, remove_progress_bar, update_job)
|
|
1584
1596
|
|
|
1585
1597
|
if progress_context:
|
|
1586
|
-
process_batch(progress_context)
|
|
1598
|
+
process_batch(progress=progress_context, remove_progress_bar=remove_progress)
|
|
1587
1599
|
else:
|
|
1588
1600
|
with create_progress_object() as create_progress:
|
|
1589
|
-
process_batch(create_progress)
|
|
1601
|
+
process_batch(progress=create_progress, remove_progress_bar=remove_progress)
|
|
1590
1602
|
|
|
1591
1603
|
return results
|
|
1592
1604
|
|
|
@@ -43,6 +43,7 @@ class SecurityPlan(RegScaleModel):
|
|
|
43
43
|
environment: Optional[str] = ""
|
|
44
44
|
lawsAndRegulations: Optional[str] = ""
|
|
45
45
|
authorizationBoundary: Optional[str] = ""
|
|
46
|
+
authorizationTerminationDate: Optional[str] = ""
|
|
46
47
|
networkArchitecture: Optional[str] = ""
|
|
47
48
|
dataFlow: Optional[str] = ""
|
|
48
49
|
overallCategorization: Optional[str] = ""
|
regscale/regscale.py
CHANGED
|
@@ -477,12 +477,21 @@ def validate_token():
|
|
|
477
477
|
cls=NotRequiredIf,
|
|
478
478
|
not_required_if=["token"],
|
|
479
479
|
)
|
|
480
|
+
@click.option(
|
|
481
|
+
"--app_id",
|
|
482
|
+
type=click.INT,
|
|
483
|
+
help="RegScale App ID to login with.",
|
|
484
|
+
default=1,
|
|
485
|
+
prompt=False,
|
|
486
|
+
required=False,
|
|
487
|
+
)
|
|
480
488
|
def login(
|
|
481
489
|
username: Optional[str],
|
|
482
490
|
password: Optional[str],
|
|
483
491
|
token: Optional[str] = None,
|
|
484
492
|
domain: Optional[str] = None,
|
|
485
493
|
mfa_token: Optional[str] = None,
|
|
494
|
+
app_id: Optional[int] = 1,
|
|
486
495
|
):
|
|
487
496
|
"""Logs the user into their RegScale instance."""
|
|
488
497
|
from regscale.core.app.application import Application
|
|
@@ -502,9 +511,10 @@ def login(
|
|
|
502
511
|
mfa_token=mfa_token,
|
|
503
512
|
app=app,
|
|
504
513
|
host=domain,
|
|
514
|
+
app_id=app_id,
|
|
505
515
|
)
|
|
506
516
|
else:
|
|
507
|
-
lg.login(str_user=username, str_password=password, app=app, host=domain)
|
|
517
|
+
lg.login(str_user=username, str_password=password, app=app, host=domain, app_id=app_id)
|
|
508
518
|
|
|
509
519
|
|
|
510
520
|
# Check the health of the RegScale Application
|