132 lines
5.7 KiB
Python
132 lines
5.7 KiB
Python
# -*- coding: utf-8 -*-
|
|
from __future__ import absolute_import
|
|
import sys
|
|
import click
|
|
from safety import __version__
|
|
from safety import safety
|
|
from safety.formatter import report
|
|
import itertools
|
|
from safety.util import read_requirements, read_vulnerabilities
|
|
from safety.errors import DatabaseFetchError, DatabaseFileNotFoundError, InvalidKeyError
|
|
|
|
try:
|
|
from json.decoder import JSONDecodeError
|
|
except ImportError:
|
|
JSONDecodeError = ValueError
|
|
|
|
@click.group()
|
|
@click.version_option(version=__version__)
|
|
def cli():
|
|
pass
|
|
|
|
|
|
@cli.command()
|
|
@click.option("--key", default="",
|
|
help="API Key for pyup.io's vulnerability database. Can be set as SAFETY_API_KEY "
|
|
"environment variable. Default: empty")
|
|
@click.option("--db", default="",
|
|
help="Path to a local vulnerability database. Default: empty")
|
|
@click.option("--json/--no-json", default=False,
|
|
help="Output vulnerabilities in JSON format. Default: --no-json")
|
|
@click.option("--full-report/--short-report", default=False,
|
|
help='Full reports include a security advisory (if available). Default: '
|
|
'--short-report')
|
|
@click.option("--bare/--not-bare", default=False,
|
|
help='Output vulnerable packages only. Useful in combination with other tools.'
|
|
'Default: --not-bare')
|
|
@click.option("--cache/--no-cache", default=False,
|
|
help="Cache requests to the vulnerability database locally. Default: --no-cache")
|
|
@click.option("--stdin/--no-stdin", default=False,
|
|
help="Read input from stdin. Default: --no-stdin")
|
|
@click.option("files", "--file", "-r", multiple=True, type=click.File(),
|
|
help="Read input from one (or multiple) requirement files. Default: empty")
|
|
@click.option("ignore", "--ignore", "-i", multiple=True, type=str, default=[],
|
|
help="Ignore one (or multiple) vulnerabilities by ID. Default: empty")
|
|
@click.option("--output", "-o", default="",
|
|
help="Path to where output file will be placed. Default: empty")
|
|
@click.option("proxyhost", "--proxy-host", "-ph", multiple=False, type=str, default=None,
|
|
help="Proxy host IP or DNS --proxy-host")
|
|
@click.option("proxyport", "--proxy-port", "-pp", multiple=False, type=int, default=80,
|
|
help="Proxy port number --proxy-port")
|
|
@click.option("proxyprotocol", "--proxy-protocol", "-pr", multiple=False, type=str, default='http',
|
|
help="Proxy protocol (https or http) --proxy-protocol")
|
|
def check(key, db, json, full_report, bare, stdin, files, cache, ignore, output, proxyprotocol, proxyhost, proxyport):
|
|
if files and stdin:
|
|
click.secho("Can't read from --stdin and --file at the same time, exiting", fg="red", file=sys.stderr)
|
|
sys.exit(-1)
|
|
|
|
if files:
|
|
packages = list(itertools.chain.from_iterable(read_requirements(f, resolve=True) for f in files))
|
|
elif stdin:
|
|
packages = list(read_requirements(sys.stdin))
|
|
else:
|
|
import pkg_resources
|
|
packages = [
|
|
d for d in pkg_resources.working_set
|
|
if d.key not in {"python", "wsgiref", "argparse"}
|
|
]
|
|
proxy_dictionary = {}
|
|
if proxyhost is not None:
|
|
if proxyprotocol in ["http", "https"]:
|
|
proxy_dictionary = {proxyprotocol: "{0}://{1}:{2}".format(proxyprotocol, proxyhost, str(proxyport))}
|
|
else:
|
|
click.secho("Proxy Protocol should be http or https only.", fg="red")
|
|
sys.exit(-1)
|
|
try:
|
|
vulns = safety.check(packages=packages, key=key, db_mirror=db, cached=cache, ignore_ids=ignore, proxy=proxy_dictionary)
|
|
output_report = report(vulns=vulns,
|
|
full=full_report,
|
|
json_report=json,
|
|
bare_report=bare,
|
|
checked_packages=len(packages),
|
|
db=db,
|
|
key=key)
|
|
|
|
if output:
|
|
with open(output, 'w+') as output_file:
|
|
output_file.write(output_report)
|
|
else:
|
|
click.secho(output_report, nl=False if bare and not vulns else True)
|
|
sys.exit(-1 if vulns else 0)
|
|
except InvalidKeyError:
|
|
click.secho("Your API Key '{key}' is invalid. See {link}".format(
|
|
key=key, link='https://goo.gl/O7Y1rS'),
|
|
fg="red",
|
|
file=sys.stderr)
|
|
sys.exit(-1)
|
|
except DatabaseFileNotFoundError:
|
|
click.secho("Unable to load vulnerability database from {db}".format(db=db), fg="red", file=sys.stderr)
|
|
sys.exit(-1)
|
|
except DatabaseFetchError:
|
|
click.secho("Unable to load vulnerability database", fg="red", file=sys.stderr)
|
|
sys.exit(-1)
|
|
|
|
|
|
@cli.command()
|
|
@click.option("--full-report/--short-report", default=False,
|
|
help='Full reports include a security advisory (if available). Default: '
|
|
'--short-report')
|
|
@click.option("--bare/--not-bare", default=False,
|
|
help='Output vulnerable packages only. Useful in combination with other tools.'
|
|
'Default: --not-bare')
|
|
@click.option("file", "--file", "-f", type=click.File(), required=True,
|
|
help="Read input from an insecure report file. Default: empty")
|
|
def review(full_report, bare, file):
|
|
if full_report and bare:
|
|
click.secho("Can't choose both --bare and --full-report/--short-report", fg="red")
|
|
sys.exit(-1)
|
|
|
|
try:
|
|
input_vulns = read_vulnerabilities(file)
|
|
except JSONDecodeError:
|
|
click.secho("Not a valid JSON file", fg="red")
|
|
sys.exit(-1)
|
|
|
|
vulns = safety.review(input_vulns)
|
|
output_report = report(vulns=vulns, full=full_report, bare_report=bare)
|
|
click.secho(output_report, nl=False if bare and not vulns else True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
cli()
|