regscale-cli 6.18.0.0__py3-none-any.whl → 6.19.0.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 (45) hide show
  1. regscale/__init__.py +1 -1
  2. regscale/integrations/api_paginator.py +932 -0
  3. regscale/integrations/api_paginator_example.py +348 -0
  4. regscale/integrations/commercial/__init__.py +11 -10
  5. regscale/integrations/commercial/{qualys.py → qualys/__init__.py} +756 -105
  6. regscale/integrations/commercial/qualys/scanner.py +1051 -0
  7. regscale/integrations/commercial/qualys/variables.py +21 -0
  8. regscale/integrations/commercial/sicura/api.py +1 -0
  9. regscale/integrations/commercial/stigv2/click_commands.py +36 -8
  10. regscale/integrations/commercial/stigv2/stig_integration.py +63 -9
  11. regscale/integrations/commercial/tenablev2/__init__.py +9 -0
  12. regscale/integrations/commercial/tenablev2/authenticate.py +23 -2
  13. regscale/integrations/commercial/tenablev2/commands.py +779 -0
  14. regscale/integrations/commercial/tenablev2/jsonl_scanner.py +1999 -0
  15. regscale/integrations/commercial/tenablev2/sc_scanner.py +600 -0
  16. regscale/integrations/commercial/tenablev2/scanner.py +7 -5
  17. regscale/integrations/commercial/tenablev2/utils.py +21 -4
  18. regscale/integrations/commercial/tenablev2/variables.py +4 -0
  19. regscale/integrations/jsonl_scanner_integration.py +523 -142
  20. regscale/integrations/scanner_integration.py +102 -26
  21. regscale/integrations/transformer/__init__.py +17 -0
  22. regscale/integrations/transformer/data_transformer.py +445 -0
  23. regscale/integrations/transformer/mappings/__init__.py +8 -0
  24. regscale/integrations/variables.py +2 -0
  25. regscale/models/__init__.py +5 -2
  26. regscale/models/integration_models/cisa_kev_data.json +5 -5
  27. regscale/models/integration_models/synqly_models/capabilities.json +1 -1
  28. regscale/models/regscale_models/asset.py +5 -2
  29. regscale/models/regscale_models/file.py +5 -2
  30. regscale/regscale.py +3 -1
  31. {regscale_cli-6.18.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/METADATA +1 -1
  32. {regscale_cli-6.18.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/RECORD +44 -28
  33. tests/regscale/core/test_version.py +22 -0
  34. tests/regscale/integrations/__init__.py +0 -0
  35. tests/regscale/integrations/test_api_paginator.py +597 -0
  36. tests/regscale/integrations/test_integration_mapping.py +60 -0
  37. tests/regscale/integrations/test_issue_creation.py +317 -0
  38. tests/regscale/integrations/test_issue_due_date.py +46 -0
  39. tests/regscale/integrations/transformer/__init__.py +0 -0
  40. tests/regscale/integrations/transformer/test_data_transformer.py +850 -0
  41. regscale/integrations/commercial/tenablev2/click.py +0 -1641
  42. {regscale_cli-6.18.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/LICENSE +0 -0
  43. {regscale_cli-6.18.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/WHEEL +0 -0
  44. {regscale_cli-6.18.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/entry_points.txt +0 -0
  45. {regscale_cli-6.18.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,779 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Tenable commands for the RegScale CLI.
5
+
6
+ This module provides Click command definitions for interacting with Tenable.io and Tenable SC.
7
+ """
8
+ import json
9
+ import logging
10
+ import os
11
+ from datetime import datetime, timedelta
12
+ from typing import List, Optional, Tuple
13
+
14
+ import click
15
+ from pathlib import Path
16
+ from requests.exceptions import RequestException
17
+ from rich.console import Console
18
+ from rich.progress import track
19
+
20
+ from regscale.core.app.utils.app_utils import (
21
+ check_license,
22
+ check_file_path,
23
+ get_current_datetime,
24
+ save_data_to,
25
+ )
26
+ from regscale.integrations.commercial.nessus.nessus_utils import get_cpe_file
27
+ from regscale.integrations.commercial.nessus.scanner import NessusIntegration
28
+ from regscale.integrations.commercial.tenablev2.authenticate import gen_tsc, gen_tio
29
+ from regscale.integrations.commercial.tenablev2.jsonl_scanner import TenableSCJsonlScanner
30
+ from regscale.integrations.commercial.tenablev2.sc_scanner import SCIntegration
31
+ from regscale.integrations.commercial.tenablev2.variables import TenableVariables
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
35
+
36
+ logger = logging.getLogger("regscale")
37
+ console = Console()
38
+ artifacts_dir = "./artifacts"
39
+
40
+
41
+ # Define a helper function for gen_client to replace the original one
42
+ def gen_client():
43
+ """
44
+ Generate the appropriate Tenable client based on configuration.
45
+
46
+ :return: Either a TenableIO or TenableSC client
47
+ """
48
+ return gen_tsc() # Default to TenableSC for now
49
+
50
+
51
+ @click.group(name="tenable", help="Tenable commands.")
52
+ def tenable():
53
+ """Tenable commands."""
54
+ pass
55
+
56
+
57
+ @tenable.group(name="sc")
58
+ def sc():
59
+ """Tenable SC commands."""
60
+ pass
61
+
62
+
63
+ @tenable.group(name="io")
64
+ def io():
65
+ """Tenable.io commands."""
66
+ pass
67
+
68
+
69
+ @tenable.group(help="Import Nessus scans and assets to RegScale.")
70
+ def nessus():
71
+ """Operations for Nessus scanner files."""
72
+ pass
73
+
74
+
75
+ def validate_tags(ctx: click.Context, param: click.Option, value: str) -> List[Tuple[str, str]]:
76
+ """
77
+ Validate the tuple elements.
78
+
79
+ :param click.Context ctx: Click context
80
+ :param click.Option param: Click option
81
+ :param str value: A string value to parse and validate
82
+ :return: Tuple of validated values
83
+ :rtype: List[Tuple[str,str]]
84
+ :raise ValueError: If the value is not in the correct format
85
+ """
86
+ if not value:
87
+ return []
88
+
89
+ tuple_list = []
90
+ for item in value.split(","):
91
+ parts = [part for part in item.strip().split(":") if part]
92
+ if len(parts) != 2:
93
+ raise ValueError(f"""Invalid format: "{item}". Expected 'key:value'""")
94
+ tuple_list.append((parts[0], parts[1]))
95
+
96
+ return tuple_list
97
+
98
+
99
+ @io.command(name="info")
100
+ def io_info():
101
+ """Display information about the configured Tenable.io instance."""
102
+ console.print("[bold]Tenable.io Configuration Information[/bold]")
103
+
104
+ try:
105
+ client = gen_tio()
106
+
107
+ # Get scanner information
108
+ scanner_info = client.scanners.details()
109
+
110
+ # Get user information
111
+ user_info = client.session.details()
112
+
113
+ # Display information
114
+ console.print(f"[bold]URL:[/bold] {TenableVariables.tenableUrl}")
115
+ console.print(f"[bold]User:[/bold] {user_info.get('username', 'Unknown')}")
116
+ console.print(f"[bold]Username:[/bold] {user_info.get('email', 'Unknown')}")
117
+ console.print(f"[bold]Scanner Count:[/bold] {len(scanner_info)}")
118
+
119
+ # Display scanner information
120
+ if scanner_info:
121
+ console.print("\n[bold]Scanner Information:[/bold]")
122
+ for scanner in scanner_info:
123
+ console.print(f" [bold]Name:[/bold] {scanner.get('name', 'Unknown')}")
124
+ console.print(f" [bold]Status:[/bold] {scanner.get('status', 'Unknown')}")
125
+ console.print("")
126
+
127
+ except Exception as e:
128
+ logger.error(f"Error getting Tenable.io information: {e}", exc_info=True)
129
+ console.print("[bold red]Error connecting to Tenable.io. Please check your configuration.[/bold red]")
130
+
131
+
132
+ @io.command(name="sync_assets")
133
+ @regscale_ssp_id(help="RegScale will create and update assets as children of this security plan.")
134
+ @click.option(
135
+ "--tags",
136
+ type=str,
137
+ help="Filter assets by tags (format: 'key:value,key2:value2').",
138
+ callback=validate_tags,
139
+ required=False,
140
+ )
141
+ def io_sync_assets(regscale_ssp_id: int, tags: List[Tuple[str, str]] = None):
142
+ """Sync assets from Tenable.io to RegScale."""
143
+ console.print("[bold]Starting Tenable.io asset synchronization...[/bold]")
144
+
145
+ try:
146
+ from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
147
+
148
+ integration = TenableIntegration(plan_id=regscale_ssp_id, tags=tags)
149
+ integration.sync_assets()
150
+
151
+ console.print("[bold green]Tenable.io asset synchronization complete.[/bold green]")
152
+ except Exception as e:
153
+ logger.error(f"Error syncing assets from Tenable.io: {e}", exc_info=True)
154
+
155
+
156
+ @io.command(name="sync_findings")
157
+ @regscale_ssp_id(help="RegScale will create findings as children of this security plan.")
158
+ @click.option(
159
+ "--tags",
160
+ type=str,
161
+ help="Filter assets by tags (format: 'key:value,key2:value2').",
162
+ callback=validate_tags,
163
+ required=False,
164
+ )
165
+ @click.option(
166
+ "--severity",
167
+ type=click.Choice(["critical", "high", "medium", "low", "all"], case_sensitive=False),
168
+ default="all",
169
+ help="Filter findings by severity.",
170
+ required=False,
171
+ )
172
+ @click.option(
173
+ "--scan_date",
174
+ type=click.DateTime(formats=["%Y-%m-%d"]),
175
+ help="The scan date of the findings.",
176
+ required=False,
177
+ )
178
+ def io_sync_findings(
179
+ regscale_ssp_id: int, tags: List[Tuple[str, str]] = None, severity: str = "all", scan_date: datetime = None
180
+ ):
181
+ """Sync vulnerability findings from Tenable.io to RegScale."""
182
+ console.print("[bold]Starting Tenable.io finding synchronization...[/bold]")
183
+
184
+ try:
185
+ from regscale.integrations.commercial.tenablev2.scanner import TenableIntegration
186
+
187
+ integration = TenableIntegration(plan_id=regscale_ssp_id, tags=tags, scan_date=scan_date)
188
+ integration.sync_findings(severity=severity)
189
+
190
+ console.print("[bold green]Tenable.io finding synchronization complete.[/bold green]")
191
+ except Exception as e:
192
+ logger.error(f"Error syncing findings from Tenable.io: {e}", exc_info=True)
193
+
194
+
195
+ @io.command(name="export_assets")
196
+ @save_output_to()
197
+ @file_types([".json", ".csv", ".xlsx"])
198
+ @click.option(
199
+ "--tags",
200
+ type=str,
201
+ help="Filter assets by tags (format: 'key:value,key2:value2').",
202
+ callback=validate_tags,
203
+ required=False,
204
+ )
205
+ def export_io_assets(save_output_to: Path, file_type: str, tags: List[Tuple[str, str]] = None):
206
+ """Export assets from Tenable.io to a .json, .csv or .xlsx file."""
207
+ console.print("[bold]Exporting Tenable.io assets...[/bold]")
208
+
209
+ try:
210
+ client = gen_tio()
211
+
212
+ # Create artifacts directory if not exists
213
+ Path(artifacts_dir).mkdir(exist_ok=True, parents=True)
214
+ current_datetime = datetime.now().strftime("%Y%m%d%H")
215
+ temp_file = Path(artifacts_dir) / Path(f"tenable_assets_{current_datetime}.json")
216
+
217
+ # Fetch assets
218
+ logger.info("Fetching Tenable.io assets...")
219
+ assets = []
220
+ assets_iterator = client.exports.assets(tags=tags) if tags else client.exports.assets()
221
+ i = 0
222
+
223
+ with open(temp_file, "w") as f:
224
+ for i, asset in enumerate(assets_iterator, 1):
225
+ f.write(json.dumps(asset) + "\n")
226
+ assets.append(asset)
227
+ if i % 100 == 0:
228
+ logger.info(f"Fetched {i} assets")
229
+
230
+ logger.info(f"Total assets fetched: {i}")
231
+
232
+ # Set the output file name
233
+ file_name = f"tenable_io_assets_{get_current_datetime('%m%d%Y')}"
234
+ output_file = Path(f"{save_output_to}/{file_name}{file_type}")
235
+
236
+ # Save the data to the selected file format
237
+ save_data_to(
238
+ file=output_file,
239
+ data=assets,
240
+ )
241
+
242
+ console.print(f"[bold green]Tenable.io assets exported to {output_file}[/bold green]")
243
+ except Exception as e:
244
+ logger.error(f"Error exporting assets from Tenable.io: {e}", exc_info=True)
245
+
246
+
247
+ @io.command(name="export_findings")
248
+ @save_output_to()
249
+ @file_types([".json", ".csv", ".xlsx"])
250
+ @click.option(
251
+ "--severity",
252
+ type=click.Choice(["critical", "high", "medium", "low", "all"], case_sensitive=False),
253
+ default="all",
254
+ help="Filter findings by severity.",
255
+ required=False,
256
+ )
257
+ @click.option(
258
+ "--days",
259
+ type=click.INT,
260
+ default=30,
261
+ help="Number of days to look back for vulnerabilities (default: 30).",
262
+ required=False,
263
+ )
264
+ def export_io_findings(save_output_to: Path, file_type: str, severity: str = "all", days: int = 30):
265
+ """Export vulnerability findings from Tenable.io to a .json, .csv or .xlsx file."""
266
+ console.print("[bold]Exporting Tenable.io vulnerability findings...[/bold]")
267
+
268
+ try:
269
+ client = gen_tio()
270
+
271
+ # Create artifacts directory if not exists
272
+ Path(artifacts_dir).mkdir(exist_ok=True, parents=True)
273
+ current_datetime = datetime.now().strftime("%Y%m%d%H")
274
+ temp_file = Path(artifacts_dir) / Path(f"tenable_findings_{current_datetime}.json")
275
+
276
+ # Calculate lookback time
277
+ lookback_time = int((datetime.now() - timedelta(days=days)).timestamp())
278
+
279
+ # Set severity filter
280
+ severity_filter = None
281
+ if severity != "all":
282
+ severity_map = {"critical": "4", "high": "3", "medium": "2", "low": "1"}
283
+ severity_filter = severity_map.get(severity.lower())
284
+
285
+ # Fetch vulnerabilities
286
+ logger.info("Fetching Tenable.io vulnerability findings...")
287
+ vulns = []
288
+
289
+ vuln_iterator = client.exports.vulnerabilities(last_found=lookback_time, severity=severity_filter)
290
+
291
+ i = 0
292
+ with open(temp_file, "w") as f:
293
+ for i, vuln in enumerate(vuln_iterator, 1):
294
+ f.write(json.dumps(vuln) + "\n")
295
+ vulns.append(vuln)
296
+ if i % 100 == 0:
297
+ logger.info(f"Fetched {i} findings")
298
+
299
+ logger.info(f"Total findings fetched: {i}")
300
+
301
+ # Set the output file name
302
+ file_name = f"tenable_io_findings_{get_current_datetime('%m%d%Y')}"
303
+ output_file = Path(f"{save_output_to}/{file_name}{file_type}")
304
+
305
+ # Save the data to the selected file format
306
+ save_data_to(
307
+ file=output_file,
308
+ data=vulns,
309
+ )
310
+
311
+ console.print(f"[bold green]Tenable.io findings exported to {output_file}[/bold green]")
312
+ except Exception as e:
313
+ logger.error(f"Error exporting findings from Tenable.io: {e}", exc_info=True)
314
+
315
+
316
+ @nessus.command(name="import_nessus")
317
+ @click.option(
318
+ "--folder_path",
319
+ prompt="Enter the folder path of the Nessus files to process",
320
+ help="RegScale will load the Nessus Scans",
321
+ type=click.Path(exists=True),
322
+ )
323
+ @click.option(
324
+ "--scan_date",
325
+ type=click.DateTime(formats=["%Y-%m-%d"]),
326
+ help="The scan date of the file.",
327
+ required=False,
328
+ )
329
+ @regscale_ssp_id()
330
+ def import_nessus(folder_path: click.Path, regscale_ssp_id: int, scan_date: datetime = None):
331
+ """Import Nessus scans, vulnerabilities and assets to RegScale."""
332
+ from regscale.validation.record import validate_regscale_object
333
+
334
+ if not validate_regscale_object(regscale_ssp_id, "securityplans"):
335
+ logger.warning("SSP #%i is not a valid RegScale Security Plan.", regscale_ssp_id)
336
+ return
337
+
338
+ console.print("[bold]Starting Nessus import...[/bold]")
339
+ NessusIntegration.sync_assets(plan_id=regscale_ssp_id, path=folder_path)
340
+ NessusIntegration.sync_findings(
341
+ plan_id=regscale_ssp_id, path=folder_path, enable_finding_date_update=True, scan_date=scan_date
342
+ )
343
+ console.print("[bold green]Nessus import complete.[/bold green]")
344
+
345
+
346
+ @nessus.command(name="update_cpe_dictionary")
347
+ def update_cpe_dictionary():
348
+ """
349
+ Manually update the CPE 2.2 dictionary from NIST.
350
+ """
351
+ console.print("[bold]Updating CPE dictionary...[/bold]")
352
+ get_cpe_file(download=True)
353
+ console.print("[bold green]CPE dictionary update complete.[/bold green]")
354
+
355
+
356
+ @sc.command(name="export_scans")
357
+ @save_output_to()
358
+ @file_types([".json", ".csv", ".xlsx"])
359
+ def export_scans(save_output_to: Path, file_type: str):
360
+ """Export scans from Tenable Host to a .json, .csv or .xlsx file."""
361
+ console.print("[bold]Exporting Tenable SC scans...[/bold]")
362
+
363
+ # get the scan results
364
+ results = get_usable_scan_list()
365
+
366
+ # check if file path exists
367
+ check_file_path(save_output_to)
368
+
369
+ # set the file name
370
+ file_name = f"tenable_scans_{get_current_datetime('%m%d%Y')}"
371
+
372
+ # save the data as the selected file by the user
373
+ save_data_to(
374
+ file=Path(f"{save_output_to}/{file_name}{file_type}"),
375
+ data=results,
376
+ )
377
+
378
+ console.print(f"[bold green]Tenable SC scans exported to {save_output_to}/{file_name}{file_type}[/bold green]")
379
+
380
+
381
+ def get_usable_scan_list() -> list:
382
+ """
383
+ Get usable scans from Tenable Host.
384
+
385
+ :return: List of scans from Tenable
386
+ :rtype: list
387
+ """
388
+ results = []
389
+ try:
390
+ client = gen_client()
391
+ results = client.scans.list()["usable"]
392
+ except Exception as ex:
393
+ logger.error(f"Error getting scan list: {ex}")
394
+ return results
395
+
396
+
397
+ def get_detailed_scans(scan_list: list = None) -> list:
398
+ """
399
+ Generate list of detailed scans.
400
+
401
+ Warning: this action could take a long time to complete.
402
+
403
+ :param list scan_list: List of scans from Tenable, defaults to None
404
+ :raise SystemExit: If there is an error with the request
405
+ :return: Detailed list of Tenable scans
406
+ :rtype: list
407
+ """
408
+ client = gen_client()
409
+ detailed_scans = []
410
+ for scan in track(scan_list, description="Fetching detailed scans..."):
411
+ try:
412
+ det = client.scans.details(id=scan["id"])
413
+ detailed_scans.append(det)
414
+ except RequestException as ex:
415
+ raise SystemExit(ex) from ex
416
+
417
+ return detailed_scans
418
+
419
+
420
+ @sc.command(name="save_queries")
421
+ @save_output_to()
422
+ @file_types([".json", ".csv", ".xlsx"])
423
+ def save_queries(save_output_to: Path, file_type: str):
424
+ """Get a list of query definitions and save them as a .json, .csv or .xlsx file."""
425
+ console.print("[bold]Exporting Tenable SC queries...[/bold]")
426
+
427
+ # get the queries from Tenable
428
+ query_list = get_queries()
429
+
430
+ # check if file path exists
431
+ check_file_path(save_output_to)
432
+
433
+ # set the file name
434
+ file_name = f"tenable_queries_{get_current_datetime('%m%d%Y')}"
435
+
436
+ # save the data as a file
437
+ save_data_to(
438
+ file=Path(f"{save_output_to}{os.sep}{file_name}{file_type}"),
439
+ data=query_list,
440
+ )
441
+
442
+ console.print(
443
+ f"[bold green]Tenable SC queries exported to {save_output_to}{os.sep}{file_name}{file_type}[/bold green]"
444
+ )
445
+
446
+
447
+ def get_queries() -> list:
448
+ """
449
+ Get list of query definitions from Tenable SC.
450
+
451
+ :return: List of queries from Tenable
452
+ :rtype: list
453
+ """
454
+ tsc = gen_tsc()
455
+ return tsc.queries.list()
456
+
457
+
458
+ @sc.command(name="query_vuln")
459
+ @click.option(
460
+ "--query_id",
461
+ type=click.INT,
462
+ help="Tenable query ID to retrieve via API",
463
+ prompt="Enter Tenable query ID",
464
+ required=True,
465
+ )
466
+ @regscale_ssp_id()
467
+ @click.option(
468
+ "--scan_date",
469
+ "-sd",
470
+ type=click.DateTime(formats=["%Y-%m-%d"]),
471
+ help="The scan date of the file.",
472
+ required=False,
473
+ )
474
+ def query_vuln(query_id: int, regscale_ssp_id: int, scan_date: datetime = None):
475
+ """Query Tenable SC vulnerabilities and sync assets to RegScale."""
476
+ try:
477
+ # Validate license
478
+ check_license()
479
+
480
+ console.print("[bold]Starting Tenable SC vulnerability query...[/bold]")
481
+
482
+ # Use the SCIntegration class method to fetch vulnerabilities by query ID
483
+ sc_integration = SCIntegration(plan_id=regscale_ssp_id, scan_date=scan_date)
484
+ sc_integration.fetch_vulns_query(query_id=query_id)
485
+
486
+ console.print("[bold green]Tenable SC vulnerability query complete.[/bold green]")
487
+ except Exception as e:
488
+ logger.error(f"Error querying Tenable SC vulnerabilities: {str(e)}", exc_info=True)
489
+ console.print(f"[bold red]Error querying Tenable SC vulnerabilities: {str(e)}[/bold red]")
490
+
491
+
492
+ @sc.command(name="list_tags")
493
+ def sc_tags():
494
+ """List the tags available in Tenable SC."""
495
+ list_tags()
496
+
497
+
498
+ def list_tags() -> None:
499
+ """
500
+ List the tags from Tenable SC.
501
+ """
502
+ tags = get_tags()
503
+ for tag in tags:
504
+ console.print(f"[bold]{tag['id']}[/bold]: {tag['name']} ({tag['description']})")
505
+
506
+
507
+ def get_tags() -> list:
508
+ """
509
+ Get the list of tags from Tenable SC.
510
+
511
+ :return: List of tags
512
+ :rtype: list
513
+ """
514
+ try:
515
+ tsc = gen_tsc()
516
+ return tsc.tags.list()["response"] if "response" in tsc.tags.list() else []
517
+ except Exception as ex:
518
+ logger.error(f"Error getting tags: {ex}")
519
+ return []
520
+
521
+
522
+ @sc.command(name="sync_jsonl")
523
+ @regscale_ssp_id()
524
+ @click.option(
525
+ "--query_id",
526
+ type=click.INT,
527
+ help="Tenable query ID to retrieve via API. Either query_id or file_path must be provided.",
528
+ required=False,
529
+ )
530
+ @click.option(
531
+ "--scan_date",
532
+ "-sd",
533
+ type=click.DateTime(formats=["%Y-%m-%d"]),
534
+ help="The scan date of the file.",
535
+ required=False,
536
+ )
537
+ @click.option(
538
+ "--file_path",
539
+ type=click.Path(exists=True),
540
+ help="Path to existing Tenable SC data files to process instead of downloading from API. Either query_id or file_path must be provided.",
541
+ required=False,
542
+ )
543
+ @click.option(
544
+ "--batch_size",
545
+ type=click.INT,
546
+ help="Number of items to process in each batch for large datasets.",
547
+ default=1000,
548
+ show_default=True,
549
+ required=False,
550
+ )
551
+ @click.option(
552
+ "--optimize-memory",
553
+ is_flag=True,
554
+ help="Enable memory optimization to reduce RAM usage.",
555
+ default=True,
556
+ show_default=True,
557
+ )
558
+ @click.option(
559
+ "--mapping_file",
560
+ type=click.Path(exists=True),
561
+ help="Optional custom mapping file for transformer-based processing.",
562
+ required=False,
563
+ )
564
+ @click.option(
565
+ "--skip-validation",
566
+ is_flag=True,
567
+ help="Skip RegScale object validation (use for development environments)",
568
+ default=True,
569
+ )
570
+ @click.option(
571
+ "--force-download",
572
+ is_flag=True,
573
+ help="Force download of fresh data from Tenable SC, ignoring any existing files.",
574
+ default=False,
575
+ )
576
+ def sc_sync_jsonl(
577
+ regscale_ssp_id: int,
578
+ query_id: int = None,
579
+ scan_date: datetime = None,
580
+ file_path: str = None,
581
+ batch_size: int = 1000,
582
+ optimize_memory: bool = True,
583
+ mapping_file: str = None,
584
+ skip_validation: bool = False,
585
+ force_download: bool = False,
586
+ ):
587
+ """
588
+ Sync Tenable SC query results to RegScale using the JSONL implementation.
589
+
590
+ This command uses the JSONLScannerIntegration to process Tenable SC data,
591
+ which provides better performance and memory efficiency for large datasets.
592
+
593
+ The implementation includes efficient batch processing and optional
594
+ transformer-based mapping of complex data fields.
595
+
596
+ Vulnerabilities are fetched from Tenable SC using the specified query ID,
597
+ or existing data files can be processed from the specified file path.
598
+
599
+ Note: Either query_id or file_path must be provided.
600
+ """
601
+ # Validate security plan (if not skipping validation)
602
+ if not skip_validation:
603
+ ssp = SecurityPlan.get_object(object_id=regscale_ssp_id)
604
+ if not ssp:
605
+ console.print(f"[bold red]Error:[/bold red] No security plan with ID {regscale_ssp_id} exists.")
606
+ return
607
+ else:
608
+ console.print("[yellow]Skipping RegScale validation for development mode[/yellow]")
609
+
610
+ # Validate that either query_id or file_path is provided
611
+ if not query_id and not file_path:
612
+ console.print("[bold red]Error:[/bold red] Either --query_id or --file_path must be provided.")
613
+ return
614
+
615
+ console.print("[bold]Starting Tenable SC sync with JSONL Scanner...[/bold]")
616
+
617
+ # Add a helpful explanation about the implementation
618
+ console.print("[yellow]This command uses efficient batch processing for optimal performance.[/yellow]")
619
+
620
+ try:
621
+ # Create the integration
622
+ scanner = TenableSCJsonlScanner(
623
+ plan_id=regscale_ssp_id,
624
+ query_id=query_id,
625
+ file_path=file_path,
626
+ scan_date=scan_date.strftime("%Y-%m-%d") if scan_date else None,
627
+ batch_size=batch_size,
628
+ optimize_memory=optimize_memory,
629
+ force_download=force_download,
630
+ )
631
+
632
+ # Run the synchronization
633
+ if mapping_file:
634
+ # If mapping file provided, use transformer-based processing
635
+ console.print("[yellow]Using custom mapping file for transformer-based processing.[/yellow]")
636
+ scanner.sync_with_transformer(mapping_file=mapping_file)
637
+ else:
638
+ # If query_id is provided but no file_path, the scanner will download data
639
+ if query_id and not file_path:
640
+ console.print(f"[yellow]Downloading data from Tenable SC using query ID: {query_id}[/yellow]")
641
+ # Find or download method will be called inside sync_assets_and_findings
642
+ scanner.sync_assets_and_findings()
643
+ else:
644
+ # Use the provided file_path for synchronization
645
+ scanner.sync_assets_and_findings()
646
+
647
+ console.print("[bold green]Tenable SC sync completed successfully.[/bold green]")
648
+ except Exception as e:
649
+ console.print(f"[bold red]Error:[/bold red] {str(e)}")
650
+ logger.error(f"Error in Tenable SC JSONL sync: {str(e)}", exc_info=True)
651
+
652
+
653
+ # Command from existing commands.py
654
+ @tenable.command(name="sync_vulns")
655
+ @regscale_id(help="RegScale will create findings as children of this security plan.")
656
+ @click.option(
657
+ "--query-id",
658
+ type=click.INT,
659
+ required=True,
660
+ help="Tenable SC Query ID to retrieve vulnerability data from.",
661
+ prompt="Enter Tenable SC Query ID",
662
+ )
663
+ def sync_vulns(regscale_id: int, query_id: int):
664
+ """
665
+ Sync vulnerabilities from Tenable SC to RegScale.
666
+
667
+ Fetches vulnerability data from Tenable SC based on the specified query ID
668
+ and syncs it to RegScale as findings under the specified security plan.
669
+ """
670
+ try:
671
+ # Use the original SC scanner for direct API sync
672
+ console.print("[bold]Starting Tenable SC vulnerability sync...[/bold]")
673
+ scanner = SCIntegration(plan_id=regscale_id, scan_date=get_current_datetime())
674
+ scanner.fetch_vulns_query(query_id=query_id)
675
+ console.print("[bold green]Tenable SC vulnerability sync complete.[/bold green]")
676
+ except Exception as e:
677
+ logger.error(f"Error syncing Tenable SC vulnerabilities: {str(e)}", exc_info=True)
678
+ console.print(f"[bold red]Error syncing Tenable SC vulnerabilities: {str(e)}[/bold red]")
679
+
680
+
681
+ # Command from existing commands.py
682
+ @tenable.command(name="sync_jsonl")
683
+ @regscale_id(help="RegScale will create findings as children of this security plan.")
684
+ @click.option(
685
+ "--query-id",
686
+ type=click.INT,
687
+ required=False,
688
+ help="Tenable SC Query ID to retrieve vulnerability data from.",
689
+ )
690
+ @click.option(
691
+ "--file-path",
692
+ type=click.Path(exists=True),
693
+ required=False,
694
+ help="Path to directory containing Tenable SC data files (json or jsonl).",
695
+ )
696
+ @click.option(
697
+ "--batch-size",
698
+ type=click.INT,
699
+ default=1000,
700
+ help="Batch size for API requests (default: 1000).",
701
+ )
702
+ @click.option(
703
+ "--mapping-file",
704
+ type=click.Path(exists=True),
705
+ help="Custom mapping file for data transformation.",
706
+ required=False,
707
+ )
708
+ @click.option(
709
+ "--skip-validation",
710
+ is_flag=True,
711
+ help="Skip RegScale object validation (use for development environments)",
712
+ default=True,
713
+ )
714
+ def sync_jsonl(
715
+ regscale_id: int,
716
+ query_id: Optional[int],
717
+ file_path: Optional[str],
718
+ batch_size: int,
719
+ mapping_file: Optional[str] = None,
720
+ skip_validation: bool = False,
721
+ ):
722
+ """
723
+ Sync Tenable SC data to RegScale using the optimized JSONL implementation.
724
+
725
+ This command uses the JSONL implementation which is optimized for handling
726
+ large datasets with better memory efficiency and performance.
727
+
728
+ The implementation includes efficient batch processing for API requests and
729
+ transformer-based mapping of complex data fields when a mapping file is provided.
730
+
731
+ If neither query-id nor file-path is provided, the command will look for
732
+ existing data files in the artifacts directory.
733
+ """
734
+ # Check license
735
+ if not check_license():
736
+ click.echo("No license available, exiting.")
737
+ return
738
+
739
+ # Validate the security plan ID (if not skipping validation)
740
+ if not skip_validation:
741
+ ssp = SecurityPlan.get_object(object_id=regscale_id)
742
+ if not ssp:
743
+ console.print(f"[bold red]Error:[/bold red] No security plan with ID {regscale_id} exists.")
744
+ return
745
+ else:
746
+ console.print("[yellow]Skipping RegScale validation for development mode[/yellow]")
747
+
748
+ # Run the integration
749
+ try:
750
+ # Create the integration with the specified parameters
751
+ scanner = TenableSCJsonlScanner(
752
+ plan_id=regscale_id,
753
+ query_id=query_id,
754
+ file_path=file_path,
755
+ batch_size=batch_size,
756
+ )
757
+
758
+ # Run the synchronization based on whether a mapping file was provided
759
+ if mapping_file:
760
+ console.print("[yellow]Using custom mapping file for transformer-based processing.[/yellow]")
761
+ scanner.sync_with_transformer(mapping_file=mapping_file)
762
+ else:
763
+ scanner.sync_assets_and_findings()
764
+
765
+ console.print("[bold green]Tenable SC sync completed successfully.[/bold green]")
766
+ except Exception as e:
767
+ console.print(f"[bold red]Error:[/bold red] {str(e)}")
768
+ logger.error(f"Error in Tenable SC JSONL sync: {str(e)}", exc_info=True)
769
+
770
+
771
+ # Add import_nessus to __all__ exports at the end of the file
772
+ __all__ = [
773
+ "tenable",
774
+ "sc",
775
+ "nessus",
776
+ "import_nessus",
777
+ "sync_vulns",
778
+ "sync_jsonl",
779
+ ]