regscale-cli 6.20.5.0__py3-none-any.whl → 6.20.7.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 (37) 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/set_permissions.py +173 -0
  5. regscale/core/app/utils/file_utils.py +11 -1
  6. regscale/core/app/utils/regscale_utils.py +1 -133
  7. regscale/core/utils/date.py +62 -29
  8. regscale/integrations/commercial/qualys/__init__.py +7 -7
  9. regscale/integrations/commercial/wizv2/click.py +9 -5
  10. regscale/integrations/commercial/wizv2/constants.py +15 -0
  11. regscale/integrations/commercial/wizv2/parsers.py +23 -0
  12. regscale/integrations/commercial/wizv2/scanner.py +84 -29
  13. regscale/integrations/commercial/wizv2/utils.py +91 -4
  14. regscale/integrations/commercial/wizv2/variables.py +2 -1
  15. regscale/integrations/commercial/wizv2/wiz_auth.py +3 -3
  16. regscale/integrations/public/fedramp/fedramp_docx.py +2 -3
  17. regscale/integrations/scanner_integration.py +7 -2
  18. regscale/models/app_models/import_validater.py +5 -1
  19. regscale/models/app_models/mapping.py +3 -1
  20. regscale/models/integration_models/cisa_kev_data.json +140 -5
  21. regscale/models/integration_models/flat_file_importer/__init__.py +2 -3
  22. regscale/models/integration_models/qualys.py +24 -4
  23. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  24. regscale/models/regscale_models/__init__.py +2 -0
  25. regscale/models/regscale_models/asset.py +1 -1
  26. regscale/models/regscale_models/modules.py +88 -1
  27. regscale/models/regscale_models/regscale_model.py +7 -1
  28. regscale/models/regscale_models/vulnerability.py +3 -3
  29. regscale/models/regscale_models/vulnerability_mapping.py +2 -2
  30. regscale/regscale.py +2 -0
  31. {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/METADATA +1 -1
  32. {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/RECORD +37 -34
  33. tests/regscale/test_init.py +94 -0
  34. {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/LICENSE +0 -0
  35. {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/WHEEL +0 -0
  36. {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/entry_points.txt +0 -0
  37. {regscale_cli-6.20.5.0.dist-info → regscale_cli-6.20.7.0.dist-info}/top_level.txt +0 -0
regscale/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "6.20.5.0"
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.7.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
- 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,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 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
 
@@ -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
@@ -6,7 +6,10 @@ import datetime
6
6
  import logging
7
7
  from typing import Any, List, Optional, Union
8
8
 
9
- from dateutil.parser import ParserError, parse
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
- if isinstance(date_object, datetime.datetime):
27
- if date_format:
28
- return date_object.strftime(date_format)
29
- return date_object.date().isoformat()
30
- if isinstance(date_object, datetime.date):
31
- if date_format:
32
- return date_object.strftime(date_format)
33
- return date_object.isoformat()
34
- if isinstance(date_object, Timestamp):
35
- return date_object.date().isoformat()
36
- 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()
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
- if isinstance(date_object, datetime.datetime):
54
- return date_object.strftime(date_format)
55
- if isinstance(date_object, datetime.date):
56
- return date_object.strftime(date_format)
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
- return None
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
- delta = date_obj(end) - date_obj(start)
172
- 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)]
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 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)]
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
- start = date_obj(start)
214
- 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)
215
225
  if excluded_dates:
216
- for excluded_date in sorted([date_obj(x) for x in excluded_dates]):
217
- 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:
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.
@@ -791,7 +791,7 @@ def export_past_scans(save_output_to: Path, days: int, export: bool = True):
791
791
 
792
792
  @qualys.command(name="import_scans")
793
793
  @FlatFileImporter.common_scanner_options(
794
- message="File path to the folder containing Qualys .csv files to process to RegScale.",
794
+ message="File path to the folder containing Qualys .csv or .xlsx files to process to RegScale.",
795
795
  prompt="File path for Qualys files",
796
796
  import_name="qualys",
797
797
  )
@@ -814,16 +814,16 @@ def import_scans(
814
814
  upload_file: bool,
815
815
  ):
816
816
  """
817
- Import vulnerability scans from Qualys CSV files.
817
+ Import vulnerability scans from Qualys CSV or Excel (.xlsx) files.
818
818
 
819
- This command processes Qualys CSV export files and imports assets and vulnerabilities
820
- into RegScale. The CSV files must contain specific required headers.
819
+ This command processes Qualys CSV or Excel export files and imports assets and vulnerabilities
820
+ into RegScale. The files must contain specific required headers.
821
821
 
822
822
  TROUBLESHOOTING:
823
823
  If you encounter "No columns to parse from file" errors, try:
824
824
  1. Run 'regscale qualys validate_csv -f <file_path>' first
825
825
  2. Adjust the --skip_rows parameter (default: 129)
826
- 3. Check that your CSV file has the required headers
826
+ 3. Check that your file has the required headers
827
827
 
828
828
  REQUIRED HEADERS:
829
829
  Severity, Title, Exploitability, CVE ID, Solution, DNS, IP,
@@ -861,7 +861,7 @@ def import_qualys_scans(
861
861
  """
862
862
  Import scans from Qualys
863
863
 
864
- :param os.PathLike[str] folder_path: File path to the folder containing Qualys .csv files to process to RegScale
864
+ :param os.PathLike[str] folder_path: File path to the folder containing Qualys .csv or .xlsx files to process to RegScale
865
865
  :param int regscale_ssp_id: The RegScale SSP ID
866
866
  :param datetime scan_date: The date of the scan
867
867
  :param os.PathLike[str] mappings_path: The path to the mappings file
@@ -876,7 +876,7 @@ def import_qualys_scans(
876
876
  FlatFileImporter.import_files(
877
877
  import_type=Qualys,
878
878
  import_name="Qualys",
879
- file_types=".csv",
879
+ file_types=[".csv", ".xlsx"],
880
880
  folder_path=folder_path,
881
881
  regscale_ssp_id=regscale_ssp_id,
882
882
  scan_date=scan_date,
@@ -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. Can also be set as an environment variable: WIZ_CLIENT_ID",
347
- default=os.environ.get("WIZ_CLIENT_ID"),
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. Can also be set as an environment variable: WIZ_CLIENT_SECRET",
355
- default=os.environ.get("WIZ_CLIENT_SECRET"),
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,