diff --git a/docs/source/pages/developers_guide/apidoc/macaron.parsers.rst b/docs/source/pages/developers_guide/apidoc/macaron.parsers.rst
index 47423e1af..833df895a 100644
--- a/docs/source/pages/developers_guide/apidoc/macaron.parsers.rst
+++ b/docs/source/pages/developers_guide/apidoc/macaron.parsers.rst
@@ -40,3 +40,11 @@ macaron.parsers.github\_workflow\_model module
:members:
:undoc-members:
:show-inheritance:
+
+macaron.parsers.pomparser module
+--------------------------------
+
+.. automodule:: macaron.parsers.pomparser
+ :members:
+ :undoc-members:
+ :show-inheritance:
diff --git a/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst b/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst
index 799681551..f17659570 100644
--- a/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst
+++ b/docs/source/pages/developers_guide/apidoc/macaron.repo_finder.rst
@@ -57,6 +57,14 @@ macaron.repo\_finder.repo\_finder\_deps\_dev module
:undoc-members:
:show-inheritance:
+macaron.repo\_finder.repo\_finder\_enums module
+-----------------------------------------------
+
+.. automodule:: macaron.repo_finder.repo_finder_enums
+ :members:
+ :undoc-members:
+ :show-inheritance:
+
macaron.repo\_finder.repo\_finder\_java module
----------------------------------------------
diff --git a/src/macaron/database/table_definitions.py b/src/macaron/database/table_definitions.py
index 61c90da2e..8cfc3c38c 100644
--- a/src/macaron/database/table_definitions.py
+++ b/src/macaron/database/table_definitions.py
@@ -36,6 +36,7 @@
from macaron.database.database_manager import ORMBase
from macaron.database.db_custom_types import RFC3339DateTime
from macaron.errors import InvalidPURLError
+from macaron.repo_finder.repo_finder_enums import CommitFinderOutcome, RepoFinderOutcome
from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, ProvenanceSubjectPURLMatcher
from macaron.slsa_analyzer.slsa_req import ReqName
@@ -177,7 +178,14 @@ class Component(PackageURLMixin, ORMBase):
lazy="immediate",
)
- def __init__(self, purl: str, analysis: Analysis, repository: "Repository | None"):
+ #: The one-to-one relationship with Repo Finder metadata.
+ repo_finder_metadata: Mapped["RepoFinderMetadata"] = relationship(
+ uselist=False, back_populates="component", lazy="immediate"
+ )
+
+ def __init__(
+ self, purl: str, analysis: Analysis, repository: "Repository | None", repo_finder_metadata: "RepoFinderMetadata"
+ ):
"""
Instantiate the software component using PURL identifier.
@@ -204,7 +212,13 @@ def __init__(self, purl: str, analysis: Analysis, repository: "Repository | None
# TODO: Explore the ``dbm`` or ``shelve`` packages to support dict type, which are part of the Python standard library.
purl_kwargs = purl_parts.to_dict(encode=True)
- super().__init__(purl=purl, analysis=analysis, repository=repository, **purl_kwargs)
+ super().__init__(
+ purl=purl,
+ analysis=analysis,
+ repository=repository,
+ repo_finder_metadata=repo_finder_metadata,
+ **purl_kwargs,
+ )
@property
def report_file_name(self) -> str:
@@ -605,3 +619,34 @@ def from_purl_and_provenance(
return cls(sha256=sha256)
return None
+
+
+class RepoFinderMetadata(ORMBase):
+ """Metadata from the Repo Finder and Commit Finder runs for an associated Component."""
+
+ __tablename__ = "_repo_finder_metadata"
+
+ #: The primary key.
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) # noqa: A003
+
+ #: The foreign key to the software component.
+ component_id: Mapped[int] = mapped_column(Integer, ForeignKey(Component.id), nullable=False)
+
+ #: A one-to-one relationship with software components.
+ component: Mapped["Component"] = relationship(back_populates="repo_finder_metadata")
+
+ #: The outcome of the Repo Finder.
+ repo_finder_outcome: Mapped[Enum] = mapped_column(
+ Enum(RepoFinderOutcome), nullable=False # pylint: disable=protected-access,no-member
+ )
+
+ #: The outcome of the Commit Finder.
+ commit_finder_outcome: Mapped[Enum] = mapped_column(
+ Enum(CommitFinderOutcome), nullable=False # pylint: disable=protected-access,no-member
+ )
+
+ #: The URL found by the Repo Finder (if applicable).
+ found_url: Mapped[str] = mapped_column(String)
+
+ #: The commit of the tag matched by the Commit Finder.
+ found_commit: Mapped[str] = mapped_column(String)
diff --git a/src/macaron/dependency_analyzer/cyclonedx.py b/src/macaron/dependency_analyzer/cyclonedx.py
index 80deddf14..dd671fa76 100644
--- a/src/macaron/dependency_analyzer/cyclonedx.py
+++ b/src/macaron/dependency_analyzer/cyclonedx.py
@@ -30,6 +30,7 @@
from macaron.errors import CycloneDXParserError, DependencyAnalyzerError
from macaron.output_reporter.scm import SCMStatus
from macaron.repo_finder.repo_finder import find_repo
+from macaron.repo_finder.repo_finder_enums import RepoFinderOutcome
from macaron.repo_finder.repo_validator import find_valid_repository_url
logger: logging.Logger = logging.getLogger(__name__)
@@ -465,12 +466,12 @@ def _resolve_more_dependencies(dependencies: dict[str, DependencyInfo]) -> None:
for item in dependencies.values():
if item["available"] != SCMStatus.MISSING_SCM:
continue
-
- item["url"] = find_repo(item["purl"])
- if item["url"] == "":
+ url, outcome = find_repo(item["purl"])
+ if outcome not in {RepoFinderOutcome.FOUND, RepoFinderOutcome.FOUND_FROM_PARENT}:
logger.debug("Failed to find url for purl: %s", item["purl"])
else:
# TODO decide how to handle possible duplicates here
+ item["url"] = url
item["available"] = SCMStatus.AVAILABLE
item["note"] = ""
diff --git a/src/macaron/repo_finder/commit_finder.py b/src/macaron/repo_finder/commit_finder.py
index 230bec9ee..9db951e3e 100644
--- a/src/macaron/repo_finder/commit_finder.py
+++ b/src/macaron/repo_finder/commit_finder.py
@@ -14,6 +14,7 @@
from macaron.repo_finder import repo_finder_deps_dev
from macaron.repo_finder.repo_finder import to_domain_from_known_purl_types
+from macaron.repo_finder.repo_finder_enums import CommitFinderOutcome
from macaron.slsa_analyzer.git_service import GIT_SERVICES
logger: logging.Logger = logging.getLogger(__name__)
@@ -122,7 +123,7 @@ class AbstractPurlType(Enum):
UNSUPPORTED = (2,)
-def find_commit(git_obj: Git, purl: PackageURL) -> str | None:
+def find_commit(git_obj: Git, purl: PackageURL) -> tuple[str | None, CommitFinderOutcome]:
"""Try to find the commit matching the passed PURL.
The PURL may be a repository type, e.g. GitHub, in which case the commit might be in its version part.
@@ -138,13 +139,13 @@ def find_commit(git_obj: Git, purl: PackageURL) -> str | None:
Returns
-------
- str | None
- The digest, or None if the commit cannot be correctly retrieved.
+ tuple[str | None, CommitFinderOutcome]
+ The digest, or None if the commit cannot be correctly retrieved, and the outcome to report.
"""
version = purl.version
if not version:
logger.debug("Missing version for analysis target: %s", purl.name)
- return None
+ return None, CommitFinderOutcome.NO_VERSION_PROVIDED
repo_type = determine_abstract_purl_type(purl)
if repo_type == AbstractPurlType.REPOSITORY:
@@ -152,7 +153,7 @@ def find_commit(git_obj: Git, purl: PackageURL) -> str | None:
if repo_type == AbstractPurlType.ARTIFACT:
return find_commit_from_version_and_name(git_obj, purl.name, version)
logger.debug("Type of PURL is not supported for commit finding: %s", purl.type)
- return None
+ return None, CommitFinderOutcome.UNSUPPORTED_PURL_TYPE
def determine_abstract_purl_type(purl: PackageURL) -> AbstractPurlType:
@@ -182,7 +183,7 @@ def determine_abstract_purl_type(purl: PackageURL) -> AbstractPurlType:
return AbstractPurlType.UNSUPPORTED
-def extract_commit_from_version(git_obj: Git, version: str) -> str | None:
+def extract_commit_from_version(git_obj: Git, version: str) -> tuple[str | None, CommitFinderOutcome]:
"""Try to extract the commit from the PURL's version parameter.
E.g.
@@ -198,8 +199,8 @@ def extract_commit_from_version(git_obj: Git, version: str) -> str | None:
Returns
-------
- str | None
- The digest, or None if the commit cannot be correctly retrieved.
+ tuple[str | None, CommitFinderOutcome]
+ The digest, or None if the commit cannot be correctly retrieved, and the outcome to report.
"""
# A commit hash is 40 characters in length, but commits are often referenced using only some of those.
commit: Commit | None = None
@@ -219,12 +220,12 @@ def extract_commit_from_version(git_obj: Git, version: str) -> str | None:
logger.debug("Failed to retrieve commit: %s", error)
if not commit:
- return None
+ return None, CommitFinderOutcome.REPO_PURL_FAILURE
- return commit.hash if commit else None
+ return commit.hash if commit else None, CommitFinderOutcome.MATCHED
-def find_commit_from_version_and_name(git_obj: Git, name: str, version: str) -> str | None:
+def find_commit_from_version_and_name(git_obj: Git, name: str, version: str) -> tuple[str | None, CommitFinderOutcome]:
"""Try to find the matching commit in a repository of a given version (and name) via tags.
The passed version is used to match with the tags in the target repository. The passed name is used in cases where
@@ -241,14 +242,19 @@ def find_commit_from_version_and_name(git_obj: Git, name: str, version: str) ->
Returns
-------
- str | None
- The digest, or None if the commit cannot be correctly retrieved.
+ tuple[str | None, CommitFinderOutcome]
+ The digest, or None if the commit cannot be correctly retrieved, and the outcome to report.
"""
logger.debug("Searching for commit of artifact version using tags: %s@%s", name, version)
# Only consider tags that have a commit.
+ repo_tags = git_obj.repo.tags
+ if not repo_tags:
+ logger.debug("No tags found for %s", name)
+ return None, CommitFinderOutcome.NO_TAGS
+
valid_tags = {}
- for tag in git_obj.repo.tags:
+ for tag in repo_tags:
commit = _get_tag_commit(tag)
if not commit:
logger.debug("No commit found for tag: %s", tag)
@@ -259,14 +265,14 @@ def find_commit_from_version_and_name(git_obj: Git, name: str, version: str) ->
if not valid_tags:
logger.debug("No tags with commits found for %s", name)
- return None
+ return None, CommitFinderOutcome.NO_TAGS_WITH_COMMITS
# Match tags.
- matched_tags = match_tags(list(valid_tags.keys()), name, version)
+ matched_tags, outcome = match_tags(list(valid_tags.keys()), name, version)
if not matched_tags:
logger.debug("No tags matched for %s", name)
- return None
+ return None, outcome
if len(matched_tags) > 1:
logger.debug("Tags found for %s: %s", name, len(matched_tags))
@@ -283,7 +289,7 @@ def find_commit_from_version_and_name(git_obj: Git, name: str, version: str) ->
hexsha = tag.commit.hexsha
except ValueError:
logger.debug("Error trying to retrieve digest of commit: %s", tag.commit)
- return None
+ return None, CommitFinderOutcome.NO_TAG_COMMIT
logger.debug(
"Found tag %s with commit %s for artifact version %s@%s",
@@ -292,7 +298,7 @@ def find_commit_from_version_and_name(git_obj: Git, name: str, version: str) ->
name,
version,
)
- return hexsha if hexsha else None
+ return hexsha if hexsha else None, CommitFinderOutcome.MATCHED
def _split_name(name: str) -> list[str]:
@@ -350,7 +356,7 @@ def _split_separators(version: str) -> list[str]:
return [item for item in split if item]
-def _build_version_pattern(name: str, version: str) -> tuple[Pattern | None, list[str]]:
+def _build_version_pattern(name: str, version: str) -> tuple[Pattern | None, list[str], CommitFinderOutcome]:
"""Build a version pattern to match the passed version string.
Parameters
@@ -363,12 +369,12 @@ def _build_version_pattern(name: str, version: str) -> tuple[Pattern | None, lis
Returns
-------
tuple[Pattern | None, list[str]]
- The tuple of the regex pattern that will match the version, and the list of version parts that were extracted.
- If an exception occurs from any regex operation, the pattern will be returned as None.
+ The tuple of the regex pattern that will match the version, the list of version parts that were extracted, and
+ the outcome to report. If an exception occurs from any regex operation, the pattern will be returned as None.
"""
if not version:
- return None, []
+ return None, [], CommitFinderOutcome.NO_VERSION_PROVIDED
# Escape input to prevent it being treated as regex.
name = re.escape(name)
@@ -377,7 +383,7 @@ def _build_version_pattern(name: str, version: str) -> tuple[Pattern | None, lis
if not parts:
logger.debug("Version contained no valid parts: %s", version)
- return None, []
+ return None, [], CommitFinderOutcome.INVALID_PURL
logger.debug("Final version parts: %s", parts)
@@ -471,14 +477,14 @@ def _build_version_pattern(name: str, version: str) -> tuple[Pattern | None, lis
# Compile the pattern.
try:
- return re.compile(this_version_pattern, flags=re.IGNORECASE), parts
+ return re.compile(this_version_pattern, flags=re.IGNORECASE), parts, CommitFinderOutcome.MATCHED
except Exception as error: # pylint: disable=broad-exception-caught
# The regex library uses an internal error that cannot be used here to satisfy pylint.
logger.debug("Error while compiling version regex: %s", error)
- return None, []
+ return None, [], CommitFinderOutcome.REGEX_COMPILE_FAILURE
-def match_tags(tag_list: list[str], name: str, version: str) -> list[str]:
+def match_tags(tag_list: list[str], name: str, version: str) -> tuple[list[str], CommitFinderOutcome]:
"""Return items of the passed tag list that match the passed artifact name and version.
Parameters
@@ -492,8 +498,8 @@ def match_tags(tag_list: list[str], name: str, version: str) -> list[str]:
Returns
-------
- list[str]
- The list of tags that matched the pattern.
+ tuple[list[str], CommitFinderOutcome]
+ The list of tags that matched the pattern, if any, and the outcome to report.
"""
logger.debug("Tag Sample: %s", tag_list[:5])
@@ -519,14 +525,14 @@ def match_tags(tag_list: list[str], name: str, version: str) -> list[str]:
if match.group(1):
prefix_match = tag
if prefix_match:
- return [prefix_match]
+ return [prefix_match], CommitFinderOutcome.MATCHED
if last_match:
- return [last_match]
+ return [last_match], CommitFinderOutcome.MATCHED
# Create the more complicated pattern for the passed version.
- pattern, parts = _build_version_pattern(name, version)
+ pattern, parts, outcome = _build_version_pattern(name, version)
if not pattern:
- return []
+ return [], outcome
# Match the tags.
matched_tags = []
@@ -547,8 +553,12 @@ def match_tags(tag_list: list[str], name: str, version: str) -> list[str]:
matched_tags = _fix_misaligned_tag_matches(matched_tags, version)
- if len(matched_tags) <= 1:
- return [_["tag"] for _ in matched_tags]
+ if not matched_tags:
+ logger.debug("Failed to match any tags.")
+ return [], CommitFinderOutcome.NO_TAGS_MATCHED
+
+ if len(matched_tags) == 1:
+ return [_["tag"] for _ in matched_tags], CommitFinderOutcome.MATCHED
# In the case of multiple matches, further work must be done.
@@ -589,7 +599,7 @@ def match_tags(tag_list: list[str], name: str, version: str) -> list[str]:
)
)
- return [_["tag"] for _ in matched_tags]
+ return [_["tag"] for _ in matched_tags], CommitFinderOutcome.MATCHED
def _fix_misaligned_tag_matches(matched_tags: list[dict[str, str]], version: str) -> list[dict[str, str]]:
diff --git a/src/macaron/repo_finder/provenance_extractor.py b/src/macaron/repo_finder/provenance_extractor.py
index 5c3307c58..5a6e2721b 100644
--- a/src/macaron/repo_finder/provenance_extractor.py
+++ b/src/macaron/repo_finder/provenance_extractor.py
@@ -320,7 +320,7 @@ def check_if_input_purl_provenance_conflict(
# Check the PURL commit against the provenance.
if not digest_input and provenance_commit_digest and purl.version:
- purl_commit = extract_commit_from_version(git_obj, purl.version)
+ purl_commit, _ = extract_commit_from_version(git_obj, purl.version)
if purl_commit and purl_commit != provenance_commit_digest:
logger.debug(
"The commit digest passed via purl input does not match what exists in the "
diff --git a/src/macaron/repo_finder/repo_finder.py b/src/macaron/repo_finder/repo_finder.py
index 2a3c779a3..fd5689c01 100644
--- a/src/macaron/repo_finder/repo_finder.py
+++ b/src/macaron/repo_finder/repo_finder.py
@@ -41,12 +41,13 @@
from macaron.config.defaults import defaults
from macaron.repo_finder.repo_finder_base import BaseRepoFinder
from macaron.repo_finder.repo_finder_deps_dev import DepsDevRepoFinder
+from macaron.repo_finder.repo_finder_enums import RepoFinderOutcome
from macaron.repo_finder.repo_finder_java import JavaRepoFinder
logger: logging.Logger = logging.getLogger(__name__)
-def find_repo(purl: PackageURL) -> str:
+def find_repo(purl: PackageURL) -> tuple[str, RepoFinderOutcome]:
"""Retrieve the repository URL that matches the given PURL.
Parameters
@@ -56,8 +57,8 @@ def find_repo(purl: PackageURL) -> str:
Returns
-------
- str :
- The repository URL found for the passed package.
+ tuple[str, RepoFinderOutcome] :
+ The repository URL for the passed package, if found, and the outcome to report.
"""
repo_finder: BaseRepoFinder
if purl.type == "maven":
@@ -71,7 +72,7 @@ def find_repo(purl: PackageURL) -> str:
repo_finder = DepsDevRepoFinder()
else:
logger.debug("No Repo Finder found for package type: %s of %s", purl.type, purl)
- return ""
+ return "", RepoFinderOutcome.UNSUPPORTED_PACKAGE_TYPE
# Call Repo Finder and return first valid URL
logger.debug("Analyzing %s with Repo Finder: %s", purl, type(repo_finder))
@@ -129,9 +130,8 @@ def to_repo_path(purl: PackageURL, available_domains: list[str]) -> str | None:
"""
domain = to_domain_from_known_purl_types(purl.type) or (purl.type if purl.type in available_domains else None)
if not domain:
- logger.info("The PURL type of %s is not valid as a repository type. Trying to find the repository...", purl)
- # Try to find the repository
- return find_repo(purl)
+ logger.info("The PURL type of %s is not valid as a repository type.", purl)
+ return None
if not purl.namespace:
logger.error("Expecting a non-empty namespace from %s.", purl)
diff --git a/src/macaron/repo_finder/repo_finder_base.py b/src/macaron/repo_finder/repo_finder_base.py
index ba177c89f..1e82aa475 100644
--- a/src/macaron/repo_finder/repo_finder_base.py
+++ b/src/macaron/repo_finder/repo_finder_base.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 - 2023, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2023 - 2024, 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 base class for the repo finders."""
@@ -7,12 +7,14 @@
from packageurl import PackageURL
+from macaron.repo_finder.repo_finder_enums import RepoFinderOutcome
+
class BaseRepoFinder(ABC):
"""This abstract class is used to represent Repository Finders."""
@abstractmethod
- def find_repo(self, purl: PackageURL) -> str:
+ def find_repo(self, purl: PackageURL) -> tuple[str, RepoFinderOutcome]:
"""
Generate iterator from _find_repo that attempts to retrieve a repository URL that matches the passed artifact.
@@ -23,6 +25,6 @@ def find_repo(self, purl: PackageURL) -> str:
Returns
-------
- str :
- The URL of the found repository.
+ tuple[str, RepoFinderOutcome] :
+ A tuple of the found URL (or an empty string), and the outcome of the Repo Finder.
"""
diff --git a/src/macaron/repo_finder/repo_finder_deps_dev.py b/src/macaron/repo_finder/repo_finder_deps_dev.py
index 7f2266051..7af0d7eac 100644
--- a/src/macaron/repo_finder/repo_finder_deps_dev.py
+++ b/src/macaron/repo_finder/repo_finder_deps_dev.py
@@ -4,6 +4,7 @@
"""This module contains the PythonRepoFinderDD class to be used for finding repositories using deps.dev."""
import json
import logging
+import urllib.parse
from enum import StrEnum
from urllib.parse import quote as encode
@@ -11,6 +12,7 @@
from macaron.json_tools import json_extract
from macaron.repo_finder.repo_finder_base import BaseRepoFinder
+from macaron.repo_finder.repo_finder_enums import RepoFinderOutcome
from macaron.repo_finder.repo_validator import find_valid_repository_url
from macaron.util import send_get_http_raw
@@ -34,7 +36,7 @@ class DepsDevType(StrEnum):
class DepsDevRepoFinder(BaseRepoFinder):
"""This class is used to find repositories using Google's Open Source Insights A.K.A. deps.dev."""
- def find_repo(self, purl: PackageURL) -> str:
+ def find_repo(self, purl: PackageURL) -> tuple[str, RepoFinderOutcome]:
"""
Attempt to retrieve a repository URL that matches the passed artifact.
@@ -45,33 +47,33 @@ def find_repo(self, purl: PackageURL) -> str:
Returns
-------
- str :
- The URL of the found repository.
+ tuple[str, RepoFinderOutcome] :
+ A tuple of the found URL (or an empty string), and the outcome of the Repo Finder.
"""
- request_urls = self._create_urls(purl)
+ request_urls, outcome = self._create_urls(purl)
if not request_urls:
logger.debug("No urls found for: %s", purl)
- return ""
+ return "", outcome
json_data = self._retrieve_json(request_urls[0])
if not json_data:
logger.debug("Failed to retrieve json data for: %s", purl)
- return ""
+ return "", RepoFinderOutcome.DDEV_JSON_FETCH_ERROR
- urls = self._read_json(json_data)
+ urls, outcome = self._read_json(json_data)
if not urls:
logger.debug("Failed to extract repository URLs from json data: %s", purl)
- return ""
+ return "", outcome
logger.debug("Found %s urls: %s", len(urls), urls)
url = find_valid_repository_url(urls)
if url:
logger.debug("Found valid url: %s", url)
- return url
+ return url, RepoFinderOutcome.FOUND
- return ""
+ return "", RepoFinderOutcome.DDEV_NO_URLS
- def _create_urls(self, purl: PackageURL) -> list[str]:
+ def _create_urls(self, purl: PackageURL) -> tuple[list[str], RepoFinderOutcome]:
"""
Create the urls to search for the metadata relating to the passed artifact.
@@ -84,40 +86,45 @@ def _create_urls(self, purl: PackageURL) -> list[str]:
Returns
-------
- list[str]
- The list of created URLs.
+ tuple[list[str], RepoFinderOutcome]
+ The list of created URLs, if any, and the outcome to report.
"""
# See https://docs.deps.dev/api/v3alpha/
- base_url = f"https://api.deps.dev/v3alpha/purl/{encode(str(purl)).replace('/', '%2F')}"
- if not base_url:
- return []
+ base_url = urllib.parse.ParseResult(
+ scheme="https",
+ netloc="api.deps.dev",
+ path="/".join(["v3alpha", "purl", encode(str(purl)).replace("/", "%2F")]),
+ params="",
+ query="",
+ fragment="",
+ ).geturl()
if purl.version:
- return [base_url]
+ return [base_url], RepoFinderOutcome.FOUND
# Find the latest version.
response = send_get_http_raw(base_url, {})
if not response:
- return []
+ return [], RepoFinderOutcome.DDEV_BAD_RESPONSE
try:
metadata: dict = json.loads(response.text)
except ValueError as error:
logger.debug("Failed to parse response from deps.dev: %s", error)
- return []
+ return [], RepoFinderOutcome.DDEV_JSON_FETCH_ERROR
versions_keys = ["package", "versions"] if "package" in metadata else ["version"]
versions = json_extract(metadata, versions_keys, list)
if not versions:
- return []
+ return [], RepoFinderOutcome.DDEV_JSON_INVALID
latest_version = json_extract(versions[-1], ["versionKey", "version"], str)
if not latest_version:
- return []
+ return [], RepoFinderOutcome.DDEV_JSON_INVALID
logger.debug("Found latest version: %s", latest_version)
- return [f"{base_url}%40{latest_version}"]
+ return [f"{base_url}%40{latest_version}"], RepoFinderOutcome.FOUND
def _retrieve_json(self, url: str) -> str:
"""
@@ -140,7 +147,7 @@ def _retrieve_json(self, url: str) -> str:
return response.text
- def _read_json(self, json_data: str) -> list[str]:
+ def _read_json(self, json_data: str) -> tuple[list[str], RepoFinderOutcome]:
"""
Parse the deps.dev json file and extract the repository links.
@@ -151,20 +158,20 @@ def _read_json(self, json_data: str) -> list[str]:
Returns
-------
- list[str] :
- The extracted contents as a list of strings.
+ tuple[list[str], RepoFinderOutcome] :
+ The extracted contents as a list, and the outcome to report.
"""
try:
parsed = json.loads(json_data)
except ValueError as error:
logger.debug("Failed to parse response from deps.dev: %s", error)
- return []
+ return [], RepoFinderOutcome.DDEV_JSON_FETCH_ERROR
links_keys = ["version", "links"] if "version" in parsed else ["links"]
links = json_extract(parsed, links_keys, list)
if not links:
logger.debug("Could not extract 'version' or 'links' from deps.dev response.")
- return []
+ return [], RepoFinderOutcome.DDEV_JSON_INVALID
result = []
for item in links:
@@ -172,4 +179,4 @@ def _read_json(self, json_data: str) -> list[str]:
if url and isinstance(url, str):
result.append(url)
- return result
+ return result, RepoFinderOutcome.FOUND
diff --git a/src/macaron/repo_finder/repo_finder_enums.py b/src/macaron/repo_finder/repo_finder_enums.py
new file mode 100644
index 000000000..a093a1a72
--- /dev/null
+++ b/src/macaron/repo_finder/repo_finder_enums.py
@@ -0,0 +1,66 @@
+# Copyright (c) 2024 - 2024, 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 Enums used to represent the outcome of Repo Finder or Commit Finder executions."""
+from enum import Enum
+
+
+class RepoFinderOutcome(Enum):
+ """An Enum of all outcomes of the Repo Finder being run for a software component."""
+
+ # States that relate to problems with user input.
+ NO_MAVEN_HOST_PROVIDED = "No maven host provided"
+ NO_POM_TAGS_PROVIDED = "No POM tags provided"
+ NO_VERSION_PROVIDED = "No version provided"
+ UNSUPPORTED_PACKAGE_TYPE = "Unsupported package type"
+
+ # States that relate to the target POM (Java).
+ POM_READ_ERROR = "POM read error"
+
+ # States that relate to the SCM in the POM (Java).
+ SCM_NO_URLS = "SCM no URLs"
+ SCM_NO_VALID_URLS = "SCM no valid URLs"
+
+ # States that relate to HTTP requests.
+ HTTP_INVALID = "HTTP invalid"
+ HTTP_NOT_FOUND = "HTTP not found"
+ HTTP_FORBIDDEN = "HTTP forbidden"
+ HTTP_OTHER = "HTTP other"
+
+ # States that relate to deps.dev (Non-Java).
+ DDEV_BAD_RESPONSE = "deps.dev bad response"
+ DDEV_JSON_FETCH_ERROR = "deps.dev fetch error"
+ DDEV_JSON_INVALID = "deps.dev JSON invalid"
+ DDEV_NO_URLS = "deps.dev no URLs"
+
+ # Success states.
+ FOUND = "Found"
+ FOUND_FROM_PARENT = "Found from parent"
+
+ # Default state.
+ NOT_USED = "Not used"
+
+
+class CommitFinderOutcome(Enum):
+ """An Enum of all outcomes of the Commit Finder being run for a software component."""
+
+ # States that relate to problems with user input.
+ NO_VERSION_PROVIDED = "No version provided"
+ UNSUPPORTED_PURL_TYPE = "Unsupported PURL type"
+
+ # States that relate to repository type PURLs.
+ REPO_PURL_FAILURE = "Repository PURL failure"
+
+ # States that relate to artifact type PURLs.
+ NO_TAGS = "No tags"
+ NO_TAGS_WITH_COMMITS = "No tags with commits"
+ NO_TAG_COMMIT = "No tag commit"
+ INVALID_PURL = "No valid parts"
+ REGEX_COMPILE_FAILURE = "Regex compile failure"
+ NO_TAGS_MATCHED = "No tags matched"
+
+ # Success state.
+ MATCHED = "Matched"
+
+ # Default state.
+ NOT_USED = "Not used"
diff --git a/src/macaron/repo_finder/repo_finder_java.py b/src/macaron/repo_finder/repo_finder_java.py
index 148c03e1b..4baf74c8e 100644
--- a/src/macaron/repo_finder/repo_finder_java.py
+++ b/src/macaron/repo_finder/repo_finder_java.py
@@ -4,6 +4,7 @@
"""This module contains the JavaRepoFinder class to be used for finding Java repositories."""
import logging
import re
+import urllib.parse
from xml.etree.ElementTree import Element # nosec
from packageurl import PackageURL
@@ -11,6 +12,7 @@
from macaron.config.defaults import defaults
from macaron.parsers.pomparser import parse_pom_string
from macaron.repo_finder.repo_finder_base import BaseRepoFinder
+from macaron.repo_finder.repo_finder_enums import RepoFinderOutcome
from macaron.repo_finder.repo_validator import find_valid_repository_url
from macaron.util import send_get_http_raw
@@ -24,7 +26,7 @@ def __init__(self) -> None:
"""Initialise the Java repository finder instance."""
self.pom_element: Element | None = None
- def find_repo(self, purl: PackageURL) -> str:
+ def find_repo(self, purl: PackageURL) -> tuple[str, RepoFinderOutcome]:
"""
Attempt to retrieve a repository URL that matches the passed artifact.
@@ -35,47 +37,57 @@ def find_repo(self, purl: PackageURL) -> str:
Yields
------
- str :
- The URL of the found repository.
+ tuple[str, RepoFinderOutcome] :
+ A tuple of the found URL (or an empty string), and the outcome of the Repo Finder.
"""
- # Perform the following in a loop:
- # - Create URLs for the current artifact POM
- # - Parse the POM
- # - Try to extract SCM metadata and return URLs
- # - Try to extract parent information and change current artifact to it
- # - Repeat
+ # Check POM tags exist.
+ tags = defaults.get_list("repofinder.java", "repo_pom_paths")
+ if not tags:
+ logger.debug("No POM tags found for URL discovery.")
+ return "", RepoFinderOutcome.NO_POM_TAGS_PROVIDED
+
group = purl.namespace or ""
artifact = purl.name
version = purl.version or ""
- limit = defaults.getint("repofinder.java", "parent_limit", fallback=10)
if not version:
logger.debug("Version missing for maven artifact: %s:%s", group, artifact)
# TODO add support for Java artifacts without a version
- return ""
+ return "", RepoFinderOutcome.NO_VERSION_PROVIDED
+ # Perform the following in a loop:
+ # - Create URLs for the current artifact POM
+ # - Parse the POM
+ # - Try to extract SCM metadata and return URLs
+ # - Try to extract parent information and change current artifact to it
+ # - Repeat
+ limit = defaults.getint("repofinder.java", "parent_limit", fallback=10)
+ initial_limit = limit
+ last_outcome = RepoFinderOutcome.FOUND
+ check_parents = defaults.getboolean("repofinder.java", "find_parents")
while group and artifact and version and limit > 0:
- # Create the URLs for retrieving the artifact's POM
+ # Create the URLs for retrieving the artifact's POM.
group = group.replace(".", "/")
request_urls = self._create_urls(group, artifact, version)
if not request_urls:
- # Abort if no URLs were created
+ # Abort if no URLs were created.
logger.debug("Failed to create request URLs for %s:%s:%s", group, artifact, version)
- return ""
+ return "", RepoFinderOutcome.NO_MAVEN_HOST_PROVIDED
- # Try each POM URL in order, terminating early if a match is found
+ # Try each POM URL in order, terminating early if a match is found.
pom = ""
+ pom_outcome = RepoFinderOutcome.FOUND
for request_url in request_urls:
- pom = self._retrieve_pom(request_url)
+ pom, pom_outcome = self._retrieve_pom(request_url)
if pom != "":
break
if pom == "":
- # Abort if no POM was found
+ # Abort if no POM was found.
logger.debug("No POM found for %s:%s:%s", group, artifact, version)
- return ""
+ return "", pom_outcome
- urls = self._read_pom(pom)
+ urls, read_outcome = self._read_pom(pom, tags)
if urls:
# If the found URLs fail to validate, finding can continue on to the next parent POM
@@ -83,18 +95,25 @@ def find_repo(self, purl: PackageURL) -> str:
url = find_valid_repository_url(urls)
if url:
logger.debug("Found valid url: %s", url)
- return url
+ return url, (
+ RepoFinderOutcome.FOUND if initial_limit == limit else RepoFinderOutcome.FOUND_FROM_PARENT
+ )
+
+ # No valid URLs were found from this POM.
+ last_outcome = RepoFinderOutcome.SCM_NO_VALID_URLS
+ else:
+ last_outcome = read_outcome
- if defaults.getboolean("repofinder.java", "find_parents") and self.pom_element is not None:
- # Attempt to extract parent information from POM
+ if check_parents and self.pom_element is not None:
+ # Attempt to extract parent information from POM.
group, artifact, version = self._find_parent(self.pom_element)
else:
break
limit = limit - 1
- # Nothing found
- return ""
+ # Nothing found.
+ return "", last_outcome
def _create_urls(self, group: str, artifact: str, version: str) -> list[str]:
"""
@@ -121,10 +140,22 @@ def _create_urls(self, group: str, artifact: str, version: str) -> list[str]:
)
urls = []
for repo in repositories:
- urls.append(f"{repo}/{group}/{artifact}/{version}/{artifact}-{version}.pom")
+ repo_url = urllib.parse.urlparse(repo)
+ pom_url = urllib.parse.ParseResult(
+ scheme=repo_url.scheme,
+ netloc=repo_url.netloc,
+ path=(
+ ((repo_url.path + "/") if repo_url.path else "")
+ + "/".join([group, artifact, version, f"{artifact}-{version}.pom"])
+ ),
+ params="",
+ query="",
+ fragment="",
+ ).geturl()
+ urls.append(pom_url)
return urls
- def _retrieve_pom(self, url: str) -> str:
+ def _retrieve_pom(self, url: str) -> tuple[str, RepoFinderOutcome]:
"""
Attempt to retrieve the file located at the passed URL.
@@ -135,18 +166,26 @@ def _retrieve_pom(self, url: str) -> str:
Returns
-------
- str :
- The retrieved file data or an empty string.
+ tuple[str, RepoFinderOutcome] :
+ The retrieved file data or an empty string, and the outcome to report.
"""
- response = send_get_http_raw(url, {})
+ response = send_get_http_raw(url, always_return_response=True)
if not response:
- return ""
+ return "", RepoFinderOutcome.HTTP_INVALID
+
+ if response.status_code == 404:
+ return "", RepoFinderOutcome.HTTP_NOT_FOUND
+ if response.status_code == 403:
+ return "", RepoFinderOutcome.HTTP_FORBIDDEN
+ if response.status_code != 200:
+ logger.debug("Failed to retrieve POM: HTTP %s", response.status_code)
+ return "", RepoFinderOutcome.HTTP_OTHER
logger.debug("Found artifact POM at: %s", url)
- return response.text
+ return response.text, RepoFinderOutcome.FOUND
- def _read_pom(self, pom: str) -> list[str]:
+ def _read_pom(self, pom: str, tags: list[str]) -> tuple[list[str], RepoFinderOutcome]:
"""
Parse the passed pom and extract the relevant tags.
@@ -157,23 +196,18 @@ def _read_pom(self, pom: str) -> list[str]:
Returns
-------
- list[str] :
- The extracted contents as a list of strings.
+ tuple[list[str], RepoFinderOutcome] :
+ A tuple of the found URLs, or an empty list, and the outcome to report.
"""
- # Retrieve tags
- tags = defaults.get_list("repofinder.java", "repo_pom_paths")
- if not any(tags):
- logger.debug("No POM tags found for URL discovery.")
- return []
-
- # Parse POM using defusedxml
+ # Parse POM using defusedxml.
pom_element = parse_pom_string(pom)
if pom_element is None:
- return []
+ return [], RepoFinderOutcome.POM_READ_ERROR
self.pom_element = pom_element
- # Attempt to extract SCM data and return URL
- return self._find_scm(pom_element, tags)
+ # Attempt to extract SCM data and return URL.
+ results = self._find_scm(pom_element, tags)
+ return results, RepoFinderOutcome.FOUND if results else RepoFinderOutcome.SCM_NO_URLS
def _find_scm(self, pom: Element, tags: list[str], resolve_properties: bool = True) -> list[str]:
"""
@@ -200,8 +234,8 @@ def _find_scm(self, pom: Element, tags: list[str], resolve_properties: bool = Tr
element: Element | None = pom
if tag.startswith("properties."):
- # Tags under properties are often "." separated
- # These can be safely split into two resulting tags as nested tags are not allowed here
+ # Tags under properties are often "." separated.
+ # These can be safely split into two resulting tags as nested tags are not allowed here.
tag_parts = ["properties", tag[11:]]
else:
# Other tags can be split into distinct elements via "."
@@ -212,10 +246,10 @@ def _find_scm(self, pom: Element, tags: list[str], resolve_properties: bool = Tr
if element is None:
break
if index == len(tag_parts) - 1 and element.text:
- # Add the contents of the final tag
+ # Add the contents of the final tag.
results.append(element.text.strip())
- # Resolve any Maven properties within the results
+ # Resolve any Maven properties within the results.
if resolve_properties:
results = self._resolve_properties(pom, results)
@@ -275,20 +309,20 @@ def _resolve_properties(self, pom: Element, values: list[str]) -> list[str]:
resolved_values = []
for value in values:
replacements: list = []
- # Calculate replacements - matches any number of ${...} entries in the current value
+ # Calculate replacements - matches any number of ${...} entries in the current value.
for match in re.finditer("\\$\\{[^}]+}", value):
text = match.group().replace("$", "").replace("{", "").replace("}", "")
if text.startswith("project."):
text = text.replace("project.", "")
else:
text = f"properties.{text}"
- # Call find_scm with property resolution flag set to False to prevent the possibility of endless looping
+ # Call find_scm with property resolution flag as False to prevent the possibility of endless looping.
result = self._find_scm(pom, [text], False)
if not result:
break
replacements.append([match.start(), result[0], match.end()])
- # Apply replacements in reverse order
+ # Apply replacements in reverse order.
# E.g.
# git@github.com:owner/project${javac.src.version}-${project.inceptionYear}.git
# ->
diff --git a/src/macaron/slsa_analyzer/analyzer.py b/src/macaron/slsa_analyzer/analyzer.py
index fa6730a36..469591710 100644
--- a/src/macaron/slsa_analyzer/analyzer.py
+++ b/src/macaron/slsa_analyzer/analyzer.py
@@ -21,7 +21,7 @@
from macaron.config.global_config import global_config
from macaron.config.target_config import Configuration
from macaron.database.database_manager import DatabaseManager, get_db_manager, get_db_session
-from macaron.database.table_definitions import Analysis, Component, ProvenanceSubject, Repository
+from macaron.database.table_definitions import Analysis, Component, ProvenanceSubject, RepoFinderMetadata, Repository
from macaron.dependency_analyzer.cyclonedx import DependencyAnalyzer, DependencyInfo
from macaron.errors import (
CloneError,
@@ -42,6 +42,7 @@
extract_repo_and_commit_from_provenance,
)
from macaron.repo_finder.provenance_finder import ProvenanceFinder, find_provenance_from_ci
+from macaron.repo_finder.repo_finder_enums import CommitFinderOutcome, RepoFinderOutcome
from macaron.slsa_analyzer import git_url
from macaron.slsa_analyzer.analyze_context import AnalyzeContext
from macaron.slsa_analyzer.asset import VirtualReleaseAsset
@@ -382,8 +383,10 @@ def run_single(
# Prepare the repo.
git_obj = None
+ commit_finder_outcome = CommitFinderOutcome.NOT_USED
+ final_digest = analysis_target.digest
if analysis_target.repo_path:
- git_obj = self._prepare_repo(
+ git_obj, commit_finder_outcome, final_digest = self._prepare_repo(
os.path.join(self.output_path, self.GIT_REPOS_DIR),
analysis_target.repo_path,
analysis_target.branch,
@@ -391,6 +394,13 @@ def run_single(
analysis_target.parsed_purl,
)
+ repo_finder_metadata = RepoFinderMetadata(
+ repo_finder_outcome=analysis_target.repo_finder_outcome,
+ commit_finder_outcome=commit_finder_outcome,
+ found_url=analysis_target.repo_path,
+ found_commit=final_digest,
+ )
+
# Check if only one of the repo or digest came from direct input.
if git_obj and (provenance_repo_url or provenance_commit_digest) and parsed_purl:
if check_if_input_purl_provenance_conflict(
@@ -414,6 +424,7 @@ def run_single(
analysis,
analysis_target,
git_obj,
+ repo_finder_metadata,
existing_records,
provenance_payload,
)
@@ -604,11 +615,15 @@ class AnalysisTarget(NamedTuple):
#: The digest of the commit to analyze.
digest: str
+ #: The outcome of the Repo Finder on this analysis target.
+ repo_finder_outcome: RepoFinderOutcome
+
def add_component(
self,
analysis: Analysis,
analysis_target: AnalysisTarget,
git_obj: Git | None,
+ repo_finder_metadata: RepoFinderMetadata,
existing_records: dict[str, Record] | None = None,
provenance_payload: InTotoPayload | None = None,
) -> Component:
@@ -625,6 +640,8 @@ def add_component(
The target of this analysis.
git_obj: Git | None
The pydriller.Git object of the repository.
+ repo_finder_metadata: RepoFinderMetadata
+ The Repo Finder metadata for this component.
existing_records : dict[str, Record] | None
The mapping of existing records that the analysis has run successfully.
provenance_payload: InTotoVPayload | None
@@ -684,6 +701,7 @@ def add_component(
purl=str(purl),
analysis=analysis,
repository=repository,
+ repo_finder_metadata=repo_finder_metadata,
)
if provenance_payload:
@@ -767,6 +785,7 @@ def to_analysis_target(
repo_path_input: str = config.get_value("path")
input_branch: str = config.get_value("branch")
input_digest: str = config.get_value("digest")
+ repo_finder_outcome = RepoFinderOutcome.NOT_USED
match (parsed_purl, repo_path_input):
case (None, ""):
@@ -787,19 +806,21 @@ def to_analysis_target(
repo_path=provenance_repo_url or "",
branch="",
digest=provenance_commit_digest or "",
+ repo_finder_outcome=repo_finder_outcome,
)
# As there is no repo or commit from provenance, use the Repo Finder to find the repo.
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.find_repo(parsed_purl)
+ repo, repo_finder_outcome = repo_finder.find_repo(parsed_purl)
return Analyzer.AnalysisTarget(
parsed_purl=parsed_purl,
repo_path=converted_repo_path or repo or "",
branch=input_branch,
digest=input_digest,
+ repo_finder_outcome=repo_finder_outcome,
)
case (_, _) | (None, _):
@@ -818,6 +839,7 @@ def to_analysis_target(
repo_path=repo_path_input,
branch=input_branch,
digest=input_digest,
+ repo_finder_outcome=repo_finder_outcome,
)
return Analyzer.AnalysisTarget(
@@ -825,6 +847,7 @@ def to_analysis_target(
repo_path=repo_path_input,
branch=input_branch,
digest=provenance_commit_digest or "",
+ repo_finder_outcome=repo_finder_outcome,
)
case _:
@@ -863,7 +886,7 @@ def _prepare_repo(
branch_name: str = "",
digest: str = "",
purl: PackageURL | None = None,
- ) -> Git | None:
+ ) -> tuple[Git | None, CommitFinderOutcome, str]:
"""Prepare the target repository for analysis.
If ``repo_path`` is a remote path, the target repo is cloned to ``{target_dir}/{unique_path}``.
@@ -889,8 +912,9 @@ def _prepare_repo(
Returns
-------
- Git | None
- The pydriller.Git object of the repository or None if error.
+ tuple[Git | None, CommitFinderOutcome, str]
+ The pydriller.Git object of the repository or None if error; the outcome of the Commit Finder; and the final
+ digest.
"""
# TODO: separate the logic for handling remote and local repos instead of putting them into this method.
logger.info(
@@ -900,15 +924,15 @@ def _prepare_repo(
digest,
)
- resolved_local_path = ""
is_remote = git_url.is_remote_repo(repo_path)
+ commit_finder_outcome = CommitFinderOutcome.NOT_USED
if is_remote:
logger.info("The path to repo %s is a remote path.", repo_path)
resolved_remote_path = git_url.get_remote_vcs_url(repo_path)
if not resolved_remote_path:
logger.error("The provided path to repo %s is not a valid remote path.", repo_path)
- return None
+ return None, commit_finder_outcome, digest
git_service = self.get_git_service(resolved_remote_path)
repo_unique_path = git_url.get_repo_dir_name(resolved_remote_path)
@@ -918,7 +942,7 @@ def _prepare_repo(
git_service.clone_repo(resolved_local_path, resolved_remote_path)
except CloneError as error:
logger.error("Cannot clone %s: %s", resolved_remote_path, str(error))
- return None
+ return None, commit_finder_outcome, digest
else:
logger.info("Checking if the path to repo %s is a local path.", repo_path)
resolved_local_path = self._resolve_local_path(self.local_repos_path, repo_path)
@@ -928,23 +952,23 @@ def _prepare_repo(
git_obj = Git(resolved_local_path)
except InvalidGitRepositoryError:
logger.error("No git repo exists at %s.", resolved_local_path)
- return None
+ return None, commit_finder_outcome, digest
else:
logger.error("Error happened while preparing the repo.")
- return None
+ return None, commit_finder_outcome, digest
if git_url.is_empty_repo(git_obj):
logger.error("The target repository does not have any commit.")
- return None
+ return None, commit_finder_outcome, digest
- # Find the digest and branch if a version has been specified
+ # Find the digest and branch if a version has been specified.
if not digest and purl and purl.version:
- found_digest = find_commit(git_obj, purl)
+ found_digest, commit_finder_outcome = find_commit(git_obj, purl)
if not found_digest:
logger.error(
"Could not map the input purl string to a specific commit in the corresponding repository."
)
- return None
+ return None, commit_finder_outcome, digest
digest = found_digest
# Checking out the specific branch or commit. This operation varies depends on the git service that the
@@ -964,18 +988,18 @@ def _prepare_repo(
# ``git_url.check_out_repo_target``.
if not git_url.check_out_repo_target(git_obj, branch_name, digest, not is_remote):
logger.error("Cannot checkout the specific branch or commit of the target repo.")
- return None
+ return None, commit_finder_outcome, digest
- return git_obj
+ return git_obj, commit_finder_outcome, digest
try:
git_service.check_out_repo(git_obj, branch_name, digest, not is_remote)
except RepoCheckOutError as error:
logger.error("Failed to check out repository at %s", resolved_local_path)
logger.error(error)
- return None
+ return None, commit_finder_outcome, digest
- return git_obj
+ return git_obj, commit_finder_outcome, digest
@staticmethod
def get_git_service(remote_path: str | None) -> BaseGitService:
diff --git a/src/macaron/util.py b/src/macaron/util.py
index 6a531084e..7e4ea973e 100644
--- a/src/macaron/util.py
+++ b/src/macaron/util.py
@@ -60,7 +60,11 @@ def send_get_http(url: str, headers: dict) -> dict:
def send_get_http_raw(
- url: str, headers: dict | None = None, timeout: int | None = None, allow_redirects: bool = True
+ url: str,
+ headers: dict | None = None,
+ timeout: int | None = None,
+ allow_redirects: bool = True,
+ always_return_response: bool = False,
) -> Response | None:
"""Send the GET HTTP request with the given url and headers.
@@ -113,7 +117,7 @@ def send_get_http_raw(
if response.status_code == 403:
check_rate_limit(response)
else:
- return None
+ return None if not always_return_response else response
retry_counter = retry_counter - 1
response = requests.get(
url=url,
diff --git a/tests/conftest.py b/tests/conftest.py
index d6b83bd78..b47aa7269 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -10,7 +10,7 @@
import macaron
from macaron.code_analyzer.call_graph import BaseNode, CallGraph
from macaron.config.defaults import create_defaults, defaults, load_defaults
-from macaron.database.table_definitions import Analysis, Component, Repository
+from macaron.database.table_definitions import Analysis, Component, RepoFinderMetadata, Repository
from macaron.parsers.bashparser import BashScriptType, create_bash_node
from macaron.parsers.github_workflow_model import Identified, Job, NormalJob, RunStep, Workflow
from macaron.slsa_analyzer.analyze_context import AnalyzeContext
@@ -413,6 +413,7 @@ def __init__(
# Must match test_provenance_finder.MockGit.MockTag.commit.
commit_sha="dig",
),
+ repo_finder_metadata=RepoFinderMetadata(),
)
super().__init__(component, *args, **kwargs)
diff --git a/tests/dependency_analyzer/cyclonedx/test_cyclonedx.py b/tests/dependency_analyzer/cyclonedx/test_cyclonedx.py
index df1eeacb0..ce421f56c 100644
--- a/tests/dependency_analyzer/cyclonedx/test_cyclonedx.py
+++ b/tests/dependency_analyzer/cyclonedx/test_cyclonedx.py
@@ -9,7 +9,7 @@
from cyclonedx.model.component import Component as CDXComponent
from macaron.config.defaults import defaults, load_defaults
-from macaron.database.table_definitions import Analysis, Component, Repository
+from macaron.database.table_definitions import Analysis, Component, RepoFinderMetadata, Repository
from macaron.dependency_analyzer.cyclonedx import CycloneDXParserError, DependencyInfo, deserialize_bom_json
from macaron.dependency_analyzer.cyclonedx_mvn import CycloneDxMaven
from macaron.slsa_analyzer.build_tool.base_build_tool import BaseBuildTool
@@ -65,6 +65,7 @@ def test_get_dep_components_java(
purl="pkg:maven/io.micronaut.aws/aws-parent@4.0.0-SNAPSHOT?type=pom",
analysis=Analysis(),
repository=Repository(complete_name="github.com/micronaut-projects/micronaut-aws", fs_path=""),
+ repo_finder_metadata=RepoFinderMetadata(),
)
# Path to the sub-project bom.json files.
@@ -107,6 +108,7 @@ def test_get_dep_components_python(
purl="pkg:pypi/requests@2.31.0",
analysis=Analysis(),
repository=Repository(complete_name="github.com/psf/requests", fs_path=""),
+ repo_finder_metadata=RepoFinderMetadata(),
)
# Path to the sub-project bom.json files.
@@ -144,6 +146,7 @@ def test_convert_components_to_artifacts_java(
purl="pkg:maven/io.micronaut.aws/aws-parent@4.0.0-SNAPSHOT?type=pom",
analysis=Analysis(),
repository=Repository(complete_name="github.com/micronaut-projects/micronaut-aws", fs_path=""),
+ repo_finder_metadata=RepoFinderMetadata(),
)
# Path to the sub-project bom.json files.
@@ -177,6 +180,7 @@ def test_convert_components_to_artifacts_python(
purl="pkg:pypi/requests@2.31.0",
analysis=Analysis(),
repository=Repository(complete_name="github.com/psf/requests", fs_path=""),
+ repo_finder_metadata=RepoFinderMetadata(),
)
# Pass the root bom.json.
@@ -210,6 +214,7 @@ def test_low_quality_bom(
purl="pkg:maven/com.amazonaws/aws-lambda-java-events@3.11.0?type=jar",
analysis=Analysis(),
repository=Repository(complete_name="github.com/aws/aws-lambda-java-libs", fs_path=""),
+ repo_finder_metadata=RepoFinderMetadata(),
)
result = dep_analyzer.get_deps_from_sbom(bom_path, target_component=component)
assert snapshot == result
@@ -236,6 +241,7 @@ def test_multiple_versions(
purl="pkg:maven/com.amazonaws/aws-lambda-java-events@3.11.0?type=jar",
analysis=Analysis(),
repository=Repository(complete_name="github.com/aws/aws-lambda-java-libs", fs_path=""),
+ repo_finder_metadata=RepoFinderMetadata(),
)
result = dep_analyzer.get_deps_from_sbom(bom_path, target_component=component)
assert snapshot == result
@@ -250,6 +256,7 @@ def test_custom_sbom_name_with_maven() -> None:
purl="pkg:maven/com.example/cyclonedx-test@1.0-SNAPSHOT?type=jar",
analysis=Analysis(),
repository=None,
+ repo_finder_metadata=RepoFinderMetadata(),
)
custom_bom_dir = RESOURCES_DIR.joinpath("sbom_name_tests")
assert cyclonedx.collect_dependencies(str(custom_bom_dir.joinpath("single_named_sbom")), target_component=component)
diff --git a/tests/integration/cases/commit_finder_tag_matching_functionality/commit_finder.py b/tests/integration/cases/commit_finder_tag_matching_functionality/commit_finder.py
index 105af78b4..857acb0d0 100644
--- a/tests/integration/cases/commit_finder_tag_matching_functionality/commit_finder.py
+++ b/tests/integration/cases/commit_finder_tag_matching_functionality/commit_finder.py
@@ -30,7 +30,7 @@ def test_commit_finder() -> int:
artifacts = item["artifacts"]
for artifact in artifacts:
purl = PackageURL.from_string(artifact["purl"])
- matched_tags = commit_finder.match_tags(item["tags"], purl.name, purl.version or "")
+ matched_tags, _ = commit_finder.match_tags(item["tags"], purl.name, purl.version or "")
matched_tag = matched_tags[0] if matched_tags else ""
expected = str(artifact["match"])
if matched_tag != expected:
diff --git a/tests/integration/cases/repo_finder_remote_calls/repo_finder.py b/tests/integration/cases/repo_finder_remote_calls/repo_finder.py
index 12f10cac1..33c9feb2d 100644
--- a/tests/integration/cases/repo_finder_remote_calls/repo_finder.py
+++ b/tests/integration/cases/repo_finder_remote_calls/repo_finder.py
@@ -12,6 +12,7 @@
from macaron.config.defaults import defaults
from macaron.repo_finder import repo_validator
from macaron.repo_finder.repo_finder import find_repo
+from macaron.repo_finder.repo_finder_enums import RepoFinderOutcome
from macaron.slsa_analyzer.git_url import clean_url
logger: logging.Logger = logging.getLogger(__name__)
@@ -42,27 +43,33 @@ def test_repo_finder() -> int:
defaults.set("git_service.gitlab", "hostname", "gitlab.com")
# Test Java package with SCM metadata in artifact POM.
- if not find_repo(PackageURL.from_string("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.2")):
+ match, outcome = find_repo(PackageURL.from_string("pkg:maven/com.fasterxml.jackson.core/jackson-databind@2.14.2"))
+ if not match or outcome != RepoFinderOutcome.FOUND:
return os.EX_UNAVAILABLE
# Test Java package with SCM metadata in artifact's parent POM.
- if not find_repo(PackageURL.from_string("pkg:maven/commons-cli/commons-cli@1.5.0")):
+ match, outcome = find_repo(PackageURL.from_string("pkg:maven/commons-cli/commons-cli@1.5.0"))
+ if not match or outcome != RepoFinderOutcome.FOUND:
return os.EX_UNAVAILABLE
# Test deps.dev API for a Python package.
- if not find_repo(PackageURL.from_string("pkg:pypi/packageurl-python@0.11.1")):
+ match, outcome = find_repo(PackageURL.from_string("pkg:pypi/packageurl-python@0.11.1"))
+ if not match or outcome != RepoFinderOutcome.FOUND:
return os.EX_UNAVAILABLE
# Test deps.dev API for a Nuget package.
- if not find_repo(PackageURL.from_string("pkg:nuget/azure.core")):
+ match, outcome = find_repo(PackageURL.from_string("pkg:nuget/azure.core"))
+ if not match or outcome != RepoFinderOutcome.FOUND:
return os.EX_UNAVAILABLE
# Test deps.dev API for an NPM package.
- if not find_repo(PackageURL.from_string("pkg:npm/@colors/colors")):
+ match, outcome = find_repo(PackageURL.from_string("pkg:npm/@colors/colors"))
+ if not match or outcome != RepoFinderOutcome.FOUND:
return os.EX_UNAVAILABLE
# Test deps.dev API for Cargo package.
- if not find_repo(PackageURL.from_string("pkg:cargo/rand_core")):
+ match, outcome = find_repo(PackageURL.from_string("pkg:cargo/rand_core"))
+ if not match or outcome != RepoFinderOutcome.FOUND:
return os.EX_UNAVAILABLE
# Test redirecting URL from Apache commons-io package.
diff --git a/tests/malware_analyzer/pypi/conftest.py b/tests/malware_analyzer/pypi/conftest.py
index a5f775531..4a583fda3 100644
--- a/tests/malware_analyzer/pypi/conftest.py
+++ b/tests/malware_analyzer/pypi/conftest.py
@@ -7,7 +7,7 @@
import pytest
-from macaron.database.table_definitions import Analysis, Component
+from macaron.database.table_definitions import Analysis, Component, RepoFinderMetadata
from macaron.slsa_analyzer.package_registry.pypi_registry import PyPIPackageJsonAsset, PyPIRegistry
@@ -23,5 +23,7 @@ def pypi_package_json() -> MagicMock:
pypi_registry = MagicMock(spec=PyPIRegistry)
pypi_package = MagicMock(spec=PyPIPackageJsonAsset)
pypi_package.pypi_registry = pypi_registry
- pypi_package.component = Component(purl="pkg:pypi/package", analysis=Analysis(), repository=None)
+ pypi_package.component = Component(
+ purl="pkg:pypi/package", analysis=Analysis(), repository=None, repo_finder_metadata=RepoFinderMetadata()
+ )
return pypi_package
diff --git a/tests/repo_finder/test_commit_finder.py b/tests/repo_finder/test_commit_finder.py
index 925fdf20d..67cc3219b 100644
--- a/tests/repo_finder/test_commit_finder.py
+++ b/tests/repo_finder/test_commit_finder.py
@@ -6,48 +6,52 @@
import os
import re
import shutil
+from typing import Any
import hypothesis
import pytest
from hypothesis import given, settings
from hypothesis.strategies import DataObject, data, text
from packageurl import PackageURL
+from pydriller.git import Git
from macaron.repo_finder import commit_finder
from macaron.repo_finder.commit_finder import AbstractPurlType
+from macaron.repo_finder.repo_finder_enums import CommitFinderOutcome
from tests.slsa_analyzer.mock_git_utils import commit_files, initiate_repo
logger: logging.Logger = logging.getLogger(__name__)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
REPO_DIR = os.path.join(BASE_DIR, "mock_repos", "commit_finder/sample_repo")
+UNICODE_VERSION = "雪" # The Japanese character for "snow".
+TAG_VERSION = "2.3.4"
+TAG_VERSION_2 = "4.5.2"
-def test_get_commit_from_version() -> None:
+@pytest.fixture(name="tag_list")
+def tag_list_() -> list[str]:
+ """Return a list of tags."""
+ return ["test-name-v1.0.1-A", "v1.0.3+test", "v_1.0.5", "50_0_2", "r78rv109", "1.0.5-JRE"]
+
+
+@pytest.mark.parametrize(
+ ("version", "name", "tag_list_index"),
+ [
+ ("1.0.1-A", "test-name-1", 0),
+ ("1.0.3+test", "test-name-2", 1),
+ ("1.0.5", "test-name-3", 2),
+ ("50.0.2", "test-name-4", 3),
+ ("78.109", "test-name-5", 4),
+ ("1.0.5-JRE", "test-name-6", 5),
+ ],
+)
+def test_get_commit_from_version(version: str, name: str, tag_list_index: int, tag_list: list[str]) -> None:
"""Test resolving commits from version tags."""
- versions = [
- "1.0.1-A", # To match a tag with a named suffix.
- "1.0.3+test", # To match a tag with a '+' suffix.
- "1.0.5", # To match a tag with a 'v_' prefix.
- "50.0.2", # To match a tag separated by '_'.
- "78.109", # To match a tag separated by characters 'r' 'rv'.
- "1.0.5-JRE", # To NOT match the similar tag without the 'JRE' suffix.
- ]
-
- tags = ["test-name-v1.0.1-A", "v1.0.3+test", "v_1.0.5", "50_0_2", "r78rv109", "1.0.5-JRE"]
-
- # Perform tests
- purl_name = "test-name"
- for count, value in enumerate(versions):
- _test_version(tags, purl_name, value, tags[count])
- purl_name = "test-name" + "-" + str(count + 1)
-
-
-def _test_version(tags: list[str], name: str, version: str, target_tag: str) -> None:
- """Retrieve tag matching version and check it is correct."""
- matched_tags = commit_finder.match_tags(tags, name, version)
+ matched_tags, outcome = commit_finder.match_tags(tag_list, name, version)
assert matched_tags
- assert matched_tags[0] == target_tag
+ assert matched_tags[0] == tag_list[tag_list_index]
+ assert outcome == CommitFinderOutcome.MATCHED
@pytest.mark.parametrize(
@@ -87,8 +91,9 @@ def test_abstract_purl_type(purls: list[str], expected: AbstractPurlType) -> Non
assert commit_finder.determine_abstract_purl_type(PackageURL.from_string(purl)) == expected
-def test_commit_finder() -> None:
- """Test commit finder using mocked repository."""
+@pytest.fixture(name="mocked_repo")
+def mocked_repo_() -> Git:
+ """Create a mocked repository."""
if os.path.exists(REPO_DIR):
shutil.rmtree(REPO_DIR)
git_obj = initiate_repo(
@@ -103,82 +108,141 @@ def test_commit_finder() -> None:
file.write("A")
commit_files(git_obj, ["file_1"])
- # Create a commit with no associated branch.
- commit_0 = git_obj.repo.index.commit(message="Commit_0")
+ return git_obj
- # No version in PURL.
- assert not commit_finder.find_commit(git_obj, PackageURL.from_string("pkg:maven/apache/maven"))
- # Unsupported PURL type.
- assert not commit_finder.find_commit(git_obj, PackageURL.from_string("pkg:gem/ruby-artifact@1"))
+@pytest.fixture(name="mocked_repo_commit")
+def mocked_repo_commit_(mocked_repo: Git) -> Any:
+ """Add a commit to the mocked repository."""
+ return mocked_repo.repo.index.commit(message="Commit_0")
- # Hash not present in repository, tests hash and tag.
- assert not commit_finder.find_commit(git_obj, PackageURL.from_string("pkg:github/apache/maven@ab4ce3e"))
- # Valid PURL but repository has no tags yet.
- assert not commit_finder.find_commit(git_obj, PackageURL.from_string("pkg:maven/apache/maven@1.0"))
+@pytest.fixture(name="mocked_repo_empty_commit")
+def mocked_repo_empty_commit_(mocked_repo: Git) -> Any:
+ """Add an empty commit to the mocked repository."""
+ return mocked_repo.repo.index.commit(message="Empty_Commit")
- # Additional setup is done here to avoid tainting earlier tests.
+
+@pytest.fixture(name="mocked_repo_expanded")
+def mocked_repo_expanded_(mocked_repo: Git, mocked_repo_commit: Any, mocked_repo_empty_commit: Any) -> Any:
+ """Add tags to the mocked repository."""
+ mocked_repo.repo.create_tag("4.5", mocked_repo_commit.hexsha)
# Create a tag from a tree.
- tag_tree_version = "1.0"
- tree = git_obj.repo.heads.master.commit.tree
- git_obj.repo.create_tag(tag_tree_version, ref=tree)
+ mocked_repo.repo.create_tag("1.0", ref=mocked_repo.repo.heads.master.commit.tree)
- # Add a new tag with an associated commit. This is the Japanese character for 'snow'.
- unicode_version = "雪"
- git_obj.repo.create_tag(unicode_version, commit_0.hexsha)
+ # Add a tag with unicode version.
+ mocked_repo.repo.create_tag(UNICODE_VERSION, mocked_repo_commit.hexsha)
# Create a more typical tag on the same commit.
- tag_version = "2.3.4"
- git_obj.repo.create_tag(tag_version, commit_0.hexsha)
+ mocked_repo.repo.create_tag(TAG_VERSION, mocked_repo_commit.hexsha)
- # Add an empty commit with some tags.
- empty_commit = git_obj.repo.index.commit("Empty commit.")
- tag_version_2 = "4.5.2"
- git_obj.repo.create_tag(f"{tag_version_2}-DEV", ref=empty_commit.hexsha)
- git_obj.repo.create_tag(f"{tag_version_2}_DEV_RC1_RELEASE", ref=empty_commit.hexsha)
- git_obj.repo.create_tag(f"rel/prefix_name-{tag_version}", ref=empty_commit.hexsha)
+ # Add more tags.
+ mocked_repo.repo.create_tag(f"{TAG_VERSION_2}-DEV", ref=mocked_repo_empty_commit.hexsha)
+ mocked_repo.repo.create_tag(f"{TAG_VERSION_2}_DEV_RC1_RELEASE", ref=mocked_repo_empty_commit.hexsha)
+ mocked_repo.repo.create_tag(f"rel/prefix_name-{TAG_VERSION}", ref=mocked_repo_empty_commit.hexsha)
- # Version with a suffix and no matching tag.
- assert not commit_finder.find_commit(git_obj, PackageURL.from_string("pkg:maven/apache/maven@1-JRE"))
+ return mocked_repo
- # Version with only one digit and no matching tag.
- assert not commit_finder.find_commit(git_obj, PackageURL.from_string("pkg:maven/apache/maven@1"))
- # Unicode version.
- assert commit_finder.find_commit(git_obj, PackageURL.from_string(f"pkg:maven/apache/maven@{unicode_version}"))
+@pytest.mark.parametrize(
+ ("purl_string", "expected_outcome"),
+ [
+ # No version in PURL.
+ ("pkg:maven/apache/maven", CommitFinderOutcome.NO_VERSION_PROVIDED),
+ # Unsupported PURL type.
+ ("pkg:gem/ruby-artifact@1", CommitFinderOutcome.UNSUPPORTED_PURL_TYPE),
+ # Hash not present in repository.
+ ("pkg:github/apache/maven@ab4ce3e", CommitFinderOutcome.REPO_PURL_FAILURE),
+ # Valid PURL but repository has no tags yet.
+ ("pkg:maven/apache/maven@1.0", CommitFinderOutcome.NO_TAGS),
+ ],
+)
+def test_commit_finder_tagless_failure(
+ mocked_repo: Git, purl_string: str, expected_outcome: CommitFinderOutcome
+) -> None:
+ """Test commit finder using mocked repository with no tags."""
+ match, outcome = commit_finder.find_commit(mocked_repo, PackageURL.from_string(purl_string))
+ assert not match
+ assert outcome == expected_outcome
- # Valid repository PURL.
- digest = commit_finder.find_commit(git_obj, PackageURL.from_string(f"pkg:github/apache/maven@{commit_0.hexsha}"))
- assert digest == commit_0.hexsha
- # Valid artifact PURL.
- digest = commit_finder.find_commit(git_obj, PackageURL.from_string(f"pkg:maven/apache/maven@{tag_version}"))
- assert digest == commit_0.hexsha
+@pytest.mark.parametrize(
+ ("purl_string", "expected_outcome"),
+ [
+ # Invalid PURL.
+ ("pkg:maven/[]@()", CommitFinderOutcome.INVALID_PURL),
+ # Version with a suffix and no matching tag.
+ ("pkg:maven/apache/maven@1-JRE", CommitFinderOutcome.NO_TAGS_MATCHED),
+ # Version with only one digit and no matching tag.
+ ("pkg:maven/apache/maven@1", CommitFinderOutcome.NO_TAGS_MATCHED),
+ ],
+)
+def test_commit_finder_tag_failure(
+ mocked_repo_expanded: Git, purl_string: str, expected_outcome: CommitFinderOutcome
+) -> None:
+ """Test commit finder using mocked repository with tags."""
+ match, outcome = commit_finder.find_commit(mocked_repo_expanded, PackageURL.from_string(purl_string))
+ assert not match
+ assert outcome == expected_outcome
- # Valid artifact PURL with an alphanumeric suffix.
- digest = commit_finder.find_commit(git_obj, PackageURL.from_string(f"pkg:maven/apache/maven@{tag_version}-RC1"))
- assert digest == commit_0.hexsha
- # Valid artifact PURL that should match a tag with a name prefix.
- digest = commit_finder.find_commit(git_obj, PackageURL.from_string(f"pkg:maven/apache/prefix_name@{tag_version}"))
- assert digest == empty_commit.hexsha
+@pytest.mark.parametrize(
+ "purl_string",
+ [
+ f"pkg:maven/apache/maven@{UNICODE_VERSION}",
+ f"pkg:maven/apache/maven@{TAG_VERSION}",
+ f"pkg:maven/apache/maven@{TAG_VERSION}-RC1",
+ ],
+)
+def test_commit_finder_success_commit(
+ mocked_repo_expanded: Git,
+ mocked_repo_commit: Any,
+ purl_string: str,
+) -> None:
+ """Test Commit Finder on mocked repository that should match valid PURLs."""
+ match, outcome = commit_finder.find_commit(mocked_repo_expanded, PackageURL.from_string(purl_string))
+ assert match == mocked_repo_commit.hexsha
+ assert outcome == CommitFinderOutcome.MATCHED
- # Valid artifact PURL that matches a version with a suffix, to a tag with the same suffix.
- digest = commit_finder.find_commit(git_obj, PackageURL.from_string(f"pkg:maven/apache/maven@{tag_version_2}-DEV"))
- assert digest == empty_commit.hexsha
- # Valid artifact PURL that matches a version with a suffix, to a tag with the same suffix part in a multi-suffix.
- digest = commit_finder.find_commit(
- git_obj, PackageURL.from_string(f"pkg:maven/apache/maven@{tag_version_2}_RELEASE")
+@pytest.mark.parametrize(
+ "purl_string",
+ [
+ # Match name prefix.
+ f"pkg:maven/apache/prefix_name@{TAG_VERSION}",
+ # Match suffix.
+ f"pkg:maven/apache/maven@{TAG_VERSION_2}-DEV",
+ # Match suffix in multi-suffix.
+ f"pkg:maven/apache/maven@{TAG_VERSION_2}_RELEASE",
+ # Match alphanumeric suffix in multi-suffix.
+ f"pkg:maven/apache/maven@{TAG_VERSION_2}_RC1",
+ ],
+)
+def test_commit_finder_success_empty_commit(
+ mocked_repo_expanded: Git, mocked_repo_empty_commit: Any, purl_string: str
+) -> None:
+ """Test Commit Finder on mocked repository that should match value PURLs."""
+ match, outcome = commit_finder.find_commit(mocked_repo_expanded, PackageURL.from_string(purl_string))
+ assert match == mocked_repo_empty_commit.hexsha
+ assert outcome == CommitFinderOutcome.MATCHED
+
+
+def test_commit_finder_repo_purl_success(mocked_repo_expanded: Git, mocked_repo_commit: Any) -> None:
+ """Test Commit Finder on mocked repository using a repo type PURL."""
+ match, outcome = commit_finder.find_commit(
+ mocked_repo_expanded, PackageURL.from_string(f"pkg:github/apache/maven@{mocked_repo_commit.hexsha}")
)
- assert digest == empty_commit.hexsha
+ assert match == mocked_repo_commit.hexsha
+ assert outcome == CommitFinderOutcome.MATCHED
+
- # Valid artifact PURL that matches a version with an alphanumeric suffix, to a tag with the same suffix part in a
- # multi-suffix.
- digest = commit_finder.find_commit(git_obj, PackageURL.from_string(f"pkg:maven/apache/maven@{tag_version_2}_RC1"))
- assert digest == empty_commit.hexsha
+def test_commit_finder_tag_no_commit(mocked_repo: Git) -> None:
+ """Test the Commit Finder on a mocked repository that has a tag with no commit."""
+ mocked_repo.repo.create_tag("TEST", ref=mocked_repo.repo.heads.master.commit.tree)
+ match, outcome = commit_finder.find_commit(mocked_repo, PackageURL.from_string("pkg:maven/apache/maven@TEST"))
+ assert not match
+ assert outcome == CommitFinderOutcome.NO_TAGS_WITH_COMMITS
@given(text())
@@ -223,7 +287,7 @@ def test_version_to_tag_matching(_data: DataObject) -> None: # noqa: PT019
if not purl.version:
return
# Build the pattern from the version.
- pattern, parts = commit_finder._build_version_pattern(purl.name, purl.version)
+ pattern, parts, _ = commit_finder._build_version_pattern(purl.name, purl.version)
if not pattern:
return
# Generate the tag from a pattern that is very similar to how version patterns are made.
diff --git a/tests/repo_finder/test_repo_finder.py b/tests/repo_finder/test_repo_finder.py
index ba0bc2b20..0e4a3d1e0 100644
--- a/tests/repo_finder/test_repo_finder.py
+++ b/tests/repo_finder/test_repo_finder.py
@@ -6,13 +6,32 @@
from pathlib import Path
import pytest
+from packageurl import PackageURL
+from pytest_httpserver import HTTPServer
from macaron.config.defaults import load_defaults
-from macaron.repo_finder.repo_finder_java import JavaRepoFinder
+from macaron.repo_finder import repo_finder
+from macaron.repo_finder.repo_finder_enums import RepoFinderOutcome
+
+
+@pytest.fixture(name="httpserver_java")
+def httpserver_java_(tmp_path: Path, httpserver: HTTPServer) -> HTTPServer:
+ """Set up the mock HTTP Server for the Repo Finder."""
+ url = httpserver.url_for("")
+ test_config = f"""
+ [repofinder.java]
+ artifact_repositories = {url}
+ """
+ test_config_path = os.path.join(tmp_path, "config.ini")
+ with open(test_config_path, "w", encoding="utf-8") as test_config_file:
+ test_config_file.write(test_config)
+ load_defaults(test_config_path)
+
+ return httpserver
@pytest.mark.parametrize(
- ("user_config_input", "expected"),
+ ("test_config", "expected"),
[
(
"""
@@ -21,7 +40,7 @@
scm.connection
scm.url
""",
- ["scm:git:git@github.com:oracle-samples/macaron.git", "https://github.com/oracle/macaron"],
+ "https://github.com/oracle-samples/macaron",
),
(
"""
@@ -30,12 +49,19 @@
scm.url
scm.connection
""",
- ["https://github.com/oracle/macaron", "scm:git:git@github.com:oracle-samples/macaron.git"],
+ "https://github.com/oracle/macaron",
),
],
)
-def test_pom_extraction_ordering(tmp_path: Path, user_config_input: str, expected: list[str]) -> None:
+def test_pom_extraction_ordering(tmp_path: Path, test_config: str, expected: str, httpserver: HTTPServer) -> None:
"""Test the ordering of elements extracted from the POM is correct and maintained."""
+ url = httpserver.url_for("")
+ test_config = test_config + f"\nartifact_repositories = {url}"
+ test_config_path = os.path.join(tmp_path, "config.ini")
+ with open(test_config_path, "w", encoding="utf-8") as test_config_file:
+ test_config_file.write(test_config)
+ load_defaults(test_config_path)
+
pom_text = """
https://example.org
@@ -48,12 +74,163 @@ def test_pom_extraction_ordering(tmp_path: Path, user_config_input: str, expecte
"""
- user_config_path = os.path.join(tmp_path, "config.ini")
- with open(user_config_path, "w", encoding="utf-8") as user_config_file:
- user_config_file.write(user_config_input)
- load_defaults(user_config_path)
- repo_finder = JavaRepoFinder()
+ group = "com.oracle.tools"
+ artifact = "oracle-tools-macaron"
+ version = "0.4"
+ target_url = "/" + "/".join(["/".join(group.split(".")), artifact, version, f"{artifact}-{version}.pom"])
+ httpserver.expect_request(target_url).respond_with_data(pom_text)
+
+ found_repo, outcome = repo_finder.find_repo(PackageURL.from_string(f"pkg:maven/{group}/{artifact}@{version}"))
+ assert found_repo
+ assert found_repo == expected
+ assert outcome == RepoFinderOutcome.FOUND
+
+
+@pytest.mark.parametrize(
+ ("test_config", "expected"),
+ [
+ (
+ """
+ [repofinder.java]
+ artifact_repositories =
+
+ """,
+ RepoFinderOutcome.NO_MAVEN_HOST_PROVIDED,
+ ),
+ (
+ """
+ [repofinder.java]
+ repo_pom_paths =
+
+ """,
+ RepoFinderOutcome.NO_POM_TAGS_PROVIDED,
+ ),
+ ],
+)
+def test_repo_finder_java_invalid_config(tmp_path: Path, test_config: str, expected: RepoFinderOutcome) -> None:
+ """Test the Repo Finder when inputs are invalid: a non-breaking space."""
+ test_config_path = os.path.join(tmp_path, "config.ini")
+ with open(test_config_path, "w", encoding="utf-8") as test_config_file:
+ test_config_file.write(test_config)
+ load_defaults(test_config_path)
+
+ found_repo, outcome = repo_finder.find_repo(PackageURL.from_string("pkg:maven/test/test@1"))
+ assert not found_repo
+ assert outcome == expected
+
+
+@pytest.mark.parametrize(
+ ("purl_string", "expected"),
+ [
+ ("pkg:maven/test/test", RepoFinderOutcome.NO_VERSION_PROVIDED),
+ ("pkg:test/test@test", RepoFinderOutcome.UNSUPPORTED_PACKAGE_TYPE),
+ ],
+)
+def test_repo_finder_java_invalid_input(purl_string: str, expected: RepoFinderOutcome) -> None:
+ """Test the Repo Finder when invalid input is provided."""
+ found_repo, outcome = repo_finder.find_repo(PackageURL.from_string(purl_string))
+ assert not found_repo
+ assert outcome == expected
+
+
+@pytest.mark.parametrize(
+ ("test_pom", "expected"),
+ [
+ (
+ """
+ #####
+
+
+
+
+ """,
+ RepoFinderOutcome.SCM_NO_URLS,
+ ),
+ (
+ """
+
+
+ TEST
+
+
+ """,
+ RepoFinderOutcome.SCM_NO_VALID_URLS,
+ ),
+ ],
+)
+def test_repo_finder_java_invalid_pom_or_scm(
+ httpserver_java: HTTPServer, test_pom: str, expected: RepoFinderOutcome
+) -> None:
+ """Test the Repo Finder when the POM or SCM metadata is invalid."""
+ group = "oracle"
+ artifact = "macaron"
+ version = "0.3"
+ target_url = "/" + "/".join([group, artifact, version, f"{artifact}-{version}.pom"])
+ httpserver_java.expect_request(target_url).respond_with_data(test_pom)
+
+ found_repo, outcome = repo_finder.find_repo(PackageURL.from_string(f"pkg:maven/{group}/{artifact}@{version}"))
+ assert not found_repo
+ assert outcome == expected
+
+
+def test_repo_finder_java_success(httpserver_java: HTTPServer) -> None:
+ """Test the Repo Finder on a repository with a valid POM."""
+ pom = """
+
+
+ https://github.com/oracle/macaron
+
+
+ """
+
+ group = "oracle"
+ artifact = "macaron"
+ version = "0.3"
+ target_url = "/" + "/".join([group, artifact, version, f"{artifact}-{version}.pom"])
+ httpserver_java.expect_request(target_url).respond_with_data(pom)
+
+ found_repo, outcome = repo_finder.find_repo(PackageURL.from_string(f"pkg:maven/{group}/{artifact}@{version}"))
+ assert found_repo
+ assert outcome == RepoFinderOutcome.FOUND
+
+
+def test_repo_finder_java_success_via_parent(httpserver_java: HTTPServer) -> None:
+ """Test the Repo Finder on a repository with a valid parent POM."""
+ pom = """
+
+
+ oracle
+ macaron
+ 0.4
+
+
+ """
+
+ parent_pom = """
+
+
+ https://github.com/oracle/macaron
+
+
+ """
+
+ group = "oracle"
+ artifact = "macaron"
+ version = "0.3"
+ target_url = "/" + "/".join([group, artifact, version, f"{artifact}-{version}.pom"])
+ httpserver_java.expect_request(target_url).respond_with_data(pom)
+
+ parent_version = "0.4"
+ parent_url = "/" + "/".join([group, artifact, parent_version, f"{artifact}-{parent_version}.pom"])
+ httpserver_java.expect_request(parent_url).respond_with_data(parent_pom)
- # Retrieve SCM from POM.
- assert expected == repo_finder._read_pom(pom_text) # pylint: disable=W0212
+ found_repo, outcome = repo_finder.find_repo(PackageURL.from_string(f"pkg:maven/{group}/{artifact}@{version}"))
+ assert found_repo
+ assert outcome == RepoFinderOutcome.FOUND_FROM_PARENT
diff --git a/tests/slsa_analyzer/checks/test_registry_e2e.py b/tests/slsa_analyzer/checks/test_registry_e2e.py
index 81be21580..63f54e07c 100644
--- a/tests/slsa_analyzer/checks/test_registry_e2e.py
+++ b/tests/slsa_analyzer/checks/test_registry_e2e.py
@@ -3,7 +3,7 @@
"""This module contains an end-to-end test for the check registry."""
-from macaron.database.table_definitions import Analysis, Component, Repository
+from macaron.database.table_definitions import Analysis, Component, RepoFinderMetadata, Repository
from macaron.slsa_analyzer.analyze_context import AnalyzeContext
from macaron.slsa_analyzer.checks.base_check import BaseCheck
from macaron.slsa_analyzer.checks.check_result import CheckResultData, CheckResultType
@@ -118,6 +118,7 @@ def test_registry_e2e(self) -> None:
purl="pkg:github.com/package-url/purl-spec@244fd47e07d1004f0aed9c",
analysis=Analysis(),
repository=Repository(complete_name="github.com/package-url/purl-spec", fs_path=""),
+ repo_finder_metadata=RepoFinderMetadata(),
)
target = AnalyzeContext(component=component)
results = registry.scan(target)
diff --git a/tests/slsa_analyzer/mock_git_utils.py b/tests/slsa_analyzer/mock_git_utils.py
index 680515983..d5a0f918f 100644
--- a/tests/slsa_analyzer/mock_git_utils.py
+++ b/tests/slsa_analyzer/mock_git_utils.py
@@ -11,7 +11,7 @@
from git.exc import GitError
from pydriller.git import Git
-from macaron.database.table_definitions import Analysis, Component, Repository
+from macaron.database.table_definitions import Analysis, Component, RepoFinderMetadata, Repository
from macaron.slsa_analyzer.analyze_context import AnalyzeContext
@@ -111,6 +111,7 @@ def prepare_repo_for_testing(
files=git_repo.files(),
fs_path=str(repo_path),
),
+ repo_finder_metadata=RepoFinderMetadata(),
)
analyze_ctx = AnalyzeContext(component=component, macaron_path=str(macaron_path), output_dir=str(output_dir))
diff --git a/tests/slsa_analyzer/test_analyzer.py b/tests/slsa_analyzer/test_analyzer.py
index f4e68f321..260d401fd 100644
--- a/tests/slsa_analyzer/test_analyzer.py
+++ b/tests/slsa_analyzer/test_analyzer.py
@@ -13,6 +13,7 @@
from macaron.config.target_config import Configuration
from macaron.errors import InvalidAnalysisTargetError, InvalidPURLError
+from macaron.repo_finder.repo_finder_enums import RepoFinderOutcome
from macaron.slsa_analyzer.analyzer import Analyzer
from ..macaron_testcase import MacaronTestCase
@@ -56,13 +57,18 @@ def test_resolve_local_path(self) -> None:
repo_path="https://github.com/apache/maven",
branch="",
digest="",
+ repo_finder_outcome=RepoFinderOutcome.NOT_USED,
),
),
(
Configuration({"purl": "", "path": "https://github.com/apache/maven"}),
["github.com", "gitlab.com", "bitbucket.org"],
Analyzer.AnalysisTarget(
- parsed_purl=None, repo_path="https://github.com/apache/maven", branch="", digest=""
+ parsed_purl=None,
+ repo_path="https://github.com/apache/maven",
+ branch="",
+ digest="",
+ repo_finder_outcome=RepoFinderOutcome.NOT_USED,
),
),
(
@@ -73,6 +79,7 @@ def test_resolve_local_path(self) -> None:
repo_path="https://github.com/apache/maven",
branch="",
digest="",
+ repo_finder_outcome=RepoFinderOutcome.NOT_USED,
),
),
(
@@ -90,6 +97,7 @@ def test_resolve_local_path(self) -> None:
repo_path="https://github.com/apache/maven",
branch="master",
digest="abcxyz",
+ repo_finder_outcome=RepoFinderOutcome.NOT_USED,
),
),
],