regscale-cli 6.20.4.1__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.

Files changed (49) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/_version.py +39 -0
  3. regscale/core/app/internal/__init__.py +13 -0
  4. regscale/core/app/internal/model_editor.py +3 -3
  5. regscale/core/app/internal/set_permissions.py +173 -0
  6. regscale/core/app/utils/file_utils.py +11 -1
  7. regscale/core/app/utils/regscale_utils.py +34 -129
  8. regscale/core/utils/date.py +86 -30
  9. regscale/integrations/commercial/defender.py +3 -0
  10. regscale/integrations/commercial/qualys/__init__.py +40 -14
  11. regscale/integrations/commercial/qualys/containers.py +324 -0
  12. regscale/integrations/commercial/qualys/scanner.py +203 -8
  13. regscale/integrations/commercial/synqly/edr.py +10 -0
  14. regscale/integrations/commercial/wizv2/click.py +11 -7
  15. regscale/integrations/commercial/wizv2/constants.py +28 -0
  16. regscale/integrations/commercial/wizv2/issue.py +3 -2
  17. regscale/integrations/commercial/wizv2/parsers.py +23 -0
  18. regscale/integrations/commercial/wizv2/scanner.py +89 -30
  19. regscale/integrations/commercial/wizv2/utils.py +208 -75
  20. regscale/integrations/commercial/wizv2/variables.py +2 -1
  21. regscale/integrations/commercial/wizv2/wiz_auth.py +3 -3
  22. regscale/integrations/public/fedramp/fedramp_cis_crm.py +98 -20
  23. regscale/integrations/public/fedramp/fedramp_docx.py +2 -3
  24. regscale/integrations/scanner_integration.py +7 -2
  25. regscale/models/integration_models/cisa_kev_data.json +187 -5
  26. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  27. regscale/models/regscale_models/__init__.py +2 -0
  28. regscale/models/regscale_models/asset.py +1 -1
  29. regscale/models/regscale_models/catalog.py +16 -0
  30. regscale/models/regscale_models/file.py +2 -1
  31. regscale/models/regscale_models/form_field_value.py +59 -1
  32. regscale/models/regscale_models/issue.py +47 -0
  33. regscale/models/regscale_models/modules.py +88 -1
  34. regscale/models/regscale_models/organization.py +30 -0
  35. regscale/models/regscale_models/regscale_model.py +20 -6
  36. regscale/models/regscale_models/security_control.py +47 -0
  37. regscale/models/regscale_models/security_plan.py +32 -0
  38. regscale/models/regscale_models/vulnerability.py +3 -3
  39. regscale/models/regscale_models/vulnerability_mapping.py +2 -2
  40. regscale/regscale.py +2 -0
  41. {regscale_cli-6.20.4.1.dist-info → regscale_cli-6.20.6.0.dist-info}/METADATA +1 -1
  42. {regscale_cli-6.20.4.1.dist-info → regscale_cli-6.20.6.0.dist-info}/RECORD +49 -44
  43. tests/fixtures/test_fixture.py +33 -4
  44. tests/regscale/core/test_app.py +53 -32
  45. tests/regscale/test_init.py +94 -0
  46. {regscale_cli-6.20.4.1.dist-info → regscale_cli-6.20.6.0.dist-info}/LICENSE +0 -0
  47. {regscale_cli-6.20.4.1.dist-info → regscale_cli-6.20.6.0.dist-info}/WHEEL +0 -0
  48. {regscale_cli-6.20.4.1.dist-info → regscale_cli-6.20.6.0.dist-info}/entry_points.txt +0 -0
  49. {regscale_cli-6.20.4.1.dist-info → regscale_cli-6.20.6.0.dist-info}/top_level.txt +0 -0
regscale/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "6.20.4.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
@@ -1365,7 +1365,7 @@ def build_object_field_list(obj: object) -> None:
1365
1365
  """
1366
1366
  # Build the list of fields for the model type
1367
1367
  pos_dict = obj.get_sort_position_dict()
1368
- field_names = obj.model_fields.keys()
1368
+ field_names = obj.__class__.model_fields.keys()
1369
1369
  extra_fields = obj.get_extra_fields()
1370
1370
  include_field_list = obj.get_include_fields()
1371
1371
  for item in include_field_list:
@@ -1375,7 +1375,7 @@ def build_object_field_list(obj: object) -> None:
1375
1375
  field_makeup = FieldMakeup(
1376
1376
  cur_field,
1377
1377
  convert_property_to_column_label(cur_field),
1378
- get_field_data_type(obj.model_fields[cur_field]),
1378
+ get_field_data_type(obj.__class__.model_fields[cur_field]),
1379
1379
  )
1380
1380
  field_makeup.sort_order = find_sort_pos(cur_field, pos_dict)
1381
1381
  field_makeup.enum_values = obj.get_enum_values(cur_field)
@@ -1444,7 +1444,7 @@ def is_field_required(obj: object, field_name: str) -> bool:
1444
1444
  :return: bool indicating if the field is required
1445
1445
  :rtype: bool
1446
1446
  """
1447
- field_info = obj.model_fields[field_name]
1447
+ field_info = obj.__class__.model_fields[field_name]
1448
1448
  if field_info.annotation == dict:
1449
1449
  return True
1450
1450
  if field_info.annotation == int:
@@ -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
- with smart_open.open(str(file_path), "r") as f:
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,42 +6,18 @@
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 convert_to_string, error_and_exit, get_file_name, get_file_type
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
 
22
- logger = create_logger()
23
-
24
-
25
- def send_email(_: Api, __: str, payload: dict) -> bool:
26
- """
27
- Function to use the RegScale email API and send an email, returns bool on whether API call was successful
28
-
29
- :param Api _: API object
30
- :param str __: RegScale URL of instance
31
- :param dict payload: email payload
32
- :return: Boolean if RegScale api was successful
33
- :rtype: bool
34
- """
35
- warnings.warn(
36
- "This function is deprecated and will be removed in a future release. Use Email().send instead.",
37
- DeprecationWarning,
38
- stacklevel=2,
39
- )
40
- from regscale.models import Email
18
+ from regscale.models.regscale_models.form_field_value import FormFieldValue
41
19
 
42
- email = Email(**payload)
43
- # use the api to post the dict payload passed
44
- return email.send() is not None
20
+ logger = create_logger()
45
21
 
46
22
 
47
23
  def update_regscale_config(str_param: str, val: Any, app: Application = None) -> str:
@@ -66,29 +42,6 @@ def update_regscale_config(str_param: str, val: Any, app: Application = None) ->
66
42
  return "Config updated"
67
43
 
68
44
 
69
- def create_regscale_assessment(_: str, new_assessment: dict, __: Api) -> Optional[int]:
70
- """
71
- Function to create a new assessment in RegScale and returns the new assessment's ID
72
-
73
- :param str _: RegScale instance URL to create the assessment
74
- :param dict new_assessment: API assessment payload
75
- :param Api __: API object
76
- :return: New RegScale assessment ID
77
- :rtype: int
78
- """
79
- warnings.warn(
80
- "This function is deprecated and will be removed in a future release. Use Assessment().create.id instead.",
81
- DeprecationWarning,
82
- stacklevel=2,
83
- )
84
- from regscale.models import Assessment
85
-
86
- new_assessment = Assessment(**new_assessment)
87
- if created_assessment := new_assessment.create():
88
- return created_assessment.id
89
- return None
90
-
91
-
92
45
  def check_module_id(parent_id: int, parent_module: str) -> bool:
93
46
  """
94
47
  Verify object exists in RegScale
@@ -192,45 +145,6 @@ def format_control(control: str) -> str:
192
145
  return new_string.lower() # Output: ac-2.1
193
146
 
194
147
 
195
- def get_user(_: Api, user_id: str) -> dict:
196
- """
197
- Function to get the provided user_id from RegScale via API
198
-
199
- :param Api _: API Object
200
- :param str user_id: the RegScale user's GUID
201
- :return: Dictionary containing the user's information
202
- :rtype: dict
203
- """
204
- warnings.warn(
205
- "This function is deprecated and will be removed in a future release. Use User.get_user_by_id() instead.",
206
- DeprecationWarning,
207
- stacklevel=2,
208
- )
209
- from regscale.models import User
210
-
211
- user = User.get_user_by_id(user_id)
212
- return user.dict()
213
-
214
-
215
- def get_threats(_: Api) -> list[dict]:
216
- """
217
- Function to get all threats from RegScale via GraphQL
218
-
219
- :param Api _: API Object
220
- :return: List containing threat descriptions
221
- :rtype: list[dict]
222
- """
223
- warnings.warn(
224
- "This function is deprecated and will be removed in a future release. Use Threat.fetch_all_threats() instead.",
225
- DeprecationWarning,
226
- stacklevel=2,
227
- )
228
- from regscale.models import Threat
229
-
230
- threats = Threat.fetch_all_threats()
231
- return [threat.dict() for threat in threats] or []
232
-
233
-
234
148
  def create_new_data_submodule(
235
149
  parent_id: int, parent_module: str, file_path: str, raw_data: dict = None, is_file: bool = True
236
150
  ) -> Optional[dict]:
@@ -274,45 +188,36 @@ def create_new_data_submodule(
274
188
  return None
275
189
 
276
190
 
277
- def create_properties(
278
- _: Api,
279
- data: dict,
280
- parent_id: int,
281
- parent_module: str,
282
- __: Optional[int] = 3,
283
- label: Optional[str] = None,
284
- ) -> bool:
285
- """
286
- Create a list of properties and upload them to RegScale for the provided asset
287
-
288
- :param Api _: API Object to post the data in RegScale
289
- :param dict data: Dictionary of data to parse and create properties from
290
- :param int parent_id: ID to create properties for
291
- :param str parent_module: Parent module to create properties for
292
- :param Optional[int] __: Number of times to retry the API call if it fails, defaults to 3
293
- :param Optional[str] label: Label to use for the properties, defaults to None
294
- :return: If batch update was successful
295
- :rtype: bool
191
+ def normalize_controlid(name: str) -> str:
296
192
  """
297
- from regscale.models import Property
193
+ Normalizes a control Id String
194
+ e.g. AC-01(02) -> ac-1.2
195
+ AC-01a.[02] -> ac-1.a.2
298
196
 
299
- warnings.warn(
300
- "This function is deprecated and will be removed in a future release. Use Property.batch_create() instead.",
301
- DeprecationWarning,
302
- stacklevel=2,
303
- )
304
- properties: list = []
305
- for key, value in data.items():
306
- # evaluate the value and convert it to a string
307
- value = convert_to_string(value)
308
- prop = Property(
309
- key=key,
310
- value=value or "NULL",
311
- label=label or None,
312
- parentId=parent_id,
313
- parentModule=parent_module,
314
- )
315
- properties.append(prop)
316
-
317
- new_props = Property.batch_create(properties)
318
- return len(new_props) > 0
197
+ :param str name: Control Id String to normalize
198
+ :return: normalized Control Id String
199
+ :rtype: str
200
+ """
201
+ # AC-01(02)
202
+ # AC-01a.[02] vs. AC-1a.2
203
+ new_string = name.replace(" ", "")
204
+ new_string = new_string.replace("(", ".") # AC-01.02) #AC-01a.[02]
205
+ new_string = new_string.replace(")", "") # AC-01.02 #AC-01.a.[02]
206
+ new_string = new_string.replace("[", "") # AC-01.02 #AC-01.a.02]
207
+ new_string = new_string.replace("]", "") # AC-01.02 #AC-01.a.02
208
+
209
+ parts = new_string.split(".")
210
+ new_string = ""
211
+ for part in parts:
212
+ part = part.lstrip("0")
213
+ new_string += f"{part}."
214
+ new_string = new_string.rstrip(".") # AC-01.2 #AC-01.a.2
215
+
216
+ parts = new_string.split("-")
217
+ new_string = ""
218
+ for part in parts:
219
+ part = part.lstrip("0")
220
+ new_string += f"{part}-"
221
+ new_string = new_string.rstrip("-") # AC-1.2 #AC-1.a.2
222
+
223
+ return new_string.lower()
@@ -4,11 +4,13 @@
4
4
 
5
5
  import datetime
6
6
  import logging
7
- from typing import List, Union, Optional
7
+ from typing import Any, List, Optional, Union
8
8
 
9
- from pandas import Timestamp
9
+
10
+ import pytz
10
11
  from dateutil.parser import parse, ParserError
11
12
 
13
+ from pandas import Timestamp
12
14
 
13
15
  logger = logging.getLogger("regscale")
14
16
  default_date_format = "%Y-%m-%dT%H:%M:%S%z"
@@ -24,17 +26,18 @@ def date_str(date_object: Union[str, datetime.datetime, datetime.date, None], da
24
26
  """
25
27
  if isinstance(date_object, str):
26
28
  date_object = date_obj(date_object)
27
- if isinstance(date_object, datetime.datetime):
28
- if date_format:
29
- return date_object.strftime(date_format)
30
- return date_object.date().isoformat()
31
- if isinstance(date_object, datetime.date):
32
- if date_format:
33
- return date_object.strftime(date_format)
34
- return date_object.isoformat()
35
- if isinstance(date_object, Timestamp):
36
- return date_object.date().isoformat()
37
- return ""
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()
38
41
 
39
42
 
40
43
  def datetime_str(
@@ -49,13 +52,14 @@ def datetime_str(
49
52
  """
50
53
  if not date_format:
51
54
  date_format = default_date_format
55
+
52
56
  if isinstance(date_object, str):
53
57
  date_object = datetime_obj(date_object)
54
- if isinstance(date_object, datetime.datetime):
55
- return date_object.strftime(date_format)
56
- if isinstance(date_object, datetime.date):
57
- return date_object.strftime(date_format)
58
- return ""
58
+
59
+ if not date_object:
60
+ return ""
61
+
62
+ return date_object.strftime(date_format)
59
63
 
60
64
 
61
65
  def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.date]:
@@ -65,14 +69,14 @@ def date_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None])
65
69
  :param Union[str, datetime.datetime, datetime.date, int] date_str: The value to convert.
66
70
  :return: The date object.
67
71
  """
68
- if isinstance(date_str, (str, int)):
69
- dt_obj = datetime_obj(date_str)
70
- return dt_obj.date() if dt_obj else None
71
72
  if isinstance(date_str, datetime.datetime):
72
73
  return date_str.date()
74
+
73
75
  if isinstance(date_str, datetime.date):
74
76
  return date_str
75
- return None
77
+
78
+ dt_obj = datetime_obj(date_str)
79
+ return dt_obj.date() if dt_obj else None
76
80
 
77
81
 
78
82
  def datetime_obj(date_str: Union[str, datetime.datetime, datetime.date, int, None]) -> Optional[datetime.datetime]:
@@ -169,8 +173,12 @@ def days_between(
169
173
  :param Union[str, datetime.datetime, datetime.date] end: The end date.
170
174
  :return: A list of dates between the start and end dates.
171
175
  """
172
- delta = date_obj(end) - date_obj(start)
173
- return [(date_obj(start) + datetime.timedelta(days=i)).strftime("%Y/%m/%d") for i in range(delta.days + 1)]
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)]
174
182
 
175
183
 
176
184
  def weekend_days_between(
@@ -179,12 +187,11 @@ def weekend_days_between(
179
187
  ) -> List[str]:
180
188
  """
181
189
  Get the weekend days between two dates.
182
-
183
190
  :param Union[str, datetime.datetime, datetime.date] start: The start date.
184
191
  :param Union[str, datetime.datetime, datetime.date] end: The end date.
185
192
  :return: A list of weekend dates between the start and end dates.
186
193
  """
187
- return [day for day in days_between(start, end) if not is_weekday(date_obj(day))]
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)]
188
195
 
189
196
 
190
197
  def days_from_today(i: int) -> datetime.date:
@@ -211,11 +218,13 @@ def get_day_increment(
211
218
  :param Optional[List[Union[str, datetime.datetime, datetime.date]]] excluded_dates: A list of dates to exclude.
212
219
  :return: The date days days from the start date, excluding the excluded dates.
213
220
  """
214
- start = date_obj(start)
215
- end = start + datetime.timedelta(days=days)
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)
216
225
  if excluded_dates:
217
- for excluded_date in sorted([date_obj(x) for x in excluded_dates]):
218
- if start <= excluded_date <= end:
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:
219
228
  end += datetime.timedelta(days=1)
220
229
  return end
221
230
 
@@ -236,3 +245,50 @@ def normalize_date(dt: str, fmt: str) -> str:
236
245
  except ValueError:
237
246
  return dt
238
247
  return dt
248
+
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
+
273
+ def normalize_timestamp(timestamp_value: Any) -> int:
274
+ """
275
+ Normalize timestamp to seconds, handling both seconds and milliseconds.
276
+
277
+ :param Any timestamp_value: The timestamp value to normalize
278
+ :return: Timestamp in seconds
279
+ :raises ValueError: If the timestamp is invalid
280
+ :rtype: int
281
+ """
282
+ if isinstance(timestamp_value, str):
283
+ if not timestamp_value.isdigit():
284
+ raise ValueError(f"Invalid timestamp value: {timestamp_value}")
285
+ timestamp_int = int(timestamp_value)
286
+ elif isinstance(timestamp_value, (int, float)):
287
+ timestamp_int = int(timestamp_value)
288
+ else:
289
+ raise ValueError(f"Invalid timestamp value type: {type(timestamp_value)}, defaulting to current datetime")
290
+
291
+ # Determine if it's epoch seconds or milliseconds based on magnitude
292
+ if timestamp_int > 9999999999: # Likely milliseconds (13+ digits)
293
+ return timestamp_int // 1000
294
+ return timestamp_int
@@ -27,6 +27,7 @@ from regscale.core.app.utils.app_utils import (
27
27
  uncamel_case,
28
28
  save_data_to,
29
29
  )
30
+ from regscale.models.app_models.click import NotRequiredIf
30
31
  from regscale.models import regscale_id, regscale_module, regscale_ssp_id, Asset, Component, File, Issue
31
32
  from regscale.models.integration_models.defender_data import DefenderData
32
33
  from regscale.models.integration_models.flat_file_importer import FlatFileImporter
@@ -128,6 +129,8 @@ def sync_cloud_resources(regscale_ssp_id: int):
128
129
  help="The name of the saved query to export from Microsoft Defender for Cloud resource graph queries.",
129
130
  prompt="Enter the name of the query to export",
130
131
  default=None,
132
+ cls=NotRequiredIf,
133
+ not_required_if=["all_queries"],
131
134
  )
132
135
  @click.option(
133
136
  "--no_upload",