Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
145 changes: 145 additions & 0 deletions src/macaron/artifact/maven.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
# 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 declares types and utilities for Maven artifacts."""

import re
from dataclasses import dataclass
from enum import Enum
from typing import NamedTuple, Self

from packageurl import PackageURL


class _MavenArtifactType(NamedTuple):
filename_pattern: str
purl_qualifiers: dict[str, str]


class MavenArtifactType(_MavenArtifactType, Enum):
"""Maven artifact types that Macaron supports.

For reference, see:
- https://maven.apache.org/ref/3.9.6/maven-core/artifact-handlers.html
- https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#maven
"""

# Enum with custom value type.
# See https://docs.python.org/3.10/library/enum.html#others.
JAR = _MavenArtifactType(
filename_pattern="{artifact_id}-{version}.jar",
purl_qualifiers={"type": "jar"},
)
POM = _MavenArtifactType(
filename_pattern="{artifact_id}-{version}.pom",
purl_qualifiers={"type": "pom"},
)
JAVADOC = _MavenArtifactType(
filename_pattern="{artifact_id}-{version}-javadoc.jar",
purl_qualifiers={"type": "javadoc"},
)
JAVA_SOURCE = _MavenArtifactType(
filename_pattern="{artifact_id}-{version}-sources.jar",
purl_qualifiers={"type": "sources"},
)


@dataclass
class MavenArtifact:
"""A Maven artifact."""

group_id: str
artifact_id: str
version: str
artifact_type: MavenArtifactType

@property
def package_url(self) -> PackageURL:
"""Get the PackageURL of this Maven artifact."""
return PackageURL(
type="maven",
namespace=self.group_id,
name=self.artifact_id,
version=self.version,
qualifiers=self.artifact_type.purl_qualifiers,
)

@classmethod
def from_package_url(cls, package_url: PackageURL) -> Self | None:
"""Create a Maven artifact from a PackageURL.

Parameters
----------
package_url : PackageURL
The PackageURL identifying a Maven artifact.

Returns
-------
Self | None
A Maven artifact, or ``None`` if the PURL is not a valid Maven artifact PURL, or if
the artifact type is not supported.
For supported artifact types, see :class:`MavenArtifactType`.
"""
if not package_url.namespace:
return None
if not package_url.version:
return None
if package_url.type != "maven":
return None
maven_artifact_type = None
for artifact_type in MavenArtifactType:
if artifact_type.purl_qualifiers == package_url.qualifiers:
maven_artifact_type = artifact_type
break
if not maven_artifact_type:
return None
return cls(
group_id=package_url.namespace,
artifact_id=package_url.name,
version=package_url.version,
artifact_type=maven_artifact_type,
)

@classmethod
def from_artifact_name(
cls,
artifact_name: str,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

artifact_name seems a bit ambiguous. Is it the filename?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is indeed the filename. Thanks for pointing out.
I have renamed artifact_name to artifact_filename in 263414b.

group_id: str,
version: str,
) -> Self | None:
"""Create a Maven artifact given an artifact name.

The artifact type is determined based on the naming pattern of the artifact.

Parameters
----------
artifact_name : str
The artifact name.
group_id : str
The group id.
version : str
The version

Returns
-------
Self | None
A Maven artifact, or ``None`` if the PURL is not a valid Maven artifact PURL, or if
the artifact type is not supported.
For supported artifact types, see :class:`MavenArtifactType`.
"""
for maven_artifact_type in MavenArtifactType:
pattern = maven_artifact_type.filename_pattern.format(
artifact_id="(.*)",
version=version,
)
match_result = re.search(pattern, artifact_name)
if not match_result:
continue
artifact_id = match_result.group(1)
return cls(
group_id=group_id,
artifact_id=artifact_id,
version=version,
artifact_type=maven_artifact_type,
)
return None
35 changes: 35 additions & 0 deletions src/macaron/database/table_definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,13 @@ class Component(PackageURLMixin, ORMBase):
secondaryjoin=components_association_table.c.child_component == id,
)

#: The optional one-to-one relationship with a provenance subject in case this
#: component represents a subject in a provenance.
provenance_subject: Mapped["ProvenanceSubject | None"] = relationship(
back_populates="component",
lazy="immediate",
)

def __init__(self, purl: str, analysis: Analysis, repository: "Repository | None"):
"""
Instantiate the software component using PURL identifier.
Expand Down Expand Up @@ -528,3 +535,31 @@ class HashDigest(ORMBase):

#: The many-to-one relationship with artifacts.
artifact: Mapped["ReleaseArtifact"] = relationship(back_populates="digests", lazy="immediate")


class ProvenanceSubject(ORMBase):
"""A subject in a provenance that matches the user-provided PackageURL.

This subject may be later populated in VSAs during policy verification.
"""

__tablename__ = "_provenance_subject"

#: The primary key.
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) # noqa: A003

#: The component id of the provenance subject.
component_id: Mapped[int] = mapped_column(
Integer,
ForeignKey("_component.id"),
nullable=False,
)

#: The required one-to-one relationship with a component.
component: Mapped[Component] = relationship(
back_populates="provenance_subject",
lazy="immediate",
)

#: The SHA256 hash of the subject.
sha256: Mapped[str] = mapped_column(String, nullable=False)
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ class ProvenanceAvailableFacts(CheckFacts):
id: Mapped[int] = mapped_column(ForeignKey("_check_facts.id"), primary_key=True) # noqa: A003

#: The provenance asset name.
asset_name: Mapped[str] = mapped_column(String, nullable=False, info={"justification": JustificationType.TEXT})
asset_name: Mapped[str] = mapped_column(String, nullable=True, info={"justification": JustificationType.TEXT})

#: The URL for the provenance asset.
asset_url: Mapped[str] = mapped_column(String, nullable=True, info={"justification": JustificationType.HREF})
Expand Down Expand Up @@ -504,6 +504,12 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData:
CheckResultData
The result of the check.
"""
if ctx.dynamic_data["provenance"]:
return CheckResultData(
result_tables=[ProvenanceAvailableFacts(confidence=Confidence.HIGH)],
result_type=CheckResultType.PASSED,
)

provenance_extensions = defaults.get_list(
"slsa.verifier",
"provenance_extensions",
Expand Down
11 changes: 11 additions & 0 deletions src/macaron/slsa_analyzer/checks/provenance_l3_content_check.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,17 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData:
logger.info("%s check was unable to find any expectations.", self.check_info.check_id)
return CheckResultData(result_tables=[], result_type=CheckResultType.UNKNOWN)

if ctx.dynamic_data["provenance"]:
if expectation.validate(ctx.dynamic_data["provenance"]):
return CheckResultData(
result_tables=[expectation],
result_type=CheckResultType.PASSED,
)
return CheckResultData(
result_tables=[expectation],
result_type=CheckResultType.FAILED,
)

package_registry_info_entries = ctx.dynamic_data["package_registries"]
ci_services = ctx.dynamic_data["ci_services"]

Expand Down
2 changes: 2 additions & 0 deletions tests/artifact/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# 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/.
Loading