regscale-cli 6.19.0.1__py3-none-any.whl → 6.19.2.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 (28) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/core/app/utils/app_utils.py +1 -1
  3. regscale/integrations/commercial/amazon/common.py +5 -4
  4. regscale/integrations/commercial/aws/scanner.py +3 -2
  5. regscale/integrations/commercial/synqly/assets.py +10 -0
  6. regscale/integrations/commercial/synqly/ticketing.py +25 -0
  7. regscale/integrations/commercial/tenablev2/commands.py +34 -4
  8. regscale/integrations/commercial/tenablev2/sync_compliance.py +550 -0
  9. regscale/integrations/commercial/wizv2/click.py +3 -3
  10. regscale/integrations/scanner_integration.py +3 -2
  11. regscale/models/app_models/import_validater.py +2 -0
  12. regscale/models/integration_models/cisa_kev_data.json +188 -10
  13. regscale/models/integration_models/flat_file_importer/__init__.py +26 -9
  14. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  15. regscale/models/regscale_models/assessment_plan.py +1 -1
  16. regscale/models/regscale_models/assessment_result.py +39 -0
  17. regscale/models/regscale_models/line_of_inquiry.py +2 -2
  18. regscale/models/regscale_models/regscale_model.py +16 -15
  19. regscale/models/regscale_models/software_inventory.py +1 -1
  20. regscale/models/regscale_models/supply_chain.py +4 -4
  21. regscale/models/regscale_models/user.py +11 -0
  22. regscale/utils/graphql_client.py +2 -1
  23. {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/METADATA +45 -45
  24. {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/RECORD +28 -26
  25. {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/LICENSE +0 -0
  26. {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/WHEEL +0 -0
  27. {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/entry_points.txt +0 -0
  28. {regscale_cli-6.19.0.1.dist-info → regscale_cli-6.19.2.0.dist-info}/top_level.txt +0 -0
@@ -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 []
@@ -128,7 +128,7 @@ def inventory(
128
128
  )
129
129
  def issues(
130
130
  wiz_project_id: str,
131
- regscale_id: int,
131
+ regscale_ssp_id: int,
132
132
  client_id: str,
133
133
  client_secret: str,
134
134
  filter_by_override: Optional[str] = None,
@@ -152,9 +152,9 @@ def issues(
152
152
 
153
153
  filter_by["project"] = wiz_project_id
154
154
 
155
- scanner = WizIssue(plan_id=regscale_id)
155
+ scanner = WizIssue(plan_id=regscale_ssp_id)
156
156
  scanner.sync_findings(
157
- plan_id=regscale_id,
157
+ plan_id=regscale_ssp_id,
158
158
  filter_by_override=filter_by_override, # type: ignore
159
159
  client_id=client_id, # type: ignore
160
160
  client_secret=client_secret, # type: ignore
@@ -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
  """
@@ -2258,7 +2259,7 @@ class ScannerIntegration(ABC):
2258
2259
  self.handle_passing_checklist(finding=finding, plan_id=self.plan_id)
2259
2260
 
2260
2261
  # Process vulnerability if applicable
2261
- if finding.status != regscale_models.IssueStatus.Closed:
2262
+ if finding.status != regscale_models.IssueStatus.Closed or ScannerVariables.ingestClosedIssues:
2262
2263
  if asset := self.get_asset_by_identifier(finding.asset_identifier):
2263
2264
  if vulnerability_id := self.handle_vulnerability(finding, asset, scan_history):
2264
2265
  current_vulnerabilities[asset.id].add(vulnerability_id)
@@ -183,6 +183,8 @@ class ImportValidater:
183
183
  df = pandas.read_csv(file_path, skiprows=self.skip_rows - 1, on_bad_lines="warn")
184
184
  else:
185
185
  df = pandas.read_csv(file_path, on_bad_lines="warn")
186
+ if self.ignore_unnamed:
187
+ df = df.loc[:, ~df.columns.str.contains("^Unnamed")]
186
188
  except pandas.errors.ParserError:
187
189
  raise ValidationException(f"Unable to parse the {CSV} file: {file_path}")
188
190
  self.validate_headers(df.columns)