Skip to content

Commit f2ed7f7

Browse files
committed
chore: introduce maven artifact types and utilities
Signed-off-by: Nathan Nguyen <[email protected]>
1 parent 1b82d5e commit f2ed7f7

File tree

3 files changed

+352
-0
lines changed

3 files changed

+352
-0
lines changed

src/macaron/artifact/maven.py

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved.
2+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
3+
4+
"""This module declares types and utilities for Maven artifacts."""
5+
6+
import re
7+
from dataclasses import dataclass
8+
from enum import Enum
9+
from typing import NamedTuple, Self
10+
11+
from packageurl import PackageURL
12+
13+
14+
class _MavenArtifactType(NamedTuple):
15+
filename_pattern: str
16+
purl_qualifiers: dict[str, str]
17+
18+
19+
class MavenArtifactType(_MavenArtifactType, Enum):
20+
"""Maven artifact types that Macaron supports.
21+
22+
For reference, see:
23+
- https://maven.apache.org/ref/3.9.6/maven-core/artifact-handlers.html
24+
- https://github.com/package-url/purl-spec/blob/master/PURL-TYPES.rst#maven
25+
"""
26+
27+
# Enum with custom value type.
28+
# See https://docs.python.org/3.10/library/enum.html#others.
29+
JAR = _MavenArtifactType(
30+
filename_pattern="{artifact_id}-{version}.jar",
31+
purl_qualifiers={"type": "jar"},
32+
)
33+
POM = _MavenArtifactType(
34+
filename_pattern="{artifact_id}-{version}.pom",
35+
purl_qualifiers={"type": "pom"},
36+
)
37+
JAVADOC = _MavenArtifactType(
38+
filename_pattern="{artifact_id}-{version}-javadoc.jar",
39+
purl_qualifiers={"type": "javadoc"},
40+
)
41+
JAVA_SOURCE = _MavenArtifactType(
42+
filename_pattern="{artifact_id}-{version}-sources.jar",
43+
purl_qualifiers={"type": "sources"},
44+
)
45+
46+
47+
@dataclass
48+
class MavenArtifact:
49+
"""A Maven artifact."""
50+
51+
group_id: str
52+
artifact_id: str
53+
version: str
54+
artifact_type: MavenArtifactType
55+
56+
@property
57+
def package_url(self) -> PackageURL:
58+
"""Get the PackageURL of this Maven artifact."""
59+
return PackageURL(
60+
type="maven",
61+
namespace=self.group_id,
62+
name=self.artifact_id,
63+
version=self.version,
64+
qualifiers=self.artifact_type.purl_qualifiers,
65+
)
66+
67+
@classmethod
68+
def from_package_url(cls, package_url: PackageURL) -> Self | None:
69+
"""Create a Maven artifact from a PackageURL.
70+
71+
Parameters
72+
----------
73+
package_url : PackageURL
74+
The PackageURL identifying a Maven artifact.
75+
76+
Returns
77+
-------
78+
Self | None
79+
A Maven artifact, or ``None`` if the PURL is not a valid Maven artifact PURL, or if
80+
the artifact type is not supported.
81+
For supported artifact types, see :class:`MavenArtifactType`.
82+
"""
83+
if not package_url.namespace:
84+
return None
85+
if not package_url.version:
86+
return None
87+
if package_url.type != "maven":
88+
return None
89+
maven_artifact_type = None
90+
for artifact_type in MavenArtifactType:
91+
if artifact_type.purl_qualifiers == package_url.qualifiers:
92+
maven_artifact_type = artifact_type
93+
break
94+
if not maven_artifact_type:
95+
return None
96+
return cls(
97+
group_id=package_url.namespace,
98+
artifact_id=package_url.name,
99+
version=package_url.version,
100+
artifact_type=maven_artifact_type,
101+
)
102+
103+
@classmethod
104+
def from_artifact_name(
105+
cls,
106+
artifact_name: str,
107+
group_id: str,
108+
version: str,
109+
) -> Self | None:
110+
"""Create a Maven artifact given an artifact name.
111+
112+
The artifact type is determined based on the naming pattern of the artifact.
113+
114+
Parameters
115+
----------
116+
artifact_name : str
117+
The artifact name.
118+
group_id : str
119+
The group id.
120+
version : str
121+
The version
122+
123+
Returns
124+
-------
125+
Self | None
126+
A Maven artifact, or ``None`` if the PURL is not a valid Maven artifact PURL, or if
127+
the artifact type is not supported.
128+
For supported artifact types, see :class:`MavenArtifactType`.
129+
"""
130+
for maven_artifact_type in MavenArtifactType:
131+
pattern = maven_artifact_type.filename_pattern.format(
132+
artifact_id="(.*)",
133+
version=version,
134+
)
135+
match_result = re.search(pattern, artifact_name)
136+
if not match_result:
137+
continue
138+
artifact_id = match_result.group(1)
139+
return cls(
140+
group_id=group_id,
141+
artifact_id=artifact_id,
142+
version=version,
143+
artifact_type=maven_artifact_type,
144+
)
145+
return None

tests/artifact/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved.
2+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.

tests/artifact/test_maven.py

Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
# Copyright (c) 2024 - 2024, Oracle and/or its affiliates. All rights reserved.
2+
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
3+
4+
"""Tests for types and utilities for Maven artifacts."""
5+
6+
import pytest
7+
from packageurl import PackageURL
8+
9+
from macaron.artifact.maven import MavenArtifact, MavenArtifactType
10+
# , MavenSubjectPURLMatcher
11+
from macaron.slsa_analyzer.provenance.intoto import InTotoPayload, validate_intoto_payload
12+
13+
14+
@pytest.mark.parametrize(
15+
("purl_str", "maven_artifact"),
16+
[
17+
pytest.param(
18+
"pkg:maven/com.fasterxml.jackson/[email protected]?type=jar",
19+
MavenArtifact(
20+
group_id="com.fasterxml.jackson",
21+
artifact_id="jackson-annotations",
22+
version="2.9.9",
23+
artifact_type=MavenArtifactType.JAR,
24+
),
25+
id="purl for jar artifact",
26+
),
27+
pytest.param(
28+
"pkg:maven/com.fasterxml.jackson/[email protected]?type=javadoc",
29+
MavenArtifact(
30+
group_id="com.fasterxml.jackson",
31+
artifact_id="jackson-annotations",
32+
version="2.9.9",
33+
artifact_type=MavenArtifactType.JAVADOC,
34+
),
35+
id="purl for javadoc artifact",
36+
),
37+
pytest.param(
38+
"pkg:maven/com.fasterxml.jackson/[email protected]?type=sources",
39+
MavenArtifact(
40+
group_id="com.fasterxml.jackson",
41+
artifact_id="jackson-annotations",
42+
version="2.9.9",
43+
artifact_type=MavenArtifactType.JAVA_SOURCE,
44+
),
45+
id="purl for java source artifact",
46+
),
47+
pytest.param(
48+
"pkg:maven/com.fasterxml.jackson/[email protected]?type=pom",
49+
MavenArtifact(
50+
group_id="com.fasterxml.jackson",
51+
artifact_id="jackson-annotations",
52+
version="2.9.9",
53+
artifact_type=MavenArtifactType.POM,
54+
),
55+
id="purl for pom artifact",
56+
),
57+
],
58+
)
59+
def test_maven_artifact_from_purl(purl_str: str, maven_artifact: MavenArtifact) -> None:
60+
"""Test creating a ``MavenArtifact`` object given a PackageURL."""
61+
assert MavenArtifact.from_package_url(PackageURL.from_string(purl_str)) == maven_artifact
62+
63+
64+
@pytest.mark.parametrize(
65+
("params", "maven_artifact"),
66+
[
67+
pytest.param(
68+
{
69+
"artifact_name": "jackson-annotations-2.9.9.jar",
70+
"group_id": "com.fasterxml.jackson",
71+
"version": "2.9.9",
72+
},
73+
MavenArtifact(
74+
group_id="com.fasterxml.jackson",
75+
artifact_id="jackson-annotations",
76+
version="2.9.9",
77+
artifact_type=MavenArtifactType.JAR,
78+
),
79+
id="jar artifact",
80+
),
81+
pytest.param(
82+
{
83+
"artifact_name": "jackson-annotations-2.9.9-javadoc.jar",
84+
"group_id": "com.fasterxml.jackson",
85+
"version": "2.9.9",
86+
},
87+
MavenArtifact(
88+
group_id="com.fasterxml.jackson",
89+
artifact_id="jackson-annotations",
90+
version="2.9.9",
91+
artifact_type=MavenArtifactType.JAVADOC,
92+
),
93+
id="javadoc artifact",
94+
),
95+
pytest.param(
96+
{
97+
"artifact_name": "jackson-annotations-2.9.9-sources.jar",
98+
"group_id": "com.fasterxml.jackson",
99+
"version": "2.9.9",
100+
},
101+
MavenArtifact(
102+
group_id="com.fasterxml.jackson",
103+
artifact_id="jackson-annotations",
104+
version="2.9.9",
105+
artifact_type=MavenArtifactType.JAVA_SOURCE,
106+
),
107+
id="java-source artifact",
108+
),
109+
pytest.param(
110+
{
111+
"artifact_name": "jackson-annotations-2.9.9.pom",
112+
"group_id": "com.fasterxml.jackson",
113+
"version": "2.9.9",
114+
},
115+
MavenArtifact(
116+
group_id="com.fasterxml.jackson",
117+
artifact_id="jackson-annotations",
118+
version="2.9.9",
119+
artifact_type=MavenArtifactType.POM,
120+
),
121+
id="pom artifact",
122+
),
123+
],
124+
)
125+
def test_maven_artifact_from_artifact_name(params: dict, maven_artifact: MavenArtifact) -> None:
126+
"""Test creating a ``MavenArtifact`` object given an artifact name."""
127+
assert MavenArtifact.from_artifact_name(**params) == maven_artifact
128+
129+
130+
@pytest.mark.parametrize(
131+
("purl_str", "subject_index"),
132+
[
133+
pytest.param(
134+
"pkg:maven/com.fasterxml.jackson/[email protected]?type=jar",
135+
0,
136+
id="purl for jar artifact",
137+
),
138+
pytest.param(
139+
"pkg:maven/com.fasterxml.jackson/[email protected]?type=javadoc",
140+
1,
141+
id="purl for javadoc artifact",
142+
),
143+
pytest.param(
144+
"pkg:maven/com.fasterxml.jackson/[email protected]?type=sources",
145+
2,
146+
id="purl for java source artifact",
147+
),
148+
pytest.param(
149+
"pkg:maven/com.fasterxml.jackson/[email protected]?type=pom",
150+
3,
151+
id="purl for pom artifact",
152+
),
153+
],
154+
)
155+
def test_to_maven_artifact_subject(
156+
purl_str: str,
157+
subject_index: int,
158+
) -> None:
159+
"""Test constructing a ``MavenArtifact`` object from a given artifact name."""
160+
purl = PackageURL.from_string(purl_str)
161+
provenance_payload: InTotoPayload = validate_intoto_payload(
162+
{
163+
"_type": "https://in-toto.io/Statement/v0.1",
164+
"subject": [
165+
{
166+
"name": "https://witness.dev/attestations/product/v0.1/file:target/jackson-annotations-2.9.9.jar",
167+
"digest": {
168+
"sha256": "6f97fe2094bd50435d6fbb7a2f6c2638fe44e6af17cfff98ce111d0abfffe17e",
169+
},
170+
},
171+
{
172+
"name": "https://witness.dev/attestations/product/v0.1/file:target/jackson-annotations-2.9.9-javadoc.jar",
173+
"digest": {
174+
"sha256": "6f97fe2094bd50435d6fbb7a2f6c2638fe44e6af17cfff98ce111d0abfffe17e",
175+
},
176+
},
177+
{
178+
"name": "https://witness.dev/attestations/product/v0.1/file:target/jackson-annotations-2.9.9-sources.jar",
179+
"digest": {
180+
"sha256": "6f97fe2094bd50435d6fbb7a2f6c2638fe44e6af17cfff98ce111d0abfffe17e",
181+
},
182+
},
183+
{
184+
"name": "https://witness.dev/attestations/product/v0.1/file:target/jackson-annotations-2.9.9.pom",
185+
"digest": {
186+
"sha256": "6f97fe2094bd50435d6fbb7a2f6c2638fe44e6af17cfff98ce111d0abfffe17e",
187+
},
188+
},
189+
{
190+
"name": "https://witness.dev/attestations/product/v0.1/file:target/foobar.txt",
191+
"digest": {
192+
"sha256": "6f97fe2094bd50435d6fbb7a2f6c2638fe44e6af17cfff98ce111d0abfffe17e",
193+
},
194+
},
195+
],
196+
"predicateType": "https://witness.testifysec.com/attestation-collection/v0.1",
197+
}
198+
)
199+
assert (
200+
MavenSubjectPURLMatcher.get_subject_in_provenance_matching_purl(
201+
provenance_payload=provenance_payload,
202+
purl=purl,
203+
)
204+
== provenance_payload.statement["subject"][subject_index]
205+
)

0 commit comments

Comments
 (0)