regscale-cli 6.19.0.0__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.0"
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
  """
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "title": "CISA Catalog of Known Exploited Vulnerabilities",
3
- "catalogVersion": "2025.04.21",
4
- "dateReleased": "2025-04-21T17:28:48.0783Z",
3
+ "catalogVersion": "2025.04.25",
4
+ "dateReleased": "2025-04-25T18:02:32.6749Z",
5
5
  "count": 1323,
6
6
  "vulnerabilities": [
7
7
  {
@@ -6664,7 +6664,7 @@
6664
6664
  "shortDescription": "Intel ethernet diagnostics driver for Windows IQVW32.sys and IQVW64.sys contain an unspecified vulnerability that allows for a denial-of-service (DoS).",
6665
6665
  "requiredAction": "Apply updates per vendor instructions.",
6666
6666
  "dueDate": "2023-03-03",
6667
- "knownRansomwareCampaignUse": "Unknown",
6667
+ "knownRansomwareCampaignUse": "Known",
6668
6668
  "notes": "https:\/\/www.intel.com\/content\/www\/us\/en\/security-center\/advisory\/intel-sa-00051.html; https:\/\/nvd.nist.gov\/vuln\/detail\/CVE-2015-2291",
6669
6669
  "cwes": [
6670
6670
  "CWE-20"