regscale-cli 6.19.0.1__py3-none-any.whl → 6.19.1.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 CHANGED
@@ -1 +1 @@
1
- __version__ = "6.19.0.1"
1
+ __version__ = "6.19.1.0"
@@ -642,7 +642,7 @@ def save_to_json(file: Path, data: Any, output_log: bool) -> None:
642
642
  with open(file, "w", encoding="utf-8") as outfile:
643
643
  outfile.write(str(data))
644
644
  if output_log:
645
- logger.info("Data successfully saved to %s", file.absolute())
645
+ logger.info("Data successfully saved to %s", file.name)
646
646
 
647
647
 
648
648
  def save_data_to(file: Path, data: Any, output_log: bool = True, transpose_data: bool = True) -> None:
@@ -30,12 +30,17 @@ from regscale.integrations.commercial.tenablev2.jsonl_scanner import TenableSCJs
30
30
  from regscale.integrations.commercial.tenablev2.sc_scanner import SCIntegration
31
31
  from regscale.integrations.commercial.tenablev2.variables import TenableVariables
32
32
  from regscale.models import regscale_id, regscale_ssp_id
33
- from regscale.models.app_models.click import save_output_to, file_types
34
- from regscale.models.regscale_models.security_plan import SecurityPlan
33
+ from regscale.models.app_models.click import file_types, hidden_file_path, save_output_to
34
+ from regscale.models.regscale_models import SecurityPlan
35
35
 
36
36
  logger = logging.getLogger("regscale")
37
37
  console = Console()
38
38
  artifacts_dir = "./artifacts"
39
+ REGSCALE_INC = "RegScale, Inc."
40
+ REGSCALE_CLI = "RegScale CLI"
41
+ FULLY_IMPLEMENTED = "Fully Implemented"
42
+ NOT_IMPLEMENTED = "Not Implemented"
43
+ IN_REMEDIATION = "In Remediation"
39
44
 
40
45
 
41
46
  # Define a helper function for gen_client to replace the original one
@@ -146,7 +151,7 @@ def io_sync_assets(regscale_ssp_id: int, tags: List[Tuple[str, str]] = None):
146
151
  from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
147
152
 
148
153
  integration = TenableIntegration(plan_id=regscale_ssp_id, tags=tags)
149
- integration.sync_assets()
154
+ integration.sync_assets(plan_id=regscale_ssp_id)
150
155
 
151
156
  console.print("[bold green]Tenable.io asset synchronization complete.[/bold green]")
152
157
  except Exception as e:
@@ -185,7 +190,7 @@ def io_sync_findings(
185
190
  from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
186
191
 
187
192
  integration = TenableIntegration(plan_id=regscale_ssp_id, tags=tags, scan_date=scan_date)
188
- integration.sync_findings(severity=severity)
193
+ integration.sync_findings(plan_id=regscale_ssp_id, severity=severity)
189
194
 
190
195
  console.print("[bold green]Tenable.io finding synchronization complete.[/bold green]")
191
196
  except Exception as e:
@@ -768,6 +773,31 @@ def sync_jsonl(
768
773
  logger.error(f"Error in Tenable SC JSONL sync: {str(e)}", exc_info=True)
769
774
 
770
775
 
776
+ @io.command(name="sync_compliance_controls")
777
+ @regscale_ssp_id()
778
+ @click.option(
779
+ "--catalog_id",
780
+ type=click.INT,
781
+ help="The ID number from RegScale Catalog that the System Security Plan's controls belong to",
782
+ prompt="Enter RegScale Catalog ID",
783
+ required=True,
784
+ )
785
+ @click.option(
786
+ "--framework",
787
+ required=True,
788
+ type=click.Choice(["800-53", "800-53r5", "CSF", "800-171"], case_sensitive=True),
789
+ help="The framework to use. from Tenable.io frameworks MUST be the same RegScale Catalog of controls",
790
+ )
791
+ @hidden_file_path(help="The file path to load control data instead of fetching from Tenable.io")
792
+ def sync_compliance_data(regscale_ssp_id: int, catalog_id: int, framework: str, offline: Optional[Path] = None):
793
+ """
794
+ Sync the compliance data from Tenable.io to create control implementations for controls in frameworks.
795
+ """
796
+ from regscale.integrations.commercial.tenablev2.sync_compliance import sync_compliance_data
797
+
798
+ sync_compliance_data(ssp_id=regscale_ssp_id, catalog_id=catalog_id, framework=framework, offline=offline)
799
+
800
+
771
801
  # Add import_nessus to __all__ exports at the end of the file
772
802
  __all__ = [
773
803
  "tenable",
@@ -0,0 +1,550 @@
1
+ """
2
+ Sync the compliance data from Tenable.io to RegScale
3
+ """
4
+
5
+ import json
6
+ import logging
7
+ from datetime import datetime, timedelta
8
+ from typing import Dict, List, Optional
9
+ from urllib.parse import urljoin
10
+
11
+ from pathlib import Path
12
+ from rich.console import Console
13
+
14
+ from regscale.core.app.api import Api
15
+ from regscale.core.app.application import Application
16
+ from regscale.core.app.utils.app_utils import (
17
+ format_dict_to_html,
18
+ get_current_datetime,
19
+ )
20
+ from regscale.models.integration_models.tenable_models.models import AssetCheck
21
+ from regscale.models.regscale_models import ControlImplementation
22
+
23
+ logger = logging.getLogger("regscale")
24
+ console = Console()
25
+ artifacts_dir = "./artifacts"
26
+ REGSCALE_INC = "RegScale, Inc."
27
+ REGSCALE_CLI = "RegScale CLI"
28
+ FULLY_IMPLEMENTED = "Fully Implemented"
29
+ NOT_IMPLEMENTED = "Not Implemented"
30
+ IN_REMEDIATION = "In Remediation"
31
+
32
+
33
+ def sync_compliance_data(ssp_id: int, catalog_id: int, framework: str, offline: Optional[Path] = None) -> None:
34
+ """
35
+ Sync the compliance data from Tenable.io to create control implementations for controls in frameworks
36
+ :param int ssp_id: The ID number from RegScale of the System Security Plan
37
+ :param int catalog_id: The ID number from RegScale Catalog that the System Security Plan's controls belong to
38
+ :param str framework: The framework to use. from Tenable.io frameworks MUST be the same RegScale Catalog of controls
39
+ :param Optional[Path] offline: The file path to load control data instead of fetching from Tenable.io, defaults to None
40
+ :rtype: None
41
+ """
42
+ logger.info("Note: This command only available for Tenable.io")
43
+ logger.info("Note: This command Requires admin access.")
44
+ app = Application()
45
+ config = app.config
46
+ # we specifically don't gen client here, so we only get the client for Tenable.io as its only supported there
47
+
48
+ compliance_data = _get_compliance_data(config=config, offline=offline) # type: ignore
49
+
50
+ dict_of_frameworks_and_asset_checks: Dict = dict()
51
+ framework_controls: Dict[str, List[str]] = {}
52
+ asset_checks: Dict[str, List[AssetCheck]] = {}
53
+ passing_controls: Dict = dict()
54
+ # partial_passing_controls: Dict = dict()
55
+ failing_controls: Dict = dict()
56
+ for findings in compliance_data:
57
+ asset_check = AssetCheck(**findings)
58
+ for ref in asset_check.reference:
59
+ if ref.framework not in framework_controls:
60
+ framework_controls[ref.framework] = []
61
+ if ref.control not in framework_controls[ref.framework]: # Avoid duplicate controls
62
+ framework_controls[ref.framework].append(ref.control)
63
+ formatted_control_id = convert_control_id(ref.control)
64
+ # sort controls by status
65
+ add_control_to_status_dict(
66
+ control_id=formatted_control_id,
67
+ status=asset_check.status,
68
+ dict_obj=failing_controls,
69
+ desired_status="FAILED",
70
+ )
71
+ add_control_to_status_dict(
72
+ control_id=formatted_control_id,
73
+ status=asset_check.status,
74
+ dict_obj=passing_controls,
75
+ desired_status="PASSED",
76
+ )
77
+ remove_passing_controls_if_in_failed_status(passing=passing_controls, failing=failing_controls)
78
+ if formatted_control_id not in asset_checks:
79
+ asset_checks[formatted_control_id] = [asset_check]
80
+ else:
81
+ asset_checks[formatted_control_id].append(asset_check)
82
+ dict_of_frameworks_and_asset_checks = {
83
+ key: {"controls": framework_controls, "asset_checks": asset_checks} for key in framework_controls.keys()
84
+ }
85
+ logger.info(f"Found {len(dict_of_frameworks_and_asset_checks)} findings to process")
86
+ framework_data = dict_of_frameworks_and_asset_checks.get(framework, None)
87
+ process_compliance_data(
88
+ framework_data=framework_data,
89
+ catalog_id=catalog_id,
90
+ ssp_id=ssp_id,
91
+ framework=framework,
92
+ passing_controls=passing_controls,
93
+ failing_controls=failing_controls,
94
+ )
95
+
96
+
97
+ def _get_compliance_data(config: dict, offline: Optional[Path] = None) -> Dict:
98
+ """
99
+ Get compliance data from Tenable.io
100
+ :param dict config: Configuration dictionary
101
+ :param Optional[Path] offline: File path to load control data instead of fetching from Tenable.io
102
+ :return: Compliance data
103
+ :rtype: Dict
104
+ """
105
+ from regscale import __version__
106
+ from tenable.io import TenableIO
107
+
108
+ if offline:
109
+ with open(offline.absolute(), "r") as f:
110
+ compliance_data = json.load(f)
111
+ else:
112
+ client = TenableIO(
113
+ url=config["tenableUrl"],
114
+ access_key=config["tenableAccessKey"],
115
+ secret_key=config["tenableSecretKey"],
116
+ vendor=REGSCALE_INC,
117
+ product=REGSCALE_CLI,
118
+ build=__version__,
119
+ )
120
+ compliance_data = client.exports.compliance()
121
+ return compliance_data
122
+
123
+
124
+ def add_control_to_status_dict(control_id: str, status: str, dict_obj: Dict, desired_status: str) -> None:
125
+ """
126
+ Add a control to a status dictionary
127
+ :param str control_id: The control id to add to the dictionary
128
+ :param str status: The status of the control
129
+ :param Dict dict_obj: The dictionary to add the control to
130
+ :param str desired_status: The desired status of the control
131
+ :rtype: None
132
+ """
133
+ friendly_control_id = control_id.lower()
134
+ if status == desired_status and friendly_control_id not in dict_obj:
135
+ dict_obj[friendly_control_id] = desired_status
136
+
137
+
138
+ def remove_passing_controls_if_in_failed_status(passing: Dict, failing: Dict) -> None:
139
+ """
140
+ Remove passing controls if they are in failed status
141
+ :param Dict passing: Dictionary of passing controls
142
+ :param Dict failing: Dictionary of failing controls
143
+ :rtype: None
144
+ """
145
+ to_remove = []
146
+ for k in passing.keys():
147
+ if k in failing.keys():
148
+ to_remove.append(k)
149
+
150
+ for k in to_remove:
151
+ del passing[k]
152
+
153
+
154
+ def process_compliance_data(
155
+ framework_data: Dict,
156
+ catalog_id: int,
157
+ ssp_id: int,
158
+ framework: str,
159
+ passing_controls: Dict,
160
+ failing_controls: Dict,
161
+ ) -> None:
162
+ """
163
+ Processes the compliance data from Tenable.io to create control implementations for controls in frameworks
164
+ :param Dict framework_data: List of tenable.io controls per framework
165
+ :param int catalog_id: The catalog id
166
+ :param int ssp_id: The ssp id
167
+ :param str framework: The framework name
168
+ :param Dict passing_controls: Dictionary of passing controls
169
+ :param Dict failing_controls: Dictionary of failing controls
170
+ :rtype: None
171
+ """
172
+ if not framework_data:
173
+ return
174
+ framework_controls = framework_data.get("controls", {})
175
+ asset_checks = framework_data.get("asset_checks", {})
176
+ existing_implementation_dict = get_existing_control_implementations(ssp_id)
177
+ catalog_controls = get_controls(catalog_id)
178
+ matched_controls = []
179
+ for tenable_framework, tenable_controls in framework_controls.items():
180
+ logger.info(f"Found {len(tenable_controls)} controls that passed for framework: {tenable_framework}")
181
+ # logger.info(f"tenable_controls: {tenable_controls[0]}") if len(tenable_controls) >0 else None
182
+ if tenable_framework == framework:
183
+ matched_controls = get_matched_controls(tenable_controls, catalog_controls)
184
+
185
+ logger.info(f"Found {len(matched_controls)} controls that matched")
186
+
187
+ control_implementations = create_control_implementations(
188
+ controls=matched_controls,
189
+ parent_id=ssp_id,
190
+ parent_module="securityplans",
191
+ existing_implementation_dict=existing_implementation_dict,
192
+ passing_controls=passing_controls,
193
+ failing_controls=failing_controls,
194
+ )
195
+
196
+ logger.info(f"SSP now has {len(control_implementations)} control implementations")
197
+ catalog_controls_dict = {c["id"]: c for c in catalog_controls}
198
+ create_assessments(control_implementations, catalog_controls_dict, asset_checks)
199
+
200
+
201
+ def create_assessments(
202
+ control_implementations: List[Dict],
203
+ catalog_controls_dict: Dict,
204
+ asset_checks: Dict,
205
+ ) -> None:
206
+ """
207
+ Create assessments from control implementations
208
+ :param List[Dict] control_implementations: List of control implementations
209
+ :param Dict catalog_controls_dict: Dictionary of catalog controls
210
+ :param Dict asset_checks: Dictionary of asset checks
211
+ :rtype: None
212
+ :return: None
213
+ """
214
+ app = Application()
215
+ user_id = app.config.get("userId", "")
216
+ assessments_to_create = []
217
+ for cim in control_implementations:
218
+ control = catalog_controls_dict.get(cim["controlID"], {})
219
+ check = asset_checks.get(control["controlId"].lower())
220
+ assessment = create_assessment_from_cim(cim, user_id, control, check)
221
+ assessments_to_create.append(assessment)
222
+ update_control_implementations(control_implementations, assessments_to_create)
223
+ post_assessments_to_api(assessments_to_create)
224
+
225
+
226
+ def get_control_assessments(control: Dict, assessments_to_create: List[Dict]) -> List[Dict]:
227
+ """
228
+ Get control assessments
229
+ :param Dict control: Control
230
+ :param List[Dict] assessments_to_create: List of assessments to create
231
+ :return: List of control assessments
232
+ :rtype: List[Dict]
233
+ """
234
+ return [
235
+ assess
236
+ for assess in assessments_to_create
237
+ if assess["controlID"] == control["id"] and assess["status"] == "Complete"
238
+ ]
239
+
240
+
241
+ def post_assessments_to_api(assessments_to_create: List[Dict]) -> None:
242
+ """
243
+ Post assessments to the API
244
+ :param List[Dict] assessments_to_create: List of assessments to create
245
+ :rtype: None
246
+ """
247
+ app = Application()
248
+ api = Api()
249
+ assessment_url = urljoin(app.config.get("domain", ""), "/api/assessments/batchCreate")
250
+ assessment_response = api.post(url=assessment_url, json=assessments_to_create)
251
+ if assessment_response.ok:
252
+ logger.info(f"Created {len(assessment_response.json())} Assessments!")
253
+ else:
254
+ logger.debug(assessment_response.status_code)
255
+ logger.error(f"Failed to insert Assessment.\n{assessment_response.text}")
256
+
257
+
258
+ def update_control_implementations(control_implementations: List[Dict], assessments_to_create: List[Dict]) -> None:
259
+ """
260
+ Update control implementations with assessments
261
+ :param List[Dict] control_implementations: List of control implementations
262
+ :param List[Dict] assessments_to_create: List of assessments to create
263
+ :rtype: None
264
+ """
265
+ for control in control_implementations:
266
+ control_assessments = get_control_assessments(control, assessments_to_create)
267
+ if sorted_assessments := sort_assessments(control_assessments):
268
+ update_control_object(control, sorted_assessments)
269
+
270
+
271
+ def update_control_object(control: Dict, sorted_assessments: List[Dict]) -> None:
272
+ """
273
+ Update control object
274
+ :param Dict control: Control
275
+ :param List[Dict] sorted_assessments: Sorted assessments
276
+ :rtype: None
277
+ """
278
+
279
+ dt_format = "%Y-%m-%d %H:%M:%S"
280
+ app = Application()
281
+ control["dateLastAssessed"] = sorted_assessments[0]["actualFinish"]
282
+ control["lastAssessmentResult"] = sorted_assessments[0]["assessmentResult"]
283
+ if control.get("lastAssessmentResult"):
284
+ control_obj = ControlImplementation(**control)
285
+ if control_obj.lastAssessmentResult == "Fail" and control_obj.status != IN_REMEDIATION:
286
+ control_obj.status = IN_REMEDIATION
287
+ control_obj.plannedImplementationDate = (datetime.now() + timedelta(30)).strftime(dt_format)
288
+ control_obj.stepsToImplement = "n/a"
289
+ elif control_obj.status == IN_REMEDIATION:
290
+ control_obj.plannedImplementationDate = (
291
+ (datetime.now() + timedelta(30)).strftime(dt_format)
292
+ if not control_obj.plannedImplementationDate
293
+ else control_obj.plannedImplementationDate
294
+ )
295
+ control_obj.stepsToImplement = "n/a" if not control_obj.stepsToImplement else control_obj.stepsToImplement
296
+ elif control_obj.lastAssessmentResult == "Pass" and control_obj.status != FULLY_IMPLEMENTED:
297
+ control_obj.status = FULLY_IMPLEMENTED
298
+ ControlImplementation.update(app=app, implementation=control_obj)
299
+
300
+
301
+ def sort_assessments(control_assessments: List[Dict]) -> List[Dict]:
302
+ """
303
+ Sort assessments by actual finish date
304
+ :param List[Dict] control_assessments: List of control assessments
305
+ :return: Sorted assessments
306
+ :rtype: List[Dict]
307
+ """
308
+ dt_format = "%Y-%m-%d %H:%M:%S"
309
+ return sorted(
310
+ control_assessments,
311
+ key=lambda x: datetime.strptime(x["actualFinish"], dt_format),
312
+ reverse=True,
313
+ )
314
+
315
+
316
+ def get_assessment_status_from_implementation_status(status: str) -> str:
317
+ """
318
+ Get the assessment status from the implementation status
319
+ :param str status: Implementation status
320
+ :return: Assessment status
321
+ :rtype: str
322
+ """
323
+ if status == FULLY_IMPLEMENTED:
324
+ return "Pass"
325
+ if status == IN_REMEDIATION:
326
+ return "Fail"
327
+ else:
328
+ return "N/A"
329
+
330
+
331
+ def create_assessment_from_cim(cim: Dict, user_id: str, control: Dict, check: List[AssetCheck]) -> Dict:
332
+ """
333
+ Create an assessment from a control implementation
334
+ :param Dict cim: Control Implementation
335
+ :param str user_id: User ID
336
+ :param Dict control: Control
337
+ :param List[AssetCheck] check: Asset Check
338
+ :return: Assessment
339
+ :rtype: Dict
340
+ """
341
+ assessment_result = get_assessment_status_from_implementation_status(cim.get("status"))
342
+ summary_dict = check[0].dict() if check else dict()
343
+ summary_dict.pop("reference", None)
344
+ title = summary_dict.get("check_name") if summary_dict else control.get("title")
345
+ html_summary = format_dict_to_html(summary_dict)
346
+ document_reviewed = check[0].audit_file if check else None
347
+ check_name = check[0].check_name if check else None
348
+ methodology = check[0].check_info if check else None
349
+ summary_of_results = check[0].description if check else None
350
+ uuid = check[0].asset_uuid if check and check[0].asset_uuid is not None else None
351
+ title_part = f"{title} - {uuid}" if uuid else f"{title}"
352
+ uuid_title = f"{title_part} Automated Assessment test"
353
+ return {
354
+ "leadAssessorId": user_id,
355
+ "title": uuid_title,
356
+ "assessmentType": "Control Testing",
357
+ "plannedStart": get_current_datetime(),
358
+ "plannedFinish": get_current_datetime(),
359
+ "status": "Complete",
360
+ "assessmentResult": assessment_result if assessment_result else "N/A",
361
+ "controlID": cim["id"],
362
+ "actualFinish": get_current_datetime(),
363
+ "assessmentReport": html_summary if html_summary else "Passed",
364
+ "parentId": cim["id"],
365
+ "parentModule": "controls",
366
+ "assessmentPlan": check_name if check_name else None,
367
+ "documentsReviewed": document_reviewed if document_reviewed else None,
368
+ "methodology": methodology if methodology else None,
369
+ "summaryOfResults": summary_of_results if summary_of_results else None,
370
+ }
371
+
372
+
373
+ def get_matched_controls(tenable_controls: List[Dict], catalog_controls: List[Dict]) -> List[Dict]:
374
+ """
375
+ Get controls that match between Tenable and the catalog
376
+ :param List[Dict] tenable_controls: List of controls from Tenable
377
+ :param List[Dict] catalog_controls: List of controls from the catalog
378
+ :return: List of matched controls
379
+ :rtype: List[Dict]
380
+ """
381
+ matched_controls = []
382
+ for control in tenable_controls:
383
+ formatted_control = convert_control_id(control)
384
+ logger.info(formatted_control)
385
+ for catalog_control in catalog_controls:
386
+ if catalog_control["controlId"].lower() == formatted_control.lower():
387
+ logger.info(f"Catalog Control {formatted_control} matched")
388
+ matched_controls.append(catalog_control)
389
+ break
390
+ return matched_controls
391
+
392
+
393
+ def create_control_implementations(
394
+ controls: list,
395
+ parent_id: int,
396
+ parent_module: str,
397
+ existing_implementation_dict: Dict,
398
+ passing_controls: Dict,
399
+ failing_controls: Dict,
400
+ ) -> List[Dict]:
401
+ """
402
+ Creates a list of control implementations
403
+ :param list controls: list of controls
404
+ :param int parent_id: parent control id
405
+ :param str parent_module: parent module
406
+ :param Dict existing_implementation_dict: Dictionary of existing control implementations
407
+ :param Dict passing_controls: Dictionary of passing controls
408
+ :param Dict failing_controls: Dictionary of failing controls
409
+ :return: list of control implementations
410
+ :rtype: List[Dict]
411
+ """
412
+ app = Application()
413
+ api = Api()
414
+ user_id = app.config.get("userId")
415
+ domain = app.config.get("domain")
416
+ control_implementations = []
417
+ to_create = []
418
+ to_update = []
419
+ for control in controls:
420
+ lower_case_control_id = control["controlId"].lower()
421
+ status = check_implementation(
422
+ passing_controls=passing_controls,
423
+ failing_controls=failing_controls,
424
+ control_id=lower_case_control_id,
425
+ )
426
+ if control["controlId"] not in existing_implementation_dict.keys():
427
+ cim = ControlImplementation(
428
+ controlOwnerId=user_id,
429
+ dateLastAssessed=get_current_datetime(),
430
+ status=status,
431
+ controlID=control["id"],
432
+ parentId=parent_id,
433
+ parentModule=parent_module,
434
+ createdById=user_id,
435
+ dateCreated=get_current_datetime(),
436
+ lastUpdatedById=user_id,
437
+ dateLastUpdated=get_current_datetime(),
438
+ ).dict()
439
+ cim["controlSource"] = "Baseline"
440
+ to_create.append(cim)
441
+
442
+ else:
443
+ # update existing control implementation data
444
+ existing_imp = existing_implementation_dict.get(control["controlId"])
445
+ existing_imp["status"] = status
446
+ existing_imp["dateLastAssessed"] = get_current_datetime()
447
+ existing_imp["lastUpdatedById"] = user_id
448
+ existing_imp["dateLastUpdated"] = get_current_datetime()
449
+ del existing_imp["createdBy"]
450
+ del existing_imp["systemRole"]
451
+ del existing_imp["controlOwner"]
452
+ del existing_imp["lastUpdatedBy"]
453
+ to_update.append(existing_imp)
454
+
455
+ if len(to_create) > 0:
456
+ ci_url = urljoin(domain, "/api/controlImplementation/batchCreate")
457
+ resp = api.post(url=ci_url, json=to_create)
458
+ if resp.ok:
459
+ control_implementations.extend(resp.json())
460
+ logger.info(f"Created {len(to_create)} Control Implementation(s), Successfully!")
461
+ else:
462
+ resp.raise_for_status()
463
+ if len(to_update) > 0:
464
+ ci_url = urljoin(domain, "/api/controlImplementation/batchUpdate")
465
+ resp = api.post(url=ci_url, json=to_update)
466
+ if resp.ok:
467
+ control_implementations.extend(resp.json())
468
+ logger.info(f"Updated {len(to_update)} Control Implementation(s), Successfully!")
469
+ else:
470
+ resp.raise_for_status()
471
+ return control_implementations
472
+
473
+
474
+ def check_implementation(passing_controls: Dict, failing_controls: Dict, control_id: str) -> str:
475
+ """
476
+ Checks the status of a control implementation
477
+ :param Dict passing_controls: Dictionary of passing controls
478
+ :param Dict failing_controls: Dictionary of failing controls
479
+ :param str control_id: control id
480
+ :return: status of control implementation
481
+ :rtype: str
482
+ """
483
+ if control_id in passing_controls.keys():
484
+ return FULLY_IMPLEMENTED
485
+ elif control_id in failing_controls.keys():
486
+ return IN_REMEDIATION
487
+ else:
488
+ return NOT_IMPLEMENTED
489
+
490
+
491
+ def convert_control_id(control_id: str) -> str:
492
+ """
493
+ Convert the control id to a format that can be used in Tenable.io
494
+ :param str control_id: The control id to convert
495
+ :return: The converted control id
496
+ :rtype: str
497
+ """
498
+ # Convert to lowercase
499
+ control_id = control_id.lower()
500
+
501
+ # Check if there's a parenthesis and replace its content
502
+ if "(" in control_id and ")" in control_id:
503
+ inner_value = control_id.split("(")[1].split(")")[0]
504
+ control_id = control_id.replace(f"({inner_value})", f".{inner_value}")
505
+
506
+ return control_id
507
+
508
+
509
+ def get_existing_control_implementations(parent_id: int) -> Dict:
510
+ """
511
+ fetch existing control implementations
512
+ :param int parent_id: parent control id
513
+ :return: Dictionary of existing control implementations
514
+ :rtype: Dict
515
+ """
516
+ app = Application()
517
+ api = Api()
518
+ domain = app.config.get("domain")
519
+ existing_implementation_dict = {}
520
+ get_url = urljoin(domain, f"/api/controlImplementation/getAllByPlan/{parent_id}")
521
+ response = api.get(get_url)
522
+ if response.ok:
523
+ existing_control_implementations_json = response.json()
524
+ for cim in existing_control_implementations_json:
525
+ existing_implementation_dict[cim["controlName"]] = cim
526
+ logger.info(f"Found {len(existing_implementation_dict)} existing control implementations")
527
+ elif response.status_code == 404:
528
+ logger.info(f"No existing control implementations found for {parent_id}")
529
+ else:
530
+ logger.warning(f"Unable to get existing control implementations. {response.text}")
531
+
532
+ return existing_implementation_dict
533
+
534
+
535
+ def get_controls(catalog_id: int) -> List[Dict]:
536
+ """
537
+ Gets all the controls
538
+ :param int catalog_id: catalog id
539
+ :return: list of controls
540
+ :rtype: List[Dict]
541
+ """
542
+ app = Application()
543
+ api = Api()
544
+ url = urljoin(app.config.get("domain"), f"/api/SecurityControls/getList/{catalog_id}")
545
+ response = api.get(url)
546
+ if response.ok:
547
+ return response.json()
548
+ else:
549
+ response.raise_for_status()
550
+ return []
@@ -14,7 +14,7 @@ import time
14
14
  from abc import ABC, abstractmethod
15
15
  from collections import defaultdict
16
16
  from concurrent.futures import ThreadPoolExecutor
17
- from typing import Any, Dict, Generic, Iterator, List, Optional, Set, TypeVar, Union, Callable
17
+ from typing import Any, Callable, Dict, Generic, Iterator, List, Optional, Set, TypeVar, Union
18
18
 
19
19
  from rich.progress import Progress, TaskID
20
20
 
@@ -616,6 +616,7 @@ class ScannerIntegration(ABC):
616
616
 
617
617
  # Close Outdated Findings
618
618
  close_outdated_findings = True
619
+ closed_count = 0
619
620
 
620
621
  def __init__(self, plan_id: int, tenant_id: int = 1, is_component: bool = False, **kwargs):
621
622
  """
@@ -4,7 +4,7 @@ from typing import Optional, Union
4
4
  from pydantic import Field, ConfigDict
5
5
 
6
6
  from regscale.core.app.utils.app_utils import get_current_datetime
7
- from regscale.models.regscale_models import RegScaleModel
7
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
8
8
 
9
9
 
10
10
  class AssesmentPlanArea(str, Enum):
@@ -0,0 +1,39 @@
1
+ from enum import Enum
2
+ from typing import Optional, Union
3
+ from datetime import datetime
4
+
5
+ from pydantic import Field, ConfigDict
6
+
7
+ from regscale.core.app.utils.app_utils import get_current_datetime
8
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
9
+
10
+
11
+ class AssessmentResult(RegScaleModel):
12
+ """Assessment Results Model"""
13
+
14
+ _module_slug = "assessmentresults"
15
+ _unique_fields = ["id"]
16
+
17
+ id: int = 0
18
+ isPublic: bool = True
19
+ uuid: Optional[str] = None
20
+ status: Optional[str] = None
21
+ loqId: Optional[int] = 0
22
+ lineOfInquiry: Optional[str] = None
23
+ requirement: Optional[str] = None
24
+ citation: Optional[str] = None
25
+ weight: Optional[int] = 0
26
+ responsibility: Optional[str] = None
27
+ guidance: Optional[str] = None
28
+ observations: Optional[str] = None
29
+ recommendations: Optional[str] = None
30
+ issuesIdentified: Optional[str] = None
31
+ riskAssessment: Optional[str] = None
32
+ samplingMethodology: Optional[str] = None
33
+ fixedDuringAssessment: bool = False
34
+ bComplete: bool = False
35
+ parentAssessmentId: Optional[int] = 0
36
+ dataDate: Optional[str] = Field(default_factory=get_current_datetime)
37
+ dataString: Optional[datetime] = None
38
+ dataDecimal: Optional[float] = 0.0
39
+ dataBoolean: Optional[bool] = False
@@ -4,7 +4,7 @@ from typing import Optional, Union
4
4
  from pydantic import Field, ConfigDict
5
5
 
6
6
  from regscale.core.app.utils.app_utils import get_current_datetime
7
- from regscale.models.regscale_models import RegScaleModel
7
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
8
8
 
9
9
 
10
10
  class LoIType(str, Enum):
@@ -48,5 +48,5 @@ class LinesOfInquiry(RegScaleModel):
48
48
  :rtype: ConfigDict
49
49
  """
50
50
  return ConfigDict( # type: ignore
51
- get_all_by_parent="/api/{model_slug}/getAllByParent/{intAssessmentID}",
51
+ get_all_by_parent="/api/{model_slug}/getAllByParent/{intParentID}",
52
52
  )
@@ -9,7 +9,7 @@ from pydantic import ConfigDict, Field
9
9
  from regscale.core.app.api import Api
10
10
  from regscale.core.app.application import Application
11
11
  from regscale.core.app.utils.app_utils import get_current_datetime
12
- from regscale.models.regscale_models import RegScaleModel
12
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
13
13
 
14
14
 
15
15
  class SoftwareInventory(RegScaleModel):
@@ -6,7 +6,7 @@ from typing import Optional, Union
6
6
 
7
7
  from pydantic import ConfigDict, Field
8
8
 
9
- from .regscale_model import RegScaleModel
9
+ from regscale.models.regscale_models.regscale_model import RegScaleModel
10
10
 
11
11
  logger = logging.getLogger("regscale")
12
12
 
@@ -103,9 +103,9 @@ class SupplyChain(RegScaleModel):
103
103
  parentModule: Optional[str] = None
104
104
  orgId: Optional[int] = None
105
105
  facilityId: Optional[int] = None
106
- contractValue: Optional[int] = 0
107
- fundedAmount: Optional[int] = 0
108
- actualCosts: Optional[int] = 0
106
+ contractValue: Optional[float] = 0.0
107
+ fundedAmount: Optional[float] = 0.0
108
+ actualCosts: Optional[float] = 0.0
109
109
  scope: Optional[str] = None
110
110
  startDate: Optional[str] = None
111
111
  endDate: Optional[str] = None
@@ -175,6 +175,17 @@ class User(RegScaleModel):
175
175
  response = cls._get_api_handler().get(endpoint=cls.get_endpoint("get_all"))
176
176
  return cast(List[T], cls._handle_list_response(response))
177
177
 
178
+ @classmethod
179
+ def get_roles(cls) -> List["User"]:
180
+ """
181
+ Get all roles from RegScale
182
+
183
+ :return: List of RegScale roles
184
+ :rtype: dict
185
+ """
186
+ response = cls._get_api_handler().get(endpoint=cls.get_endpoint("get_roles"))
187
+ return cast(List[T], cls._handle_list_response(response))
188
+
178
189
  def assign_role(self, role_id: str) -> bool:
179
190
  """
180
191
  Assign a role to a user
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: regscale-cli
3
- Version: 6.19.0.1
3
+ Version: 6.19.1.0
4
4
  Summary: Command Line Interface (CLI) for bulk processing/loading data into RegScale
5
5
  Home-page: https://github.com/RegScale/regscale-cli
6
6
  Author: Travis Howerton
@@ -78,7 +78,7 @@ Requires-Dist: PyMuPDF >=1.24.7 ; extra == 'airflow'
78
78
  Requires-Dist: PyYAML ==6.0.1 ; extra == 'airflow'
79
79
  Requires-Dist: apache-airflow-providers-common-sql ; extra == 'airflow'
80
80
  Requires-Dist: apache-airflow-providers-postgres ; extra == 'airflow'
81
- Requires-Dist: apache-airflow >=2.10.3 ; extra == 'airflow'
81
+ Requires-Dist: apache-airflow ==2.10.3 ; extra == 'airflow'
82
82
  Requires-Dist: beautifulsoup4 ; extra == 'airflow'
83
83
  Requires-Dist: boto3 ==1.35.10 ; extra == 'airflow'
84
84
  Requires-Dist: botocore ==1.35.10 ; extra == 'airflow'
@@ -146,7 +146,7 @@ Requires-Dist: PyYAML ==6.0.1 ; extra == 'airflow-azure'
146
146
  Requires-Dist: apache-airflow-providers-common-sql ; extra == 'airflow-azure'
147
147
  Requires-Dist: apache-airflow-providers-microsoft-azure ; extra == 'airflow-azure'
148
148
  Requires-Dist: apache-airflow-providers-postgres ; extra == 'airflow-azure'
149
- Requires-Dist: apache-airflow >=2.10.3 ; extra == 'airflow-azure'
149
+ Requires-Dist: apache-airflow ==2.10.3 ; extra == 'airflow-azure'
150
150
  Requires-Dist: azure-storage-blob ; extra == 'airflow-azure'
151
151
  Requires-Dist: beautifulsoup4 ; extra == 'airflow-azure'
152
152
  Requires-Dist: boto3 ==1.35.10 ; extra == 'airflow-azure'
@@ -216,7 +216,7 @@ Requires-Dist: PyYAML ==6.0.1 ; extra == 'airflow-sqlserver'
216
216
  Requires-Dist: apache-airflow-providers-common-sql ; extra == 'airflow-sqlserver'
217
217
  Requires-Dist: apache-airflow-providers-microsoft-mssql ; extra == 'airflow-sqlserver'
218
218
  Requires-Dist: apache-airflow-providers-postgres ; extra == 'airflow-sqlserver'
219
- Requires-Dist: apache-airflow >=2.10.3 ; extra == 'airflow-sqlserver'
219
+ Requires-Dist: apache-airflow ==2.10.3 ; extra == 'airflow-sqlserver'
220
220
  Requires-Dist: beautifulsoup4 ; extra == 'airflow-sqlserver'
221
221
  Requires-Dist: boto3 ==1.35.10 ; extra == 'airflow-sqlserver'
222
222
  Requires-Dist: botocore ==1.35.10 ; extra == 'airflow-sqlserver'
@@ -286,7 +286,7 @@ Requires-Dist: PyYAML ==6.0.1 ; extra == 'all'
286
286
  Requires-Dist: ansible ; extra == 'all'
287
287
  Requires-Dist: apache-airflow-providers-common-sql ; extra == 'all'
288
288
  Requires-Dist: apache-airflow-providers-postgres ; extra == 'all'
289
- Requires-Dist: apache-airflow >=2.10.3 ; extra == 'all'
289
+ Requires-Dist: apache-airflow ==2.10.3 ; extra == 'all'
290
290
  Requires-Dist: beautifulsoup4 ; extra == 'all'
291
291
  Requires-Dist: boto3 ==1.35.10 ; extra == 'all'
292
292
  Requires-Dist: botocore ==1.35.10 ; extra == 'all'
@@ -1,4 +1,4 @@
1
- regscale/__init__.py,sha256=BVzwcyu7M9vUKN8JF2eHy61Le8zrZvupLmlwkhOLLag,25
1
+ regscale/__init__.py,sha256=nj8VCAjjre7hxmOERduZFW-KJyVxE8MitYAHu5V7Sic,25
2
2
  regscale/regscale.py,sha256=t4uLMzy9P_KeKazI9ZTQupTkbgh28uMilLSpmzn7adE,30983
3
3
  regscale/airflow/__init__.py,sha256=yMwN0Bz4JbM0nl5qY_hPegxo_O2ilhTOL9PY5Njhn-s,270
4
4
  regscale/airflow/click_dags.py,sha256=H3SUR5jkvInNMv1gu-VG-Ja_H-kH145CpQYNalWNAbE,4520
@@ -55,7 +55,7 @@ regscale/core/app/internal/workflow.py,sha256=SpgYk1QyzdilVLOK1fFzaKhdLspumaugf5
55
55
  regscale/core/app/utils/XMLIR.py,sha256=M_RrCsbjznihatkucCKw6dPgHTPQczXyqIdUXWhuCLI,8328
56
56
  regscale/core/app/utils/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
57
57
  regscale/core/app/utils/api_handler.py,sha256=T1meKw6Yi3ZAgRbQ1xuKDVh9Q9B8mbMqqN_LrSwIlAM,11765
58
- regscale/core/app/utils/app_utils.py,sha256=PnZeSU3AJqqe4NtdqABYowH1c3emiPSGDgRUdVp1kDc,36246
58
+ regscale/core/app/utils/app_utils.py,sha256=zLEHHHWR4likFQYY_bZsWLTAoRajyyaWrESkotVrFlM,36240
59
59
  regscale/core/app/utils/file_utils.py,sha256=URKWVEiR9aFnwoW3-Io7R22tBVeROTC3sX1wOZuhqXw,8912
60
60
  regscale/core/app/utils/parser_utils.py,sha256=aBEgcFwbJMD-ARf3wzf-tyWwR6NHvzEcdYcPMm8hGqo,2533
61
61
  regscale/core/app/utils/pickle_file_handler.py,sha256=iMdv4N8z00TB5LyPdxIcLKNRpDQVWQ8ZQWAqCKpqmF0,1695
@@ -111,7 +111,7 @@ regscale/integrations/api_paginator.py,sha256=73rjaNM9mGv8evHAeoObXEjZPg-bJuGPo6
111
111
  regscale/integrations/api_paginator_example.py,sha256=lEuYI-xEGcjnXuIzbCobCP0YRuukLF0s8S3d382SAH4,12119
112
112
  regscale/integrations/integration_override.py,sha256=PH7t_bf-RCe_it3FJ61tlKX5UghqHuSEQNJWDfCamAg,5480
113
113
  regscale/integrations/jsonl_scanner_integration.py,sha256=fglDgJN9alUvbJzNYKrlIf_25oqQ8kIU0moE3f9tDMQ,56717
114
- regscale/integrations/scanner_integration.py,sha256=ECWOq2bZ-4LwwXxlEXd5VkCqtlXRhcNeRn2pHMYyZUU,134678
114
+ regscale/integrations/scanner_integration.py,sha256=fAHnFQ7twysa6ouo_gIVOR7WbbCHQq6-MQm7r-l9fPQ,134699
115
115
  regscale/integrations/variables.py,sha256=A0R76VAeJFj48wHnNgUY-xaD7QKcSmGO9-9cQLlqDJ8,2071
116
116
  regscale/integrations/commercial/__init__.py,sha256=BZcCIUdyW027NjOGkcBOT52RPSLrtUv69UEPIkAmq1M,13319
117
117
  regscale/integrations/commercial/ad.py,sha256=YXSmK8vRf6yi2GnREGa5GrE6GelhFrLj44SY8AO1pK0,15509
@@ -210,11 +210,12 @@ regscale/integrations/commercial/synqly/ticketing.py,sha256=6UhmDa0l-GYTJAyc93Hi
210
210
  regscale/integrations/commercial/synqly/vulnerabilities.py,sha256=B8GwfKVDdoOIikjZO-4KqDNf-ApXlcJNkwmno0FjDOQ,7842
211
211
  regscale/integrations/commercial/tenablev2/__init__.py,sha256=UpSY_oww83kz9c7amdbptJKwDB1gAOBQDS-Q9WFp588,295
212
212
  regscale/integrations/commercial/tenablev2/authenticate.py,sha256=VPTmxaVCaah2gJYNeU9P1KoQ734ohGQ-wcVy6JfqDTE,1247
213
- regscale/integrations/commercial/tenablev2/commands.py,sha256=MedjPqMXTudoYP32gErio-1FVgDwzrDnh6CxelMotaQ,26180
213
+ regscale/integrations/commercial/tenablev2/commands.py,sha256=zFwCbGAMlvQXZq5Jw1Ii7NRD5xXH8wtFpX09bawXU6A,27473
214
214
  regscale/integrations/commercial/tenablev2/jsonl_scanner.py,sha256=zNruS_Oi6ls6lJTUxwdD6jRiXCvVLuGI2nvpF29pvU4,83485
215
215
  regscale/integrations/commercial/tenablev2/sc_scanner.py,sha256=Y70eZf_1DM3Np9tlrvZ_OHHjqzHjqrizFcHG8esqLQQ,23220
216
216
  regscale/integrations/commercial/tenablev2/scanner.py,sha256=bKm7MmZ_xAjKwNT0x16WvuRbQcip3pXg8xqPNHejgKs,21584
217
217
  regscale/integrations/commercial/tenablev2/stig_parsers.py,sha256=01h5ImYMUsjrVHaGgqj5JVBx6Jzlhg06ufu0SL_uBEs,5983
218
+ regscale/integrations/commercial/tenablev2/sync_compliance.py,sha256=xViAWWKI6wAN6OoRVocXWbynKCgAFVki6mbgsKLhM0o,21927
218
219
  regscale/integrations/commercial/tenablev2/utils.py,sha256=_MmvcR71PmvH4dQ1k4M-q4PYAg0EE_aQ9w4cUdP7SwE,3359
219
220
  regscale/integrations/commercial/tenablev2/variables.py,sha256=CHK8HpSFHMUF4HWj0xosfEQ-RRNDoWdeRvjJok_mKpU,991
220
221
  regscale/integrations/commercial/trivy/__init__.py,sha256=fTTABeR1Z3Wei3l-A54SoTPn2mrMQrKLmr9SwTyYLTM,117
@@ -347,7 +348,8 @@ regscale/models/integration_models/tenable_models/integration.py,sha256=lplL8zmj
347
348
  regscale/models/integration_models/tenable_models/models.py,sha256=dmG7btkN4YkDWwnfW5Ldc3tWEAGjPiaRgJjrqMOkPEU,15846
348
349
  regscale/models/regscale_models/__init__.py,sha256=OmFpksQ7e1P52xkBfP5_CTew1A4JiB65cvNuwchGXWU,1900
349
350
  regscale/models/regscale_models/assessment.py,sha256=ekzNlcsfDGBu97PMCi7hBRGbzVgxk7Ij0RfrdGh1Rfw,20440
350
- regscale/models/regscale_models/assessment_plan.py,sha256=8AEj-lSq5kk7nSMJ7wJmDwkY9h94PB-oD7qKfLfT9cE,1701
351
+ regscale/models/regscale_models/assessment_plan.py,sha256=qo2YA5ckSbUKDHnC_2BUc2I9kMTje9Gq-qTCXqvEKCY,1716
352
+ regscale/models/regscale_models/assessment_result.py,sha256=K48yjYKwgY1-d_Y3aQUDcCvaqcTIVYdbKV5Wgicf4Ts,1283
351
353
  regscale/models/regscale_models/asset.py,sha256=-ws6o6hRl2sMLNcT8CIw0SQvOwLtrVSeEp5kF80ydAQ,19840
352
354
  regscale/models/regscale_models/asset_mapping.py,sha256=HFlkAoPZHy2xPYq28cXuzLpFoP36SI08HTL8mH_Q-uE,6851
353
355
  regscale/models/regscale_models/case.py,sha256=hLVTwZXzusnXR9avqh7xSVLJwPJ1rPI_Nla_VAkpZcg,1192
@@ -387,7 +389,7 @@ regscale/models/regscale_models/inherited_control.py,sha256=RuQJgVyZgfoIrUG_vvwQ
387
389
  regscale/models/regscale_models/interconnection.py,sha256=B8Y4I6KnN-T_gid08QM_o50OwGZcFfO8SJFY2uB68Ak,1256
388
390
  regscale/models/regscale_models/issue.py,sha256=wFt_pWARHYG6YikIPePgmvEVlinDPveKx7VB4zbCFvQ,46791
389
391
  regscale/models/regscale_models/leveraged_authorization.py,sha256=OUrL8JQV3r7T3ldHlL6Y_ZLv6KuQIC-3eZW5wZ7XFUk,4192
390
- regscale/models/regscale_models/line_of_inquiry.py,sha256=u8oPt15LvdFNv7FSSZIbBbjMqPF4lLbLEr3EN6mQa1Q,1702
392
+ regscale/models/regscale_models/line_of_inquiry.py,sha256=Uu0lQEhif0W6yTSkJo27GyQGmExSngJvyqGBTr4Q8Fg,1713
391
393
  regscale/models/regscale_models/link.py,sha256=lAY4Ig3Menm1EqfcAbVJ7jsCsRO5tWtJIf-9-G9FXT8,6593
392
394
  regscale/models/regscale_models/master_assessment.py,sha256=thG3dDTuhKTXxjarLlHlLG-KkpfQOZ7UUVMQFhypIrQ,6758
393
395
  regscale/models/regscale_models/meta_data.py,sha256=Fg8rrWSTx3K00QkF4glH9UdY9OFWJ4_UqxleLSSbx8I,2482
@@ -415,17 +417,17 @@ regscale/models/regscale_models/scan_history.py,sha256=o4e9P2rQlqlLj4mbgSPX44jut
415
417
  regscale/models/regscale_models/search.py,sha256=rPbFDCnnBRHY5JJv9Ev3_6GjMlkdhUAsaUzC97eE2Ys,1015
416
418
  regscale/models/regscale_models/security_control.py,sha256=tob8y9zaMGdITju4ffrCAa5s6qhdchdkuxJdZ6VdkOA,4614
417
419
  regscale/models/regscale_models/security_plan.py,sha256=4hBnAqHpjVXd6i1dsO2r2HbPKjeyM-QnMKCmdk1sYGw,7549
418
- regscale/models/regscale_models/software_inventory.py,sha256=xZyLl1SgFxRjrbASFkzTsmK3bQty2ZlO-zXPjuElwmM,5540
420
+ regscale/models/regscale_models/software_inventory.py,sha256=FRAIfoUlS0kaX1HQRDyV5q4yxwRHilXbS52NSj6exo0,5555
419
421
  regscale/models/regscale_models/stake_holder.py,sha256=JIuDTIky_3acDl-NOMwylTHkppN38JgPDZ1A6wM-BGE,1956
420
422
  regscale/models/regscale_models/stig.py,sha256=y-PQuGg3pwDTfsNZGW6anaNAjIZBQoNe7GOLMiT5zfw,26329
421
- regscale/models/regscale_models/supply_chain.py,sha256=wG0Jc8fN69_-1i6dwLc0YUHHL-dkBqyZ86b3E_7Uaj0,5111
423
+ regscale/models/regscale_models/supply_chain.py,sha256=tMP0IUt5mrJ1QaV5YtcdV7KW0wPQbKdPO3dX0se0gZo,5154
422
424
  regscale/models/regscale_models/system_role.py,sha256=cGKhdekD0ZlT_cuUGDhzED9J46UzWdAVzdoq_TxG0KA,6406
423
425
  regscale/models/regscale_models/system_role_external_assignment.py,sha256=vCI-paDP4gARkHyGareBUdkmK0hcp1cDSe7yFdYMSug,1333
424
426
  regscale/models/regscale_models/tag.py,sha256=D4n5ABDzjI7u1ukjRyHgmgyVd8iNTNJlQrdi55AhYmM,1130
425
427
  regscale/models/regscale_models/tag_mapping.py,sha256=QtafVsWjpBR8BAxRhabk3FL3E4WI5OdCv95fuvNOrZs,657
426
428
  regscale/models/regscale_models/task.py,sha256=eKVdR89Gb6M9E2plhK4fru8teQI3zPJpgtJ0f43FOrY,4919
427
429
  regscale/models/regscale_models/threat.py,sha256=4TNZcRnTgmlDwBsYu5Pbh9GRd8ZWAtqqr0Xph3uPNAA,7255
428
- regscale/models/regscale_models/user.py,sha256=2OfrIlwD-vtEMnC2Pn_ch2Ze5S_p7_SgBD4ejXmestI,6854
430
+ regscale/models/regscale_models/user.py,sha256=wiU2qKwp3aYtmaHEmTtv8BbMEgFb913dHgc2VnmsAkg,7186
429
431
  regscale/models/regscale_models/user_group.py,sha256=vzlXHvPNsgJd38H0R3osi46Oj19QO5oPx0qXntQBKWI,1891
430
432
  regscale/models/regscale_models/vulnerability.py,sha256=VKgWMVvAQ74Sq46myYsivGfIeW63dHq3bv3cRCkDcbg,10960
431
433
  regscale/models/regscale_models/vulnerability_mapping.py,sha256=cq44xkkCH7rfp0BJxavr4DLiEAHouyyPmi5EaizH6NI,6261
@@ -500,9 +502,9 @@ tests/regscale/models/test_regscale_model.py,sha256=ZsrEZkC4EtdIsoQuayn1xv2gEGcV
500
502
  tests/regscale/models/test_report.py,sha256=eiSvS_zS0aVeL0HBvtmHVvEzcfF9ZFVn2twj5g8KttY,970
501
503
  tests/regscale/models/test_tenable_integrations.py,sha256=PNJC2Zu6lv1xj7y6e1yOsz5FktSU3PRKb5x3n5YG3w0,4072
502
504
  tests/regscale/models/test_user_model.py,sha256=e9olv28qBApgnvK6hFHOgXjUC-pkaV8aGDirEIWASL4,4427
503
- regscale_cli-6.19.0.1.dist-info/LICENSE,sha256=ytNhYQ9Rmhj_m-EX2pPq9Ld6tH5wrqqDYg-fCf46WDU,1076
504
- regscale_cli-6.19.0.1.dist-info/METADATA,sha256=wWhMYlFSOvacarUAvTVNBKFy-HAUeWd_br8BZQH0Jho,30913
505
- regscale_cli-6.19.0.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
506
- regscale_cli-6.19.0.1.dist-info/entry_points.txt,sha256=cLOaIP1eRv1yZ2u7BvpE3aB4x3kDrDwkpeisKOu33z8,269
507
- regscale_cli-6.19.0.1.dist-info/top_level.txt,sha256=Uv8VUCAdxRm70bgrD4YNEJUmDhBThad_1aaEFGwRByc,15
508
- regscale_cli-6.19.0.1.dist-info/RECORD,,
505
+ regscale_cli-6.19.1.0.dist-info/LICENSE,sha256=ytNhYQ9Rmhj_m-EX2pPq9Ld6tH5wrqqDYg-fCf46WDU,1076
506
+ regscale_cli-6.19.1.0.dist-info/METADATA,sha256=eMCQ4kKNLhTBf2WOaVp35o_ZK0k7s13eNZ7OBEFNPy4,30913
507
+ regscale_cli-6.19.1.0.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92
508
+ regscale_cli-6.19.1.0.dist-info/entry_points.txt,sha256=cLOaIP1eRv1yZ2u7BvpE3aB4x3kDrDwkpeisKOu33z8,269
509
+ regscale_cli-6.19.1.0.dist-info/top_level.txt,sha256=Uv8VUCAdxRm70bgrD4YNEJUmDhBThad_1aaEFGwRByc,15
510
+ regscale_cli-6.19.1.0.dist-info/RECORD,,