Skip to content

feat: add GitHub attestation discovery #1020

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 16 commits into from
Jun 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 58 additions & 2 deletions src/macaron/artifact/local_artifact.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,21 @@
# 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 declares types and utilities for handling local artifacts."""

import fnmatch
import glob
import hashlib
import logging
import os

from packageurl import PackageURL

from macaron.artifact.maven import construct_maven_repository_path
from macaron.artifact.maven import construct_maven_repository_path, construct_primary_jar_file_name
from macaron.errors import LocalArtifactFinderError

logger: logging.Logger = logging.getLogger(__name__)


def construct_local_artifact_dirs_glob_pattern_maven_purl(maven_purl: PackageURL) -> list[str] | None:
"""Return a list of glob pattern(s) representing the directory that contains the local maven artifacts for ``maven_purl``.
Expand Down Expand Up @@ -247,3 +251,55 @@ def get_local_artifact_dirs(
)

raise LocalArtifactFinderError(f"Unsupported PURL type {purl_type}")


def get_local_artifact_hash(purl: PackageURL, artifact_dirs: list[str]) -> str | None:
"""Compute the hash of the local artifact.

Parameters
----------
purl: PackageURL
The PURL of the artifact being sought.
artifact_dirs: list[str]
The list of directories that may contain the artifact file.

Returns
-------
str | None
The hash, or None if not found.
"""
if not artifact_dirs:
logger.debug("No artifact directories provided.")
return None

if not purl.version:
logger.debug("PURL is missing version.")
return None

artifact_target = None
if purl.type == "maven":
artifact_target = construct_primary_jar_file_name(purl)

# TODO add support for other PURL types here.
# Other purl types can be easily supported if user provided artifacts are accepted from the command line.
# See https://github.com/oracle/macaron/issues/498.

if not artifact_target:
logger.debug("PURL type not supported: %s", purl.type)
return None

for artifact_dir in artifact_dirs:
full_path = os.path.join(artifact_dir, artifact_target)
if not os.path.exists(full_path):
continue

with open(full_path, "rb") as file:
try:
hash_result = hashlib.file_digest(file, "sha256")
except ValueError as error:
logger.debug("Error while hashing file: %s", error)
continue

return hash_result.hexdigest()

return None
21 changes: 20 additions & 1 deletion src/macaron/artifact/maven.py
Original file line number Diff line number Diff line change
@@ -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 declares types and utilities for Maven artifacts."""
Expand Down Expand Up @@ -196,3 +196,22 @@ def construct_maven_repository_path(
if asset_name:
path = "/".join([path, asset_name])
return path


def construct_primary_jar_file_name(purl: PackageURL) -> str | None:
"""Return the name of the primary JAR for the passed PURL based on the Maven registry standard.
Parameters
----------
purl: PackageURL
The PURL of the artifact.
Returns
-------
str | None
The artifact file name, or None if invalid.
"""
if not purl.version:
return None

return purl.name + "-" + purl.version + ".jar"
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def _get_setup_source_code(self, pypi_package_json: PyPIPackageJsonAsset) -> str
response = requests.get(sourcecode_url, stream=True, timeout=40)
response.raise_for_status()
except requests.exceptions.HTTPError as http_err:
logger.debug("HTTP error occurred: %s", http_err)
logger.debug("HTTP error occurred when trying to download source: %s", http_err)
return None

if response.status_code != 200:
Expand Down
7 changes: 6 additions & 1 deletion src/macaron/repo_finder/repo_finder.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,12 @@ def find_repo_alternative(
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)
logger.debug(
"Could not find repository using type specific (%s) methods for PURL %s. Outcome: %s",
purl.type,
purl,
outcome,
)

return found_repo, outcome

Expand Down
39 changes: 15 additions & 24 deletions src/macaron/repo_finder/repo_finder_pypi.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
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.package_registry.pypi_registry import PyPIPackageJsonAsset, find_or_create_pypi_asset
from macaron.slsa_analyzer.specs.package_registry_spec import PackageRegistryInfo

logger: logging.Logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -44,38 +44,29 @@ def find_repo(
),
None,
)
if not pypi_info:
return "", RepoFinderInfo.PYPI_NO_REGISTRY

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
if not purl.version:
return "", RepoFinderInfo.NO_VERSION_PROVIDED

pypi_asset = None
from_metadata = False
# Create the asset.
if pypi_info:
for existing_asset in pypi_info.metadata:
if not isinstance(existing_asset, PyPIPackageJsonAsset):
continue

if existing_asset.component_name == purl.name:
pypi_asset = existing_asset
from_metadata = True
break
pypi_asset = find_or_create_pypi_asset(purl.name, purl.version, pypi_info)
else:
# If this function has been reached via find-source, we do not store the asset.
pypi_registry = next((registry for registry in PACKAGE_REGISTRIES if isinstance(registry, PyPIRegistry)), None)
if not pypi_registry:
return "", RepoFinderInfo.PYPI_NO_REGISTRY
pypi_asset = PyPIPackageJsonAsset(purl.name, purl.version, False, pypi_registry, {}, "")

if not pypi_asset:
pypi_asset = PyPIPackageJsonAsset(purl.name, purl.version, False, pypi_registry, {}, "")
# This should be unreachable, as the pypi_registry has already been confirmed to be of type PyPIRegistry.
return "", RepoFinderInfo.PYPI_NO_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
Expand Down
Loading
Loading