Skip to content

Commit f5ed45e

Browse files
committed
chore: add back provenance asset information
Signed-off-by: Ben Selwyn-Smith <[email protected]>
1 parent b4e0944 commit f5ed45e

File tree

5 files changed

+80
-36
lines changed

5 files changed

+80
-36
lines changed

src/macaron/database/table_definitions.py

+6
Original file line numberDiff line numberDiff line change
@@ -499,6 +499,12 @@ class Provenance(ORMBase):
499499
#: The provenance payload.
500500
provenance_payload: Mapped[InTotoPayload] = mapped_column(ProvenancePayload, nullable=False)
501501

502+
#: The name of the provenance asset.
503+
provenance_asset_name: Mapped[str] = mapped_column(String, nullable=True)
504+
505+
#: The URL of the provenance asset.
506+
provenance_asset_url: Mapped[str] = mapped_column(String, nullable=True)
507+
502508
#: The verified status of the provenance.
503509
verified: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
504510

src/macaron/provenance/provenance_finder.py

+32-16
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import logging
66
import os
77
import tempfile
8+
from dataclasses import dataclass
89
from functools import partial
910

1011
from packageurl import PackageURL
@@ -28,6 +29,15 @@
2829
logger: logging.Logger = logging.getLogger(__name__)
2930

3031

32+
@dataclass(frozen=True)
33+
class ProvenanceAsset:
34+
"""This class exists to hold a provenance payload with the original asset's name and URL."""
35+
36+
payload: InTotoPayload
37+
name: str
38+
url: str
39+
40+
3141
class ProvenanceFinder:
3242
"""This class is used to find and retrieve provenance files from supported registries."""
3343

@@ -42,7 +52,7 @@ def __init__(self) -> None:
4252
elif isinstance(registry, JFrogMavenRegistry):
4353
self.jfrog_registry = registry
4454

45-
def find_provenance(self, purl: PackageURL) -> list[InTotoPayload]:
55+
def find_provenance(self, purl: PackageURL) -> list[ProvenanceAsset]:
4656
"""Find the provenance file(s) of the passed PURL.
4757
4858
Parameters
@@ -52,8 +62,8 @@ def find_provenance(self, purl: PackageURL) -> list[InTotoPayload]:
5262
5363
Returns
5464
-------
55-
list[InTotoPayload]
56-
The provenance payload, or an empty list if not found.
65+
list[ProvenanceAsset]
66+
The provenance asset, or an empty list if not found.
5767
"""
5868
logger.debug("Seeking provenance of: %s", purl)
5969

@@ -82,7 +92,7 @@ def find_provenance(self, purl: PackageURL) -> list[InTotoPayload]:
8292
logger.debug("Provenance finding not supported for PURL type: %s", purl.type)
8393
return []
8494

85-
def _find_provenance(self, discovery_functions: list[partial[list[InTotoPayload]]]) -> list[InTotoPayload]:
95+
def _find_provenance(self, discovery_functions: list[partial[list[ProvenanceAsset]]]) -> list[ProvenanceAsset]:
8696
"""Find the provenance file(s) using the passed discovery functions.
8797
8898
Parameters
@@ -93,7 +103,7 @@ def _find_provenance(self, discovery_functions: list[partial[list[InTotoPayload]
93103
Returns
94104
-------
95105
list[InTotoPayload]
96-
The provenance payload(s) from the first successful function, or an empty list if none were.
106+
The provenance asset(s) from the first successful function, or an empty list if none were.
97107
"""
98108
if not discovery_functions:
99109
return []
@@ -108,7 +118,7 @@ def _find_provenance(self, discovery_functions: list[partial[list[InTotoPayload]
108118
return []
109119

110120

111-
def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[InTotoPayload]:
121+
def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[ProvenanceAsset]:
112122
"""Find and download the NPM based provenance for the passed PURL.
113123
114124
Two kinds of attestation can be retrieved from npm: "Provenance" and "Publish". The "Provenance" attestation
@@ -125,8 +135,8 @@ def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[InTotoP
125135
126136
Returns
127137
-------
128-
list[InTotoPayload]
129-
The provenance payload(s), or an empty list if not found.
138+
list[ProvenanceAsset]
139+
The provenance asset(s), or an empty list if not found.
130140
"""
131141
if not registry.enabled:
132142
logger.debug("The npm registry is not enabled.")
@@ -172,16 +182,19 @@ def find_npm_provenance(purl: PackageURL, registry: NPMRegistry) -> list[InTotoP
172182
publish_payload = load_provenance_payload(signed_download_path)
173183
except LoadIntotoAttestationError as error:
174184
logger.error("Error while loading publish attestation: %s", error)
175-
return [provenance_payload]
185+
return [ProvenanceAsset(provenance_payload, npm_provenance_asset.name, npm_provenance_asset.url)]
176186

177-
return [provenance_payload, publish_payload]
187+
return [
188+
ProvenanceAsset(provenance_payload, npm_provenance_asset.name, npm_provenance_asset.url),
189+
ProvenanceAsset(publish_payload, npm_provenance_asset.name, npm_provenance_asset.url),
190+
]
178191

179192
except OSError as error:
180193
logger.error("Error while storing provenance in the temporary directory: %s", error)
181194
return []
182195

183196

184-
def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[InTotoPayload]:
197+
def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[ProvenanceAsset]:
185198
"""Find and download the GAV based provenance for the passed PURL.
186199
187200
Parameters
@@ -193,8 +206,8 @@ def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[
193206
194207
Returns
195208
-------
196-
list[InTotoPayload] | None
197-
The provenance payload if found, or an empty list otherwise.
209+
list[ProvenanceAsset] | None
210+
The provenance asset if found, or an empty list otherwise.
198211
199212
Raises
200213
------
@@ -263,7 +276,7 @@ def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[
263276
if not is_witness_provenance_payload(provenance_payload, witness_verifier_config.predicate_types):
264277
continue
265278

266-
provenances.append(provenance_payload)
279+
provenances.append(ProvenanceAsset(provenance_payload, provenance_asset.name, provenance_asset.url))
267280
except OSError as error:
268281
logger.error("Error while storing provenance in the temporary directory: %s", error)
269282

@@ -277,7 +290,7 @@ def find_gav_provenance(purl: PackageURL, registry: JFrogMavenRegistry) -> list[
277290

278291
def find_provenance_from_ci(
279292
analyze_ctx: AnalyzeContext, git_obj: Git | None, download_path: str
280-
) -> InTotoPayload | None:
293+
) -> ProvenanceAsset | None:
281294
"""Try to find provenance from CI services of the repository.
282295
283296
Note that we stop going through the CI services once we encounter a CI service
@@ -372,7 +385,10 @@ def find_provenance_from_ci(
372385
download_provenances_from_ci_service(ci_info, download_path)
373386

374387
# TODO consider how to handle multiple payloads here.
375-
return ci_info["provenances"][0].payload if ci_info["provenances"] else None
388+
if ci_info["provenances"]:
389+
provenance = ci_info["provenances"][0]
390+
return ProvenanceAsset(provenance.payload, provenance.asset.name, provenance.asset.url)
391+
return None
376392

377393
else:
378394
logger.debug("CI service not supported for provenance finding: %s", ci_service.name)

src/macaron/provenance/provenance_verifier.py

+15-11
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from macaron.config.defaults import defaults
1818
from macaron.config.global_config import global_config
1919
from macaron.provenance.provenance_extractor import ProvenancePredicate
20+
from macaron.provenance.provenance_finder import ProvenanceAsset
2021
from macaron.repo_finder.commit_finder import AbstractPurlType, determine_abstract_purl_type
2122
from macaron.slsa_analyzer.analyze_context import AnalyzeContext
2223
from macaron.slsa_analyzer.asset import AssetLocator
@@ -28,15 +29,15 @@
2829
logger: logging.Logger = logging.getLogger(__name__)
2930

3031

31-
def verify_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool:
32+
def verify_provenance(purl: PackageURL, provenance_assets: list[ProvenanceAsset]) -> bool:
3233
"""Verify the passed provenance.
3334
3435
Parameters
3536
----------
3637
purl: PackageURL
3738
The PURL of the analysis target.
38-
provenance: list[InTotoPayload]
39-
The list of provenance.
39+
provenance_assets: list[ProvenanceAsset]
40+
The list of provenance assets.
4041
4142
Returns
4243
-------
@@ -50,7 +51,7 @@ def verify_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool
5051
verification_function = None
5152

5253
if purl.type == "npm":
53-
verification_function = partial(verify_npm_provenance, purl, provenance)
54+
verification_function = partial(verify_npm_provenance, purl, provenance_assets)
5455

5556
# TODO other verification functions go here.
5657

@@ -61,30 +62,33 @@ def verify_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool
6162
return False
6263

6364

64-
def verify_npm_provenance(purl: PackageURL, provenance: list[InTotoPayload]) -> bool:
65+
def verify_npm_provenance(purl: PackageURL, provenance_assets: list[ProvenanceAsset]) -> bool:
6566
"""Compare the unsigned payload subject digest with the signed payload digest, if available.
6667
6768
Parameters
6869
----------
6970
purl: PackageURL
7071
The PURL of the analysis target.
71-
provenance: list[InTotoPayload]
72-
The provenances to verify.
72+
provenance_assets: list[ProvenanceAsset]
73+
The provenance assets to verify.
7374
7475
Returns
7576
-------
7677
bool
7778
True if the provenance was verified, or False otherwise.
7879
"""
79-
if len(provenance) != 2:
80-
logger.debug("Expected unsigned and signed provenance.")
80+
if len(provenance_assets) != 2:
81+
logger.debug("Expected unsigned and signed provenance assets.")
8182
return False
8283

83-
signed_subjects = provenance[1].statement.get("subject")
84+
signed_provenance = provenance_assets[1].payload
85+
unsigned_provenance = provenance_assets[0].payload
86+
87+
signed_subjects = signed_provenance.statement.get("subject")
8488
if not signed_subjects:
8589
return False
8690

87-
unsigned_subjects = provenance[0].statement.get("subject")
91+
unsigned_subjects = unsigned_provenance.statement.get("subject")
8892
if not unsigned_subjects:
8993
return False
9094

src/macaron/slsa_analyzer/analyzer.py

+8-3
Original file line numberDiff line numberDiff line change
@@ -340,12 +340,14 @@ def run_single(
340340
)
341341

342342
provenance_is_verified = False
343+
provenance_asset = None
343344
if not provenance_payload and parsed_purl:
344345
# Try to find the provenance file for the parsed PURL.
345346
provenance_finder = ProvenanceFinder()
346347
provenances = provenance_finder.find_provenance(parsed_purl)
347348
if provenances:
348-
provenance_payload = provenances[0]
349+
provenance_asset = provenances[0]
350+
provenance_payload = provenance_asset.payload
349351
if defaults.getboolean("analyzer", "verify_provenance"):
350352
provenance_is_verified = verify_provenance(parsed_purl, provenances)
351353

@@ -454,10 +456,11 @@ def run_single(
454456
if not provenance_payload:
455457
# Look for provenance using the CI.
456458
with tempfile.TemporaryDirectory() as temp_dir:
457-
provenance_payload = find_provenance_from_ci(analyze_ctx, git_obj, temp_dir)
459+
provenance_asset = find_provenance_from_ci(analyze_ctx, git_obj, temp_dir)
458460
# If found, validate analysis target against new provenance.
459-
if provenance_payload:
461+
if provenance_asset:
460462
# If repository URL was not provided as input, check the one found during analysis.
463+
provenance_payload = provenance_asset.payload
461464
if not repo_path_input and component.repository:
462465
repo_path_input = component.repository.remote_path
463466
provenance_repo_url = provenance_commit_digest = None
@@ -501,6 +504,8 @@ def run_single(
501504
provenance_payload=provenance_payload,
502505
slsa_level=slsa_level,
503506
slsa_version=slsa_version,
507+
provenance_asset_name=provenance_asset.name if provenance_asset else None,
508+
provenance_asset_url=provenance_asset.url if provenance_asset else None,
504509
# TODO Add release tag, release digest.
505510
)
506511

src/macaron/slsa_analyzer/checks/provenance_available_check.py

+19-6
Original file line numberDiff line numberDiff line change
@@ -74,18 +74,31 @@ def run_check(self, ctx: AnalyzeContext) -> CheckResultData:
7474
CheckResultData
7575
The result of the check.
7676
"""
77-
available = (
78-
ctx.dynamic_data["provenance_info"]
79-
and ctx.dynamic_data["provenance_info"].provenance_payload
80-
and not ctx.dynamic_data["is_inferred_prov"]
81-
)
77+
provenance_info = None
78+
inferred = False
79+
if ctx.dynamic_data["provenance_info"]:
80+
provenance_info = ctx.dynamic_data["provenance_info"]
81+
inferred = ctx.dynamic_data["is_inferred_prov"]
82+
83+
if not provenance_info or not provenance_info.provenance_payload or inferred:
84+
return CheckResultData(
85+
result_tables=[
86+
ProvenanceAvailableFacts(
87+
confidence=Confidence.HIGH,
88+
)
89+
],
90+
result_type=CheckResultType.FAILED,
91+
)
92+
8293
return CheckResultData(
8394
result_tables=[
8495
ProvenanceAvailableFacts(
8596
confidence=Confidence.HIGH,
97+
asset_name=provenance_info.provenance_asset_name,
98+
asset_url=provenance_info.provenance_asset_url,
8699
)
87100
],
88-
result_type=CheckResultType.PASSED if available else CheckResultType.FAILED,
101+
result_type=CheckResultType.PASSED,
89102
)
90103

91104

0 commit comments

Comments
 (0)