Skip to content

Commit 7fa98d6

Browse files
committed
chore: match a witness provenance subject against a maven artifact purl
Signed-off-by: Nathan Nguyen <[email protected]>
1 parent 12066e3 commit 7fa98d6

File tree

5 files changed

+161
-12
lines changed

5 files changed

+161
-12
lines changed

src/macaron/artifact/maven.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,15 @@
1010

1111
from packageurl import PackageURL
1212

13+
from macaron.slsa_analyzer.provenance.intoto import InTotoPayload
14+
from macaron.slsa_analyzer.provenance.intoto.v01 import InTotoV01Subject
15+
from macaron.slsa_analyzer.provenance.intoto.v1 import InTotoV1ResourceDescriptor
16+
from macaron.slsa_analyzer.provenance.witness import (
17+
extract_build_artifacts_from_witness_subjects,
18+
is_witness_provenance_payload,
19+
load_witness_verifier_config,
20+
)
21+
1322

1423
class _MavenArtifactType(NamedTuple):
1524
filename_pattern: str
@@ -143,3 +152,52 @@ def from_artifact_name(
143152
artifact_type=maven_artifact_type,
144153
)
145154
return None
155+
156+
157+
class MavenSubjectPURLMatcher:
158+
"""A matcher matching a PURL identifying a Maven artifact to a provenance subject."""
159+
160+
@staticmethod
161+
def get_subject_in_provenance_matching_purl(
162+
provenance_payload: InTotoPayload, purl: PackageURL
163+
) -> InTotoV01Subject | InTotoV1ResourceDescriptor | None:
164+
"""Get the subject in the provenance matching the PURL.
165+
166+
In this case where the provenance is assumed to be built from a Java project,
167+
the subject must be a Maven artifact.
168+
169+
Parameters
170+
----------
171+
provenance_payload : InTotoPayload
172+
The provenance payload.
173+
purl : PackageURL
174+
The PackageURL identifying the matching subject.
175+
176+
Returns
177+
-------
178+
InTotoV01Subject | InTotoV1ResourceDescriptor | None
179+
The subject in the provenance matching the given PURL.
180+
"""
181+
if (maven_artifact := MavenArtifact.from_package_url(purl)) and is_witness_provenance_payload(
182+
payload=provenance_payload,
183+
predicate_types=load_witness_verifier_config().predicate_types,
184+
):
185+
artifact_subjects = extract_build_artifacts_from_witness_subjects(provenance_payload)
186+
187+
maven_artifact_subject_pairs = []
188+
for subject in artifact_subjects:
189+
_, _, artifact_name = subject["name"].rpartition("/")
190+
artifact = MavenArtifact.from_artifact_name(
191+
artifact_name=artifact_name,
192+
group_id=maven_artifact.group_id,
193+
version=maven_artifact.version,
194+
)
195+
if artifact is None:
196+
continue
197+
maven_artifact_subject_pairs.append((artifact, subject))
198+
199+
for artifact, subject in maven_artifact_subject_pairs:
200+
if artifact.package_url == purl:
201+
return subject
202+
203+
return None

src/macaron/database/table_definitions.py

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import string
1616
from datetime import datetime
1717
from pathlib import Path
18-
from typing import Any
18+
from typing import Any, Self
1919

2020
from packageurl import PackageURL
2121
from sqlalchemy import (
@@ -32,9 +32,11 @@
3232
)
3333
from sqlalchemy.orm import Mapped, mapped_column, relationship
3434

35+
from macaron.artifact.maven import MavenSubjectPURLMatcher
3536
from macaron.database.database_manager import ORMBase
3637
from macaron.database.rfc3339_datetime import RFC3339DateTime
3738
from macaron.errors import InvalidPURLError
39+
from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, ProvenanceSubjectPURLMatcher
3840
from macaron.slsa_analyzer.slsa_req import ReqName
3941

4042
logger: logging.Logger = logging.getLogger(__name__)
@@ -563,3 +565,43 @@ class ProvenanceSubject(ORMBase):
563565

564566
#: The SHA256 hash of the subject.
565567
sha256: Mapped[str] = mapped_column(String, nullable=False)
568+
569+
@classmethod
570+
def from_purl_and_provenance(
571+
cls,
572+
purl: PackageURL,
573+
provenance_payload: InTotoPayload,
574+
) -> Self | None:
575+
"""Create a ``ProvenanceSubject`` entry if there is a provenance subject matching the PURL.
576+
577+
Parameters
578+
----------
579+
purl : PackageURL
580+
The PackageURL identifying the software component being analyzed.
581+
provenance_payload : InTotoPayload
582+
The provenance payload.
583+
584+
Returns
585+
-------
586+
Self | None
587+
A ``ProvenanceSubject`` entry with the SHA256 digest of the provenance subject
588+
matching the given PURL.
589+
"""
590+
subject_artifact_types: list[ProvenanceSubjectPURLMatcher] = [MavenSubjectPURLMatcher]
591+
592+
for subject_artifact_type in subject_artifact_types:
593+
subject = subject_artifact_type.get_subject_in_provenance_matching_purl(
594+
provenance_payload,
595+
purl,
596+
)
597+
if subject is None:
598+
return None
599+
digest = subject["digest"]
600+
if digest is None:
601+
return None
602+
sha256 = digest.get("sha256")
603+
if not sha256:
604+
return None
605+
return cls(sha256=sha256)
606+
607+
return None

src/macaron/slsa_analyzer/analyzer.py

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
from macaron.config.global_config import global_config
2020
from macaron.config.target_config import Configuration
2121
from macaron.database.database_manager import DatabaseManager, get_db_manager, get_db_session
22-
from macaron.database.table_definitions import Analysis, Component, Repository
22+
from macaron.database.table_definitions import Analysis, Component, ProvenanceSubject, Repository
2323
from macaron.dependency_analyzer import DependencyAnalyzer, DependencyInfo
2424
from macaron.errors import (
2525
CloneError,
@@ -332,7 +332,12 @@ def run_single(
332332
# Create the component.
333333
component = None
334334
try:
335-
component = self.add_component(analysis, analysis_target, existing_records)
335+
component = self.add_component(
336+
analysis,
337+
analysis_target,
338+
existing_records,
339+
provenance_payload,
340+
)
336341
except PURLNotFoundError as error:
337342
logger.error(error)
338343
return Record(
@@ -484,6 +489,7 @@ def add_component(
484489
analysis: Analysis,
485490
analysis_target: AnalysisTarget,
486491
existing_records: dict[str, Record] | None = None,
492+
provenance_payload: InTotoPayload | None = None,
487493
) -> Component:
488494
"""Add a software component if it does not exist in the DB already.
489495
@@ -547,18 +553,30 @@ def add_component(
547553
raise PURLNotFoundError(
548554
f"The repository {analysis_target.repo_path} is not available and no PURL is provided from the user."
549555
)
550-
551-
repo_snapshot_purl = PackageURL(
556+
purl = PackageURL(
552557
type=repository.type,
553558
namespace=repository.owner,
554559
name=repository.name,
555560
version=repository.commit_sha,
556561
)
557-
return Component(purl=str(repo_snapshot_purl), analysis=analysis, repository=repository)
562+
else:
563+
# If the PURL is available, we always create the software component with it whether the repository is
564+
# available or not.
565+
purl = analysis_target.parsed_purl
566+
567+
component = Component(
568+
purl=str(purl),
569+
analysis=analysis,
570+
repository=repository,
571+
)
572+
573+
if provenance_payload:
574+
component.provenance_subject = ProvenanceSubject.from_purl_and_provenance(
575+
purl=purl,
576+
provenance_payload=provenance_payload,
577+
)
558578

559-
# If the PURL is available, we always create the software component with it whether the repository is
560-
# available or not.
561-
return Component(purl=str(analysis_target.parsed_purl), analysis=analysis, repository=repository)
579+
return component
562580

563581
@staticmethod
564582
def parse_purl(config: Configuration) -> PackageURL | None:

src/macaron/slsa_analyzer/provenance/intoto/__init__.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,14 @@
66
from __future__ import annotations
77

88
from collections.abc import Mapping
9-
from typing import NamedTuple, TypeVar
9+
from typing import NamedTuple, Protocol, TypeVar
10+
11+
from packageurl import PackageURL
1012

1113
from macaron.slsa_analyzer.provenance.intoto import v01, v1
1214
from macaron.slsa_analyzer.provenance.intoto.errors import ValidateInTotoPayloadError
15+
from macaron.slsa_analyzer.provenance.intoto.v01 import InTotoV01Subject
16+
from macaron.slsa_analyzer.provenance.intoto.v1 import InTotoV1ResourceDescriptor
1317
from macaron.util import JsonType
1418

1519
# Type of an in-toto statement.
@@ -119,3 +123,31 @@ def validate_intoto_payload(payload: dict[str, JsonType]) -> InTotoPayload:
119123
raise error
120124

121125
raise ValidateInTotoPayloadError("Invalid value for the attribute '_type' of the provenance payload.")
126+
127+
128+
class ProvenanceSubjectPURLMatcher(Protocol):
129+
"""Interface for a matcher that matches a PURL to a subject in the provenance."""
130+
131+
@staticmethod
132+
def get_subject_in_provenance_matching_purl(
133+
provenance_payload: InTotoPayload,
134+
purl: PackageURL,
135+
) -> InTotoV01Subject | InTotoV1ResourceDescriptor | None:
136+
"""Obtain the subject in the provenance payload matching the given PackageURL.
137+
138+
This function assumes there is only one such subject. If there are multiple
139+
such subjects, the first matching subject is returned. However, this should not
140+
happen since the PackageURL should be specific enough to identify a single subject.
141+
142+
Parameters
143+
----------
144+
provenance_payload : InTotoPayload
145+
The provenance payload.
146+
purl : PackageURL
147+
The PackageURL identifying the matching subject.
148+
149+
Returns
150+
-------
151+
InTotoV01Subject | InTotoV1ResourceDescriptor | None
152+
The subject in the provenance matching the given PURL.
153+
"""

tests/artifact/test_maven.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@
66
import pytest
77
from packageurl import PackageURL
88

9-
from macaron.artifact.maven import MavenArtifact, MavenArtifactType
10-
# , MavenSubjectPURLMatcher
9+
from macaron.artifact.maven import MavenArtifact, MavenArtifactType, MavenSubjectPURLMatcher
1110
from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, validate_intoto_payload
1211

1312

0 commit comments

Comments
 (0)