diff --git a/README.md b/README.md index c77b009b..49e84eeb 100644 --- a/README.md +++ b/README.md @@ -44,15 +44,11 @@ guarddog pypi scan requests --rules exec-base64 --rules code-execution # Scan the 'requests' package using all rules but one guarddog pypi scan requests --exclude-rules exec-base64 -# Scan a local package +# Scan a local package archive guarddog pypi scan /tmp/triage.tar.gz -# Scan a local directory, the packages need to be located in the root directory -# For instance you have several pypi packages in ./samples/ like: -# ./samples/package1.tar.gz ./samples/package2.zip ./samples/package3.whl -# FYI if a file not supported by guarddog is found you will get an error -# Here is the command to scan a directory: -guarddog pypi scan ./samples/ +# Scan a local package directory +guarddog pypi scan /tmp/triage/ # Scan every package referenced in a requirements.txt file of a local folder guarddog pypi verify workspace/guarddog/requirements.txt diff --git a/guarddog/cli.py b/guarddog/cli.py index e577d055..cacdf371 100644 --- a/guarddog/cli.py +++ b/guarddog/cli.py @@ -9,6 +9,7 @@ import logging import os import sys +import tempfile from typing import Optional, cast import click @@ -21,6 +22,7 @@ from guarddog.reporters.sarif import report_verify_sarif from guarddog.scanners import get_scanner from guarddog.scanners.scanner import PackageScanner +from guarddog.utils.archives import safe_extract EXIT_CODE_ISSUES_FOUND = 1 @@ -213,41 +215,30 @@ def _scan( sys.stderr.write(f"Command scan is not supported for ecosystem {ecosystem}") sys.exit(1) - results = [] - if os.path.isdir(identifier): - log.debug(f"Considering that '{identifier}' is a local directory") - for package in os.listdir(identifier): - result = scanner.scan_local(f"{identifier}/{package}", rule_param) - result["package"] = package - results.append(result) - elif os.path.isfile(identifier): - log.debug(f"Considering that '{identifier}' is a local file") - result = scanner.scan_local(identifier, rule_param) - result["package"] = identifier - results.append(result) - else: - log.debug(f"Considering that '{identifier}' is a remote target") - try: - result = scanner.scan_remote(identifier, version, rule_param) - result["package"] = identifier - results.append(result) - except Exception as e: - sys.stderr.write(f"\nError '{e}' occurred while scanning remote package.") - sys.exit(1) + result = {"package": identifier} + try: + if os.path.isdir(identifier): + log.debug(f"Considering that '{identifier}' is a local directory") + result |= scanner.scan_local(identifier, rule_param) + elif os.path.isfile(identifier): + log.debug(f"Considering that '{identifier}' is a local archive file") + with tempfile.TemporaryDirectory() as tempdir: + safe_extract(identifier, tempdir) + result |= scanner.scan_local(tempdir, rule_param) + else: + log.debug(f"Considering that '{identifier}' is a remote target") + result |= scanner.scan_remote(identifier, version, rule_param) + except Exception as e: + sys.stderr.write(f"Error occurred while scanning target {identifier}: '{e}'\n") + sys.exit(1) if output_format == "json": - if len(results) == 1: - # return only a json like {} - print(js.dumps(results[0])) - else: - # Return a list of result like [{},{}] - print(js.dumps(results)) + print(js.dumps(result)) else: - for result in results: - print_scan_results(result, result["package"]) + print_scan_results(result, result["package"]) if exit_non_zero_on_finding: - exit_with_status_code(results) + exit_with_status_code([result]) def _list_rules(ecosystem: ECOSYSTEM): diff --git a/guarddog/scanners/scanner.py b/guarddog/scanners/scanner.py index f23cdafe..610ad6b7 100644 --- a/guarddog/scanners/scanner.py +++ b/guarddog/scanners/scanner.py @@ -231,7 +231,7 @@ def scan_local( Scans local package Args: - path (str): path to package + path (str): Path to the directory containing the package to analyze rules (set, optional): Set of rule names to use. Defaults to all rules. callback (typing.Callable[[dict], None], optional): Callback to apply to Analyzer output @@ -245,16 +245,7 @@ def scan_local( if rules is not None: rules = set(rules) - results = None - if os.path.isdir(path): - results = self.analyzer.analyze_sourcecode(path, rules=rules) - elif os.path.isfile(path): - with tempfile.TemporaryDirectory() as tempdir: - safe_extract(path, tempdir) - results = self.analyzer.analyze_sourcecode(tempdir, rules=rules) - else: - raise Exception(f"Local scan target {path} is neither a directory nor a file.") - + results = self.analyzer.analyze_sourcecode(path, rules=rules) callback(results) return results diff --git a/tests/core/test_cli.py b/tests/core/test_cli.py index 82f5f4a7..8f0eed18 100644 --- a/tests/core/test_cli.py +++ b/tests/core/test_cli.py @@ -2,8 +2,8 @@ import unittest.mock as mock import guarddog.cli -import guarddog.scanners.scanner as scanner from guarddog.ecosystems import ECOSYSTEM +import guarddog.scanners.scanner as scanner class TestCli(unittest.TestCase): @@ -62,7 +62,7 @@ def _test_local_directory_template(self, directory: str): cm.output ) self.assertNotIn( - f"DEBUG:guarddog:Considering that '{directory}' is a local file", + f"DEBUG:guarddog:Considering that '{directory}' is a local archive file", cm.output ) self.assertNotIn( @@ -83,7 +83,7 @@ def _test_local_directory_template(self, directory: str): cm.output ) self.assertNotIn( - f"DEBUG:guarddog:Considering that '{directory}' is a local file", + f"DEBUG:guarddog:Considering that '{directory}' is a local archive file", cm.output ) self.assertIn( @@ -97,21 +97,31 @@ def _test_local_file_template(self, filename: str): isdir.return_value = False with mock.patch("os.path.isfile") as isfile: isfile.return_value = True - with mock.patch.object(scanner.PackageScanner, 'scan_local', return_value={}) as _: - with self.assertLogs("guarddog", level="DEBUG") as cm: - guarddog.cli._scan(filename, "0.1.0", (), (), None, False, ECOSYSTEM.PYPI) - self.assertNotIn( - f"DEBUG:guarddog:Considering that '{filename}' is a local directory", - cm.output - ) - self.assertIn( - f"DEBUG:guarddog:Considering that '{filename}' is a local file", - cm.output - ) - self.assertNotIn( - f"DEBUG:guarddog:Considering that '{filename}' is a remote target", - cm.output - ) + # The next two patches are to make sure we don't try + # to extract the test filename + with mock.patch("guarddog.utils.archives.is_tar_archive") as istar: + istar.return_value = False + with mock.patch("guarddog.utils.archives.is_zip_archive") as iszip: + iszip.return_value = False + with mock.patch.object(scanner.PackageScanner, 'scan_local', return_value={}) as _: + try: + with self.assertLogs("guarddog", level="DEBUG") as cm: + guarddog.cli._scan(filename, "0.1.0", (), (), None, False, ECOSYSTEM.PYPI) + # Since is_tar_archive and is_zip_archive have been + # patched accordingly, we always end up here + except SystemExit: + self.assertNotIn( + f"DEBUG:guarddog:Considering that '{filename}' is a local directory", + cm.output + ) + self.assertIn( + f"DEBUG:guarddog:Considering that '{filename}' is a local archive file", + cm.output + ) + self.assertNotIn( + f"DEBUG:guarddog:Considering that '{filename}' is a remote target", + cm.output + ) # `filename` is neither a directory nor a file with mock.patch("os.path.isdir") as isdir: @@ -126,7 +136,7 @@ def _test_local_file_template(self, filename: str): cm.output ) self.assertNotIn( - f"DEBUG:guarddog:Considering that '{filename}' is a local file", + f"DEBUG:guarddog:Considering that '{filename}' is a local archive file", cm.output ) self.assertIn(