diff --git a/README.md b/README.md index 5e7d4907..f573e96b 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,22 @@ and displays a status on GitHub. ![Safety CI](https://github.com/pyupio/safety/raw/master/safety_ci.png) +## Using Safety from setup.py + +Safety includes a [distutils extension] that runs the check command. + +``` +python setup.py safety --full-report +``` + +The command-line options can also be configured in *setup.cfg*: + +``` +[safety] +full-report yes +``` + +[distutils extension]: https://setuptools.readthedocs.io/en/latest/setuptools.html#extending-and-reusing-setuptools # Using Safety in production diff --git a/safety/command.py b/safety/command.py new file mode 100644 index 00000000..9b0c1b16 --- /dev/null +++ b/safety/command.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- +from __future__ import absolute_import +from distutils import cmd +import os + +from safety import cli + +class SafetyCommand(cmd.Command): + description = 'Check this project using the safety utility' + user_options = [ + ("bare", None, "Output vulnerable packages only."), + ("cache", None, "Cache requests to vulnerability database locally."), + ("db=", None, "Path to a local vulnerability database."), + ("files=", None, "Read input from one or more requirement files."), + ("full-report", None, "Generate a full report."), + ("ignore=", None, "Ignore one or more vulnerabilities by ID."), + ("json", None, "Output vulnerabilities in JSON format."), + ("key=", None, "API key for pyup.io's vulnerability database."), + ("output=", None, "Path to where output file will be placed."), + ("proxy-host=", None, "Proxy host IP or DNS."), + ("proxy-port=", None, "Proxy port number."), + ("proxy-protocol=", None, "Proxy protocol (https or http)."), + ("stdin", None, "Read input from stdin."), + ] + boolean_options = ["bare", "cache", "full-report", "json", "stdin"] + + def initialize_options(self): + self.bare = False + self.cache = False + self.db = "" + self.files = [] + self.full_report = False + self.ignore = [] + self.json = False + self.key = os.environ.get("SAFETY_API_KEY", "") + self.output = "" + self.proxy_host = None + self.proxy_port = None + self.proxy_protocol = None + self.stdin = False + + def finalize_options(self): # pragma: no-cover + if len(self.files) > 0: + self.ensure_string_list("files") + if len(self.ignore) > 0: + self.ensure_string_list("ignore") + + def run(self): + args = [] + if self.bare: + args.append("--bare") + if self.cache: + args.append("--cache") + if self.db: + args.extend(["--db", self.db]) + for file_name in self.files: + args.extend(["--files", file_name]) + if self.full_report: + args.append("--full-report") + for ignore_id in self.ignore: + args.extend(["--ignore", ignore_id]) + if self.json: + args.append("--json") + if self.key: + args.extend(["--key", self.key]) + if self.output: + args.extend(["--output", self.output]) + if self.proxy_host: + args.extend(["--proxy-host", self.proxy_host]) + if self.proxy_protocol: + args.extend(["--proxy-protocol", self.proxy_protocol]) + if self.proxy_port: + args.extend(["--proxy-port", self.proxy_port]) + if self.stdin: + args.append("--stdin") + cli.check(args=args) diff --git a/setup.py b/setup.py index 1ef01762..d4f73c7e 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,7 @@ ] test_requirements = [ - # TODO: put package test requirements here + 'mock==1.0.1', ] setup( @@ -46,6 +46,9 @@ entry_points={ 'console_scripts': [ 'safety=safety.cli:cli' + ], + 'distutils.commands': [ + 'safety=safety.command:SafetyCommand' ] }, include_package_data=True, diff --git a/tests/test_command.py b/tests/test_command.py new file mode 100644 index 00000000..5819a3ad --- /dev/null +++ b/tests/test_command.py @@ -0,0 +1,85 @@ +from __future__ import absolute_import +from distutils import dist +import unittest + +from safety import command +import mock + + +class CommandFinalizeOptionsTests(unittest.TestCase): + def setUp(self): + super(CommandFinalizeOptionsTests, self).setUp() + self.command = command.SafetyCommand(dist.Distribution()) + self.command.initialize_options() + + def test_files_option_parsing(self): + self.command.files = "one two three" + self.command.finalize_options() + self.assertEqual(self.command.files, ["one", "two", "three"]) + + def test_ignore_option_parsing(self): + self.command.ignore = "ID1 ID2 ID3" + self.command.finalize_options() + self.assertEqual(self.command.ignore, ["ID1", "ID2", "ID3"]) + + +class CommandRunTests(unittest.TestCase): + @staticmethod + def run_command_with_options(*option_tuples): + with mock.patch("safety.command.cli.check") as check_function: + cmd = command.SafetyCommand(dist.Distribution()) + cmd.initialize_options() + for option_name, value in option_tuples: + opt = option_name.replace("-", "_") + setattr(cmd, opt, value) + cmd.finalize_options() + cmd.run() + return check_function + + def test_that_default_command_is_empty(self): + check_function = self.run_command_with_options() + check_function.assert_called_once_with(args=[]) + + def test_boolean_options(self): + for opt in command.SafetyCommand.boolean_options: + check_function = self.run_command_with_options((opt, True)) + check_function.assert_called_once_with(args=["--"+opt]) + + def test_simple_options(self): + for opt in ("db", "key", "output"): + check_function = self.run_command_with_options((opt, "value")) + check_function.assert_called_once_with(args=["--"+opt, "value"]) + + def test_list_options(self): + for opt in ("files", "ignore"): + check_function = self.run_command_with_options((opt, "one two")) + check_function.assert_called_once_with(args=[ + "--"+opt, "one", "--"+opt, "two"]) + + def test_proxy_options(self): + check_function = self.run_command_with_options(("proxy-host", "host")) + check_function.assert_called_once_with(args=["--proxy-host", "host"]) + + check_function = self.run_command_with_options( + ("proxy-host", "host"), ("proxy-port", "1234")) + check_function.assert_called_once_with(args=[ + "--proxy-host", "host", "--proxy-port", "1234"]) + + check_function = self.run_command_with_options( + ("proxy-host", "host"), ("proxy-protocol", "https")) + check_function.assert_called_once_with(args=[ + "--proxy-host", "host", "--proxy-protocol", "https"]) + + check_function = self.run_command_with_options( + ("proxy-host", "host"), ("proxy-port", "1234"), + ("proxy-protocol", "https")) + check_function.assert_called_once_with(args=[ + "--proxy-host", "host", "--proxy-protocol", "https", + "--proxy-port", "1234"]) + + check_function = self.run_command_with_options(("proxy-port", "1234")) + check_function.assert_called_once_with(args=[]) + + check_function = self.run_command_with_options( + ("proxy-protocol", "http")) + check_function.assert_called_once_with(args=[])