diff --git a/guarddog/cli.py b/guarddog/cli.py index e3793c56..60f6920f 100644 --- a/guarddog/cli.py +++ b/guarddog/cli.py @@ -218,6 +218,19 @@ def _scan( else: log.debug(f"Considering that '{identifier}' is a remote target") result |= scanner.scan_remote(identifier, version, rule_param) + + scanned_version = version + if version is None and ecosystem == ECOSYSTEM.PYPI: + from guarddog.utils.package_info import get_package_info + try: + package_info = get_package_info(identifier) + scanned_version = package_info["info"]["version"] + except Exception: + pass + + result["is_remote"] = True + result["ecosystem"] = ecosystem + result["scanned_version"] = scanned_version except Exception as e: log.error(f"Error occurred while scanning target {identifier}: '{e}'\n") sys.exit(1) diff --git a/guarddog/reporters/human_readable.py b/guarddog/reporters/human_readable.py index 3d2423d1..27f65504 100644 --- a/guarddog/reporters/human_readable.py +++ b/guarddog/reporters/human_readable.py @@ -110,6 +110,16 @@ def _format_code_line_for_output(code) -> str: ) lines.append("") + if ( + num_issues > 0 + and results.get("is_remote") + and results.get("ecosystem") == ECOSYSTEM.PYPI + ): + scanned_version = results.get("scanned_version") + if scanned_version: + inspector_url = f"https://inspector.pypi.io/project/{safe_identifier}/{scanned_version}" + lines.append(f"For more details, see: {inspector_url}") + return "\n".join(lines) @staticmethod diff --git a/tests/reporters/test_human_readable.py b/tests/reporters/test_human_readable.py index d06e6b77..3a0ead0a 100644 --- a/tests/reporters/test_human_readable.py +++ b/tests/reporters/test_human_readable.py @@ -1,6 +1,7 @@ import re from guarddog.reporters.human_readable import HumanReadableReporter, _sanitize +from guarddog.ecosystems import ECOSYSTEM # Strips ANSI SGR/CSI/OSC sequences emitted by termcolor so assertions can match # the underlying text instead of color codes. @@ -157,3 +158,78 @@ def test_print_scan_results_benign_input_is_preserved(): assert "matched a benign-looking pattern" in plain assert "requests" in plain assert "rule-name" in plain + + +def test_pypi_inspector_link_shown_for_remote_pypi_with_issues(): + results = { + "issues": 1, + "errors": {}, + "results": { + "some-rule": "found suspicious behavior" + }, + "is_remote": True, + "ecosystem": ECOSYSTEM.PYPI, + "scanned_version": "2.28.1" + } + out = HumanReadableReporter.print_scan_results("requests", results) + plain = _strip_color(out) + assert "https://inspector.pypi.io/project/requests/2.28.1" in plain + + +def test_pypi_inspector_link_not_shown_for_local_scan(): + results = { + "issues": 1, + "errors": {}, + "results": { + "some-rule": "found suspicious behavior" + } + } + out = HumanReadableReporter.print_scan_results("requests", results) + plain = _strip_color(out) + assert "inspector.pypi.io" not in plain + + +def test_pypi_inspector_link_not_shown_when_no_issues(): + results = { + "issues": 0, + "errors": {}, + "results": {}, + "is_remote": True, + "ecosystem": ECOSYSTEM.PYPI, + "scanned_version": "2.28.1" + } + out = HumanReadableReporter.print_scan_results("requests", results) + plain = _strip_color(out) + assert "inspector.pypi.io" not in plain + + +def test_pypi_inspector_link_not_shown_for_npm(): + results = { + "issues": 1, + "errors": {}, + "results": { + "some-rule": "found suspicious behavior" + }, + "is_remote": True, + "ecosystem": ECOSYSTEM.NPM, + "scanned_version": "1.0.0" + } + out = HumanReadableReporter.print_scan_results("lodash", results) + plain = _strip_color(out) + assert "inspector.pypi.io" not in plain + + +def test_pypi_inspector_link_not_shown_when_version_is_none(): + results = { + "issues": 1, + "errors": {}, + "results": { + "some-rule": "found suspicious behavior" + }, + "is_remote": True, + "ecosystem": ECOSYSTEM.PYPI, + "scanned_version": None + } + out = HumanReadableReporter.print_scan_results("requests", results) + plain = _strip_color(out) + assert "inspector.pypi.io" not in plain