From a969a01519f29dccea7638ec3aad01d5466178e0 Mon Sep 17 00:00:00 2001 From: Carl Flottmann Date: Mon, 15 Sep 2025 16:41:50 +1000 Subject: [PATCH] chore: refactor heuristic names and rules Signed-off-by: Carl Flottmann --- src/macaron/malware_analyzer/README.md | 26 ++++--- .../pypi_heuristics/heuristics.py | 8 +-- ...ption.py => package_description_intent.py} | 14 ++-- .../metadata/similar_projects.py | 2 + .../{minimal_content.py => type_stub_file.py} | 10 +-- .../checks/detect_malicious_metadata_check.py | 27 ++++---- .../pypi/test_package_description_intent.py | 59 ++++++++++++++++ ...imal_content.py => test_type_stub_file.py} | 24 +++---- .../pypi/test_unsecure_description.py | 67 ------------------- 9 files changed, 120 insertions(+), 117 deletions(-) rename src/macaron/malware_analyzer/pypi_heuristics/metadata/{unsecure_description.py => package_description_intent.py} (74%) rename src/macaron/malware_analyzer/pypi_heuristics/metadata/{minimal_content.py => type_stub_file.py} (87%) create mode 100644 tests/malware_analyzer/pypi/test_package_description_intent.py rename tests/malware_analyzer/pypi/{test_minimal_content.py => test_type_stub_file.py} (79%) delete mode 100644 tests/malware_analyzer/pypi/test_unsecure_description.py diff --git a/src/macaron/malware_analyzer/README.md b/src/macaron/malware_analyzer/README.md index 468aafc1b..d2d5517a9 100644 --- a/src/macaron/malware_analyzer/README.md +++ b/src/macaron/malware_analyzer/README.md @@ -65,27 +65,35 @@ When a heuristic fails, with `HeuristicResult.FAIL`, then that is an indicator b > ``` > The script will download the top 5000 PyPI packages and update the resource file automatically. -11. **Fake Email** +11. **Similar Projects** + - **Description**: Checks whether the maintainer(s) of the package have released other packages with close structural similarity. + - **Rule**: Return 'HeuristicResult.FAIL` upon finding the first similar package. Return `HeuristicResult.PASS` if no similar packages are found. + - **Dependency**: None + +12. **Fake Email** - **Description**: Checks if the package maintainer or author has a suspicious or invalid email. - **Rule**: Return `HeuristicResult.FAIL` if the email is invalid; otherwise, return `HeuristicResult.PASS`. - **Dependency**: None. - -12. **Minimal Content** - - **Description**: Checks if the package has a small number of files. - - **Rule**: Return `HeuristicResult.FAIL` if the number of files is strictly less than FILES_THRESHOLD; otherwise, return `HeuristicResult.PASS`. +13. **Type Stub File** + - **Description**: Checks if the package has a small number of `.pyi` stub files. + - **Rule**: Return `HeuristicResult.FAIL` if the number of `.pyi` files is strictly less than FILES_THRESHOLD; otherwise, return `HeuristicResult.PASS`. - **Dependency**: None. -13. **Unsecure Description** - - **Description**: Checks if the package description is unsecure, such as not having a descriptive keywords that indicates its a stub package . - - **Rule**: Return `HeuristicResult.FAIL` if no descriptive word is found in the package description or summary ; otherwise, return `HeuristicResult.PASS`. +14. **Package Description Intent** + - **Description**: Checks if the package description contains keywords indicating it is a stub package or dependency confusion prevention placeholder. + - **Rule**: Return `HeuristicResult.FAIL` if no keyword is found in the package description or summary ; otherwise, return `HeuristicResult.PASS`. - **Dependency**: None. +15. **Stub Name** + - **Description**: Checks if the package name contains the `"stub"` keyword, indicating that it is likely intended to be a stub package and not downloaded. + - **Rule**: Return `HeuristicResult.PASS` if the keywork `"stub"` is found in the package name; otherwise, return `HeuristicResult.FAIL`. + ### Source Code Analysis with Semgrep **PyPI Source Code Analyzer** - **Description**: Uses Semgrep, with default rules written in `src/macaron/resources/pypi_malware_rules` and custom rules available by supplying a path to `custom_semgrep_rules` in `defaults.ini`, to scan the package `.tar` source code. - **Rule**: If any Semgrep rule is triggered, the heuristic fails with `HeuristicResult.FAIL` and subsequently fails the package with `CheckResultType.FAILED`. If no rule is triggered, the heuristic passes with `HeuristicResult.PASS` and the `CheckResultType` result from the combination of all other heuristics is maintained. -- **Dependency**: Will be run if the Source Code Repo fails. This dependency can be bypassed by suppying `--force-analyze-source` in the CLI. +- **Dependency**: Will be run if the Source Code Repo fails. This dependency can be bypassed by supplying `--force-analyze-source` in the CLI. This feature is currently a work in progress, and supports detection of code obfuscation techniques and remote exfiltration behaviors. It uses Semgrep OSS for detection. `defaults.ini` may be used to provide custom rules and exclude them: - `disabled_default_rulesets`: supply to this a comma separated list of the names of default Semgrep rule files (excluding the `.yaml` extension) to disable all rule IDs in that file. diff --git a/src/macaron/malware_analyzer/pypi_heuristics/heuristics.py b/src/macaron/malware_analyzer/pypi_heuristics/heuristics.py index 02c738c6f..9699066f6 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/heuristics.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/heuristics.py @@ -49,11 +49,11 @@ class Heuristics(str, Enum): #: Indicates that the package has a similar structure to other packages maintained by the same user. SIMILAR_PROJECTS = "similar_projects" - #: Indicates that the package has minimal content. - MINIMAL_CONTENT = "minimal_content" + #: Indicates that the package has minimal .pyi type stub files. + TYPE_STUB_FILE = "type_stub_file" - #: Indicates that the package's description is unsecure, such as not having a descriptive keywords. - UNSECURE_DESCRIPTION = "unsecure_description" + #: Indicates from the package's description it is intended to be used as a stub or placeholder package. + PACKAGE_DESCRIPTION_INTENT = "package_description_intent" #: Indicates that the package contains stub files. STUB_NAME = "stub_name" diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/unsecure_description.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/package_description_intent.py similarity index 74% rename from src/macaron/malware_analyzer/pypi_heuristics/metadata/unsecure_description.py rename to src/macaron/malware_analyzer/pypi_heuristics/metadata/package_description_intent.py index c9ffce52c..28ac082a4 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/unsecure_description.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/package_description_intent.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. -"""This analyzer checks if a PyPI package has unsecure description.""" +"""This analyzer checks if a PyPI package is a stub or placeholder package, using its description and summary.""" import logging import re @@ -15,17 +15,17 @@ logger: logging.Logger = logging.getLogger(__name__) -class UnsecureDescriptionAnalyzer(BaseHeuristicAnalyzer): - """Check whether the package's description is unsecure.""" +class PackageDescriptionIntentAnalyzer(BaseHeuristicAnalyzer): + """Package description contains keywords indicating it is a stub package or dependency confusion prevention placeholder.""" SECURE_DESCRIPTION_REGEX = re.compile( - r"\b(?:internal|private|stub|placeholder|dependency confusion|security|namespace protection|reserved|harmless|prevent)\b", + r"\b(?:stub|placeholder|dependency confusion|security|namespace protection|reserved|prevent)\b", re.IGNORECASE, ) def __init__(self) -> None: super().__init__( - name="unsecure_description_analyzer", heuristic=Heuristics.UNSECURE_DESCRIPTION, depends_on=None + name="package_description_intent", heuristic=Heuristics.PACKAGE_DESCRIPTION_INTENT, depends_on=None ) def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicResult, dict[str, JsonType]]: @@ -52,5 +52,5 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes summary = json_extract(package_json, ["info", "summary"], str) data = f"{description} {summary}" if self.SECURE_DESCRIPTION_REGEX.search(data): - return HeuristicResult.PASS, {"message": "Package description is secure"} - return HeuristicResult.FAIL, {"message": "Package description is unsecure"} + return HeuristicResult.PASS, {"message": "Package description indicates a stub or placeholder package."} + return HeuristicResult.FAIL, {"message": "Package description does not indicate a stub or placeholder package."} diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/similar_projects.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/similar_projects.py index 0296cfc38..56f1b2834 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/similar_projects.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/similar_projects.py @@ -24,6 +24,8 @@ def __init__(self) -> None: super().__init__( name="similar_project_analyzer", heuristic=Heuristics.SIMILAR_PROJECTS, + # TODO: these dependencies are used as this heuristic currently downloads many package sourcecode + # tarballs. Refactoring this heuristic to run more efficiently means this should have depends_on=None. depends_on=[ (Heuristics.EMPTY_PROJECT_LINK, HeuristicResult.FAIL), (Heuristics.ONE_RELEASE, HeuristicResult.FAIL), diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/minimal_content.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/type_stub_file.py similarity index 87% rename from src/macaron/malware_analyzer/pypi_heuristics/metadata/minimal_content.py rename to src/macaron/malware_analyzer/pypi_heuristics/metadata/type_stub_file.py index 3e544c9f8..a1447bac1 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/minimal_content.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/type_stub_file.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. -"""This analyzer checks if a PyPI package has minimal content.""" +"""This analyzer checks if a PyPI package has minimal .pyi stub content.""" import logging import os @@ -15,15 +15,15 @@ logger: logging.Logger = logging.getLogger(__name__) -class MinimalContentAnalyzer(BaseHeuristicAnalyzer): - """Check whether the package has minimal content.""" +class TypeStubFileAnalyzer(BaseHeuristicAnalyzer): + """Check whether the package has minimal .pyi stub content.""" FILES_THRESHOLD = 10 def __init__(self) -> None: super().__init__( - name="minimal_content_analyzer", - heuristic=Heuristics.MINIMAL_CONTENT, + name="type_stub_file", + heuristic=Heuristics.TYPE_STUB_FILE, depends_on=None, ) diff --git a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py index ee012c290..da49be6bb 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -22,13 +22,15 @@ from macaron.malware_analyzer.pypi_heuristics.metadata.empty_project_link import EmptyProjectLinkAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.fake_email import FakeEmailAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.high_release_frequency import HighReleaseFrequencyAnalyzer -from macaron.malware_analyzer.pypi_heuristics.metadata.minimal_content import MinimalContentAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.one_release import OneReleaseAnalyzer +from macaron.malware_analyzer.pypi_heuristics.metadata.package_description_intent import ( + PackageDescriptionIntentAnalyzer, +) from macaron.malware_analyzer.pypi_heuristics.metadata.similar_projects import SimilarProjectAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.source_code_repo import SourceCodeRepoAnalyzer +from macaron.malware_analyzer.pypi_heuristics.metadata.type_stub_file import TypeStubFileAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.typosquatting_presence import TyposquattingPresenceAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.unchanged_release import UnchangedReleaseAnalyzer -from macaron.malware_analyzer.pypi_heuristics.metadata.unsecure_description import UnsecureDescriptionAnalyzer from macaron.malware_analyzer.pypi_heuristics.metadata.wheel_absence import WheelAbsenceAnalyzer from macaron.malware_analyzer.pypi_heuristics.sourcecode.pypi_sourcecode_analyzer import PyPISourcecodeAnalyzer from macaron.malware_analyzer.pypi_heuristics.sourcecode.suspicious_setup import SuspiciousSetupAnalyzer @@ -368,8 +370,8 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: TyposquattingPresenceAnalyzer, FakeEmailAnalyzer, SimilarProjectAnalyzer, - UnsecureDescriptionAnalyzer, - MinimalContentAnalyzer, + PackageDescriptionIntentAnalyzer, + TypeStubFileAnalyzer, ] # name used to query the result of all problog rules, so it can be accessed outside the model. @@ -419,20 +421,17 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: failed({Heuristics.CLOSER_RELEASE_JOIN_DATE.value}), forceSetup. - % Package released with a name similar to a popular package. + % Package released recently with little detail, forcing setup.py to run, and suspected of typosquatting. {Confidence.HIGH.value}::trigger(malware_high_confidence_4) :- quickUndetailed, forceSetup, - failed({Heuristics.TYPOSQUATTING_PRESENCE.value}), - failed({Heuristics.STUB_NAME.value}). + failed({Heuristics.TYPOSQUATTING_PRESENCE.value}). - % Package released with dependency confusion . + % Package forces setup.py to run, has a high version number and is not intended to be a stub package. {Confidence.HIGH.value}::trigger(malware_high_confidence_5) :- forceSetup, - failed({Heuristics.MINIMAL_CONTENT.value}), failed({Heuristics.STUB_NAME.value}), - failed({Heuristics.ANOMALOUS_VERSION.value}), - failed({Heuristics.UNSECURE_DESCRIPTION.value}). + failed({Heuristics.ANOMALOUS_VERSION.value}). % Package released recently with little detail, with multiple releases as a trust marker, but frequent and with % the same code. @@ -442,12 +441,14 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: failed({Heuristics.UNCHANGED_RELEASE.value}), passed({Heuristics.SUSPICIOUS_SETUP.value}). - % Package released recently with little detail and an anomalous version number for a single-release package. + % Package released recently with little detail and an anomalous version number for a single-release package. The + % package is not intended to be a stub package. {Confidence.MEDIUM.value}::trigger(malware_medium_confidence_2) :- quickUndetailed, failed({Heuristics.ONE_RELEASE.value}), failed({Heuristics.ANOMALOUS_VERSION.value}), - failed({Heuristics.UNSECURE_DESCRIPTION.value}). + failed({Heuristics.TYPE_STUB_FILE.value}), + failed({Heuristics.PACKAGE_DESCRIPTION_INTENT.value}). % Package has no links, one release or multiple quick releases, and a suspicious maintainer who recently % joined, has a fake email address, and other similarly-structured projects. diff --git a/tests/malware_analyzer/pypi/test_package_description_intent.py b/tests/malware_analyzer/pypi/test_package_description_intent.py new file mode 100644 index 000000000..25c2d8b82 --- /dev/null +++ b/tests/malware_analyzer/pypi/test_package_description_intent.py @@ -0,0 +1,59 @@ +# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. +# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. + +"""Tests for the PackageDescriptionIntentAnalyzer heuristic.""" + +from unittest.mock import MagicMock + +import pytest + +from macaron.errors import HeuristicAnalyzerValueError +from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult +from macaron.malware_analyzer.pypi_heuristics.metadata.package_description_intent import ( + PackageDescriptionIntentAnalyzer, +) + + +@pytest.fixture(name="analyzer") +def analyzer_() -> PackageDescriptionIntentAnalyzer: + """Pytest fixture to create an PackageDescriptionIntentAnalyzer instance.""" + return PackageDescriptionIntentAnalyzer() + + +def test_no_info(analyzer: PackageDescriptionIntentAnalyzer, pypi_package_json: MagicMock) -> None: + """Test the analyzer raises an error when no package info is found.""" + pypi_package_json.package_json = {} + with pytest.raises(HeuristicAnalyzerValueError): + analyzer.analyze(pypi_package_json) + + +@pytest.mark.parametrize( + ("metadata", "expected_result"), + [ + pytest.param( + {"description": "A harmless package to prevent typosquatting attacks"}, + HeuristicResult.PASS, + id="test_harmless_package_description", + ), + pytest.param( + {"summary": "placeholder package to prevent dependency confusion attacks"}, + HeuristicResult.PASS, + id="test_harmless_package_summary", + ), + pytest.param( + {"description": "A regular public package", "summary": "does regular things"}, + HeuristicResult.FAIL, + id="test_no_intention", + ), + ], +) +def test_analyze_scenarios( + analyzer: PackageDescriptionIntentAnalyzer, + pypi_package_json: MagicMock, + metadata: dict, + expected_result: HeuristicResult, +) -> None: + """Test the analyzer with various metadata scenarios.""" + pypi_package_json.package_json = {"info": metadata} + result, _ = analyzer.analyze(pypi_package_json) + assert result == expected_result diff --git a/tests/malware_analyzer/pypi/test_minimal_content.py b/tests/malware_analyzer/pypi/test_type_stub_file.py similarity index 79% rename from tests/malware_analyzer/pypi/test_minimal_content.py rename to tests/malware_analyzer/pypi/test_type_stub_file.py index fe6f24646..fed53963b 100644 --- a/tests/malware_analyzer/pypi/test_minimal_content.py +++ b/tests/malware_analyzer/pypi/test_type_stub_file.py @@ -1,7 +1,7 @@ # Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. # Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. -"""Tests for the MinimalContentAnalyzer heuristic.""" +"""Tests for the TypeStubFileAnalyzer heuristic.""" from unittest.mock import MagicMock, patch @@ -9,16 +9,16 @@ from macaron.errors import SourceCodeError from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult -from macaron.malware_analyzer.pypi_heuristics.metadata.minimal_content import MinimalContentAnalyzer +from macaron.malware_analyzer.pypi_heuristics.metadata.type_stub_file import TypeStubFileAnalyzer @pytest.fixture(name="analyzer") -def analyzer_() -> MinimalContentAnalyzer: - """Pytest fixture to create a MinimalContentAnalyzer instance.""" - return MinimalContentAnalyzer() +def analyzer_() -> TypeStubFileAnalyzer: + """Pytest fixture to create a TypeStubFileAnalyzer instance.""" + return TypeStubFileAnalyzer() -def test_analyze_sufficient_files_pass(analyzer: MinimalContentAnalyzer, pypi_package_json: MagicMock) -> None: +def test_analyze_sufficient_files_pass(analyzer: TypeStubFileAnalyzer, pypi_package_json: MagicMock) -> None: """Test the analyzer passes when the package has sufficient files.""" pypi_package_json.download_sourcecode.return_value = True pypi_package_json.package_sourcecode_path = "/fake/path" @@ -30,7 +30,7 @@ def test_analyze_sufficient_files_pass(analyzer: MinimalContentAnalyzer, pypi_pa pypi_package_json.download_sourcecode.assert_called_once() -def test_analyze_exactly_threshold_files_pass(analyzer: MinimalContentAnalyzer, pypi_package_json: MagicMock) -> None: +def test_analyze_exactly_threshold_files_pass(analyzer: TypeStubFileAnalyzer, pypi_package_json: MagicMock) -> None: """Test the analyzer passes when the package has exactly the threshold number of files.""" pypi_package_json.download_sourcecode.return_value = True pypi_package_json.package_sourcecode_path = "/fake/path" @@ -41,7 +41,7 @@ def test_analyze_exactly_threshold_files_pass(analyzer: MinimalContentAnalyzer, assert result == HeuristicResult.PASS -def test_analyze_insufficient_files_fail(analyzer: MinimalContentAnalyzer, pypi_package_json: MagicMock) -> None: +def test_analyze_insufficient_files_fail(analyzer: TypeStubFileAnalyzer, pypi_package_json: MagicMock) -> None: """Test the analyzer fails when the package has insufficient files.""" pypi_package_json.download_sourcecode.return_value = True pypi_package_json.package_sourcecode_path = "/fake/path" @@ -52,7 +52,7 @@ def test_analyze_insufficient_files_fail(analyzer: MinimalContentAnalyzer, pypi_ assert result == HeuristicResult.FAIL -def test_analyze_no_files_fail(analyzer: MinimalContentAnalyzer, pypi_package_json: MagicMock) -> None: +def test_analyze_no_files_fail(analyzer: TypeStubFileAnalyzer, pypi_package_json: MagicMock) -> None: """Test the analyzer fails when the package has no files.""" pypi_package_json.download_sourcecode.return_value = True pypi_package_json.package_sourcecode_path = "/fake/path" @@ -63,7 +63,7 @@ def test_analyze_no_files_fail(analyzer: MinimalContentAnalyzer, pypi_package_js assert result == HeuristicResult.FAIL -def test_analyze_download_failed_raises_error(analyzer: MinimalContentAnalyzer, pypi_package_json: MagicMock) -> None: +def test_analyze_download_failed_raises_error(analyzer: TypeStubFileAnalyzer, pypi_package_json: MagicMock) -> None: """Test the analyzer raises SourceCodeError when source code download fails.""" pypi_package_json.download_sourcecode.return_value = False @@ -84,8 +84,8 @@ def test_analyze_download_failed_raises_error(analyzer: MinimalContentAnalyzer, (15, HeuristicResult.PASS), ], ) -def test_analyze_various_file_counts( - analyzer: MinimalContentAnalyzer, +def test_analyze_file_counts( + analyzer: TypeStubFileAnalyzer, pypi_package_json: MagicMock, file_count: int, expected_result: HeuristicResult, diff --git a/tests/malware_analyzer/pypi/test_unsecure_description.py b/tests/malware_analyzer/pypi/test_unsecure_description.py deleted file mode 100644 index d8c7ae805..000000000 --- a/tests/malware_analyzer/pypi/test_unsecure_description.py +++ /dev/null @@ -1,67 +0,0 @@ -# Copyright (c) 2024 - 2025, Oracle and/or its affiliates. All rights reserved. -# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/. - -"""Tests for the UnsecureDescriptionAnalyzer heuristic.""" - -from unittest.mock import MagicMock - -import pytest - -from macaron.errors import HeuristicAnalyzerValueError -from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult -from macaron.malware_analyzer.pypi_heuristics.metadata.unsecure_description import UnsecureDescriptionAnalyzer - - -@pytest.fixture(name="analyzer") -def analyzer_() -> UnsecureDescriptionAnalyzer: - """Pytest fixture to create an UnsecureDescriptionAnalyzer instance.""" - return UnsecureDescriptionAnalyzer() - - -def test_analyze_secure_description_pass(analyzer: UnsecureDescriptionAnalyzer, pypi_package_json: MagicMock) -> None: - """Test the analyzer passes when the package description is secure.""" - pypi_package_json.package_json = {"info": {"description": "This is an internal package."}} - result, info = analyzer.analyze(pypi_package_json) - assert result == HeuristicResult.PASS - assert info["message"] == "Package description is secure" - - -def test_analyze_unsecure_description_fail(analyzer: UnsecureDescriptionAnalyzer, pypi_package_json: MagicMock) -> None: - """Test the analyzer fails when the package description is unsecure.""" - pypi_package_json.package_json = {"info": {"description": "A public utility library."}} - result, info = analyzer.analyze(pypi_package_json) - assert result == HeuristicResult.FAIL - assert info["message"] == "Package description is unsecure" - - -def test_analyze_no_info_skip(analyzer: UnsecureDescriptionAnalyzer, pypi_package_json: MagicMock) -> None: - """Test the analyzer raises an error when no package info is found.""" - pypi_package_json.package_json = {} - with pytest.raises(HeuristicAnalyzerValueError) as exc_info: - analyzer.analyze(pypi_package_json) - assert "No package info found in metadata" in str(exc_info.value) - - -@pytest.mark.parametrize( - ("metadata", "expected_result"), - [ - ({"description": "For internal use only"}, HeuristicResult.PASS), - ({"summary": "This is a private package"}, HeuristicResult.PASS), - ({"description": "A placeholder for a future project"}, HeuristicResult.PASS), - ({"summary": "Used for dependency confusion testing"}, HeuristicResult.PASS), - ({"description": "A package for security research"}, HeuristicResult.PASS), - ({"summary": "This name is reserved for namespace protection"}, HeuristicResult.PASS), - ({"description": "This is a stub package"}, HeuristicResult.PASS), - ({"description": "A regular package", "summary": "Does regular things"}, HeuristicResult.FAIL), - ], -) -def test_analyze_scenarios( - analyzer: UnsecureDescriptionAnalyzer, - pypi_package_json: MagicMock, - metadata: dict, - expected_result: HeuristicResult, -) -> None: - """Test the analyzer with various metadata scenarios.""" - pypi_package_json.package_json = {"info": metadata} - result, _ = analyzer.analyze(pypi_package_json) - assert result == expected_result