Skip to content

Commit

Permalink
Standardize local scanning behavior (#426)
Browse files Browse the repository at this point in the history
  • Loading branch information
ikretz authored Jul 25, 2024
1 parent 8545867 commit ddd99e7
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 67 deletions.
10 changes: 3 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 21 additions & 30 deletions guarddog/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import logging
import os
import sys
import tempfile
from typing import Optional, cast

import click
Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand Down
13 changes: 2 additions & 11 deletions guarddog/scanners/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
48 changes: 29 additions & 19 deletions tests/core/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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(
Expand All @@ -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(
Expand All @@ -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:
Expand All @@ -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(
Expand Down

0 comments on commit ddd99e7

Please sign in to comment.