diff --git a/src/macaron/__main__.py b/src/macaron/__main__.py index 93aca76d7..03549db7f 100644 --- a/src/macaron/__main__.py +++ b/src/macaron/__main__.py @@ -276,6 +276,8 @@ def perform_action(action_args: argparse.Namespace) -> None: try: for git_service in GIT_SERVICES: git_service.load_defaults() + for package_registry in PACKAGE_REGISTRIES: + package_registry.load_defaults() except ConfigurationError as error: logger.error(error) sys.exit(os.EX_USAGE) diff --git a/src/macaron/json_tools.py b/src/macaron/json_tools.py index 3cd7a7d37..a69b0eaa8 100644 --- a/src/macaron/json_tools.py +++ b/src/macaron/json_tools.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# 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 module provides utility functions for JSON data.""" @@ -53,5 +53,5 @@ def json_extract(entry: dict | list, keys: Sequence[str | int], type_: type[T]) if isinstance(entry, type_): return entry - logger.debug("Found value of incorrect type: %s instead of %s.", type(entry), type(type_)) + logger.debug("Found value of incorrect type: %s instead of %s.", type(entry), type_) return None diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py index 4ff41a619..bfa9a0704 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/closer_release_join_date.py @@ -95,7 +95,7 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes The result and related information collected during the analysis. """ maintainers_join_date: list[datetime] | None = self._get_maintainers_join_date( - pypi_package_json.pypi_registry, pypi_package_json.component.name + pypi_package_json.pypi_registry, pypi_package_json.component_name ) latest_release_date: datetime | None = self._get_latest_release_date(pypi_package_json) detail_info: dict[str, JsonType] = { diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/source_code_repo.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/source_code_repo.py index 8d8c9619d..708301807 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/source_code_repo.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/source_code_repo.py @@ -41,6 +41,6 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes The result and related information collected during the analysis. """ # If a sourcecode repo exists, then this will have already been validated - if not pypi_package_json.component.repository: + if not pypi_package_json.has_repository: return HeuristicResult.FAIL, {} return HeuristicResult.PASS, {} diff --git a/src/macaron/malware_analyzer/pypi_heuristics/metadata/wheel_absence.py b/src/macaron/malware_analyzer/pypi_heuristics/metadata/wheel_absence.py index 2a8217353..3a3033e22 100644 --- a/src/macaron/malware_analyzer/pypi_heuristics/metadata/wheel_absence.py +++ b/src/macaron/malware_analyzer/pypi_heuristics/metadata/wheel_absence.py @@ -61,7 +61,7 @@ def analyze(self, pypi_package_json: PyPIPackageJsonAsset) -> tuple[HeuristicRes logger.debug(error_msg) raise HeuristicAnalyzerValueError(error_msg) - version = pypi_package_json.component.version + version = pypi_package_json.component_version if version is None: # check latest release version version = pypi_package_json.get_latest_version() diff --git a/src/macaron/repo_finder/repo_finder.py b/src/macaron/repo_finder/repo_finder.py index f98f2688e..fc1a4d625 100644 --- a/src/macaron/repo_finder/repo_finder.py +++ b/src/macaron/repo_finder/repo_finder.py @@ -43,7 +43,7 @@ from macaron.config.defaults import defaults from macaron.config.global_config import global_config from macaron.errors import CloneError, RepoCheckOutError -from macaron.repo_finder import to_domain_from_known_purl_types +from macaron.repo_finder import repo_finder_pypi, to_domain_from_known_purl_types from macaron.repo_finder.commit_finder import find_commit, match_tags from macaron.repo_finder.repo_finder_base import BaseRepoFinder from macaron.repo_finder.repo_finder_deps_dev import DepsDevRepoFinder @@ -66,11 +66,16 @@ list_remote_references, resolve_local_path, ) +from macaron.slsa_analyzer.specs.package_registry_spec import PackageRegistryInfo logger: logging.Logger = logging.getLogger(__name__) -def find_repo(purl: PackageURL, check_latest_version: bool = True) -> tuple[str, RepoFinderInfo]: +def find_repo( + purl: PackageURL, + check_latest_version: bool = True, + package_registries_info: list[PackageRegistryInfo] | None = None, +) -> tuple[str, RepoFinderInfo]: """Retrieve the repository URL that matches the given PURL. Parameters @@ -79,6 +84,9 @@ def find_repo(purl: PackageURL, check_latest_version: bool = True) -> tuple[str, The parsed PURL to convert to the repository path. check_latest_version: bool A flag that determines whether the latest version of the PURL is also checked. + package_registries_info: list[PackageRegistryInfo] | None + The list of package registry information if available. + If no package registries are loaded, this can be set to None. Returns ------- @@ -103,6 +111,9 @@ def find_repo(purl: PackageURL, check_latest_version: bool = True) -> tuple[str, logger.debug("Analyzing %s with Repo Finder: %s", purl, type(repo_finder)) found_repo, outcome = repo_finder.find_repo(purl) + if not found_repo: + found_repo, outcome = find_repo_alternative(purl, outcome, package_registries_info) + if check_latest_version and not defaults.getboolean("repofinder", "try_latest_purl", fallback=True): check_latest_version = False @@ -117,6 +128,12 @@ def find_repo(purl: PackageURL, check_latest_version: bool = True) -> tuple[str, return "", RepoFinderInfo.NO_NEWER_VERSION found_repo, outcome = DepsDevRepoFinder().find_repo(latest_version_purl) + if found_repo: + return found_repo, outcome + + if not found_repo: + found_repo, outcome = find_repo_alternative(latest_version_purl, outcome, package_registries_info) + if not found_repo: logger.debug("Could not find repo from latest version of PURL: %s", latest_version_purl) return "", RepoFinderInfo.LATEST_VERSION_INVALID @@ -124,6 +141,36 @@ def find_repo(purl: PackageURL, check_latest_version: bool = True) -> tuple[str, return found_repo, outcome +def find_repo_alternative( + purl: PackageURL, outcome: RepoFinderInfo, package_registries_info: list[PackageRegistryInfo] | None = None +) -> tuple[str, RepoFinderInfo]: + """Use PURL type specific methods to find the repository when the standard methods have failed. + + Parameters + ---------- + purl : PackageURL + The parsed PURL to convert to the repository path. + outcome: RepoFinderInfo + A previous outcome to report if this method does nothing. + package_registries_info: list[PackageRegistryInfo] | None + The list of package registry information if available. + If no package registries are loaded, this can be set to None. + + Returns + ------- + tuple[str, RepoFinderOutcome] : + The repository URL for the passed package, if found, and the outcome to report. + """ + found_repo = "" + if purl.type == "pypi": + found_repo, outcome = repo_finder_pypi.find_repo(purl, package_registries_info) + + if not found_repo: + logger.debug("Could not find repository using type specific (%s) methods for PURL: %s", purl.type, purl) + + return found_repo, outcome + + def to_repo_path(purl: PackageURL, available_domains: list[str]) -> str | None: """Return the repository path from the PURL string. diff --git a/src/macaron/repo_finder/repo_finder_enums.py b/src/macaron/repo_finder/repo_finder_enums.py index 4d088a5cc..43e8d5e8b 100644 --- a/src/macaron/repo_finder/repo_finder_enums.py +++ b/src/macaron/repo_finder/repo_finder_enums.py @@ -57,6 +57,18 @@ class RepoFinderInfo(Enum): #: Reported if deps.dev returns data that does not contain the desired SCM URL. E.g. The repository URL. DDEV_NO_URLS = "deps.dev no URLs" + #: Reported if there was an error with the request sent to the PyPI registry. + PYPI_HTTP_ERROR = "PyPI HTTP error" + + #: Reported if there was an error parsing the JSON returned by the PyPI registry. + PYPI_JSON_ERROR = "PyPI JSON error" + + #: Reported if there was no matching URLs in the JSON returned by the PyPI registry. + PYPI_NO_URLS = "PyPI no matching URLs" + + #: Reported if the PyPI registry is disabled or not present in the list of package registries. + PYPI_NO_REGISTRY = "PyPI registry disabled or absent" + #: Reported if the provided PURL did not produce a result, but a more recent version could not be found. NO_NEWER_VERSION = "No newer version than provided which failed" @@ -70,7 +82,10 @@ class RepoFinderInfo(Enum): FOUND_FROM_PARENT = "Found from parent" #: Reported when a repository is found from a more recent version than was provided by the user. - FOUND_FROM_LATEST = "Found form latest" + FOUND_FROM_LATEST = "Found from latest" + + #: Reported when a repository could only be found by checking the PyPI registry JSON. + FOUND_FROM_PYPI = "Found from PyPI" #: Default value. Reported if the Repo Finder was not called. E.g. Because the repository URL was already present. NOT_USED = "Not used" diff --git a/src/macaron/repo_finder/repo_finder_pypi.py b/src/macaron/repo_finder/repo_finder_pypi.py new file mode 100644 index 000000000..7525c3779 --- /dev/null +++ b/src/macaron/repo_finder/repo_finder_pypi.py @@ -0,0 +1,90 @@ +# Copyright (c) 2025 - 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 module contains the logic for finding repositories of PyPI projects.""" +import logging + +from packageurl import PackageURL + +from macaron.repo_finder.repo_finder_enums import RepoFinderInfo +from macaron.repo_finder.repo_validator import find_valid_repository_url +from macaron.slsa_analyzer.package_registry import PACKAGE_REGISTRIES, PyPIRegistry +from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset +from macaron.slsa_analyzer.specs.package_registry_spec import PackageRegistryInfo + +logger: logging.Logger = logging.getLogger(__name__) + + +def find_repo( + purl: PackageURL, package_registries_info: list[PackageRegistryInfo] | None = None +) -> tuple[str, RepoFinderInfo]: + """Retrieve the repository URL that matches the given PyPI PURL. + + Parameters + ---------- + purl : PackageURL + The parsed PURL to convert to the repository path. + package_registries_info: list[PackageRegistryInfo] | None + The list of package registry information if available. + If no package registries are loaded, this can be set to None. + + Returns + ------- + tuple[str, RepoFinderOutcome] : + The repository URL for the passed package, if found, and the outcome to report. + """ + pypi_info = None + if package_registries_info: + # Find the package registry info object that contains the PyPI registry and has the pypi build tool. + pypi_info = next( + ( + info + for info in package_registries_info + if isinstance(info.package_registry, PyPIRegistry) and info.build_tool_name in {"poetry", "pip"} + ), + None, + ) + + if not pypi_info or not isinstance(pypi_info.package_registry, PyPIRegistry): + pypi_registry = next((registry for registry in PACKAGE_REGISTRIES if isinstance(registry, PyPIRegistry)), None) + else: + pypi_registry = pypi_info.package_registry + + if not pypi_registry: + logger.debug("PyPI package registry not available.") + return "", RepoFinderInfo.PYPI_NO_REGISTRY + + pypi_asset = None + from_metadata = False + if pypi_info: + for existing_asset in pypi_info.metadata: + if not isinstance(existing_asset, PyPIPackageJsonAsset): + continue + + if existing_asset.component_name == purl.name and existing_asset.component_version == purl.version: + pypi_asset = existing_asset + from_metadata = True + break + + if not pypi_asset: + pypi_asset = PyPIPackageJsonAsset(purl.name, purl.version, False, pypi_registry, {}) + + if not pypi_asset.package_json and not pypi_asset.download(dest=""): + return "", RepoFinderInfo.PYPI_HTTP_ERROR + + if not from_metadata and pypi_info: + # Save the asset for later use. + pypi_info.metadata.append(pypi_asset) + + url_dict = pypi_asset.get_project_links() + if not url_dict: + return "", RepoFinderInfo.PYPI_JSON_ERROR + + # Look for the repository URL. + fixed_url = find_valid_repository_url(url_dict.values()) + if not fixed_url: + return "", RepoFinderInfo.PYPI_NO_URLS + + logger.debug("Found repository URL from PyPI: %s", fixed_url) + pypi_asset.has_repository = True + return fixed_url, RepoFinderInfo.FOUND_FROM_PYPI diff --git a/src/macaron/repo_finder/repo_validator.py b/src/macaron/repo_finder/repo_validator.py index dd78ec10f..4e2e7d639 100644 --- a/src/macaron/repo_finder/repo_validator.py +++ b/src/macaron/repo_finder/repo_validator.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 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 module exists to validate URLs in terms of their use as a repository that can be analyzed.""" diff --git a/src/macaron/slsa_analyzer/analyzer.py b/src/macaron/slsa_analyzer/analyzer.py index d17567110..514c8d35e 100644 --- a/src/macaron/slsa_analyzer/analyzer.py +++ b/src/macaron/slsa_analyzer/analyzer.py @@ -353,6 +353,9 @@ def run_single( status=SCMStatus.ANALYSIS_FAILED, ) + # Pre-populate all package registries so assets can be stored for later. + package_registries_info = self._populate_package_registry_info() + provenance_is_verified = False if not provenance_payload and parsed_purl: # Try to find the provenance file for the parsed PURL. @@ -385,7 +388,12 @@ def run_single( available_domains = [git_service.hostname for git_service in GIT_SERVICES if git_service.hostname] try: analysis_target = Analyzer.to_analysis_target( - config, available_domains, parsed_purl, provenance_repo_url, provenance_commit_digest + config, + available_domains, + parsed_purl, + provenance_repo_url, + provenance_commit_digest, + package_registries_info, ) except InvalidAnalysisTargetError as error: return Record( @@ -464,7 +472,7 @@ def run_single( logger.info("With PURL: %s", component.purl) logger.info("=====================================") - analyze_ctx = self.get_analyze_ctx(component) + analyze_ctx = self.create_analyze_ctx(component) analyze_ctx.dynamic_data["expectation"] = self.expectations.get_expectation_for_target( analyze_ctx.component.purl.split("@")[0] ) @@ -474,7 +482,7 @@ def run_single( self._determine_build_tools(analyze_ctx, git_service) if parsed_purl is not None: self._verify_repository_link(parsed_purl, analyze_ctx) - self._determine_package_registries(analyze_ctx) + self._determine_package_registries(analyze_ctx, package_registries_info) provenance_l3_verified = False if not provenance_payload: @@ -802,6 +810,7 @@ def to_analysis_target( parsed_purl: PackageURL | None, provenance_repo_url: str | None = None, provenance_commit_digest: str | None = None, + package_registries_info: list[PackageRegistryInfo] | None = None, ) -> AnalysisTarget: """Resolve the details of a software component from user input. @@ -818,6 +827,9 @@ def to_analysis_target( The repository URL extracted from provenance, or None if not found or no provenance. provenance_commit_digest: str | None The commit extracted from provenance, or None if not found or no provenance. + package_registries_info: list[PackageRegistryInfo] | None + The list of package registry information if available. + If no package registries are loaded, this can be set to None. Returns ------- @@ -860,7 +872,9 @@ def to_analysis_target( converted_repo_path = repo_finder.to_repo_path(parsed_purl, available_domains) if converted_repo_path is None: # Try to find repo from PURL - repo, repo_finder_outcome = repo_finder.find_repo(parsed_purl) + repo, repo_finder_outcome = repo_finder.find_repo( + parsed_purl, package_registries_info=package_registries_info + ) return Analyzer.AnalysisTarget( parsed_purl=parsed_purl, @@ -904,8 +918,8 @@ def to_analysis_target( "Cannot determine the analysis target: PURL and repository path are missing." ) - def get_analyze_ctx(self, component: Component) -> AnalyzeContext: - """Return the analyze context for a target component. + def create_analyze_ctx(self, component: Component) -> AnalyzeContext: + """Create and return an analysis context for the passed component. Parameters ---------- @@ -1011,20 +1025,39 @@ def _determine_ci_services(self, analyze_ctx: AnalyzeContext, git_service: BaseG ) ) - def _determine_package_registries(self, analyze_ctx: AnalyzeContext) -> None: + def _populate_package_registry_info(self) -> list[PackageRegistryInfo]: + """Add all possible package registries to the analysis context.""" + package_registries = [] + for package_registry in PACKAGE_REGISTRIES: + for build_tool in BUILD_TOOLS: + build_tool_name = build_tool.name + if build_tool_name not in package_registry.build_tool_names: + continue + package_registries.append( + PackageRegistryInfo( + build_tool_name=build_tool_name, + build_tool_purl_type=build_tool.purl_type, + package_registry=package_registry, + ) + ) + return package_registries + + def _determine_package_registries( + self, analyze_ctx: AnalyzeContext, package_registries_info: list[PackageRegistryInfo] + ) -> None: """Determine the package registries used by the software component based on its build tools.""" build_tools = ( analyze_ctx.dynamic_data["build_spec"]["tools"] or analyze_ctx.dynamic_data["build_spec"]["purl_tools"] ) - for package_registry in PACKAGE_REGISTRIES: - for build_tool in build_tools: - if package_registry.is_detected(build_tool): - analyze_ctx.dynamic_data["package_registries"].append( - PackageRegistryInfo( - build_tool=build_tool, - package_registry=package_registry, - ) - ) + build_tool_names = {build_tool.name for build_tool in build_tools} + relevant_package_registries = [] + for package_registry in package_registries_info: + if package_registry.build_tool_name not in build_tool_names: + continue + relevant_package_registries.append(package_registry) + + # Assign the updated list of registries. + analyze_ctx.dynamic_data["package_registries"] = relevant_package_registries def _verify_repository_link(self, parsed_purl: PackageURL, analyze_ctx: AnalyzeContext) -> None: """Verify whether the claimed repository links back to the artifact.""" 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 9d20e12bc..857c726ed 100644 --- a/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py +++ b/src/macaron/slsa_analyzer/checks/detect_malicious_metadata_check.py @@ -29,8 +29,6 @@ from macaron.malware_analyzer.pypi_heuristics.pypi_sourcecode_analyzer import PyPISourcecodeAnalyzer from macaron.malware_analyzer.pypi_heuristics.sourcecode.suspicious_setup import SuspiciousSetupAnalyzer from macaron.slsa_analyzer.analyze_context import AnalyzeContext -from macaron.slsa_analyzer.build_tool.pip import Pip -from macaron.slsa_analyzer.build_tool.poetry import Poetry from macaron.slsa_analyzer.checks.base_check import BaseCheck from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType, Confidence, JustificationType from macaron.slsa_analyzer.package_registry.deps_dev import APIAccessError, DepsDevService @@ -230,6 +228,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # First check if this package is a known malware data = {"package": {"purl": ctx.component.purl}} + package_exists = False try: package_exists = bool(DepsDevService.get_package_info(ctx.component.purl)) except APIAccessError as error: @@ -266,19 +265,35 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: match package_registry_info_entry: # Currently, only PyPI packages are supported. case PackageRegistryInfo( - build_tool=Pip() | Poetry(), + build_tool_name="pip" | "poetry", + build_tool_purl_type="pypi", package_registry=PyPIRegistry() as pypi_registry, ) as pypi_registry_info: - - # Create an AssetLocator object for the PyPI package JSON object. - pypi_package_json = PyPIPackageJsonAsset( - component=ctx.component, pypi_registry=pypi_registry, package_json={} + # Retrieve the pre-existing AssetLocator object for the PyPI package JSON object, if it exists. + pypi_package_json = next( + ( + asset + for asset in pypi_registry_info.metadata + if isinstance(asset, PyPIPackageJsonAsset) + and asset.component_name == ctx.component.name + and asset.component_version == ctx.component.version + ), + None, ) + if not pypi_package_json: + # Create an AssetLocator object for the PyPI package JSON object. + pypi_package_json = PyPIPackageJsonAsset( + component_name=ctx.component.name, + component_version=ctx.component.version, + has_repository=ctx.component.repository is not None, + pypi_registry=pypi_registry, + package_json={}, + ) pypi_registry_info.metadata.append(pypi_package_json) # Download the PyPI package JSON, but no need to persist it to the filesystem. - if pypi_package_json.download(dest=""): + if pypi_package_json.package_json or pypi_package_json.download(dest=""): try: result, detail_info = self.run_heuristics(pypi_package_json) except HeuristicAnalyzerValueError: diff --git a/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py b/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py index 96f83cefc..c02fa8380 100644 --- a/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py +++ b/src/macaron/slsa_analyzer/checks/infer_artifact_pipeline_check.py @@ -123,7 +123,7 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData: # Look for the artifact in the corresponding registry and find the publish timestamp. artifact_published_date = None for registry_info in ctx.dynamic_data["package_registries"]: - if registry_info.build_tool.purl_type == ctx.component.type: + if registry_info.build_tool_purl_type == ctx.component.type: try: artifact_published_date = registry_info.package_registry.find_publish_timestamp(ctx.component.purl) break diff --git a/src/macaron/slsa_analyzer/package_registry/jfrog_maven_registry.py b/src/macaron/slsa_analyzer/package_registry/jfrog_maven_registry.py index f7a546911..02188de1d 100644 --- a/src/macaron/slsa_analyzer/package_registry/jfrog_maven_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/jfrog_maven_registry.py @@ -17,9 +17,6 @@ from macaron.config.defaults import defaults from macaron.errors import ConfigurationError from macaron.json_tools import JsonType -from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool -from macaron.slsa_analyzer.build_tool.gradle import Gradle -from macaron.slsa_analyzer.build_tool.maven import Maven from macaron.slsa_analyzer.package_registry.package_registry import PackageRegistry logger: logging.Logger = logging.getLogger(__name__) @@ -126,7 +123,7 @@ def __init__( self.request_timeout = request_timeout or 10 self.download_timeout = download_timeout or 120 self.enabled = enabled or False - super().__init__("JFrog Maven Registry") + super().__init__("JFrog Maven Registry", {"maven", "gradle"}) def load_defaults(self) -> None: """Load the .ini configuration for the current package registry. @@ -173,31 +170,6 @@ def load_defaults(self) -> None: self.enabled = True - def is_detected(self, build_tool: BaseBuildTool) -> bool: - """Detect if artifacts of the repo under analysis can possibly be published to this package registry. - - The detection here is based on the repo's detected build tool. - If the package registry is compatible with the given build tool, it can be a - possible place where the artifacts produced from the repo are published. - - ``JFrogMavenRegistry`` is compatible with Maven and Gradle. - - Parameters - ---------- - build_tool : BaseBuildTool - A detected build tool of the repository under analysis. - - Returns - ------- - bool - ``True`` if the repo under analysis can be published to this package registry, - based on the given build tool. - """ - if not self.enabled: - return False - compatible_build_tool_classes = [Maven, Gradle] - return any(isinstance(build_tool, build_tool_class) for build_tool_class in compatible_build_tool_classes) - def fetch_artifact_ids(self, group_id: str) -> list[str]: """Get all artifact ids under a group id. diff --git a/src/macaron/slsa_analyzer/package_registry/maven_central_registry.py b/src/macaron/slsa_analyzer/package_registry/maven_central_registry.py index a73ef519c..131051b66 100644 --- a/src/macaron/slsa_analyzer/package_registry/maven_central_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/maven_central_registry.py @@ -12,9 +12,6 @@ from macaron.config.defaults import defaults from macaron.errors import ConfigurationError, InvalidHTTPResponseError -from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool -from macaron.slsa_analyzer.build_tool.gradle import Gradle -from macaron.slsa_analyzer.build_tool.maven import Maven from macaron.slsa_analyzer.package_registry.package_registry import PackageRegistry from macaron.util import send_get_http_raw @@ -108,7 +105,7 @@ def __init__( self.registry_url_scheme = registry_url_scheme or "" self.registry_url = "" # Created from the registry_url_scheme and registry_url_netloc. self.request_timeout = request_timeout or 10 - super().__init__("Maven Central Registry") + super().__init__("Maven Central Registry", {"maven", "gradle"}) def load_defaults(self) -> None: """Load the .ini configuration for the current package registry. @@ -159,29 +156,6 @@ def load_defaults(self) -> None: f"of the .ini configuration file is invalid: {error}", ) from error - def is_detected(self, build_tool: BaseBuildTool) -> bool: - """Detect if artifacts of the repo under analysis can possibly be published to this package registry. - - The detection here is based on the repo's detected build tools. - If the package registry is compatible with the given build tools, it can be a - possible place where the artifacts produced from the repo are published. - - ``MavenCentralRegistry`` is compatible with Maven and Gradle. - - Parameters - ---------- - build_tool : BaseBuildTool - A detected build tool of the repository under analysis. - - Returns - ------- - bool - ``True`` if the repo under analysis can be published to this package registry, - based on the given build tool. - """ - compatible_build_tool_classes = [Maven, Gradle] - return any(isinstance(build_tool, build_tool_class) for build_tool_class in compatible_build_tool_classes) - def find_publish_timestamp(self, purl: str) -> datetime: """Make a search request to Maven Central to find the publishing timestamp of an artifact. diff --git a/src/macaron/slsa_analyzer/package_registry/npm_registry.py b/src/macaron/slsa_analyzer/package_registry/npm_registry.py index f200bb5e0..fe009cc34 100644 --- a/src/macaron/slsa_analyzer/package_registry/npm_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/npm_registry.py @@ -12,9 +12,6 @@ from macaron.config.defaults import defaults from macaron.errors import ConfigurationError, InvalidHTTPResponseError -from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool -from macaron.slsa_analyzer.build_tool.npm import NPM -from macaron.slsa_analyzer.build_tool.yarn import Yarn from macaron.slsa_analyzer.package_registry.package_registry import PackageRegistry from macaron.util import send_get_http_raw @@ -53,7 +50,7 @@ def __init__( self.attestation_endpoint = attestation_endpoint or "" self.request_timeout = request_timeout or 10 self.enabled = enabled - super().__init__("npm Registry") + super().__init__("npm Registry", {"npm", "yarn"}) def load_defaults(self) -> None: """Load the .ini configuration for the current package registry. @@ -95,34 +92,6 @@ def load_defaults(self) -> None: f"of the .ini configuration file is invalid: {error}", ) from error - def is_detected(self, build_tool: BaseBuildTool) -> bool: - """Detect if artifacts under analysis can be published to this package registry. - - The detection here is based on the repo's detected build tools. - If the package registry is compatible with the given build tools, it can be a - possible place where the artifacts are published. - - ``NPMRegistry`` is compatible with npm and Yarn build tools. - - Note: if the npm registry is disabled through the ini configuration, this method returns False. - - Parameters - ---------- - build_tool : BaseBuildTool - A detected build tool of the repository under analysis. - - Returns - ------- - bool - ``True`` if the repo under analysis can be published to this package registry, - based on the given build tool. - """ - if not self.enabled: - logger.debug("Support for the npm registry is disabled.") - return False - compatible_build_tool_classes = [NPM, Yarn] - return any(isinstance(build_tool, build_tool_class) for build_tool_class in compatible_build_tool_classes) - def download_attestation_payload(self, url: str, download_path: str) -> bool: """Download the npm attestation from npm registry. diff --git a/src/macaron/slsa_analyzer/package_registry/package_registry.py b/src/macaron/slsa_analyzer/package_registry/package_registry.py index 146958252..9e71fc595 100644 --- a/src/macaron/slsa_analyzer/package_registry/package_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/package_registry.py @@ -9,7 +9,6 @@ from macaron.errors import InvalidHTTPResponseError from macaron.json_tools import json_extract -from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool from macaron.slsa_analyzer.package_registry.deps_dev import APIAccessError, DepsDevService logger: logging.Logger = logging.getLogger(__name__) @@ -18,15 +17,16 @@ class PackageRegistry(ABC): """Base package registry class.""" - def __init__(self, name: str) -> None: + def __init__(self, name: str, build_tool_names: set[str]) -> None: self.name = name + self.build_tool_names = build_tool_names + self.enabled: bool = True @abstractmethod def load_defaults(self) -> None: """Load the .ini configuration for the current package registry.""" - @abstractmethod - def is_detected(self, build_tool: BaseBuildTool) -> bool: + def is_detected(self, build_tool_name: str) -> bool: """Detect if artifacts of the repo under analysis can possibly be published to this package registry. The detection here is based on the repo's detected build tool. @@ -35,8 +35,8 @@ def is_detected(self, build_tool: BaseBuildTool) -> bool: Parameters ---------- - build_tool : BaseBuildTool - A detected build tool of the repository under analysis. + build_tool_name: str + The name of a detected build tool of the repository under analysis. Returns ------- @@ -44,6 +44,9 @@ def is_detected(self, build_tool: BaseBuildTool) -> bool: ``True`` if the repo under analysis can be published to this package registry, based on the given build tool. """ + if not self.enabled: + return False + return build_tool_name in self.build_tool_names def find_publish_timestamp(self, purl: str) -> datetime: """Retrieve the publication timestamp for a package specified by its purl from the deps.dev repository by default. diff --git a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py index e349663b0..20f75db08 100644 --- a/src/macaron/slsa_analyzer/package_registry/pypi_registry.py +++ b/src/macaron/slsa_analyzer/package_registry/pypi_registry.py @@ -17,12 +17,9 @@ from requests import RequestException from macaron.config.defaults import defaults -from macaron.database.table_definitions import Component from macaron.errors import ConfigurationError, InvalidHTTPResponseError from macaron.json_tools import json_extract from macaron.malware_analyzer.datetime_parser import parse_datetime -from macaron.slsa_analyzer.build_tool import Pip, Poetry -from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool from macaron.slsa_analyzer.package_registry.package_registry import PackageRegistry from macaron.util import send_get_http_raw @@ -75,7 +72,7 @@ def __init__( self.request_timeout = request_timeout or 10 self.enabled = enabled self.registry_url = "" - super().__init__("PyPI Registry") + super().__init__("PyPI Registry", {"pip", "poetry"}) def load_defaults(self) -> None: """Load the .ini configuration for the current package registry. @@ -129,29 +126,6 @@ def load_defaults(self) -> None: f"of the .ini configuration file is invalid: {error}", ) from error - def is_detected(self, build_tool: BaseBuildTool) -> bool: - """Detect if artifacts of the repo under analysis can possibly be published to this package registry. - - The detection here is based on the repo's detected build tools. - If the package registry is compatible with the given build tools, it can be a - possible place where the artifacts produced from the repo are published. - - ``PyPIRegistry`` is compatible with Pip and Poetry. - - Parameters - ---------- - build_tool: BaseBuildTool - A detected build tool of the repository under analysis. - - Returns - ------- - bool - ``True`` if the repo under analysis can be published to this package registry, - based on the given build tool. - """ - compatible_build_tool_classes = [Pip, Poetry] - return any(isinstance(build_tool, build_tool_class) for build_tool_class in compatible_build_tool_classes) - def download_package_json(self, url: str) -> dict: """Download the package JSON metadata from pypi registry. @@ -366,8 +340,14 @@ def get_maintainer_join_date(self, username: str) -> datetime | None: class PyPIPackageJsonAsset: """The package JSON hosted on the PyPI registry.""" - #: The target pypi software component. - component: Component + #: The target pypi software component name. + component_name: str + + #: The target pypi software component version. + component_version: str | None + + #: Whether the component of this asset has a related repository. + has_repository: bool #: The pypi registry. pypi_registry: PyPIRegistry @@ -397,7 +377,7 @@ def url(self) -> str: ------- str """ - json_endpoint = f"pypi/{self.component.name}/json" + json_endpoint = f"pypi/{self.component_name}/json" return urllib.parse.urljoin(self.pypi_registry.registry_url, json_endpoint) def download(self, dest: str) -> bool: # pylint: disable=unused-argument @@ -459,8 +439,8 @@ def get_sourcecode_url(self) -> str | None: The URL of the source distribution. """ urls: list | None = None - if self.component.version: - urls = json_extract(self.package_json, ["releases", self.component.version], list) + if self.component_version: + urls = json_extract(self.package_json, ["releases", self.component_version], list) else: # Get the latest version. urls = json_extract(self.package_json, ["urls"], list) diff --git a/src/macaron/slsa_analyzer/specs/package_registry_spec.py b/src/macaron/slsa_analyzer/specs/package_registry_spec.py index e28d9c6d8..84b2a69e7 100644 --- a/src/macaron/slsa_analyzer/specs/package_registry_spec.py +++ b/src/macaron/slsa_analyzer/specs/package_registry_spec.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 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/. @@ -7,7 +7,6 @@ from dataclasses import dataclass, field from macaron.slsa_analyzer.asset import AssetLocator -from macaron.slsa_analyzer.build_tool import BaseBuildTool from macaron.slsa_analyzer.package_registry import PackageRegistry from macaron.slsa_analyzer.provenance.provenance import DownloadedProvenanceData @@ -16,8 +15,10 @@ class PackageRegistryInfo: """This class contains data for one package registry that is matched against a repository.""" - #: The build tool matched against the repository. - build_tool: BaseBuildTool + #: The name of the build tool matched against the repository. + build_tool_name: str + #: The purl type of the build tool matched against the repository. + build_tool_purl_type: str #: The package registry matched against the repository. This is dependent on the build tool detected. package_registry: PackageRegistry #: The provenances matched against the current repo. diff --git a/tests/find_source/compare_source_reports.py b/tests/find_source/compare_source_reports.py new file mode 100644 index 000000000..714a63f29 --- /dev/null +++ b/tests/find_source/compare_source_reports.py @@ -0,0 +1,35 @@ +# Copyright (c) 2025 - 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 module compares the contents of two JSON find_source files and reports on their equality.""" + +import logging + +logger: logging.Logger = logging.getLogger(__name__) + +# Set logging debug level. +logger.setLevel(logging.DEBUG) + + +def compare_find_source_reports(first: dict, second: dict) -> int: + """Compare the content of the two report files.""" + result = 0 + + for key in first: + if key not in second: + logger.error("Key mismatch: %s -> MISSING", key) + result += 1 + + for key in second: + if key not in first: + logger.error("Key mismatch: MISSING -> %s", key) + result += 1 + + for key in first: + if key not in second: + continue + if first[key] != second[key]: + logger.error("Value mismatch for key '%s': '%s' != '%s'", key, first[key], second[key]) + result += 1 + + return result diff --git a/tests/integration/cases/find_source_avaje/avaje-prisms.source.json b/tests/integration/cases/find_source_avaje/avaje-prisms.source.json new file mode 100644 index 000000000..f926a12ce --- /dev/null +++ b/tests/integration/cases/find_source_avaje/avaje-prisms.source.json @@ -0,0 +1,8 @@ +{ + "purl": "pkg:maven/io.avaje/avaje-prisms@1.1", + "commit": "1f6f953df0b58f0c35b5e136f62f63ba7a22bc03", + "repo": "https://github.com/avaje/avaje-prisms", + "repo_validated": false, + "commit_validated": false, + "url": "https://github.com/avaje/avaje-prisms/commit/1f6f953df0b58f0c35b5e136f62f63ba7a22bc03" +} diff --git a/tests/integration/cases/find_source_avaje/test.yaml b/tests/integration/cases/find_source_avaje/test.yaml index 116171722..ac98b9b76 100644 --- a/tests/integration/cases/find_source_avaje/test.yaml +++ b/tests/integration/cases/find_source_avaje/test.yaml @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# 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/. description: | @@ -21,6 +21,12 @@ steps: kind: json_schema schema: find_source_json_report result: output/reports/maven/io_avaje/avaje-prisms/avaje-prisms.source.json +- name: Compare the contents of the report against a known correct one + kind: compare + options: + kind: find_source + result: output/reports/maven/io_avaje/avaje-prisms/avaje-prisms.source.json + expected: avaje-prisms.source.json - name: Check that the repository was not cloned kind: shell options: diff --git a/tests/integration/cases/repo_finder_pypi/policy.dl b/tests/integration/cases/repo_finder_pypi/policy.dl new file mode 100644 index 000000000..38b2dd9f4 --- /dev/null +++ b/tests/integration/cases/repo_finder_pypi/policy.dl @@ -0,0 +1,10 @@ +/* Copyright (c) 2025 - 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/. */ + +#include "prelude.dl" + +Policy("test_policy", component_id, "") :- + check_passed(component_id, "mcn_version_control_system_1"). + +apply_policy_to("test_policy", component_id) :- + is_component(component_id, "pkg:pypi/torch@2.6.0"). diff --git a/tests/integration/cases/repo_finder_pypi/test.yaml b/tests/integration/cases/repo_finder_pypi/test.yaml new file mode 100644 index 000000000..d3cf1c557 --- /dev/null +++ b/tests/integration/cases/repo_finder_pypi/test.yaml @@ -0,0 +1,20 @@ +# Copyright (c) 2025 - 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/. + +description: | + Analyzing a PyPI PURL that is not correctly found by deps.dev and must be sought on the package registry directly. + +tags: +- macaron-python-package + +steps: +- name: Run macaron analyze + kind: analyze + options: + command_args: + - -purl + - pkg:pypi/torch@2.6.0 +- name: Run macaron verify-policy to verify passed/failed checks + kind: verify + options: + policy: policy.dl diff --git a/tests/integration/cases/repo_finder_pypi_find_source/test.yaml b/tests/integration/cases/repo_finder_pypi_find_source/test.yaml new file mode 100644 index 000000000..dee8cdf08 --- /dev/null +++ b/tests/integration/cases/repo_finder_pypi_find_source/test.yaml @@ -0,0 +1,28 @@ +# Copyright (c) 2025 - 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/. + +description: | + Finding the source of a PyPI PURL that is not correctly found by deps.dev and must be sought on the package registry directly. + +tags: +- macaron-python-package + +steps: +- name: Run macaron analyze + kind: find-source + options: + command_args: + - -purl + - pkg:pypi/torch@2.6.0 +- name: Validate the produced report + kind: validate_schema + options: + kind: json_schema + schema: find_source_json_report + result: output/reports/pypi/torch/torch.source.json +- name: Compare the contents of the report against a known correct one + kind: compare + options: + kind: find_source + result: output/reports/pypi/torch/torch.source.json + expected: torch.source.json diff --git a/tests/integration/cases/repo_finder_pypi_find_source/torch.source.json b/tests/integration/cases/repo_finder_pypi_find_source/torch.source.json new file mode 100644 index 000000000..244f7941c --- /dev/null +++ b/tests/integration/cases/repo_finder_pypi_find_source/torch.source.json @@ -0,0 +1,8 @@ +{ + "purl": "pkg:pypi/torch@2.6.0", + "commit": "1eba9b3aa3c43f86f4a2c807ac8e12c4a7767340", + "repo": "https://github.com/pytorch/pytorch", + "repo_validated": false, + "commit_validated": false, + "url": "https://github.com/pytorch/pytorch/commit/1eba9b3aa3c43f86f4a2c807ac8e12c4a7767340" +} diff --git a/tests/integration/run.py b/tests/integration/run.py index a2dad017c..2cb77025b 100644 --- a/tests/integration/run.py +++ b/tests/integration/run.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# 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/. """Integration test utility.""" @@ -79,6 +79,7 @@ def configure_logging(verbose: bool) -> None: "policy_report": ["tests", "policy_engine", "compare_policy_reports.py"], "deps_report": ["tests", "dependency_analyzer", "compare_dependencies.py"], "vsa": ["tests", "vsa", "compare_vsa.py"], + "find_source": ["tests", "find_source", "compare_source_reports.py"], } VALIDATE_SCHEMA_SCRIPTS: dict[str, Sequence[str]] = { diff --git a/tests/malware_analyzer/pypi/test_closer_release_join_date.py b/tests/malware_analyzer/pypi/test_closer_release_join_date.py index 4ed1a9b24..309574a21 100644 --- a/tests/malware_analyzer/pypi/test_closer_release_join_date.py +++ b/tests/malware_analyzer/pypi/test_closer_release_join_date.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# 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 closer release join date heuristic.""" @@ -17,6 +17,7 @@ def test_analyze_pass(pypi_package_json: MagicMock) -> None: pypi_package_json.pypi_registry.get_maintainers_of_package.return_value = ["maintainer1", "maintainer2"] pypi_package_json.pypi_registry.get_maintainer_join_date.side_effect = [datetime(2018, 1, 1), datetime(2019, 1, 1)] pypi_package_json.get_latest_release_upload_time.return_value = "2022-06-20T12:00:00" + pypi_package_json.component_name = "mock1" # Call the method. result, detail_info = analyzer.analyze(pypi_package_json) @@ -35,6 +36,7 @@ def test_analyze_process(pypi_package_json: MagicMock) -> None: pypi_package_json.pypi_registry.get_maintainers_of_package.return_value = ["maintainer1"] pypi_package_json.pypi_registry.get_maintainer_join_date.side_effect = [datetime(2022, 6, 18)] pypi_package_json.get_latest_release_upload_time.return_value = "2022-06-20T12:00:00" + pypi_package_json.component_name = "mock1" # Call the method. result, detail_info = analyzer.analyze(pypi_package_json) @@ -52,6 +54,7 @@ def test_analyze_skip(pypi_package_json: MagicMock) -> None: # Set up mock return values. pypi_package_json.pypi_registry.get_maintainers_of_package.return_value = None pypi_package_json.get_latest_release_upload_time.return_value = "2022-06-20T12:00:00" + pypi_package_json.component_name = "mock1" # Call the method. result, detail_info = analyzer.analyze(pypi_package_json) diff --git a/tests/malware_analyzer/pypi/test_source_code_repo.py b/tests/malware_analyzer/pypi/test_source_code_repo.py index 668c80865..3cc9db15d 100644 --- a/tests/malware_analyzer/pypi/test_source_code_repo.py +++ b/tests/malware_analyzer/pypi/test_source_code_repo.py @@ -14,19 +14,13 @@ @pytest.mark.parametrize( ("repository", "expected_result"), [ - pytest.param(None, HeuristicResult.FAIL, id="test_no_repo"), - pytest.param( - MagicMock(), - HeuristicResult.PASS, - id="test_valid_repo", - ), + pytest.param(False, HeuristicResult.FAIL, id="test_no_repo"), + pytest.param(True, HeuristicResult.PASS, id="test_valid_repo"), ], ) -def test_repo_existence( - pypi_package_json: MagicMock, repository: MagicMock | None, expected_result: HeuristicResult -) -> None: +def test_repo_existence(pypi_package_json: MagicMock, repository: bool, expected_result: HeuristicResult) -> None: """Test if the source code repo exists.""" - pypi_package_json.component.repository = repository + pypi_package_json.has_repository = repository analyzer = SourceCodeRepoAnalyzer() result, _ = analyzer.analyze(pypi_package_json) assert result == expected_result diff --git a/tests/malware_analyzer/pypi/test_wheel_absence.py b/tests/malware_analyzer/pypi/test_wheel_absence.py index a2eebd554..3cfccfbe7 100644 --- a/tests/malware_analyzer/pypi/test_wheel_absence.py +++ b/tests/malware_analyzer/pypi/test_wheel_absence.py @@ -67,10 +67,11 @@ def test_analyze_tar_present(mock_send_head_http_raw: MagicMock, pypi_package_js pypi_package_json.get_releases.return_value = release pypi_package_json.get_latest_version.return_value = version - pypi_package_json.component.version = None + pypi_package_json.component_version = None pypi_package_json.package_json = {"info": {"name": "ttttttttest_nester"}} pypi_package_json.pypi_registry.inspector_url_scheme = "https" pypi_package_json.pypi_registry.inspector_url_netloc = "inspector.pypi.io" + mock_send_head_http_raw.return_value = MagicMock() # assume valid URL for testing purposes expected_detail_info = { @@ -126,7 +127,7 @@ def test_analyze_whl_present(mock_send_head_http_raw: MagicMock, pypi_package_js } pypi_package_json.get_releases.return_value = release - pypi_package_json.component.version = version + pypi_package_json.component_version = version pypi_package_json.package_json = {"info": {"name": "ttttttttest_nester"}} pypi_package_json.pypi_registry.inspector_url_scheme = "https" pypi_package_json.pypi_registry.inspector_url_netloc = "inspector.pypi.io" @@ -214,7 +215,7 @@ def test_analyze_both_present(mock_send_head_http_raw: MagicMock, pypi_package_j } pypi_package_json.get_releases.return_value = release - pypi_package_json.component.version = version + pypi_package_json.component_version = version pypi_package_json.package_json = {"info": {"name": "ttttttttest_nester"}} pypi_package_json.pypi_registry.inspector_url_scheme = "https" pypi_package_json.pypi_registry.inspector_url_netloc = "inspector.pypi.io" diff --git a/tests/slsa_analyzer/checks/test_detect_malicious_metadata_check.py b/tests/slsa_analyzer/checks/test_detect_malicious_metadata_check.py index ca4f17ddf..f39864dec 100644 --- a/tests/slsa_analyzer/checks/test_detect_malicious_metadata_check.py +++ b/tests/slsa_analyzer/checks/test_detect_malicious_metadata_check.py @@ -13,7 +13,6 @@ from macaron.config.defaults import load_defaults from macaron.malware_analyzer.pypi_heuristics.heuristics import HeuristicResult, Heuristics -from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool from macaron.slsa_analyzer.checks.check_result import CheckResultType from macaron.slsa_analyzer.checks.detect_malicious_metadata_check import DetectMaliciousMetadataCheck from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIRegistry @@ -26,8 +25,8 @@ @pytest.mark.parametrize( ("purl", "expected"), [ - # TODO: This check is expected to FAIL for pkg:pypi/zlibxjson. However, after introducing the wheel presence heuristic, - # a false negative has been introduced. Note that if the unit test were allowed to access the OSV + # TODO: This check is expected to FAIL for pkg:pypi/zlibxjson. However, after introducing the wheel presence + # heuristic, a false negative has been introduced. Note that if the unit test were allowed to access the OSV # knowledge base, it would report the package as malware. However, we intentionally block unit tests # from reaching the network. ("pkg:pypi/zlibxjson", CheckResultType.PASSED), @@ -36,7 +35,7 @@ ], ) def test_detect_malicious_metadata( - httpserver: HTTPServer, tmp_path: Path, pip_tool: BaseBuildTool, macaron_path: Path, purl: str, expected: str + httpserver: HTTPServer, tmp_path: Path, macaron_path: Path, purl: str, expected: str ) -> None: """Test that the check handles repositories correctly.""" check = DetectMaliciousMetadataCheck() @@ -44,7 +43,7 @@ def test_detect_malicious_metadata( # Set up the context object with PyPIRegistry instance. ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="", purl=purl) pypi_registry = PyPIRegistry() - ctx.dynamic_data["package_registries"] = [PackageRegistryInfo(pip_tool, pypi_registry)] + ctx.dynamic_data["package_registries"] = [PackageRegistryInfo("pip", "pypi", pypi_registry)] # Set up responses of PyPI endpoints using the httpserver plugin. with open(os.path.join(RESOURCE_PATH, "pypi_files", "zlibxjson.html"), encoding="utf8") as page: @@ -102,12 +101,12 @@ def test_detect_malicious_metadata( @pytest.mark.parametrize( - ("combination"), + "combination", [ pytest.param( { - # similar to rule ID malware_high_confidence_1, but SUSPICIOUS_SETUP is skipped since the file does not exist, - # so the rule should not trigger. + # similar to rule ID malware_high_confidence_1, but SUSPICIOUS_SETUP is skipped since the file does not + # exist, so the rule should not trigger. Heuristics.EMPTY_PROJECT_LINK: HeuristicResult.FAIL, Heuristics.SOURCE_CODE_REPO: HeuristicResult.SKIP, Heuristics.ONE_RELEASE: HeuristicResult.FAIL, diff --git a/tests/slsa_analyzer/checks/test_repo_verification_check.py b/tests/slsa_analyzer/checks/test_repo_verification_check.py index f0f3dd923..dcc15af43 100644 --- a/tests/slsa_analyzer/checks/test_repo_verification_check.py +++ b/tests/slsa_analyzer/checks/test_repo_verification_check.py @@ -1,4 +1,4 @@ -# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved. +# 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/. """Module to test the repository verification check.""" @@ -23,7 +23,9 @@ def test_repo_verification_pass(maven_tool: BaseBuildTool, macaron_path: Path) - ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="", purl="pkg:maven/test/test") maven_registry = MavenCentralRegistry() - ctx.dynamic_data["package_registries"] = [PackageRegistryInfo(maven_tool, maven_registry)] + ctx.dynamic_data["package_registries"] = [ + PackageRegistryInfo(maven_tool.name, maven_tool.purl_type, maven_registry) + ] ctx.dynamic_data["repo_verification"] = [ RepositoryVerificationResult( status=RepositoryVerificationStatus.PASSED, @@ -41,7 +43,9 @@ def test_repo_verification_fail(maven_tool: BaseBuildTool, macaron_path: Path) - ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="", purl="pkg:maven/test/test") maven_registry = MavenCentralRegistry() - ctx.dynamic_data["package_registries"] = [PackageRegistryInfo(maven_tool, maven_registry)] + ctx.dynamic_data["package_registries"] = [ + PackageRegistryInfo(maven_tool.name, maven_tool.purl_type, maven_registry) + ] ctx.dynamic_data["repo_verification"] = [ RepositoryVerificationResult( status=RepositoryVerificationStatus.FAILED, @@ -59,7 +63,9 @@ def test_check_unknown_for_unknown_repo_verification(maven_tool: BaseBuildTool, ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="", purl="pkg:maven/test/test") maven_registry = MavenCentralRegistry() - ctx.dynamic_data["package_registries"] = [PackageRegistryInfo(maven_tool, maven_registry)] + ctx.dynamic_data["package_registries"] = [ + PackageRegistryInfo(maven_tool.name, maven_tool.purl_type, maven_registry) + ] ctx.dynamic_data["repo_verification"] = [ RepositoryVerificationResult( status=RepositoryVerificationStatus.UNKNOWN, @@ -77,6 +83,6 @@ def test_check_unknown_for_unsupported_build_tools(pip_tool: BaseBuildTool, maca ctx = MockAnalyzeContext(macaron_path=macaron_path, output_dir="", purl="pkg:pypi/test/test") pypi_registry = PyPIRegistry() - ctx.dynamic_data["package_registries"] = [PackageRegistryInfo(pip_tool, pypi_registry)] + ctx.dynamic_data["package_registries"] = [PackageRegistryInfo(pip_tool.name, pip_tool.purl_type, pypi_registry)] assert check.run_check(ctx).result_type == CheckResultType.UNKNOWN diff --git a/tests/slsa_analyzer/package_registry/test_jfrog_maven_registry.py b/tests/slsa_analyzer/package_registry/test_jfrog_maven_registry.py index ebb960366..ef7276dcf 100644 --- a/tests/slsa_analyzer/package_registry/test_jfrog_maven_registry.py +++ b/tests/slsa_analyzer/package_registry/test_jfrog_maven_registry.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 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 ``JFrogMavenRegistry`` class.""" @@ -129,12 +129,12 @@ def test_is_detected( expected_result: bool, ) -> None: """Test the ``is_detected`` method.""" - assert jfrog_maven.is_detected(build_tool) == expected_result + assert jfrog_maven.is_detected(build_tool.name) == expected_result # The method always returns False when the jfrog_maven instance is not enabled # (in the ini config). jfrog_maven.enabled = False - assert jfrog_maven.is_detected(build_tool) is False + assert jfrog_maven.is_detected(build_tool.name) is False @pytest.mark.parametrize( diff --git a/tests/slsa_analyzer/package_registry/test_maven_central_registry.py b/tests/slsa_analyzer/package_registry/test_maven_central_registry.py index 8a0287b36..62b9fdca0 100644 --- a/tests/slsa_analyzer/package_registry/test_maven_central_registry.py +++ b/tests/slsa_analyzer/package_registry/test_maven_central_registry.py @@ -1,4 +1,4 @@ -# Copyright (c) 2023 - 2024, Oracle and/or its affiliates. All rights reserved. +# Copyright (c) 2023 - 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 Maven Central registry.""" @@ -14,7 +14,6 @@ from macaron.config.defaults import load_defaults from macaron.errors import ConfigurationError, InvalidHTTPResponseError -from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool from macaron.slsa_analyzer.package_registry.maven_central_registry import MavenCentralRegistry @@ -124,12 +123,11 @@ def test_load_defaults_with_invalid_config(tmp_path: Path, user_config_input: st ) def test_is_detected( maven_central: MavenCentralRegistry, - build_tools: dict[str, BaseBuildTool], build_tool_name: str, expected_result: bool, ) -> None: """Test the ``is_detected`` method.""" - assert maven_central.is_detected(build_tools[build_tool_name]) == expected_result + assert maven_central.is_detected(build_tool_name) == expected_result @pytest.mark.parametrize( diff --git a/tests/slsa_analyzer/package_registry/test_npm_registry.py b/tests/slsa_analyzer/package_registry/test_npm_registry.py index a6cadb4ba..a180ea78b 100644 --- a/tests/slsa_analyzer/package_registry/test_npm_registry.py +++ b/tests/slsa_analyzer/package_registry/test_npm_registry.py @@ -13,7 +13,6 @@ from macaron.config.defaults import load_defaults from macaron.errors import ConfigurationError, InvalidHTTPResponseError -from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool from macaron.slsa_analyzer.build_tool.npm import NPM from macaron.slsa_analyzer.package_registry.npm_registry import NPMAttestationAsset, NPMRegistry @@ -45,7 +44,7 @@ def test_disable_npm_registry(npm_registry: NPMRegistry, tmp_path: Path, npm_too npm_registry.load_defaults() assert npm_registry.enabled is False - assert npm_registry.is_detected(build_tool=npm_tool) is False + assert npm_registry.is_detected(npm_tool.name) is False @pytest.mark.parametrize( @@ -87,12 +86,10 @@ def test_npm_registry_invalid_config(npm_registry: NPMRegistry, tmp_path: Path, ("maven", False), ], ) -def test_is_detected( - npm_registry: NPMRegistry, build_tools: dict[str, BaseBuildTool], build_tool_name: str, expected: bool -) -> None: +def test_is_detected(npm_registry: NPMRegistry, build_tool_name: str, expected: bool) -> None: """Test that the registry is correctly detected for a build tool.""" npm_registry.load_defaults() - assert npm_registry.is_detected(build_tool=build_tools[build_tool_name]) == expected + assert npm_registry.is_detected(build_tool_name) == expected @pytest.mark.parametrize(