regscale-cli 6.17.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.
- regscale/__init__.py +1 -1
- regscale/core/app/api.py +5 -0
- regscale/core/login.py +3 -0
- regscale/integrations/api_paginator.py +932 -0
- regscale/integrations/api_paginator_example.py +348 -0
- regscale/integrations/commercial/__init__.py +11 -10
- regscale/integrations/commercial/burp.py +4 -0
- regscale/integrations/commercial/{qualys.py → qualys/__init__.py} +756 -105
- regscale/integrations/commercial/qualys/scanner.py +1051 -0
- regscale/integrations/commercial/qualys/variables.py +21 -0
- regscale/integrations/commercial/sicura/api.py +1 -0
- regscale/integrations/commercial/stigv2/click_commands.py +36 -8
- regscale/integrations/commercial/stigv2/stig_integration.py +63 -9
- regscale/integrations/commercial/tenablev2/__init__.py +9 -0
- regscale/integrations/commercial/tenablev2/authenticate.py +23 -2
- regscale/integrations/commercial/tenablev2/commands.py +779 -0
- regscale/integrations/commercial/tenablev2/jsonl_scanner.py +1999 -0
- regscale/integrations/commercial/tenablev2/sc_scanner.py +600 -0
- regscale/integrations/commercial/tenablev2/scanner.py +7 -5
- regscale/integrations/commercial/tenablev2/utils.py +21 -4
- regscale/integrations/commercial/tenablev2/variables.py +4 -0
- regscale/integrations/jsonl_scanner_integration.py +523 -142
- regscale/integrations/scanner_integration.py +102 -26
- regscale/integrations/transformer/__init__.py +17 -0
- regscale/integrations/transformer/data_transformer.py +445 -0
- regscale/integrations/transformer/mappings/__init__.py +8 -0
- regscale/integrations/variables.py +2 -0
- regscale/models/__init__.py +5 -2
- regscale/models/integration_models/cisa_kev_data.json +63 -7
- regscale/models/integration_models/synqly_models/capabilities.json +1 -1
- regscale/models/regscale_models/asset.py +5 -2
- regscale/models/regscale_models/file.py +5 -2
- regscale/regscale.py +3 -1
- {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/METADATA +1 -1
- {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/RECORD +47 -31
- tests/regscale/core/test_version.py +22 -0
- tests/regscale/integrations/__init__.py +0 -0
- tests/regscale/integrations/test_api_paginator.py +597 -0
- tests/regscale/integrations/test_integration_mapping.py +60 -0
- tests/regscale/integrations/test_issue_creation.py +317 -0
- tests/regscale/integrations/test_issue_due_date.py +46 -0
- tests/regscale/integrations/transformer/__init__.py +0 -0
- tests/regscale/integrations/transformer/test_data_transformer.py +850 -0
- regscale/integrations/commercial/tenablev2/click.py +0 -1637
- {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/LICENSE +0 -0
- {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/WHEEL +0 -0
- {regscale_cli-6.17.0.0.dist-info → regscale_cli-6.19.0.0.dist-info}/entry_points.txt +0 -0
- {regscale_cli-6.17.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
|
+
]
|