Skip to content

Commit 405ecff

Browse files
authored
feat: add support for GitHub provenances passed as input (#732)
GitHub has announced the Artifact Attestations feature to generate SLSA provenances for artifacts built on GitHub. This PR adds support for such provenances when passed as input. To test this feature, the attestation generated on an example maven project is passed as input, and the policy enforcement and generated VSA are checked. Signed-off-by: behnazh-w <[email protected]>
1 parent 2d0632c commit 405ecff

File tree

15 files changed

+251
-50
lines changed

15 files changed

+251
-50
lines changed

scripts/dev_scripts/integration_tests.sh

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -761,28 +761,40 @@ $RUN_POLICY -f $POLICY_FILE -d "$WORKSPACE/output/macaron.db" || log_fail
761761
check_or_update_expected_output $COMPARE_POLICIES $POLICY_RESULT $POLICY_EXPECTED || log_fail
762762

763763
echo -e "\n----------------------------------------------------------------------------------"
764-
echo "behnazh-w/example-maven-app as a local repository"
765-
echo "Test Witness provenance as an input, Cue expectation validation, Policy CLI and VSA generation."
764+
echo "behnazh-w/example-maven-app as a local and remote repository"
765+
echo "Test the Witness and GitHub provenances as an input, Cue expectation validation, Policy CLI and VSA generation."
766766
echo -e "----------------------------------------------------------------------------------\n"
767767
RUN_POLICY="macaron verify-policy"
768768
POLICY_FILE=$WORKSPACE/tests/policy_engine/resources/policies/example-maven-project/policy.dl
769769
POLICY_RESULT=$WORKSPACE/output/policy_report.json
770770
POLICY_EXPECTED=$WORKSPACE/tests/policy_engine/expected_results/example-maven-project/example_maven_project_policy_report.json
771771
VSA_RESULT=$WORKSPACE/output/vsa.intoto.jsonl
772-
VSA_PAYLOAD_EXPECTED=$WORKSPACE/tests/vsa/integration/local_witness_example-maven-project/vsa_payload.json
773-
EXPECTATION_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/expectations/cue/resources/valid_expectations/example-maven-project.cue
774-
PROVENANCE_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/resources/valid_provenances/example-maven-project.json
772+
VSA_PAYLOAD_EXPECTED=$WORKSPACE/tests/vsa/integration/example-maven-project/vsa_payload.json
773+
774+
# Test the local repo with Witness provenance.
775+
WITNESS_EXPECTATION_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/expectations/cue/resources/valid_expectations/witness-example-maven-project.cue
776+
WITNESS_PROVENANCE_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/resources/valid_provenances/witness-example-maven-project.json
775777

776778
# Cloning the repository locally
777779
git clone https://github.com/behnazh-w/example-maven-app.git $WORKSPACE/output/git_repos/local_repos/example-maven-app || log_fail
778780

779-
$RUN_MACARON analyze -pf $PROVENANCE_FILE -pe $EXPECTATION_FILE -purl pkg:maven/io.github.behnazh-w.demo/[email protected]?type=jar --repo-path example-maven-app --skip-deps || log_fail
781+
# Check the Witness provenance.
782+
$RUN_MACARON analyze -pf $WITNESS_PROVENANCE_FILE -pe $WITNESS_EXPECTATION_FILE -purl pkg:maven/io.github.behnazh-w.demo/[email protected]?type=jar --repo-path example-maven-app --skip-deps || log_fail
783+
784+
# Test the remote repo with GitHub provenance.
785+
GITHUB_EXPECTATION_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/expectations/cue/resources/valid_expectations/github-example-maven-project.cue
786+
GITHUB_PROVENANCE_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/resources/valid_provenances/github-example-maven-project.json
787+
788+
# Check the GitHub provenance.
789+
$RUN_MACARON analyze -pf $GITHUB_PROVENANCE_FILE -pe $GITHUB_EXPECTATION_FILE -purl pkg:maven/io.github.behnazh-w.demo/[email protected]?type=jar --skip-deps || log_fail
780790

791+
# Verify the policy and VSA for all the software components generated from behnazh-w/example-maven-app repo.
781792
$RUN_POLICY -f $POLICY_FILE -d "$WORKSPACE/output/macaron.db" || log_fail
782793

783-
check_or_update_expected_output $COMPARE_POLICIES $POLICY_RESULT $POLICY_EXPECTED || log_fail
794+
check_or_update_expected_output "$COMPARE_POLICIES" "$POLICY_RESULT" "$POLICY_EXPECTED" || log_fail
784795
check_or_update_expected_output "$COMPARE_VSA" "$VSA_RESULT" "$VSA_PAYLOAD_EXPECTED" || log_fail
785796

797+
786798
# Testing the Repo Finder's remote calls.
787799
# This requires the 'packageurl' Python module
788800
echo -e "\n----------------------------------------------------------------------------------"

scripts/dev_scripts/integration_tests_docker.sh

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -161,25 +161,37 @@ python $COMPARE_POLICIES $POLICY_RESULT $POLICY_EXPECTED || log_fail
161161
python "$COMPARE_VSA" "$VSA_RESULT" "$VSA_PAYLOAD_EXPECTED" || log_fail
162162

163163
echo -e "\n----------------------------------------------------------------------------------"
164-
echo "behnazh-w/example-maven-app as a local repository"
165-
echo "Test Witness provenance as an input, Cue expectation validation, Policy CLI and VSA generation."
164+
echo "behnazh-w/example-maven-app as a local and remote repository"
165+
echo "Test the Witness and GitHub provenances as an input, Cue expectation validation, Policy CLI and VSA generation."
166166
echo -e "----------------------------------------------------------------------------------\n"
167+
RUN_POLICY="macaron verify-policy"
167168
POLICY_FILE=$WORKSPACE/tests/policy_engine/resources/policies/example-maven-project/policy.dl
168169
POLICY_RESULT=$WORKSPACE/output/policy_report.json
169170
POLICY_EXPECTED=$WORKSPACE/tests/policy_engine/expected_results/example-maven-project/example_maven_project_policy_report.json
170171
VSA_RESULT=$WORKSPACE/output/vsa.intoto.jsonl
171-
VSA_PAYLOAD_EXPECTED=$WORKSPACE/tests/vsa/integration/local_witness_example-maven-project/vsa_payload.json
172-
EXPECTATION_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/expectations/cue/resources/valid_expectations/example-maven-project.cue
173-
PROVENANCE_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/resources/valid_provenances/example-maven-project.json
172+
VSA_PAYLOAD_EXPECTED=$WORKSPACE/tests/vsa/integration/example-maven-project/vsa_payload.json
173+
174+
# Test the local repo with Witness provenance.
175+
WITNESS_EXPECTATION_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/expectations/cue/resources/valid_expectations/witness-example-maven-project.cue
176+
WITNESS_PROVENANCE_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/resources/valid_provenances/witness-example-maven-project.json
174177

175178
# Cloning the repository locally
176179
git clone https://github.com/behnazh-w/example-maven-app.git $WORKSPACE/output/git_repos/local_repos/example-maven-app || log_fail
177180

178-
$RUN_MACARON_SCRIPT analyze -pf $PROVENANCE_FILE -pe $EXPECTATION_FILE -purl pkg:maven/io.github.behnazh-w.demo/[email protected]?type=jar --repo-path example-maven-app --skip-deps || log_fail
181+
# Check the Witness provenance.
182+
$RUN_MACARON_SCRIPT analyze -pf $WITNESS_PROVENANCE_FILE -pe $WITNESS_EXPECTATION_FILE -purl pkg:maven/io.github.behnazh-w.demo/[email protected]?type=jar --repo-path example-maven-app --skip-deps || log_fail
183+
184+
# Test the remote repo with GitHub provenance.
185+
GITHUB_EXPECTATION_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/expectations/cue/resources/valid_expectations/github-example-maven-project.cue
186+
GITHUB_PROVENANCE_FILE=$WORKSPACE/tests/slsa_analyzer/provenance/resources/valid_provenances/github-example-maven-project.json
179187

188+
# Check the GitHub provenance.
189+
$RUN_MACARON_SCRIPT analyze -pf $GITHUB_PROVENANCE_FILE -pe $GITHUB_EXPECTATION_FILE -purl pkg:maven/io.github.behnazh-w.demo/[email protected]?type=jar --skip-deps || log_fail
190+
191+
# Verify the policy and VSA for all the software components generated from behnazh-w/example-maven-app repo.
180192
$RUN_MACARON_SCRIPT verify-policy -f $POLICY_FILE -d "$WORKSPACE/output/macaron.db" || log_fail
181193

182-
python $COMPARE_POLICIES $POLICY_RESULT $POLICY_EXPECTED || log_fail
194+
python "$COMPARE_POLICIES" "$POLICY_RESULT" "$POLICY_EXPECTED" || log_fail
183195
python "$COMPARE_VSA" "$VSA_RESULT" "$VSA_PAYLOAD_EXPECTED" || log_fail
184196

185197
echo -e "\n----------------------------------------------------------------------------------"

src/macaron/artifact/maven.py

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@
33

44
"""This module declares types and utilities for Maven artifacts."""
55

6+
from collections.abc import Sequence
7+
68
from packageurl import PackageURL
79

10+
from macaron.config.defaults import defaults
811
from macaron.slsa_analyzer.provenance.intoto import InTotoPayload
912
from macaron.slsa_analyzer.provenance.intoto.v01 import InTotoV01Subject
1013
from macaron.slsa_analyzer.provenance.intoto.v1 import InTotoV1ResourceDescriptor
14+
from macaron.slsa_analyzer.provenance.slsa import extract_build_artifacts_from_slsa_subjects, is_slsa_provenance_payload
1115
from macaron.slsa_analyzer.provenance.witness import (
1216
extract_build_artifacts_from_witness_subjects,
1317
is_witness_provenance_payload,
@@ -46,15 +50,29 @@ def get_subject_in_provenance_matching_purl(
4650
if purl.type != "maven":
4751
return None
4852

49-
if not is_witness_provenance_payload(
53+
artifact_subjects: Sequence[InTotoV01Subject | InTotoV1ResourceDescriptor] = []
54+
if is_witness_provenance_payload(
5055
payload=provenance_payload,
5156
predicate_types=load_witness_verifier_config().predicate_types,
5257
):
58+
artifact_subjects = extract_build_artifacts_from_witness_subjects(provenance_payload)
59+
elif is_slsa_provenance_payload(
60+
payload=provenance_payload,
61+
predicate_types=defaults.get_list(
62+
"slsa.verifier",
63+
"predicate_types",
64+
fallback=[],
65+
),
66+
):
67+
artifact_subjects = extract_build_artifacts_from_slsa_subjects(provenance_payload)
68+
else:
5369
return None
54-
artifact_subjects = extract_build_artifacts_from_witness_subjects(provenance_payload)
5570

5671
for subject in artifact_subjects:
57-
_, _, artifact_filename = subject["name"].rpartition("/")
72+
subject_name = subject["name"]
73+
if not subject_name:
74+
continue
75+
_, _, artifact_filename = subject_name.rpartition("/")
5876
subject_purl = create_maven_purl_from_artifact_filename(
5977
artifact_filename=artifact_filename,
6078
group_id=purl.namespace,

src/macaron/config/defaults.ini

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,9 @@ max_download_size = 70000000
476476
timeout = 120
477477
# The allowed hostnames for URL file links for provenance download
478478
url_link_hostname_allowlist =
479+
predicate_types =
480+
https://slsa.dev/provenance/v0.2
481+
https://slsa.dev/provenance/v1
479482

480483
# Witness provenance. See: https://github.com/testifysec/witness.
481484
[provenance.witness]

src/macaron/slsa_analyzer/analyzer.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,8 @@ def run_single(
366366
analyze_ctx.component.purl.split("@")[0]
367367
)
368368
analyze_ctx.dynamic_data["provenance"] = provenance_payload
369+
if provenance_payload:
370+
analyze_ctx.dynamic_data["is_inferred_prov"] = False
369371
analyze_ctx.check_results = self.perform_checks(analyze_ctx)
370372

371373
return Record(

src/macaron/slsa_analyzer/provenance/loader.py

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import configparser
88
import gzip
99
import json
10+
import logging
1011
import zlib
1112
from urllib.parse import urlparse
1213

@@ -15,6 +16,8 @@
1516
from macaron.slsa_analyzer.provenance.intoto.errors import LoadIntotoAttestationError, ValidateInTotoPayloadError
1617
from macaron.util import JsonType, send_get_http_raw
1718

19+
logger: logging.Logger = logging.getLogger(__name__)
20+
1821

1922
def _try_read_url_link_file(file_content: bytes) -> str | None:
2023
parser = configparser.ConfigParser()
@@ -70,7 +73,15 @@ def _load_provenance_file_content(
7073
"Cannot deserialize the file content as JSON.",
7174
) from error
7275

73-
provenance_payload = provenance.get("payload", None)
76+
# The GitHub Attestation stores the DSSE envelope in `dsseEnvelope` property.
77+
dsse_envelope = provenance.get("dsseEnvelope", None)
78+
if dsse_envelope:
79+
provenance_payload = dsse_envelope.get("payload", None)
80+
logger.debug("Found dsseEnvelope property in the provenance.")
81+
else:
82+
# Some provenances, such as Witness may not include the DSSE envelope `dsseEnvelope`
83+
# property but contain its value directly.
84+
provenance_payload = provenance.get("payload", None)
7485
if not provenance_payload:
7586
raise LoadIntotoAttestationError(
7687
'Cannot find the "payload" field in the decoded provenance.',
@@ -104,6 +115,14 @@ def load_provenance_file(filepath: str) -> dict[str, JsonType]:
104115
a "URL" field inside an "InternetShortcut" section), it will be transparently
105116
downloaded.
106117
118+
Note: We have observed that GitHub provenances store the DSSE envelope using the
119+
`dsseEnvelope` property in the bundle. The bundle also includes Sigstore verification
120+
material, such as `publicKey` and `x509CertificateChain`. However, provenances generated by
121+
Witness and SLSA GitHub generator store the DSSE envelope content only.
122+
This function supports both types of provenances. See the Sigstore bundle schema, which is
123+
used in GitHub provenances:
124+
https://github.com/sigstore/protobuf-specs/blob/2bfc122984e8c30fc83f5892b2947af7d113b411/gen/jsonschema/schemas/Bundle.schema.json
125+
107126
Parameters
108127
----------
109128
filepath : str

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

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

44
"""This module implements SLSA provenance abstractions."""
@@ -7,6 +7,8 @@
77

88
from macaron.slsa_analyzer.asset import AssetLocator
99
from macaron.slsa_analyzer.provenance.intoto import InTotoPayload
10+
from macaron.slsa_analyzer.provenance.intoto.v01 import InTotoV01Subject
11+
from macaron.slsa_analyzer.provenance.intoto.v1 import InTotoV1ResourceDescriptor
1012

1113

1214
class SLSAProvenanceData(NamedTuple):
@@ -16,3 +18,56 @@ class SLSAProvenanceData(NamedTuple):
1618
asset: AssetLocator
1719
#: The provenance payload.
1820
payload: InTotoPayload
21+
22+
23+
def extract_build_artifacts_from_slsa_subjects(
24+
payload: InTotoPayload,
25+
) -> list[InTotoV01Subject | InTotoV1ResourceDescriptor]:
26+
"""Extract subjects that are build artifacts from the ``"subject"`` field of the provenance.
27+
28+
Each artifact subject is assumed to have a sha256 digest. If a sha256 digest is not present for
29+
a subject, that subject is ignored.
30+
31+
Parameters
32+
----------
33+
payload : InTotoPayload
34+
The provenance payload.
35+
36+
Returns
37+
-------
38+
list[InTotoV01Subject | InTotoV1ResourceDescriptor]
39+
A list of subjects in the ``"subject"`` field of the provenance that are build artifacts.
40+
"""
41+
subjects = payload.statement["subject"]
42+
artifact_subjects = []
43+
for subject in subjects:
44+
digest = subject["digest"]
45+
if not digest:
46+
continue
47+
sha256 = digest.get("sha256")
48+
if not sha256 or not isinstance(sha256, str):
49+
continue
50+
artifact_subjects.append(subject)
51+
52+
return artifact_subjects
53+
54+
55+
def is_slsa_provenance_payload(
56+
payload: InTotoPayload,
57+
predicate_types: list[str],
58+
) -> bool:
59+
"""Check if the given provenance payload is a SLSA provenance payload.
60+
61+
Parameters
62+
----------
63+
payload : InTotoPayload
64+
The provenance payload.
65+
predicate_types : list[str]
66+
The allowed values for the ``"predicateType"`` field of the provenance payload.
67+
68+
Returns
69+
-------
70+
bool
71+
``True`` if the payload is a witness provenance payload, ``False`` otherwise.
72+
"""
73+
return payload.statement["predicateType"] in predicate_types

tests/policy_engine/expected_results/example-maven-project/example_maven_project_policy_report.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,17 @@
33
[
44
"1",
55
"pkg:maven/io.github.behnazh-w.demo/[email protected]?type=jar",
6-
"gh_witness_provenance_policy"
6+
"example_maven_app_policy"
7+
],
8+
[
9+
"2",
10+
"pkg:maven/io.github.behnazh-w.demo/[email protected]?type=jar",
11+
"example_maven_app_policy"
712
]
813
],
914
"passed_policies": [
1015
[
11-
"gh_witness_provenance_policy"
16+
"example_maven_app_policy"
1217
]
1318
],
1419
"component_violates_policy": [],

tests/policy_engine/resources/policies/example-maven-project/policy.dl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,13 @@
33

44
#include "prelude.dl"
55

6-
Policy("gh_witness_provenance_policy", component_id, "Policy for github Maven project with witness provenances") :-
6+
Policy("example_maven_app_policy", component_id, "Policy for github Maven project with Witness and GitHub provenances") :-
77
check_passed(component_id, "mcn_build_service_1"),
88
check_passed(component_id, "mcn_build_script_1"),
99
check_passed(component_id, "mcn_provenance_available_1"),
1010
check_passed(component_id, "mcn_provenance_expectation_1").
1111

12-
apply_policy_to("gh_witness_provenance_policy", component_id) :-
12+
apply_policy_to("example_maven_app_policy", component_id) :-
1313
is_repo(
1414
_, // repo_id
1515
"github.com/behnazh-w/example-maven-app", // http URL to the repo but without the "http://"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
{
2+
target: "pkg:maven/io.github.behnazh-w.demo/example-maven-app",
3+
predicate: {
4+
buildDefinition: {
5+
externalParameters: {
6+
workflow: {
7+
ref: "refs/heads/main",
8+
repository: "https://github.com/behnazh-w/example-maven-app",
9+
path: ".github/workflows/main.yaml"
10+
}
11+
}
12+
}
13+
}
14+
}

0 commit comments

Comments
 (0)