Repository: pyupio/safety

Issue:
Let 'ignore' take a list of coma separated ID
As it is now, in case we want to ignore multiple vulnerabilities, we have to use
`safety check -i 1234 -i 4567 -i 89101`

I'd like to suggest to start supporting coma separated list in a similar fashion as well known similar tools are doing (flake8, pylint, etc)

`safety check -i 1234,4567,89101`

Would you welcome a PR?

Comments:

Patch:
diff --git a/.gitignore b/.gitignore
index c6372ce..6e1aa8a 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,7 +66,6 @@ target/
 # pyenv python configuration file
 .python-version
 sandbox.py
-<<<<<<< HEAD
 
 # PyCharm
 .idea
@@ -74,4 +73,4 @@ sandbox.py
 
 # Virtual Env
 venv/
-.venv/
\ No newline at end of file
+.venv/
diff --git a/CHANGELOG.md b/CHANGELOG.md
index de7b18c..5b7fafd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,7 @@ The format is partly based on [Keep a Changelog](https://keepachangelog.com/en/1
 and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and [PEP 440](https://peps.python.org/pep-0440/)
 
 ## [Unreleased] 2.4.0.dev
+- Add support for coma separated ignore (--ignore=123,456) on top of existing --ignore=123 --ignore=456
 
 ## [2.3.5] - 2022-12-08
 - Pinned packaging dependency to a compatible range.
diff --git a/README.md b/README.md
index 1a98563..a26b2b5 100644
--- a/README.md
+++ b/README.md
@@ -466,6 +466,10 @@ safety check -i 1234
 safety check --ignore=1234
 ```
 ```bash
+safety check -i 1234,4567,89101
+```
+The following is also supported (backward compatibility)
+```bash
 safety check -i 1234 -i 4567 -i 89101
 ```
 
diff --git a/safety/cli.py b/safety/cli.py
index 149fcba..feed6cf 100644
--- a/safety/cli.py
+++ b/safety/cli.py
@@ -113,7 +113,7 @@ def clean_check_command(f):
               mutually_exclusive=["stdin"],
               help="Read input from one (or multiple) requirement files. Default: empty")
 @click.option("--ignore", "-i", multiple=True, type=str, default=[], callback=transform_ignore,
-              help="Ignore one (or multiple) vulnerabilities by ID. Default: empty")
+              help="Ignore one (or multiple) vulnerabilities by ID (coma separated). Default: empty")
 @click.option('--json', default=False, cls=MutuallyExclusiveOption, mutually_exclusive=["output", "bare"],
               with_values={"output": ['screen', 'text', 'bare', 'json'], "bare": [True, False]}, callback=json_alias,
               hidden=True, is_flag=True, show_default=True)
@@ -165,7 +165,6 @@ def check(ctx, key, db, full_report, stdin, files, cache, ignore, output, json,
         ignore_severity_rules = None
         ignore, ignore_severity_rules, exit_code = get_processed_options(policy_file, ignore,
                                                                          ignore_severity_rules, exit_code)
-
         is_env_scan = not stdin and not files
         params = {'stdin': stdin, 'files': files, 'policy_file': policy_file, 'continue_on_error': not exit_code,
                   'ignore_severity_rules': ignore_severity_rules, 'project': project,
diff --git a/safety/util.py b/safety/util.py
index 95403ba..e124909 100644
--- a/safety/util.py
+++ b/safety/util.py
@@ -354,8 +354,15 @@ class DependentOption(click.Option):
 
 
 def transform_ignore(ctx, param, value):
+    ignored_default_dict = {'reason': '', 'expires': None}
     if isinstance(value, tuple):
-        return dict(zip(value, [{'reason': '', 'expires': None} for _ in range(len(value))]))
+        # Following code is required to support the 2 ways of providing 'ignore'
+        # --ignore=1234,567,789
+        # or, the historical way (supported for backward compatibility)
+        # -i 1234 -i 567
+        combined_value = ','.join(value)
+        ignore_ids = {vuln_id.strip() for vuln_id in combined_value.split(',')}
+        return {ignore_id: dict(ignored_default_dict) for ignore_id in ignore_ids}
 
     return {}
 


Test patch:
diff --git a/tests/test_cli.py b/tests/test_cli.py
index c4675fc..8f77fc9 100644
--- a/tests/test_cli.py
+++ b/tests/test_cli.py
@@ -198,6 +198,28 @@ class TestSafetyCLI(unittest.TestCase):
         # TODO: Add test for the screen formatter, this only test that the command doesn't crash
         self.assertEqual(result.exit_code, 0)
 
+    @patch("safety.safety.check")
+    def test_check_ignore_format_backward_compatible(self, check):
+        runner = CliRunner()
+
+        check.return_value = []
+
+        dirname = os.path.dirname(__file__)
+        reqs_path = os.path.join(dirname, "reqs_4.txt")
+
+        _ = runner.invoke(cli.cli, ['check', '--file', reqs_path, '--ignore', "123,456", '--ignore', "789"])
+        try:
+            check_call_kwargs = check.call_args[1]  # Python < 3.8
+        except IndexError:
+            check_call_kwargs = check.call_args.kwargs
+
+        ignored_transformed = {
+            '123': {'expires': None, 'reason': ''},
+            '456': {'expires': None, 'reason': ''},
+            '789': {'expires': None, 'reason': ''}
+        }
+        self.assertEqual(check_call_kwargs['ignore_vulns'], ignored_transformed)
+
     def test_validate_with_unsupported_argument(self):
         result = self.runner.invoke(cli.cli, ['validate', 'safety_ci'])
         msg = 'This Safety version only supports "policy_file" validation. "safety_ci" is not supported.\n'
diff --git a/tests/test_safety.py b/tests/test_safety.py
index 28ac318..49d7f2c 100644
--- a/tests/test_safety.py
+++ b/tests/test_safety.py
@@ -60,6 +60,29 @@ class TestSafety(unittest.TestCase):
         )
         self.assertEqual(len(vulns), 2)
 
+    def test_check_ignores(self):
+        reqs = StringIO("Django==1.8.1")
+        packages = util.read_requirements(reqs)
+
+        # Second that ignore works
+        ignored_vulns = {'some id': {'expires': None, 'reason': ''}}
+
+        vulns, _ = safety.check(
+            packages=packages,
+            key=None,
+            db_mirror=os.path.join(
+                os.path.dirname(os.path.realpath(__file__)),
+                "test_db"
+            ),
+            cached=0,
+            ignore_vulns=ignored_vulns,
+            ignore_severity_rules=None,
+            proxy={},
+            telemetry=False
+        )
+        self.assertEqual(len(vulns), 1)
+        self.assertEqual(vulns[0].vulnerability_id, "some other id")
+
     def test_check_from_file_with_hash_pins(self):
         reqs = StringIO(("Django==1.8.1 "
                          "--hash=sha256:c6c7e7a961e2847d050d214ca96dc3167bb5f2b25cd5c6cb2eea96e1717f4ade"))
diff --git a/tests/test_util.py b/tests/test_util.py
index 43cb3cf..8df8286 100644
--- a/tests/test_util.py
+++ b/tests/test_util.py
@@ -7,7 +7,7 @@ from unittest.mock import patch, Mock
 import click as click
 
 from safety import util
-from safety.util import read_requirements, get_processed_options, SafetyPolicyFile
+from safety.util import read_requirements, get_processed_options, SafetyPolicyFile, transform_ignore
 
 
 class ReadRequirementsTestCase(unittest.TestCase):
@@ -122,5 +122,16 @@ class ReadRequirementsTestCase(unittest.TestCase):
         EXPECTED = not security_pf.get('continue-on-vulnerability-error')
         self.assertEqual(exit_code, EXPECTED)
 
-
-
+    def test_transform_ignore(self):
+        ignored_transformed = {'123': {'expires': None, 'reason': ''}, '456': {'expires': None, 'reason': ''}}
+        self.assertEqual(transform_ignore(None, None, value=("123", "456")), ignored_transformed)
+        self.assertEqual(transform_ignore(None, None, value=("123,456",)), ignored_transformed)
+
+    def test_transform_ignore_mixed_arguments(self):
+        # mix old and new way of providing --ignore
+        ignored_transformed = {
+            '123': {'expires': None, 'reason': ''},
+            '456': {'expires': None, 'reason': ''},
+            '789': {'expires': None, 'reason': ''}
+        }
+        self.assertEqual(transform_ignore(None, None, value=("123,456", "789")), ignored_transformed)