regscale-cli 6.20.5.0__py3-none-any.whl → 6.20.6.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/__init__.py +1 -1
- regscale/_version.py +39 -0
- regscale/core/app/internal/__init__.py +13 -0
- regscale/core/app/internal/set_permissions.py +173 -0
- regscale/core/app/utils/file_utils.py +11 -1
- regscale/core/app/utils/regscale_utils.py +1 -133
- regscale/core/utils/date.py +62 -29
- regscale/integrations/commercial/wizv2/click.py +9 -5
- regscale/integrations/commercial/wizv2/constants.py +15 -0
- regscale/integrations/commercial/wizv2/parsers.py +23 -0
- regscale/integrations/commercial/wizv2/scanner.py +84 -29
- regscale/integrations/commercial/wizv2/utils.py +91 -4
- regscale/integrations/commercial/wizv2/variables.py +2 -1
- regscale/integrations/commercial/wizv2/wiz_auth.py +3 -3
- regscale/integrations/public/fedramp/fedramp_docx.py +2 -3
- regscale/integrations/scanner_integration.py +7 -2
- regscale/models/integration_models/cisa_kev_data.json +50 -5
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/__init__.py +2 -0
- regscale/models/regscale_models/asset.py +1 -1
- regscale/models/regscale_models/modules.py +88 -1
- regscale/models/regscale_models/regscale_model.py +7 -1
- regscale/models/regscale_models/vulnerability.py +3 -3
- regscale/models/regscale_models/vulnerability_mapping.py +2 -2
- regscale/regscale.py +2 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.6.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.6.0.dist-info}/RECORD +32 -29
- tests/regscale/test_init.py +94 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.6.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.6.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.6.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.6.0.dist-info}/top_level.txt +0 -0
regscale/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
from ._version import __version__
|
regscale/_version.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Version information for regscale-cli."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_version_from_pyproject() -> str:
|
|
9
|
+
"""
|
|
10
|
+
Extract version from pyproject.toml
|
|
11
|
+
|
|
12
|
+
:return: Version string if found, otherwise a fallback version
|
|
13
|
+
:rtype: str
|
|
14
|
+
"""
|
|
15
|
+
pyproject_file_name = "pyproject.toml"
|
|
16
|
+
try:
|
|
17
|
+
# Try multiple possible locations for pyproject.toml
|
|
18
|
+
possible_paths = [
|
|
19
|
+
# From the package directory
|
|
20
|
+
Path(__file__).parent.parent / pyproject_file_name,
|
|
21
|
+
# From current working directory
|
|
22
|
+
Path.cwd() / pyproject_file_name,
|
|
23
|
+
# From the project root (assuming we're in a subdirectory)
|
|
24
|
+
Path.cwd().parent / pyproject_file_name,
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
for pyproject_path in possible_paths:
|
|
28
|
+
if pyproject_path.exists():
|
|
29
|
+
content = pyproject_path.read_text()
|
|
30
|
+
# Look for version = "x.y.z" pattern
|
|
31
|
+
match = re.search(r'version\s*=\s*["\']([^"\']+)["\']', content)
|
|
32
|
+
if match:
|
|
33
|
+
return match.group(1)
|
|
34
|
+
except Exception:
|
|
35
|
+
pass
|
|
36
|
+
return "6.20.6.0" # fallback version
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
__version__ = get_version_from_pyproject()
|
|
@@ -134,3 +134,16 @@ def model():
|
|
|
134
134
|
def issues():
|
|
135
135
|
"""Performs actions on Issues CLI Feature to create new or update issues to RegScale."""
|
|
136
136
|
pass
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
@click.group(
|
|
140
|
+
cls=LazyGroup,
|
|
141
|
+
lazy_subcommands={
|
|
142
|
+
"new": "regscale.core.app.internal.set_permissions.generate_new_file",
|
|
143
|
+
"load": "regscale.core.app.internal.set_permissions.import_permissions",
|
|
144
|
+
},
|
|
145
|
+
name="set_permissions",
|
|
146
|
+
)
|
|
147
|
+
def set_permissions():
|
|
148
|
+
"""Builk sets permissions on records in RegScale from a generated spreadsheet"""
|
|
149
|
+
pass
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import click
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
import os
|
|
4
|
+
from openpyxl import Workbook, load_workbook
|
|
5
|
+
from openpyxl.styles import Protection, Font, NamedStyle
|
|
6
|
+
from openpyxl.worksheet.worksheet import Worksheet
|
|
7
|
+
from openpyxl.worksheet.datavalidation import DataValidation
|
|
8
|
+
from openpyxl import Workbook, load_workbook
|
|
9
|
+
from openpyxl.utils.dataframe import dataframe_to_rows
|
|
10
|
+
|
|
11
|
+
from regscale.core.app.logz import create_logger
|
|
12
|
+
from regscale.models.regscale_models.modules import Modules
|
|
13
|
+
from regscale.models.regscale_models.rbac import RBAC
|
|
14
|
+
from regscale.models.regscale_models.group import Group
|
|
15
|
+
from regscale.models.app_models import ImportValidater
|
|
16
|
+
|
|
17
|
+
logger = create_logger()
|
|
18
|
+
PUBLIC_PRIVATE = "public | private"
|
|
19
|
+
READ_UPDATE = "Read | Read Update"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@click.group(name="set_permissions")
|
|
23
|
+
def set_permissions():
|
|
24
|
+
"""
|
|
25
|
+
Sets permissions on RegScale records
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Make Empty Spreadsheet for creating new assessments.
|
|
30
|
+
@set_permissions.command(name="new")
|
|
31
|
+
@click.option(
|
|
32
|
+
"--path",
|
|
33
|
+
type=click.Path(exists=False, dir_okay=True, path_type=Path),
|
|
34
|
+
help="Provide the desired path into which the excel template file is saved.",
|
|
35
|
+
default=os.path.join(os.getcwd(), "artifacts"),
|
|
36
|
+
required=True,
|
|
37
|
+
)
|
|
38
|
+
def generate_new_file(path: Path):
|
|
39
|
+
"""This function will build an Excel spreadsheet for users to be
|
|
40
|
+
load bulk ACLs on RegScale Records."""
|
|
41
|
+
create_workbook(path)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def create_workbook(path: Path):
|
|
45
|
+
"""
|
|
46
|
+
Creates a new Excel workbook with a worksheet for setting permissions in RegScale
|
|
47
|
+
|
|
48
|
+
:param Path path: The path where the workbook will be saved
|
|
49
|
+
"""
|
|
50
|
+
import pandas as pd
|
|
51
|
+
|
|
52
|
+
workbook_filename = os.path.join(os.getcwd(), path, "import_acls.xlsx")
|
|
53
|
+
workbook = Workbook()
|
|
54
|
+
worksheet = workbook.active
|
|
55
|
+
worksheet.title = "RegScale_Set_Permissions"
|
|
56
|
+
workbook.save(filename=workbook_filename)
|
|
57
|
+
|
|
58
|
+
filler = []
|
|
59
|
+
for _index_ in range(100):
|
|
60
|
+
filler.append("")
|
|
61
|
+
|
|
62
|
+
data = {
|
|
63
|
+
"regscale_id": filler,
|
|
64
|
+
"regscale_module": filler,
|
|
65
|
+
PUBLIC_PRIVATE: filler,
|
|
66
|
+
"group_id": filler,
|
|
67
|
+
READ_UPDATE: filler,
|
|
68
|
+
}
|
|
69
|
+
df = pd.DataFrame(data)
|
|
70
|
+
# Create the headers and filler data
|
|
71
|
+
for r in dataframe_to_rows(df, index=False, header=True):
|
|
72
|
+
worksheet.append(r)
|
|
73
|
+
|
|
74
|
+
# Set pick lists
|
|
75
|
+
dv_mode = DataValidation(type="list", formula1='"public, private"', allow_blank=True)
|
|
76
|
+
dv_mode.add(cell="C2:C100")
|
|
77
|
+
worksheet.add_data_validation(dv_mode)
|
|
78
|
+
dv_right = DataValidation(type="list", formula1='"R, RU"', allow_blank=True)
|
|
79
|
+
dv_right.add(cell="E2:E100")
|
|
80
|
+
worksheet.add_data_validation(dv_right)
|
|
81
|
+
|
|
82
|
+
# Fix width
|
|
83
|
+
for col in worksheet.columns:
|
|
84
|
+
max_length = 0
|
|
85
|
+
column_letter = col[0].column_letter
|
|
86
|
+
|
|
87
|
+
for cell in col:
|
|
88
|
+
# Determine max length for column width
|
|
89
|
+
cell_length = len(str(cell.value))
|
|
90
|
+
max_length = max(max_length, cell_length)
|
|
91
|
+
|
|
92
|
+
# Set adjusted column width
|
|
93
|
+
adjusted_width = (max_length + 2) * 1.2
|
|
94
|
+
worksheet.column_dimensions[column_letter].width = adjusted_width
|
|
95
|
+
|
|
96
|
+
# Write out the formatting to the file
|
|
97
|
+
workbook.save(filename=workbook_filename)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@set_permissions.command(name="load")
|
|
101
|
+
@click.option(
|
|
102
|
+
"--file",
|
|
103
|
+
type=click.Path(exists=False, dir_okay=True, path_type=Path),
|
|
104
|
+
help="Provide the path and filename of the file containing the permissions to load",
|
|
105
|
+
required=True,
|
|
106
|
+
)
|
|
107
|
+
def import_permissions(file: Path):
|
|
108
|
+
"""This function creates permissions on RegScale records recorded
|
|
109
|
+
in a spreadsheet"""
|
|
110
|
+
import_permissions_workbook(file)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def import_permissions_workbook(file: Path):
|
|
114
|
+
"""
|
|
115
|
+
Imports permissions from a workbook file and applies them to RegScale records
|
|
116
|
+
|
|
117
|
+
:param Path file: The path to the workbook file containing permissions
|
|
118
|
+
"""
|
|
119
|
+
# Read in the spreadsheet
|
|
120
|
+
records = get_records(file=file)
|
|
121
|
+
for record in records:
|
|
122
|
+
if not Group.get_object(record["group_id"]):
|
|
123
|
+
logger.error(f"Group {record['group_id']} doesn't exist in this instance. Skipping row")
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
regscale_module_id = Modules.get_module_to_id(record["regscale_module"])
|
|
127
|
+
regscale_id = record["regscale_id"]
|
|
128
|
+
if record[READ_UPDATE] == "R":
|
|
129
|
+
permissions = 1
|
|
130
|
+
elif record[READ_UPDATE] == "RU":
|
|
131
|
+
permissions = 2
|
|
132
|
+
else:
|
|
133
|
+
permissions = 0
|
|
134
|
+
|
|
135
|
+
# Set the record to private
|
|
136
|
+
my_class = Modules.module_to_class(record["regscale_module"])
|
|
137
|
+
obj = my_class.get_object(regscale_id)
|
|
138
|
+
if not obj:
|
|
139
|
+
logger.error(f"RegScale {record['regscale_module']} record {regscale_id} doesn't exist. Skipping row")
|
|
140
|
+
continue
|
|
141
|
+
|
|
142
|
+
# Add the permission
|
|
143
|
+
RBAC.add(
|
|
144
|
+
module_id=regscale_module_id,
|
|
145
|
+
parent_id=regscale_id,
|
|
146
|
+
group_id=record["group_id"],
|
|
147
|
+
permission_type=permissions,
|
|
148
|
+
)
|
|
149
|
+
RBAC.public(
|
|
150
|
+
module_id=regscale_module_id,
|
|
151
|
+
parent_id=regscale_id,
|
|
152
|
+
is_public=0 if record[PUBLIC_PRIVATE] == "private" else 1,
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_records(file: Path) -> list:
|
|
157
|
+
"""
|
|
158
|
+
Takes the file name and returns the records in the file
|
|
159
|
+
|
|
160
|
+
:param Path file: spreadsheet containing permission records
|
|
161
|
+
:return: records from file
|
|
162
|
+
:return_type: list
|
|
163
|
+
"""
|
|
164
|
+
required_headers = ["regscale_id", "regscale_module", PUBLIC_PRIVATE, "group_id", READ_UPDATE]
|
|
165
|
+
validator = ImportValidater(
|
|
166
|
+
file_path=file,
|
|
167
|
+
required_headers=required_headers,
|
|
168
|
+
worksheet_name="RegScale_Set_Permissions",
|
|
169
|
+
skip_rows=1,
|
|
170
|
+
disable_mapping=True,
|
|
171
|
+
mapping_file_path="./",
|
|
172
|
+
)
|
|
173
|
+
return validator.data.to_dict(orient="records")
|
|
@@ -37,7 +37,17 @@ def read_file(file_path: Union[str, Path]) -> str:
|
|
|
37
37
|
"""
|
|
38
38
|
import smart_open # type: ignore # Optimize import performance
|
|
39
39
|
|
|
40
|
-
|
|
40
|
+
encodings = ["utf-8", "latin-1", "cp1252", "iso-8859-1"]
|
|
41
|
+
|
|
42
|
+
for encoding in encodings:
|
|
43
|
+
try:
|
|
44
|
+
with smart_open.open(str(file_path), "r", encoding=encoding) as f:
|
|
45
|
+
return f.read()
|
|
46
|
+
except UnicodeDecodeError:
|
|
47
|
+
continue
|
|
48
|
+
|
|
49
|
+
# Fallback to utf-8 with error replacement
|
|
50
|
+
with smart_open.open(str(file_path), "r", encoding="utf-8", errors="replace") as f:
|
|
41
51
|
return f.read()
|
|
42
52
|
|
|
43
53
|
|
|
@@ -6,16 +6,12 @@
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import re
|
|
9
|
-
import warnings
|
|
10
9
|
from typing import Any, Optional
|
|
11
|
-
from urllib.parse import urljoin
|
|
12
|
-
|
|
13
|
-
from requests import JSONDecodeError
|
|
14
10
|
|
|
15
11
|
from regscale.core.app.api import Api
|
|
16
12
|
from regscale.core.app.application import Application
|
|
17
13
|
from regscale.core.app.logz import create_logger
|
|
18
|
-
from regscale.core.app.utils.app_utils import
|
|
14
|
+
from regscale.core.app.utils.app_utils import error_and_exit, get_file_name, get_file_type
|
|
19
15
|
from regscale.models import Data
|
|
20
16
|
from regscale.models.regscale_models.modules import Modules
|
|
21
17
|
|
|
@@ -24,28 +20,6 @@ from regscale.models.regscale_models.form_field_value import FormFieldValue
|
|
|
24
20
|
logger = create_logger()
|
|
25
21
|
|
|
26
22
|
|
|
27
|
-
def send_email(_: Api, __: str, payload: dict) -> bool:
|
|
28
|
-
"""
|
|
29
|
-
Function to use the RegScale email API and send an email, returns bool on whether API call was successful
|
|
30
|
-
|
|
31
|
-
:param Api _: API object
|
|
32
|
-
:param str __: RegScale URL of instance
|
|
33
|
-
:param dict payload: email payload
|
|
34
|
-
:return: Boolean if RegScale api was successful
|
|
35
|
-
:rtype: bool
|
|
36
|
-
"""
|
|
37
|
-
warnings.warn(
|
|
38
|
-
"This function is deprecated and will be removed in a future release. Use Email().send instead.",
|
|
39
|
-
DeprecationWarning,
|
|
40
|
-
stacklevel=2,
|
|
41
|
-
)
|
|
42
|
-
from regscale.models import Email
|
|
43
|
-
|
|
44
|
-
email = Email(**payload)
|
|
45
|
-
# use the api to post the dict payload passed
|
|
46
|
-
return email.send() is not None
|
|
47
|
-
|
|
48
|
-
|
|
49
23
|
def update_regscale_config(str_param: str, val: Any, app: Application = None) -> str:
|
|
50
24
|
"""
|
|
51
25
|
Update config in init.yaml
|
|
@@ -68,29 +42,6 @@ def update_regscale_config(str_param: str, val: Any, app: Application = None) ->
|
|
|
68
42
|
return "Config updated"
|
|
69
43
|
|
|
70
44
|
|
|
71
|
-
def create_regscale_assessment(_: str, new_assessment: dict, __: Api) -> Optional[int]:
|
|
72
|
-
"""
|
|
73
|
-
Function to create a new assessment in RegScale and returns the new assessment's ID
|
|
74
|
-
|
|
75
|
-
:param str _: RegScale instance URL to create the assessment
|
|
76
|
-
:param dict new_assessment: API assessment payload
|
|
77
|
-
:param Api __: API object
|
|
78
|
-
:return: New RegScale assessment ID
|
|
79
|
-
:rtype: int
|
|
80
|
-
"""
|
|
81
|
-
warnings.warn(
|
|
82
|
-
"This function is deprecated and will be removed in a future release. Use Assessment().create.id instead.",
|
|
83
|
-
DeprecationWarning,
|
|
84
|
-
stacklevel=2,
|
|
85
|
-
)
|
|
86
|
-
from regscale.models import Assessment
|
|
87
|
-
|
|
88
|
-
new_assessment = Assessment(**new_assessment)
|
|
89
|
-
if created_assessment := new_assessment.create():
|
|
90
|
-
return created_assessment.id
|
|
91
|
-
return None
|
|
92
|
-
|
|
93
|
-
|
|
94
45
|
def check_module_id(parent_id: int, parent_module: str) -> bool:
|
|
95
46
|
"""
|
|
96
47
|
Verify object exists in RegScale
|
|
@@ -194,45 +145,6 @@ def format_control(control: str) -> str:
|
|
|
194
145
|
return new_string.lower() # Output: ac-2.1
|
|
195
146
|
|
|
196
147
|
|
|
197
|
-
def get_user(_: Api, user_id: str) -> dict:
|
|
198
|
-
"""
|
|
199
|
-
Function to get the provided user_id from RegScale via API
|
|
200
|
-
|
|
201
|
-
:param Api _: API Object
|
|
202
|
-
:param str user_id: the RegScale user's GUID
|
|
203
|
-
:return: Dictionary containing the user's information
|
|
204
|
-
:rtype: dict
|
|
205
|
-
"""
|
|
206
|
-
warnings.warn(
|
|
207
|
-
"This function is deprecated and will be removed in a future release. Use User.get_user_by_id() instead.",
|
|
208
|
-
DeprecationWarning,
|
|
209
|
-
stacklevel=2,
|
|
210
|
-
)
|
|
211
|
-
from regscale.models import User
|
|
212
|
-
|
|
213
|
-
user = User.get_user_by_id(user_id)
|
|
214
|
-
return user.dict()
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def get_threats(_: Api) -> list[dict]:
|
|
218
|
-
"""
|
|
219
|
-
Function to get all threats from RegScale via GraphQL
|
|
220
|
-
|
|
221
|
-
:param Api _: API Object
|
|
222
|
-
:return: List containing threat descriptions
|
|
223
|
-
:rtype: list[dict]
|
|
224
|
-
"""
|
|
225
|
-
warnings.warn(
|
|
226
|
-
"This function is deprecated and will be removed in a future release. Use Threat.fetch_all_threats() instead.",
|
|
227
|
-
DeprecationWarning,
|
|
228
|
-
stacklevel=2,
|
|
229
|
-
)
|
|
230
|
-
from regscale.models import Threat
|
|
231
|
-
|
|
232
|
-
threats = Threat.fetch_all_threats()
|
|
233
|
-
return [threat.dict() for threat in threats] or []
|
|
234
|
-
|
|
235
|
-
|
|
236
148
|
def create_new_data_submodule(
|
|
237
149
|
parent_id: int, parent_module: str, file_path: str, raw_data: dict = None, is_file: bool = True
|
|
238
150
|
) -> Optional[dict]:
|
|
@@ -276,50 +188,6 @@ def create_new_data_submodule(
|
|
|
276
188
|
return None
|
|
277
189
|
|
|
278
190
|
|
|
279
|
-
def create_properties(
|
|
280
|
-
_: Api,
|
|
281
|
-
data: dict,
|
|
282
|
-
parent_id: int,
|
|
283
|
-
parent_module: str,
|
|
284
|
-
__: Optional[int] = 3,
|
|
285
|
-
label: Optional[str] = None,
|
|
286
|
-
) -> bool:
|
|
287
|
-
"""
|
|
288
|
-
Create a list of properties and upload them to RegScale for the provided asset
|
|
289
|
-
|
|
290
|
-
:param Api _: API Object to post the data in RegScale
|
|
291
|
-
:param dict data: Dictionary of data to parse and create properties from
|
|
292
|
-
:param int parent_id: ID to create properties for
|
|
293
|
-
:param str parent_module: Parent module to create properties for
|
|
294
|
-
:param Optional[int] __: Number of times to retry the API call if it fails, defaults to 3
|
|
295
|
-
:param Optional[str] label: Label to use for the properties, defaults to None
|
|
296
|
-
:return: If batch update was successful
|
|
297
|
-
:rtype: bool
|
|
298
|
-
"""
|
|
299
|
-
from regscale.models import Property
|
|
300
|
-
|
|
301
|
-
warnings.warn(
|
|
302
|
-
"This function is deprecated and will be removed in a future release. Use Property.batch_create() instead.",
|
|
303
|
-
DeprecationWarning,
|
|
304
|
-
stacklevel=2,
|
|
305
|
-
)
|
|
306
|
-
properties: list = []
|
|
307
|
-
for key, value in data.items():
|
|
308
|
-
# evaluate the value and convert it to a string
|
|
309
|
-
value = convert_to_string(value)
|
|
310
|
-
prop = Property(
|
|
311
|
-
key=key,
|
|
312
|
-
value=value or "NULL",
|
|
313
|
-
label=label or None,
|
|
314
|
-
parentId=parent_id,
|
|
315
|
-
parentModule=parent_module,
|
|
316
|
-
)
|
|
317
|
-
properties.append(prop)
|
|
318
|
-
|
|
319
|
-
new_props = Property.batch_create(properties)
|
|
320
|
-
return len(new_props) > 0
|
|
321
|
-
|
|
322
|
-
|
|
323
191
|
def normalize_controlid(name: str) -> str:
|
|
324
192
|
"""
|
|
325
193
|
Normalizes a control Id String
|
regscale/core/utils/date.py
CHANGED
|
@@ -6,7 +6,10 @@ import datetime
|
|
|
6
6
|
import logging
|
|
7
7
|
from typing import Any, List, Optional, Union
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
import pytz
|
|
11
|
+
from dateutil.parser import parse, ParserError
|
|
12
|
+
|
|
10
13
|
from pandas import Timestamp
|
|
11
14
|
|
|
12
15
|
logger = logging.getLogger("regscale")
|
|
@@ -23,17 +26,18 @@ def date_str(date_object: Union[str, datetime.datetime, datetime.date, None], da
|
|
|
23
26
|
"""
|
|
24
27
|
if isinstance(date_object, str):
|
|
25
28
|
date_object = date_obj(date_object)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
if
|
|
35
|
-
return date_object.
|
|
36
|
-
|
|
29
|
+
|
|
30
|
+
# Handles passed None and date_obj returning None
|
|
31
|
+
if not date_object:
|
|
32
|
+
return ""
|
|
33
|
+
|
|
34
|
+
if isinstance(date_object, (datetime.datetime, Timestamp)):
|
|
35
|
+
date_object = date_object.date()
|
|
36
|
+
|
|
37
|
+
if date_format:
|
|
38
|
+
return date_object.strftime(date_format)
|
|
39
|
+
|
|
40
|
+
return date_object.isoformat()
|
|
37
41
|
|
|
38
42
|
|
|
39
43
|
def datetime_str(
|
|
@@ -48,13 +52,14 @@ def datetime_str(
|
|
|
48
52
|
"""
|
|
49
53
|
if not date_format:
|
|
50
54
|
date_format = default_date_format
|
|
55
|
+
|
|
51
56
|
if isinstance(date_object, str):
|
|
52
57
|
date_object = datetime_obj(date_object)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return
|
|
58
|
+
|
|
59
|
+
if not date_object:
|
|
60
|
+
return ""
|
|
61
|
+
|
|
62
|
+
return date_object.strftime(date_format)
|
|
58
63
|
|
|
59
64
|
|
|
60
65
|
def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.date]:
|
|
@@ -64,14 +69,14 @@ def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None])
|
|
|
64
69
|
:param Union[str, datetime.datetime, datetime.date, int] date_str: The value to convert.
|
|
65
70
|
:return: The date object.
|
|
66
71
|
"""
|
|
67
|
-
if isinstance(date_str, (str, int)):
|
|
68
|
-
dt_obj = datetime_obj(date_str)
|
|
69
|
-
return dt_obj.date() if dt_obj else None
|
|
70
72
|
if isinstance(date_str, datetime.datetime):
|
|
71
73
|
return date_str.date()
|
|
74
|
+
|
|
72
75
|
if isinstance(date_str, datetime.date):
|
|
73
76
|
return date_str
|
|
74
|
-
|
|
77
|
+
|
|
78
|
+
dt_obj = datetime_obj(date_str)
|
|
79
|
+
return dt_obj.date() if dt_obj else None
|
|
75
80
|
|
|
76
81
|
|
|
77
82
|
def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.datetime]:
|
|
@@ -168,8 +173,12 @@ def days_between(
|
|
|
168
173
|
:param Union[str, datetime.datetime, datetime.date] end: The end date.
|
|
169
174
|
:return: A list of dates between the start and end dates.
|
|
170
175
|
"""
|
|
171
|
-
|
|
172
|
-
|
|
176
|
+
start_dt = date_obj(start)
|
|
177
|
+
end_dt = date_obj(end)
|
|
178
|
+
if start_dt is None or end_dt is None:
|
|
179
|
+
return []
|
|
180
|
+
delta = end_dt - start_dt
|
|
181
|
+
return [(start_dt + datetime.timedelta(days=i)).strftime("%Y/%m/%d") for i in range(delta.days + 1)]
|
|
173
182
|
|
|
174
183
|
|
|
175
184
|
def weekend_days_between(
|
|
@@ -178,12 +187,11 @@ def weekend_days_between(
|
|
|
178
187
|
) -> List[str]:
|
|
179
188
|
"""
|
|
180
189
|
Get the weekend days between two dates.
|
|
181
|
-
|
|
182
190
|
:param Union[str, datetime.datetime, datetime.date] start: The start date.
|
|
183
191
|
:param Union[str, datetime.datetime, datetime.date] end: The end date.
|
|
184
192
|
:return: A list of weekend dates between the start and end dates.
|
|
185
193
|
"""
|
|
186
|
-
return [day for day in days_between(start, end) if
|
|
194
|
+
return [day for day in days_between(start, end) if day and (dt := date_obj(day)) is not None and not is_weekday(dt)]
|
|
187
195
|
|
|
188
196
|
|
|
189
197
|
def days_from_today(i: int) -> datetime.date:
|
|
@@ -210,11 +218,13 @@ def get_day_increment(
|
|
|
210
218
|
:param Optional[List[Union[str, datetime.datetime, datetime.date]]] excluded_dates: A list of dates to exclude.
|
|
211
219
|
:return: The date days days from the start date, excluding the excluded dates.
|
|
212
220
|
"""
|
|
213
|
-
|
|
214
|
-
|
|
221
|
+
start_dt = date_obj(start)
|
|
222
|
+
if start_dt is None:
|
|
223
|
+
raise ValueError("Invalid start date")
|
|
224
|
+
end = start_dt + datetime.timedelta(days=days)
|
|
215
225
|
if excluded_dates:
|
|
216
|
-
for excluded_date in sorted([date_obj(x) for x in excluded_dates]):
|
|
217
|
-
if
|
|
226
|
+
for excluded_date in sorted([d for d in [date_obj(x) for x in excluded_dates] if d is not None]):
|
|
227
|
+
if start_dt <= excluded_date <= end:
|
|
218
228
|
end += datetime.timedelta(days=1)
|
|
219
229
|
return end
|
|
220
230
|
|
|
@@ -237,6 +247,29 @@ def normalize_date(dt: str, fmt: str) -> str:
|
|
|
237
247
|
return dt
|
|
238
248
|
|
|
239
249
|
|
|
250
|
+
def format_to_regscale_iso(date_input: Union[str, datetime.datetime]) -> str:
|
|
251
|
+
"""
|
|
252
|
+
Format a date string or datetime object to RegScale-compatible ISO 8601 with 3 milliseconds and 'Z'.
|
|
253
|
+
|
|
254
|
+
:param Union[str, datetime.datetime] date_input: Input date as string or datetime.
|
|
255
|
+
:return: Formatted ISO string.
|
|
256
|
+
:rtype: str
|
|
257
|
+
"""
|
|
258
|
+
if isinstance(date_input, str):
|
|
259
|
+
try:
|
|
260
|
+
dt = parse(date_input)
|
|
261
|
+
except ParserError:
|
|
262
|
+
logger.warning(f"Unable to parse date: {date_input}")
|
|
263
|
+
return date_input
|
|
264
|
+
else:
|
|
265
|
+
dt = date_input
|
|
266
|
+
if dt.tzinfo is None:
|
|
267
|
+
dt = dt.replace(tzinfo=pytz.UTC)
|
|
268
|
+
else:
|
|
269
|
+
dt = dt.astimezone(pytz.UTC)
|
|
270
|
+
return dt.strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
|
|
271
|
+
|
|
272
|
+
|
|
240
273
|
def normalize_timestamp(timestamp_value: Any) -> int:
|
|
241
274
|
"""
|
|
242
275
|
Normalize timestamp to seconds, handling both seconds and milliseconds.
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
|
|
5
5
|
# standard python imports
|
|
6
6
|
import logging
|
|
7
|
-
import os
|
|
8
7
|
from typing import Optional
|
|
9
8
|
|
|
10
9
|
import click
|
|
@@ -343,16 +342,16 @@ def add_report_evidence(
|
|
|
343
342
|
@click.option( # type: ignore
|
|
344
343
|
"--client_id",
|
|
345
344
|
"-i",
|
|
346
|
-
help="Wiz Client ID
|
|
347
|
-
default=
|
|
345
|
+
help="Wiz Client ID, or can be set as environment variable wizClientId",
|
|
346
|
+
default="",
|
|
348
347
|
hide_input=False,
|
|
349
348
|
required=False,
|
|
350
349
|
)
|
|
351
350
|
@click.option( # type: ignore
|
|
352
351
|
"--client_secret",
|
|
353
352
|
"-s",
|
|
354
|
-
help="Wiz Client Secret
|
|
355
|
-
default=
|
|
353
|
+
help="Wiz Client Secret, or can be set as environment variable wizClientSecret",
|
|
354
|
+
default="",
|
|
356
355
|
hide_input=True,
|
|
357
356
|
required=False,
|
|
358
357
|
)
|
|
@@ -384,6 +383,11 @@ def sync_compliance(
|
|
|
384
383
|
"""Sync compliance posture from Wiz to RegScale"""
|
|
385
384
|
from regscale.integrations.commercial.wizv2.utils import _sync_compliance
|
|
386
385
|
|
|
386
|
+
if not client_secret:
|
|
387
|
+
client_secret = WizVariables.wizClientSecret
|
|
388
|
+
if not client_id:
|
|
389
|
+
client_id = WizVariables.wizClientId
|
|
390
|
+
|
|
387
391
|
_sync_compliance(
|
|
388
392
|
wiz_project_id=wiz_project_id,
|
|
389
393
|
regscale_id=regscale_id,
|
|
@@ -131,6 +131,9 @@ INVENTORY_QUERY = """
|
|
|
131
131
|
graphEntity{
|
|
132
132
|
id
|
|
133
133
|
providerUniqueId
|
|
134
|
+
publicExposures(first: 5) {
|
|
135
|
+
totalCount
|
|
136
|
+
}
|
|
134
137
|
name
|
|
135
138
|
type
|
|
136
139
|
projects {
|
|
@@ -435,6 +438,18 @@ VULNERABILITY_QUERY = """
|
|
|
435
438
|
name
|
|
436
439
|
detailedName
|
|
437
440
|
description
|
|
441
|
+
commentThread {
|
|
442
|
+
comments(first:100) {
|
|
443
|
+
edges {
|
|
444
|
+
node {
|
|
445
|
+
body,
|
|
446
|
+
author {
|
|
447
|
+
name
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
},
|
|
438
453
|
severity: vendorSeverity
|
|
439
454
|
weightedSeverity
|
|
440
455
|
status
|
|
@@ -271,6 +271,29 @@ def get_ip_address_from_props(network_dict: Dict) -> Optional[str]:
|
|
|
271
271
|
return network_dict.get("ip4_address") or network_dict.get("ip6_address")
|
|
272
272
|
|
|
273
273
|
|
|
274
|
+
def get_ip_v4_from_props(network_dict: Dict) -> Optional[str]:
|
|
275
|
+
"""
|
|
276
|
+
Get IPv4 address from properties
|
|
277
|
+
:param Dict network_dict: Network dictionary
|
|
278
|
+
:return: IPv4 address if it can be parsed from the network dictionary
|
|
279
|
+
:rtype: Optional[str]
|
|
280
|
+
"""
|
|
281
|
+
ip = network_dict.get("address")
|
|
282
|
+
if ip:
|
|
283
|
+
logger.info("get_ip_v4_from_props: %s", ip)
|
|
284
|
+
return network_dict.get("address")
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def get_ip_v6_from_props(network_dict: Dict) -> Optional[str]:
|
|
288
|
+
"""
|
|
289
|
+
Get IPv6 address from properties
|
|
290
|
+
:param Dict network_dict: Network dictionary
|
|
291
|
+
:return: IPv6 address if it can be parsed from the network dictionary
|
|
292
|
+
:rtype: Optional[str]
|
|
293
|
+
"""
|
|
294
|
+
return network_dict.get("ip6_address")
|
|
295
|
+
|
|
296
|
+
|
|
274
297
|
def fetch_wiz_data(
|
|
275
298
|
query: str,
|
|
276
299
|
variables: dict,
|