Skip to content

Commit fe20e49

Browse files
committed
adopt new CLI
Signed-off-by: Keshav Priyadarshi <[email protected]>
1 parent 78dd5ae commit fe20e49

File tree

1 file changed

+382
-0
lines changed

1 file changed

+382
-0
lines changed

vulntotal/vulntotal_cli.py

Lines changed: 382 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,382 @@
1+
#!/usr/bin/env python
2+
# -*- coding: utf-8 -*-
3+
#
4+
# Copyright (c) nexB Inc. and others. All rights reserved.
5+
# http://nexb.com and https://github.com/nexB/vulnerablecode/
6+
# The VulnTotal software is licensed under the Apache License version 2.0.
7+
# Data generated with VulnTotal require an acknowledgment.
8+
#
9+
# You may not use this software except in compliance with the License.
10+
# You may obtain a copy of the License at: http://apache.org/licenses/LICENSE-2.0
11+
# Unless required by applicable law or agreed to in writing, software distributed
12+
# under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
13+
# CONDITIONS OF ANY KIND, either express or implied. See the License for the
14+
# specific language governing permissions and limitations under the License.
15+
#
16+
# When you publish or redistribute any data created with VulnTotal or any VulnTotal
17+
# derivative work, you must accompany this data with the following acknowledgment:
18+
#
19+
# Generated with VulnTotal and provided on an "AS IS" BASIS, WITHOUT WARRANTIES
20+
# OR CONDITIONS OF ANY KIND, either express or implied. No content created from
21+
# VulnTotal should be considered or used as legal advice. Consult an Attorney
22+
# for any legal advice.
23+
# VulnTotal is a free software code scanning tool from nexB Inc. and others.
24+
# Visit https://github.com/nexB/vulnerablecode/ for support and download.
25+
26+
import concurrent.futures
27+
import json
28+
import pydoc
29+
import sys
30+
31+
import click
32+
import yaml
33+
from packageurl import PackageURL
34+
from texttable import Texttable
35+
36+
from vulntotal.datasources import DATASOURCE_REGISTRY
37+
from vulntotal.validator import VendorData
38+
39+
40+
@click.command()
41+
@click.option(
42+
"-l",
43+
"--list",
44+
"list_source",
45+
is_flag=True,
46+
multiple=False,
47+
required=False,
48+
help="Lists all the available DataSources.",
49+
)
50+
@click.option(
51+
"-e",
52+
"--enable",
53+
"enable",
54+
hidden=True,
55+
multiple=True,
56+
type=click.Choice(DATASOURCE_REGISTRY.keys()),
57+
required=False,
58+
help="Enable these datasource/s only.",
59+
)
60+
@click.option(
61+
"-d",
62+
"--disable",
63+
"disable",
64+
hidden=True,
65+
multiple=True,
66+
type=click.Choice(DATASOURCE_REGISTRY.keys()),
67+
required=False,
68+
help="Disable these datasource/s.",
69+
)
70+
@click.option(
71+
"--ecosystem",
72+
"ecosystem",
73+
hidden=True,
74+
is_flag=True,
75+
required=False,
76+
help="Lists ecosystem supported by active DataSources",
77+
)
78+
@click.option(
79+
"--raw",
80+
"raw_output",
81+
is_flag=True,
82+
hidden=True,
83+
multiple=False,
84+
required=False,
85+
help="List of all the raw response from DataSources.",
86+
)
87+
@click.option(
88+
"--no-threading",
89+
"no_threading",
90+
is_flag=True,
91+
hidden=True,
92+
multiple=False,
93+
required=False,
94+
help="Run DataSources sequentially.",
95+
)
96+
@click.option(
97+
"-p",
98+
"--pagination",
99+
"pagination",
100+
is_flag=True,
101+
hidden=True,
102+
multiple=False,
103+
required=False,
104+
help="Enable default pagination.",
105+
)
106+
@click.option(
107+
"--json",
108+
"json_output",
109+
type=click.File("w"),
110+
required=False,
111+
metavar="FILE",
112+
help="Write output as pretty-printed JSON to FILE. ",
113+
)
114+
@click.option(
115+
"--yaml",
116+
"yaml_output",
117+
type=click.File("w"),
118+
required=False,
119+
metavar="FILE",
120+
help="Write output as YAML to FILE. ",
121+
)
122+
@click.option(
123+
"--no-group",
124+
"no_group",
125+
is_flag=True,
126+
hidden=True,
127+
multiple=False,
128+
required=False,
129+
help="Don't group by CVE.",
130+
)
131+
@click.argument("purl", required=False)
132+
@click.help_option("-h", "--help")
133+
def handler(
134+
purl,
135+
list_source,
136+
enable,
137+
disable,
138+
ecosystem,
139+
raw_output,
140+
no_threading,
141+
pagination,
142+
json_output,
143+
yaml_output,
144+
no_group,
145+
):
146+
"""
147+
Runs the PURL through all the available datasources and group vulnerability by CVEs.
148+
Use the special '-' file name to print JSON or YAML results on screen/stdout.
149+
"""
150+
active_datasource = (
151+
get_enabled_datasource(enable)
152+
if enable
153+
else (get_undisabled_datasource(disable) if disable else DATASOURCE_REGISTRY)
154+
)
155+
156+
if list_source:
157+
list_datasources()
158+
159+
elif not active_datasource:
160+
click.echo("No datasources available!", err=True)
161+
162+
elif ecosystem:
163+
list_supported_ecosystem(active_datasource)
164+
165+
elif raw_output:
166+
if purl:
167+
get_raw_response(purl, active_datasource)
168+
169+
elif json_output:
170+
write_json_output(purl, active_datasource, json_output, no_threading)
171+
172+
elif yaml_output:
173+
write_yaml_output(purl, active_datasource, yaml_output, no_threading)
174+
175+
elif no_group:
176+
prettyprint(purl, active_datasource, pagination, no_threading)
177+
178+
elif purl:
179+
prettyprint_group_by_cve(purl, active_datasource, pagination, no_threading)
180+
181+
182+
def get_valid_datasources(datasources):
183+
valid_datasources = {}
184+
unknown_datasources = []
185+
for datasource in datasources:
186+
key = datasource.lower()
187+
try:
188+
valid_datasources[key] = DATASOURCE_REGISTRY[key]
189+
except KeyError:
190+
unknown_datasources.append(key)
191+
if unknown_datasources:
192+
raise CommandError(f"Unknown datasource: {unknown_datasources}")
193+
return valid_datasources
194+
195+
196+
def get_undisabled_datasource(datasources):
197+
disabled = get_valid_datasources(datasources)
198+
return {key: value for key, value in DATASOURCE_REGISTRY.items() if key not in disabled}
199+
200+
201+
def get_enabled_datasource(datasources):
202+
return get_valid_datasources(datasources)
203+
204+
205+
def list_datasources():
206+
datasources = [x.upper() for x in list(DATASOURCE_REGISTRY)]
207+
click.echo("Currently supported datasources:")
208+
click.echo("\n".join(sorted(datasources)))
209+
210+
211+
def list_supported_ecosystem(datasources):
212+
ecosystems = []
213+
for key, datasource in datasources.items():
214+
vendor_supported_ecosystem = datasource.supported_ecosystem()
215+
ecosystems.extend([x.upper() for x in vendor_supported_ecosystem.keys()])
216+
217+
active_datasource = [x.upper() for x in datasources.keys()]
218+
click.echo("Active DataSources: %s\n" % ", ".join(sorted(active_datasource)))
219+
click.echo("Ecosystem supported by active datasources")
220+
click.echo("\n".join(sorted(set(ecosystems))))
221+
222+
223+
def formatted_row(datasource, advisory):
224+
aliases = "\n".join(advisory.aliases)
225+
affected = " ".join(advisory.affected_versions)
226+
fixed = " ".join(advisory.fixed_versions)
227+
return [datasource.upper(), aliases, affected, fixed]
228+
229+
230+
def get_raw_response(purl, datasources):
231+
all_raw_responses = {}
232+
for key, datasource in datasources.items():
233+
vendor = datasource()
234+
vendor_advisories = list(vendor.datasource_advisory(PackageURL.from_string(purl)))
235+
all_raw_responses[key] = vendor.raw_dump
236+
click.echo(json.dumps(all_raw_responses, indent=2))
237+
238+
239+
def run_datasources(purl, datasources, no_threading=False):
240+
vulnerabilities = {}
241+
if not no_threading:
242+
with concurrent.futures.ThreadPoolExecutor(max_workers=len(datasources)) as executor:
243+
future_to_advisory = {
244+
executor.submit(
245+
datasource().datasource_advisory, PackageURL.from_string(purl)
246+
): datasource
247+
for key, datasource in datasources.items()
248+
}
249+
for future in concurrent.futures.as_completed(future_to_advisory):
250+
vendor = future_to_advisory[future].__name__[:-10].lower()
251+
try:
252+
vendor_advisories = future.result()
253+
vulnerabilities[vendor] = []
254+
if vendor_advisories:
255+
vulnerabilities[vendor].extend([advisory for advisory in vendor_advisories])
256+
except Exception as exc:
257+
click.echo("%s generated an exception: %s" % (vendor, exc))
258+
else:
259+
for key, datasource in datasources.items():
260+
vendor_advisories = datasource().datasource_advisory(PackageURL.from_string(purl))
261+
vulnerabilities[key] = []
262+
if vendor_advisories:
263+
vulnerabilities[key].extend([advisory for advisory in vendor_advisories])
264+
265+
return vulnerabilities
266+
267+
268+
class VendorDataEncoder(json.JSONEncoder):
269+
def default(self, obj):
270+
if isinstance(obj, VendorData):
271+
return obj.to_dict()
272+
return json.JSONEncoder.default(self, obj)
273+
274+
275+
def write_json_output(purl, datasources, json_output, no_threading):
276+
vulnerabilities = run_datasources(purl, datasources, no_threading)
277+
return json.dump(vulnerabilities, json_output, cls=VendorDataEncoder, indent=2)
278+
279+
280+
def noop(self, *args, **kw):
281+
pass
282+
283+
284+
yaml.emitter.Emitter.process_tag = noop
285+
286+
287+
def write_yaml_output(purl, datasources, yaml_output, no_threading):
288+
vulnerabilities = run_datasources(purl, datasources, no_threading)
289+
return yaml.dump(vulnerabilities, yaml_output, default_flow_style=False, indent=2)
290+
291+
292+
def prettyprint(purl, datasources, pagination, no_threading):
293+
vulnerabilities = run_datasources(purl, datasources, no_threading)
294+
if not vulnerabilities:
295+
return
296+
297+
active_datasource = ", ".join(sorted([x.upper() for x in datasources.keys()]))
298+
metadata = f"PURL: {purl}\nActive DataSources: {active_datasource}\n\n"
299+
300+
table = Texttable()
301+
table.set_cols_dtype(["t", "t", "t", "t"])
302+
table.set_cols_align(["c", "l", "l", "l"])
303+
table.set_cols_valign(["t", "t", "a", "t"])
304+
table.header(["DATASOURCE", "ALIASES", "AFFECTED", "FIXED"])
305+
306+
for datasource, advisories in vulnerabilities.items():
307+
if not advisories:
308+
table.add_row([datasource.upper(), "", "", ""])
309+
continue
310+
311+
for advisory in advisories:
312+
table.add_row(formatted_row(datasource, advisory))
313+
314+
pydoc.pager(metadata + table.draw()) if pagination else click.echo(metadata + table.draw())
315+
316+
317+
def group_by_cve(vulnerabilities):
318+
grouped_by_cve = {}
319+
nocve = []
320+
noadvisory = []
321+
for datasource, advisories in vulnerabilities.items():
322+
if not advisories:
323+
noadvisory.append([datasource.upper(), "", "", ""])
324+
325+
for advisory in advisories:
326+
cve = next((x for x in advisory.aliases if x.startswith("CVE")), None)
327+
if not cve:
328+
nocve.append(formatted_row(datasource, advisory))
329+
continue
330+
if cve not in grouped_by_cve:
331+
grouped_by_cve[cve] = []
332+
grouped_by_cve[cve].append(formatted_row(datasource, advisory))
333+
grouped_by_cve["NOCVE"] = nocve
334+
grouped_by_cve["NOADVISORY"] = noadvisory
335+
return grouped_by_cve
336+
337+
338+
def prettyprint_group_by_cve(purl, datasources, pagination, no_threading):
339+
vulnerabilities = run_datasources(purl, datasources, no_threading)
340+
if not vulnerabilities:
341+
return
342+
grouped_by_cve = group_by_cve(vulnerabilities)
343+
344+
active_datasource = ", ".join(sorted([x.upper() for x in datasources.keys()]))
345+
metadata = f"PURL: {purl}\nActive DataSources: {active_datasource}\n\n"
346+
347+
table = Texttable()
348+
table.set_cols_dtype(["a", "a", "a", "a", "a"])
349+
table.set_cols_align(["l", "l", "l", "l", "l"])
350+
table.set_cols_valign(["t", "t", "t", "a", "t"])
351+
table.header(["CVE", "DATASOURCE", "ALIASES", "AFFECTED", "FIXED"])
352+
353+
for cve, advisories in grouped_by_cve.items():
354+
for count, advisory in enumerate(advisories):
355+
table.add_row([cve] + advisory)
356+
357+
pydoc.pager(metadata + table.draw()) if pagination else click.echo(metadata + table.draw())
358+
359+
360+
if __name__ == "__main__":
361+
handler()
362+
363+
"""
364+
Advanced Usage: vulntotal_cli.py [OPTIONS] [PURL]
365+
366+
Runs the PURL through all the available datasources and group vulnerability
367+
by CVEs. Use the special '-' file name to print JSON or YAML results on
368+
screen/stdout.
369+
370+
Options:
371+
-l, --list Lists all the available DataSources.
372+
--json FILE Write output as pretty-printed JSON to FILE.
373+
--yaml FILE Write output as YAML to FILE.
374+
-e, --enable Enable these datasource/s only.
375+
-d, --disable Disable these datasource/s.
376+
--ecosystem Lists ecosystem supported by active DataSources
377+
--raw List of all the raw response from DataSources.
378+
--no-threading Run DataSources sequentially.
379+
-p, --pagination Enable default pagination.
380+
--no-group Don't group by CVE.
381+
-h, --help Show this message and exit.
382+
"""

0 commit comments

Comments
 (0)