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
regscale/core/login.py
CHANGED
|
@@ -6,6 +6,7 @@ from os import getenv
|
|
|
6
6
|
from typing import Optional, Tuple
|
|
7
7
|
from urllib.parse import urljoin
|
|
8
8
|
|
|
9
|
+
from requests.exceptions import HTTPError
|
|
9
10
|
from regscale.core.app.api import Api
|
|
10
11
|
from regscale.core.app.utils.app_utils import error_and_exit
|
|
11
12
|
|
|
@@ -14,10 +15,11 @@ logger = logging.getLogger("regscale")
|
|
|
14
15
|
|
|
15
16
|
def get_regscale_token(
|
|
16
17
|
api: Api,
|
|
17
|
-
username: str = getenv("REGSCALE_USER"),
|
|
18
|
-
password: str = getenv("REGSCALE_PASSWORD"),
|
|
19
|
-
domain: str = getenv("REGSCALE_DOMAIN"),
|
|
18
|
+
username: Optional[str] = getenv("REGSCALE_USER"),
|
|
19
|
+
password: Optional[str] = getenv("REGSCALE_PASSWORD"),
|
|
20
|
+
domain: Optional[str] = getenv("REGSCALE_DOMAIN"),
|
|
20
21
|
mfa_token: Optional[str] = "",
|
|
22
|
+
app_id: Optional[int] = 1,
|
|
21
23
|
) -> Tuple[str, str]:
|
|
22
24
|
"""
|
|
23
25
|
Authenticate with RegScale and return a token
|
|
@@ -27,6 +29,7 @@ def get_regscale_token(
|
|
|
27
29
|
:param str password: a string defaulting to the envar REGSCALE_PASSWORD
|
|
28
30
|
:param str domain: a string representing the RegScale domain, checks environment REGSCALE_DOMAIN
|
|
29
31
|
:param Optional[str] mfa_token: MFA token to login with
|
|
32
|
+
:param Optional[int] app_id: The app ID to login with
|
|
30
33
|
:raises EnvironmentError: if domain is not passed or retrieved
|
|
31
34
|
:return: a tuple of user_id and auth_token
|
|
32
35
|
:rtype: Tuple[str, str]
|
|
@@ -47,7 +50,19 @@ def get_regscale_token(
|
|
|
47
50
|
logger.info("Logging into: %s", domain)
|
|
48
51
|
# suggest structuring the login paths so that they all exist in one place
|
|
49
52
|
url = urljoin(domain, "/api/authentication/login")
|
|
50
|
-
|
|
53
|
+
try:
|
|
54
|
+
# Try to authenticate with the new API version
|
|
55
|
+
auth["appId"] = app_id
|
|
56
|
+
response = api.post(url=url, json=auth, headers={"X-Api-Version": "2.0"})
|
|
57
|
+
if response is None:
|
|
58
|
+
raise HTTPError("No response received from api.post(). Possible connection issue or internal error.")
|
|
59
|
+
response.raise_for_status()
|
|
60
|
+
app_id_compatible = True
|
|
61
|
+
except HTTPError:
|
|
62
|
+
# Fallback to the old API version
|
|
63
|
+
del auth["appId"]
|
|
64
|
+
response = api.post(url=url, json=auth, headers={})
|
|
65
|
+
app_id_compatible = False
|
|
51
66
|
error_msg = "Unable to authenticate with RegScale. Please check your credentials."
|
|
52
67
|
if response is None:
|
|
53
68
|
logger.error("No response received from api.post(). Possible connection issue or internal error.")
|
|
@@ -63,4 +78,6 @@ def get_regscale_token(
|
|
|
63
78
|
error_and_exit(f"{error_msg}\n{response.status_code}: {response.text}")
|
|
64
79
|
if isinstance(response_dict, str):
|
|
65
80
|
response_dict = json.loads(response_dict)
|
|
81
|
+
if app_id_compatible:
|
|
82
|
+
return response_dict["accessToken"]["id"], response_dict["accessToken"]["authToken"]
|
|
66
83
|
return response_dict["id"], response_dict["auth_token"]
|
regscale/core/utils/date.py
CHANGED
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
# -*- coding: utf-8 -*-
|
|
3
3
|
"""Utility functions for handling date and datetime conversions"""
|
|
4
4
|
|
|
5
|
+
import calendar
|
|
5
6
|
import datetime
|
|
6
7
|
import logging
|
|
8
|
+
import re
|
|
7
9
|
from typing import Any, List, Optional, Union
|
|
8
10
|
|
|
9
11
|
|
|
@@ -39,7 +41,8 @@ def date_str(date_object: Union[str, datetime.datetime, datetime.date, None], da
|
|
|
39
41
|
return date_object.strftime(date_format)
|
|
40
42
|
|
|
41
43
|
return date_object.isoformat()
|
|
42
|
-
except
|
|
44
|
+
except (AttributeError, TypeError, ValueError) as e:
|
|
45
|
+
logger.debug(f"Error converting date object to string: {e}")
|
|
43
46
|
return ""
|
|
44
47
|
|
|
45
48
|
|
|
@@ -82,6 +85,7 @@ def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None])
|
|
|
82
85
|
def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.datetime]:
|
|
83
86
|
"""
|
|
84
87
|
Convert a string, datetime, date, integer, or timestamp string to a datetime object.
|
|
88
|
+
If the day of the month is invalid (e.g., November 31), adjusts to the last valid day of that month.
|
|
85
89
|
|
|
86
90
|
:param Union[str, datetime.datetime, datetime.date, int, None] date_str: The value to convert.
|
|
87
91
|
:return: The datetime object.
|
|
@@ -93,6 +97,10 @@ def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, Non
|
|
|
93
97
|
try:
|
|
94
98
|
return parse(date_str)
|
|
95
99
|
except ParserError as e:
|
|
100
|
+
# Try to fix invalid day of month (e.g., 2023/11/31 -> 2023/11/30)
|
|
101
|
+
if fixed_date := _fix_invalid_day_of_month(date_str):
|
|
102
|
+
return fixed_date
|
|
103
|
+
|
|
96
104
|
if date_str and str(date_str).lower() not in ["n/a", "none"]:
|
|
97
105
|
logger.warning(f"Warning could not parse date string: {date_str}\n{e}")
|
|
98
106
|
return None
|
|
@@ -105,6 +113,74 @@ def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, Non
|
|
|
105
113
|
return None
|
|
106
114
|
|
|
107
115
|
|
|
116
|
+
def _parse_date_components(match_groups: tuple) -> tuple[int, int, int]:
|
|
117
|
+
"""
|
|
118
|
+
Parse year, month, day from regex match groups.
|
|
119
|
+
|
|
120
|
+
:param tuple match_groups: Tuple of matched groups from regex
|
|
121
|
+
:return: Tuple of (year, month, day)
|
|
122
|
+
:rtype: tuple[int, int, int]
|
|
123
|
+
"""
|
|
124
|
+
if len(match_groups[0]) == 4: # First group is year (YYYY/MM/DD)
|
|
125
|
+
return int(match_groups[0]), int(match_groups[1]), int(match_groups[2])
|
|
126
|
+
# Last group is year (MM/DD/YYYY)
|
|
127
|
+
return int(match_groups[2]), int(match_groups[0]), int(match_groups[1])
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _adjust_invalid_day(year: int, month: int, day: int) -> Optional[datetime.datetime]:
|
|
131
|
+
"""
|
|
132
|
+
Adjust invalid day of month to last valid day.
|
|
133
|
+
|
|
134
|
+
:param int year: Year
|
|
135
|
+
:param int month: Month
|
|
136
|
+
:param int day: Day
|
|
137
|
+
:return: Adjusted datetime or None if invalid
|
|
138
|
+
:rtype: Optional[datetime.datetime]
|
|
139
|
+
"""
|
|
140
|
+
last_valid_day = calendar.monthrange(year, month)[1]
|
|
141
|
+
if day <= last_valid_day:
|
|
142
|
+
return None
|
|
143
|
+
|
|
144
|
+
logger.warning(f"Invalid day {day} for month {month}/{year}. Adjusting to last valid day: {last_valid_day}")
|
|
145
|
+
try:
|
|
146
|
+
return datetime.datetime(year, month, last_valid_day)
|
|
147
|
+
except ValueError:
|
|
148
|
+
return None
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _fix_invalid_day_of_month(date_str: str) -> Optional[datetime.datetime]:
|
|
152
|
+
"""
|
|
153
|
+
Attempt to fix an invalid day of month in a date string.
|
|
154
|
+
For example, 2023/11/31 would become 2023/11/30.
|
|
155
|
+
|
|
156
|
+
:param str date_str: The date string to fix
|
|
157
|
+
:return: A datetime object with a valid day, or None if it can't be fixed
|
|
158
|
+
:rtype: Optional[datetime.datetime]
|
|
159
|
+
"""
|
|
160
|
+
try:
|
|
161
|
+
patterns = [
|
|
162
|
+
r"(\d{4})[/-](\d{1,2})[/-](\d{1,2})", # YYYY/MM/DD or YYYY-MM-DD
|
|
163
|
+
r"(\d{1,2})[/-](\d{1,2})[/-](\d{4})", # MM/DD/YYYY or MM-DD-YYYY
|
|
164
|
+
]
|
|
165
|
+
|
|
166
|
+
for pattern in patterns:
|
|
167
|
+
if not (match := re.search(pattern, date_str)):
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
year, month, day = _parse_date_components(match.groups())
|
|
171
|
+
|
|
172
|
+
if not (1 <= month <= 12):
|
|
173
|
+
continue
|
|
174
|
+
|
|
175
|
+
if result := _adjust_invalid_day(year, month, day):
|
|
176
|
+
return result
|
|
177
|
+
|
|
178
|
+
return None
|
|
179
|
+
except (ValueError, TypeError, AttributeError, IndexError) as e:
|
|
180
|
+
logger.debug(f"Could not fix invalid day of month for: {date_str} - {e}")
|
|
181
|
+
return None
|
|
182
|
+
|
|
183
|
+
|
|
108
184
|
def time_str(time_obj: Union[str, datetime.datetime, datetime.time]) -> str:
|
|
109
185
|
"""
|
|
110
186
|
Convert a datetime/time object to a string.
|
|
@@ -706,16 +706,20 @@ Description: {description if isinstance(description, str) else ''}"""
|
|
|
706
706
|
|
|
707
707
|
aws_secret_key_id = kwargs.get("aws_access_key_id") or os.getenv("AWS_ACCESS_KEY_ID")
|
|
708
708
|
aws_secret_access_key = kwargs.get("aws_secret_access_key") or os.getenv("AWS_SECRET_ACCESS_KEY")
|
|
709
|
-
region = kwargs.get("region") or os.getenv("AWS_REGION"
|
|
710
|
-
|
|
709
|
+
region = kwargs.get("region") or os.getenv("AWS_REGION")
|
|
710
|
+
profile = kwargs.get("profile")
|
|
711
|
+
|
|
712
|
+
# Profile or credentials required
|
|
713
|
+
if not profile and (not aws_secret_key_id or not aws_secret_access_key):
|
|
711
714
|
raise ValueError(
|
|
712
715
|
"AWS Access Key ID and Secret Access Key are required.\nPlease update in environment "
|
|
713
716
|
"variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY) or pass as arguments."
|
|
714
717
|
)
|
|
715
718
|
if not region:
|
|
716
719
|
logger.warning("AWS region not provided. Defaulting to 'us-east-1'.")
|
|
720
|
+
region = "us-east-1"
|
|
717
721
|
session = boto3.Session(
|
|
718
|
-
region_name=
|
|
722
|
+
region_name=region,
|
|
719
723
|
aws_access_key_id=aws_secret_key_id,
|
|
720
724
|
aws_secret_access_key=aws_secret_access_key,
|
|
721
725
|
aws_session_token=kwargs.get("aws_session_token"),
|
|
@@ -353,7 +353,7 @@ class DefenderApi:
|
|
|
353
353
|
|
|
354
354
|
data = self.get_items_from_azure(url=url, parse_value=kwargs.get("parse_value", True))
|
|
355
355
|
save_path = Path(
|
|
356
|
-
os.path.join(ENTRA_SAVE_DIR, f"azure_entra_{endpoint_key}_{get_current_datetime('%Y%m%d')}.
|
|
356
|
+
os.path.join(ENTRA_SAVE_DIR, f"azure_entra_{endpoint_key}_{get_current_datetime('%Y%m%d')}.xlsx")
|
|
357
357
|
)
|
|
358
358
|
save_data_to(file=save_path, data=data, transpose_data=False)
|
|
359
359
|
return [save_path]
|
|
@@ -203,7 +203,6 @@ class SicuraAPI:
|
|
|
203
203
|
FILTER_TYPE = "filter[type]"
|
|
204
204
|
FILTER_REJECTED = "filter[rejected]"
|
|
205
205
|
FILTER_TASK_ID = "filter[task_id]"
|
|
206
|
-
csrf_token: Optional[str] = None
|
|
207
206
|
|
|
208
207
|
def __init__(self):
|
|
209
208
|
"""
|
|
@@ -246,12 +245,9 @@ class SicuraAPI:
|
|
|
246
245
|
|
|
247
246
|
if data:
|
|
248
247
|
self.session.headers["Content-Type"] = "application/json"
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
if self.csrf_token:
|
|
253
|
-
self.session.headers["X-CSRF-TOKEN"] = str(self.csrf_token)
|
|
254
|
-
self.session.headers["auth-token-signature"] = SicuraVariables.sicuraToken
|
|
248
|
+
|
|
249
|
+
# Always set the auth token signature for API authentication
|
|
250
|
+
self.session.headers["auth-token-signature"] = SicuraVariables.sicuraToken
|
|
255
251
|
|
|
256
252
|
try:
|
|
257
253
|
# Use the session object to maintain cookies between requests
|
|
@@ -313,22 +309,6 @@ class SicuraAPI:
|
|
|
313
309
|
logging.error(f"Error validating response: {e}", exc_info=True)
|
|
314
310
|
return None
|
|
315
311
|
|
|
316
|
-
def get_csrf_token(self) -> Optional[AuthResponse]:
|
|
317
|
-
"""
|
|
318
|
-
Get authentication token from Sicura API.
|
|
319
|
-
|
|
320
|
-
:return: Authentication response
|
|
321
|
-
:rtype: Optional[AuthResponse]
|
|
322
|
-
:raises requests.exceptions.RequestException: If the request fails
|
|
323
|
-
"""
|
|
324
|
-
try:
|
|
325
|
-
response = self._make_request("GET", "/auth/token")
|
|
326
|
-
self.csrf_token = response
|
|
327
|
-
return response
|
|
328
|
-
except requests.exceptions.RequestException as e:
|
|
329
|
-
logger.error(f"Error getting authentication token: {e}", exc_info=True)
|
|
330
|
-
raise
|
|
331
|
-
|
|
332
312
|
class Device(SicuraModel):
|
|
333
313
|
"""Model for Sicura device information."""
|
|
334
314
|
|
|
@@ -384,11 +364,56 @@ class SicuraAPI:
|
|
|
384
364
|
logger.error(f"Failed to get devices: {e}", exc_info=True)
|
|
385
365
|
return []
|
|
386
366
|
|
|
367
|
+
def create_or_update_control_profile(self, profile_name: str, controls: list[dict]) -> Optional[dict]:
|
|
368
|
+
"""
|
|
369
|
+
Create or update a control profile.
|
|
370
|
+
|
|
371
|
+
:param str profile_name: Name of the control profile
|
|
372
|
+
:param list[dict] controls: List of controls to add to the profile
|
|
373
|
+
:return: The control profile if successfully created or updated, None otherwise
|
|
374
|
+
:rtype: Optional[dict]
|
|
375
|
+
"""
|
|
376
|
+
profile_id = None
|
|
377
|
+
profile_data = None
|
|
378
|
+
params = {"verbose": "true"}
|
|
379
|
+
try:
|
|
380
|
+
# see if the profile already exists
|
|
381
|
+
response = self._make_request("GET", "/api/jaeger/v1/control_profiles", params=params)
|
|
382
|
+
for profile in response:
|
|
383
|
+
if profile["name"] == profile_name:
|
|
384
|
+
profile_id = profile["id"]
|
|
385
|
+
break
|
|
386
|
+
payload = {
|
|
387
|
+
"name": profile_name,
|
|
388
|
+
"description": f"Profile for {profile_name} with {len(controls)} controls.",
|
|
389
|
+
"controls": controls,
|
|
390
|
+
}
|
|
391
|
+
if not profile_id:
|
|
392
|
+
crud_operation = "Created"
|
|
393
|
+
response = self._make_request("POST", "/api/jaeger/v1/control_profiles", data=payload, params=params)
|
|
394
|
+
profile_id = response
|
|
395
|
+
profile_data = self._make_request("GET", f"/api/jaeger/v1/control_profiles/{profile_id}", params=params)
|
|
396
|
+
else:
|
|
397
|
+
crud_operation = "Updated"
|
|
398
|
+
response = self._make_request(
|
|
399
|
+
"PUT", f"/api/jaeger/v1/control_profiles/{profile_id}", data=payload, params=params
|
|
400
|
+
)
|
|
401
|
+
profile_id = response["id"]
|
|
402
|
+
profile_data = response
|
|
403
|
+
logger.info(f"{crud_operation} control profile #{profile_id} in Sicura with {len(controls)} controls.")
|
|
404
|
+
return profile_data
|
|
405
|
+
|
|
406
|
+
return profile_id
|
|
407
|
+
except Exception as e:
|
|
408
|
+
logger.error(f"Failed to create or update control profile: {e}", exc_info=True)
|
|
409
|
+
return None
|
|
410
|
+
|
|
387
411
|
def create_scan_task(
|
|
388
412
|
self,
|
|
389
413
|
device_id: int,
|
|
390
414
|
platform: str,
|
|
391
|
-
profile: SicuraProfile,
|
|
415
|
+
profile: Union[SicuraProfile, str],
|
|
416
|
+
author: Optional[str] = None,
|
|
392
417
|
task_name: Optional[str] = None,
|
|
393
418
|
scheduled_time: Optional[datetime.datetime] = None,
|
|
394
419
|
) -> Optional[str]:
|
|
@@ -398,6 +423,7 @@ class SicuraAPI:
|
|
|
398
423
|
:param int device_id: ID of the device to scan
|
|
399
424
|
:param str platform: Platform name (e.g., 'Red Hat Enterprise Linux 9')
|
|
400
425
|
:param SicuraProfile profile: Scan profile name (e.g., 'I - Mission Critical Classified')
|
|
426
|
+
:param Optional[str] author: Author of the scan task (default: None)
|
|
401
427
|
:param Optional[str] task_name: Name for the scan task (default: auto-generated)
|
|
402
428
|
:param Optional[datetime.datetime] scheduled_time: When to run the scan (default: now)
|
|
403
429
|
:return: Task ID if successful, None otherwise
|
|
@@ -425,6 +451,9 @@ class SicuraAPI:
|
|
|
425
451
|
"scanAttributes": {"platform": platform, "profile": profile},
|
|
426
452
|
}
|
|
427
453
|
|
|
454
|
+
if author:
|
|
455
|
+
payload["scanAttributes"]["author"] = author
|
|
456
|
+
|
|
428
457
|
result = self._make_request("POST", "/api/jaeger/v1/tasks/", data=payload)
|
|
429
458
|
|
|
430
459
|
if result:
|
|
@@ -483,14 +512,16 @@ class SicuraAPI:
|
|
|
483
512
|
self,
|
|
484
513
|
fqdn: str,
|
|
485
514
|
platform: Optional[str] = None,
|
|
486
|
-
profile: SicuraProfile = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
|
|
515
|
+
profile: Union[SicuraProfile, str] = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
|
|
516
|
+
author: Optional[str] = None,
|
|
487
517
|
) -> Optional[ScanReport]:
|
|
488
518
|
"""
|
|
489
519
|
Get scan results for a specific device.
|
|
490
520
|
|
|
491
521
|
:param str fqdn: Fully qualified domain name of the device
|
|
492
522
|
:param Optional[str] platform: Platform name to filter results (e.g., 'Red Hat Enterprise Linux 9')
|
|
493
|
-
:param SicuraProfile profile: Profile name to filter results
|
|
523
|
+
:param Union[SicuraProfile, str] profile: Profile name to filter results, defaults to I - Mission Critical Classified
|
|
524
|
+
:param Optional[str] author: Author of the scan task (default: None)
|
|
494
525
|
:return: Scan report containing device info and scan results, or None if not found
|
|
495
526
|
:rtype: Optional[ScanReport]
|
|
496
527
|
"""
|
|
@@ -503,6 +534,9 @@ class SicuraAPI:
|
|
|
503
534
|
if profile:
|
|
504
535
|
params["profile"] = profile
|
|
505
536
|
|
|
537
|
+
if author:
|
|
538
|
+
params["author"] = author
|
|
539
|
+
|
|
506
540
|
response = self._make_request("GET", "/api/jaeger/v1/nodes", params=params)
|
|
507
541
|
|
|
508
542
|
# Handle 404 or empty response
|
|
@@ -560,7 +594,8 @@ class SicuraAPI:
|
|
|
560
594
|
task_id: Union[int, str],
|
|
561
595
|
fqdn: str,
|
|
562
596
|
platform: Optional[str] = None,
|
|
563
|
-
profile: SicuraProfile = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
|
|
597
|
+
profile: Union[SicuraProfile, str] = SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED,
|
|
598
|
+
author: Optional[str] = None,
|
|
564
599
|
max_wait_time: int = 600,
|
|
565
600
|
poll_interval: int = 10,
|
|
566
601
|
) -> Optional[Union[ScanReport, Dict[str, Any]]]:
|
|
@@ -570,7 +605,8 @@ class SicuraAPI:
|
|
|
570
605
|
:param Union[int, str] task_id: ID of the scan task to monitor
|
|
571
606
|
:param str fqdn: Fully qualified domain name of the device
|
|
572
607
|
:param Optional[str] platform: Platform name to filter results
|
|
573
|
-
:param SicuraProfile profile: Profile name to filter results
|
|
608
|
+
:param Union[SicuraProfile, str] profile: Profile name to filter results, defaults to I - Mission Critical Classified
|
|
609
|
+
:param Optional[str] author: Author of the scan task (default: None)
|
|
574
610
|
:param int max_wait_time: Maximum time to wait in seconds (default: 10 minutes)
|
|
575
611
|
:param int poll_interval: Time between status checks in seconds (default: 10 seconds)
|
|
576
612
|
:return: Scan results once the task is complete, or None if timeout or error
|
|
@@ -602,7 +638,7 @@ class SicuraAPI:
|
|
|
602
638
|
logger.info(f"Scan task {task_id} completed successfully, fetching results...")
|
|
603
639
|
# Wait a moment for results to be processed
|
|
604
640
|
time.sleep(2)
|
|
605
|
-
return self.get_scan_results(fqdn, platform, profile)
|
|
641
|
+
return self.get_scan_results(fqdn=fqdn, platform=platform, profile=profile, author=author)
|
|
606
642
|
else:
|
|
607
643
|
logger.error(f"Scan task {task_id} ended with status {latest_status}")
|
|
608
644
|
return None
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
RegScale Sicura Integration
|
|
5
5
|
"""
|
|
6
6
|
import datetime
|
|
7
|
-
from typing import Generator, Iterator,
|
|
7
|
+
from typing import Any, Generator, Iterator, Union
|
|
8
8
|
|
|
9
9
|
from regscale.core.utils.date import date_str
|
|
10
10
|
from regscale.integrations.commercial.sicura.api import SicuraAPI, ScanReport, SicuraProfile, Device, ScanResult
|
|
@@ -55,6 +55,8 @@ class SicuraIntegration(ScannerIntegration):
|
|
|
55
55
|
"""
|
|
56
56
|
super().__init__(*args, **kwargs)
|
|
57
57
|
self.api = SicuraAPI()
|
|
58
|
+
self.control_scan = False
|
|
59
|
+
self.control_scan_profile = None
|
|
58
60
|
|
|
59
61
|
def fetch_findings(self, **kwargs) -> Generator[IntegrationFinding, None, None]:
|
|
60
62
|
"""
|
|
@@ -103,23 +105,28 @@ class SicuraIntegration(ScannerIntegration):
|
|
|
103
105
|
return
|
|
104
106
|
|
|
105
107
|
# Profiles to scan
|
|
106
|
-
profiles = [SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED
|
|
108
|
+
profiles = [SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED]
|
|
109
|
+
if self.control_scan:
|
|
110
|
+
profiles = [self.control_scan_profile]
|
|
107
111
|
|
|
108
112
|
for profile in profiles:
|
|
109
113
|
yield from self._process_profile_findings(device, profile)
|
|
110
114
|
|
|
111
115
|
def _process_profile_findings(
|
|
112
|
-
self, device: Device, profile: SicuraProfile
|
|
116
|
+
self, device: Device, profile: Union[SicuraProfile, str]
|
|
113
117
|
) -> Generator[IntegrationFinding, None, None]:
|
|
114
118
|
"""
|
|
115
119
|
Process findings for a device with a specific profile
|
|
116
120
|
|
|
117
121
|
:param Device device: The device to process
|
|
118
|
-
:param SicuraProfile profile: The profile to process
|
|
122
|
+
:param Union[SicuraProfile, str] profile: The profile to process
|
|
119
123
|
:yield: IntegrationFinding objects
|
|
120
124
|
:rtype: Generator[IntegrationFinding, None, None]
|
|
121
125
|
"""
|
|
122
|
-
|
|
126
|
+
if self.control_scan and profile == self.control_scan_profile:
|
|
127
|
+
scan_report = self.api.get_scan_results(fqdn=device.fqdn, profile=profile, author="control")
|
|
128
|
+
else:
|
|
129
|
+
scan_report = self.api.get_scan_results(fqdn=device.fqdn, profile=profile)
|
|
123
130
|
|
|
124
131
|
if not scan_report:
|
|
125
132
|
logger.warning(f"No scan results found for device: {device.fqdn} with profile: {profile}")
|
|
@@ -458,17 +465,21 @@ class SicuraIntegration(ScannerIntegration):
|
|
|
458
465
|
:param Device device: The device to trigger a scan for
|
|
459
466
|
:return: None
|
|
460
467
|
"""
|
|
468
|
+
profile = self.control_scan_profile if self.control_scan else SicuraProfile.I_MISSION_CRITICAL_CLASSIFIED
|
|
469
|
+
author = "control" if self.control_scan else None
|
|
461
470
|
task_id = self.api.create_scan_task(
|
|
462
471
|
device_id=device.id,
|
|
463
472
|
platform=device.platforms,
|
|
464
|
-
profile=
|
|
473
|
+
profile=profile,
|
|
474
|
+
author=author,
|
|
465
475
|
)
|
|
466
476
|
if task_id:
|
|
467
477
|
self.api.wait_for_scan_results(
|
|
468
478
|
task_id=task_id,
|
|
469
479
|
fqdn=device.fqdn,
|
|
470
480
|
platform=device.platforms,
|
|
471
|
-
profile=
|
|
481
|
+
profile=profile,
|
|
482
|
+
author=author,
|
|
472
483
|
)
|
|
473
484
|
else:
|
|
474
485
|
logger.warning(f"Failed to create scan task for device {device.fqdn}")
|
|
@@ -480,6 +491,24 @@ class SicuraIntegration(ScannerIntegration):
|
|
|
480
491
|
:param list[Device] devices: The devices to trigger scans for
|
|
481
492
|
:return: None
|
|
482
493
|
"""
|
|
494
|
+
# get the SSP's controlImplementations
|
|
495
|
+
if control_imps := regscale_models.ControlImplementation.get_list_by_parent(
|
|
496
|
+
regscale_id=self.plan_id, regscale_module=regscale_models.SecurityPlan.get_module_slug()
|
|
497
|
+
):
|
|
498
|
+
if profile := self.api.create_or_update_control_profile(
|
|
499
|
+
profile_name=f"regscale_ssp_id_{self.plan_id}",
|
|
500
|
+
controls=control_imps,
|
|
501
|
+
):
|
|
502
|
+
self.control_scan = True
|
|
503
|
+
self.control_scan_profile = profile["name"]
|
|
504
|
+
else:
|
|
505
|
+
logger.warning("Failed to create or update control profile")
|
|
506
|
+
self.control_scan = False
|
|
507
|
+
self.control_scan_profile = None
|
|
508
|
+
else:
|
|
509
|
+
self.control_scan = False
|
|
510
|
+
self.control_scan_profile = None
|
|
511
|
+
|
|
483
512
|
if len(devices) > 1:
|
|
484
513
|
from regscale.utils.threading import ThreadManager
|
|
485
514
|
|
|
@@ -353,13 +353,16 @@ def _display_filter_result(provider: str, filters: List[str]):
|
|
|
353
353
|
for i, filter_str in enumerate(filters, 1):
|
|
354
354
|
click.echo(f" {i}. {filter_str}")
|
|
355
355
|
|
|
356
|
+
connector = provider.split("_")[0]
|
|
357
|
+
filter_flag = "asset_filter" if connector == "vulnerabilities" else "filter"
|
|
358
|
+
|
|
356
359
|
click.echo("\nComplete command:")
|
|
357
360
|
provider_name = provider.replace(provider.split("_")[0] + "_", "")
|
|
358
361
|
|
|
359
362
|
# Build command with semicolon-separated filters
|
|
360
363
|
if filters:
|
|
361
364
|
filter_string = ";".join(filters)
|
|
362
|
-
filter_option = f'--
|
|
365
|
+
filter_option = f'--{filter_flag} "{filter_string}"'
|
|
363
366
|
else:
|
|
364
367
|
filter_option = ""
|
|
365
368
|
|
|
@@ -154,8 +154,8 @@ def io_sync_assets(regscale_ssp_id: int, tags: List[Tuple[str, str]] = None):
|
|
|
154
154
|
try:
|
|
155
155
|
from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
|
|
156
156
|
|
|
157
|
-
integration = TenableIntegration(plan_id=regscale_ssp_id
|
|
158
|
-
integration.sync_assets(plan_id=regscale_ssp_id)
|
|
157
|
+
integration = TenableIntegration(plan_id=regscale_ssp_id)
|
|
158
|
+
integration.sync_assets(plan_id=regscale_ssp_id, tags=tags)
|
|
159
159
|
|
|
160
160
|
console.print("[bold green]Tenable.io asset synchronization complete.[/bold green]")
|
|
161
161
|
except Exception as e:
|
|
@@ -193,8 +193,8 @@ def io_sync_findings(
|
|
|
193
193
|
try:
|
|
194
194
|
from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
|
|
195
195
|
|
|
196
|
-
integration = TenableIntegration(plan_id=regscale_ssp_id,
|
|
197
|
-
integration.sync_findings(plan_id=regscale_ssp_id, severity=severity)
|
|
196
|
+
integration = TenableIntegration(plan_id=regscale_ssp_id, scan_date=scan_date)
|
|
197
|
+
integration.sync_findings(plan_id=regscale_ssp_id, severity=severity, tags=tags)
|
|
198
198
|
|
|
199
199
|
console.print("[bold green]Tenable.io finding synchronization complete.[/bold green]")
|
|
200
200
|
except Exception as e:
|
|
@@ -44,7 +44,7 @@ class TenableIntegration(ScannerIntegration):
|
|
|
44
44
|
"low": regscale_models.IssueSeverity.Low,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
|
-
def __init__(self, plan_id: int, tenant_id: int = 1,
|
|
47
|
+
def __init__(self, plan_id: int, tenant_id: int = 1, **kwargs):
|
|
48
48
|
"""
|
|
49
49
|
Initialize the TenableIntegration.
|
|
50
50
|
|
|
@@ -53,7 +53,6 @@ class TenableIntegration(ScannerIntegration):
|
|
|
53
53
|
"""
|
|
54
54
|
super().__init__(plan_id, tenant_id, **kwargs)
|
|
55
55
|
self.client = None
|
|
56
|
-
self.tags = tags or []
|
|
57
56
|
self.scan_date = kwargs.get("scan_date", get_current_datetime())
|
|
58
57
|
|
|
59
58
|
def authenticate(self) -> None:
|