diff --git a/examples/jenkins-slsa/Jenkinsfile b/examples/jenkins-slsa/Jenkinsfile new file mode 100644 index 0000000..151fe05 --- /dev/null +++ b/examples/jenkins-slsa/Jenkinsfile @@ -0,0 +1,88 @@ +pipeline { + agent any + tools { + jfrog 'jfrog-cli-latest' + maven 'maven' + } + environment { + PROJECT_WORKING_DIR = 'examples/jenkins-slsa' + PACKAGE_REPO_NAME = 'maven-libs-release-local' + MARKDOWN_FILE_NAME = 'JenkinsSLSA.md' + PREDICATE_FILE_NAME = '${PROJECT_WORKING_DIR}/decoded-payload.json' + PREDICATE_TYPE = 'http://slsa.dev/provenance/v1' + } + stages { + stage('Checkout') { + steps { + git branch: 'jenkins-slsa', url: 'https://github.com/jfrog/Evidence-Examples.git', credentialsId: 'github' + } + } + + stage('JFrog CLI Configuration') { + steps { + script { + withCredentials([ + string(credentialsId: 'ARTIFACTORY_URL', variable: 'ARTIFACTORY_URL'), + string(credentialsId: 'ACCESS_TOKEN', variable: 'ACCESS_TOKEN') + ]) { + jf 'c add jenkins-slsa-evidence --url=${ARTIFACTORY_URL} --access-token=${ACCESS_TOKEN}' + } + } + } + } + + stage('Build and Publish') { + steps { + script { + jf 'c use jenkins-slsa-evidence' + jf 'mvn-config \ + --repo-resolve-releases=maven-libs-release \ + --repo-resolve-snapshots=maven-libs-snapshot \ + --repo-deploy-releases=maven-libs-release \ + --repo-deploy-snapshots=maven-libs-snapshot' + jf 'mvn clean install -f ${PROJECT_WORKING_DIR}/pom.xml' + // Retrieve Maven project artifactId and version for use in later steps + env.PACKAGE_NAME = sh(script: "mvn help:evaluate -f ${PROJECT_WORKING_DIR}/pom.xml -Dexpression=project.artifactId -q -DforceStdout", returnStdout: true).trim() + env.PACKAGE_VERSION = sh(script: "mvn help:evaluate -f ${PROJECT_WORKING_DIR}/pom.xml -Dexpression=project.version -q -DforceStdout", returnStdout: true).trim() + } + } + } + } + + post { + success { + provenanceRecorder artifactFilter: '${PROJECT_WORKING_DIR}/target/*.jar', targetDirectory: '${PROJECT_WORKING_DIR}/build/slsa' + script { + def slsaDir = '${PROJECT_WORKING_DIR}/build/slsa' + def jsonlFiles = sh(script: "ls ${slsaDir}/*.jsonl 2>/dev/null || true", returnStdout: true).trim().split("\\r?\\n") + def jsonlFile = jsonlFiles.find { it } + if (!jsonlFile) { + echo "No .jsonl file found in ${slsaDir}/" + return + } + echo "Found JSONL file: ${jsonlFile}" + def jsonlText = readFile(jsonlFile) + def jsonlMap = new groovy.json.JsonSlurperClassic().parseText(jsonlText) + def decodedPayload = new String(jsonlMap.decodedPayload.decodeBase64(), 'UTF-8') + def prettyJson = groovy.json.JsonOutput.prettyPrint(decodedPayload) + writeFile file: "${PROJECT_WORKING_DIR}/decoded-payload.json", text: prettyJson + echo "Decoded payload saved to examples/jenkins-slsa/decoded-payload.json" + sh 'python3 ${PROJECT_WORKING_DIR}/json-to-md.py' + } + withCredentials([ + file(credentialsId: 'PRIVATE_PEM', variable: 'PRIVATE_PEM'), + string(credentialsId: 'KEY_ALIAS', variable: 'KEY_ALIAS') + ]) { + jf 'evd create \ + --package-name ${PACKAGE_NAME} \ + --package-version ${PACKAGE_VERSION} \ + --package-repo-name ${PACKAGE_REPO_NAME} \ + --key ${PRIVATE_PEM} \ + --key-alias ${KEY_ALIAS} \ + --predicate ${PREDICATE_FILE_NAME} \ + --predicate-type ${PREDICATE_TYPE} \ + --markdown ${MARKDOWN_FILE_NAME}' + } + } + } +} \ No newline at end of file diff --git a/examples/jenkins-slsa/README.md b/examples/jenkins-slsa/README.md new file mode 100644 index 0000000..2b9b67b --- /dev/null +++ b/examples/jenkins-slsa/README.md @@ -0,0 +1,85 @@ +# Jenkins SLSA Evidence Example + +This project demonstrates how to automate Maven builds, generate SLSA provenance, convert it to Markdown, and attach the signed provenance evidence to the Maven package in JFrog Artifactory using Jenkins Pipeline and JFrog CLI. + +## Overview + +The pipeline builds a Maven project, generates SLSA provenance, converts the provenance JSON to Markdown, publishes the artifact to Artifactory, and attaches the signed provenance as evidence to the Maven package. This enables traceability and compliance for your Java artifacts in CI/CD. + +## Prerequisites + +- JFrog CLI +- Artifactory configured as a Maven repository +- The following Jenkins credentials: + - `ARTIFACTORY_URL` (Artifactory URL) + - `ACCESS_TOKEN_ID` (Artifactory access token) + - `PRIVATE_PEM` (Private key for signing evidence) + - `KEY_ALIAS` (Key alias for signing evidence) + - `github` (GitHub credentials for source checkout) + +## Environment Variables Used + +- `PACKAGE_REPO_NAME` - Maven repository name in Artifactory +- `PACKAGE_NAME` - Maven artifactId (extracted from pom.xml) +- `PACKAGE_VERSION` - Maven version (extracted from pom.xml) +- `PREDICATE_FILE_NAME` - Path to SLSA provenance JSON file +- `PREDICATE_TYPE` - Predicate type URL for SLSA +- `MARKDOWN_FILE_NAME` - Path to the generated Markdown file from provenance + +## Pipeline Stages + +1. **Checkout** + - Clones the source code from GitHub. +2. **JFrog CLI Configuration** + - Configures JFrog CLI with Artifactory credentials. +3. **Build and Publish** + - Builds the Maven project and publishes artifacts to Artifactory. + - Extracts artifactId and version for evidence attachment. +4. **Provenance Generation and Evidence Attachment** + - Generates SLSA provenance. + - Converts the provenance JSON to Markdown. + - Attaches the signed provenance (JSON and Markdown) as evidence to the Maven package in Artifactory. + +## Example Usage + +Trigger the pipeline in Jenkins. The pipeline will: + +- Build and publish the Maven artifact +- Generate and convert the SLSA provenance +- Attach the provenance as evidence + +## Key Commands Used + +- **Configure JFrog CLI:** + ```bash + jf c add jenkins-slsa-evidence --url=https://evidencetrial.jfrog.io --access-token=$ACCESS_TOKEN + ``` +- **Build and Publish Maven Artifact:** + ```bash + jf mvn clean install + ``` +- **Extract Maven Coordinates:** + ```bash + mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout + mvn help:evaluate -Dexpression=project.version -q -DforceStdout + ``` +- **Convert Provenance JSON to Markdown:** + ```bash + python3 json-to-md.py + ``` +- **Attach Evidence:** + ```bash + jf evd create --package-name="$PACKAGE_NAME" --package-version="$PACKAGE_VERSION" --package-repo-name="$PACKAGE_REPO_NAME" --key="$PRIVATE_PEM" --key-alias="$KEY_ALIAS" --predicate="$PREDICATE_FILE_NAME" --predicate-type="$PREDICATE_TYPE" --markdown="$MARKDOWN_FILE_NAME" + ``` + +## Limitation + +**Note:** The current pipeline and evidence attachment process expects a single Maven artifact (JAR) is produced per build. It does **not** support multiple subjects or multiple JARs in a single pipeline execution. This is a known limitation and should be considered when working with this example. + +## References + +- [SLSA Provenance](https://slsa.dev/spec/v1.1/provenance) +- [Jenkins SLSA Plugin](https://plugins.jenkins.io/slsa/) +- [JFrog Evidence Management](https://jfrog.com/help/r/jfrog-artifactory-documentation/evidence-management) +- [JFrog CLI Documentation](https://jfrog.com/getcli/) +- [How to use JFrog CLI in Jenkins using JFrog Plugin](https://jfrog.com/help/r/artifactory-how-to-use-jfrog-cli-in-jenkins-using-jfrog-plugin/artifactory-how-to-use-jfrog-cli-in-jenkins-using-jfrog-plugin) \ No newline at end of file diff --git a/examples/jenkins-slsa/json-to-md.py b/examples/jenkins-slsa/json-to-md.py new file mode 100644 index 0000000..7124506 --- /dev/null +++ b/examples/jenkins-slsa/json-to-md.py @@ -0,0 +1,88 @@ +import json + +def format_digests(digests): + if not isinstance(digests, dict): + return "" + sha1 = digests.get("sha1") + sha256 = digests.get("sha256") + if sha1 and sha256: + return f"sha1: {sha1}, sha256: {sha256}" + elif sha1: + return f"sha1: {sha1}" + elif sha256: + return f"sha256: {sha256}" + return "" + +def main(): + with open('examples/jenkins-slsa/decoded-payload.json', 'r') as f: + data = json.load(f) + + lines = [] + lines.append("# SLSA Provenance Statement") + lines.append(f"- **predicateType**: `{data.get('predicateType', '')}`") + lines.append(f"- **_type**: `{data.get('_type', '')}`\n") + + # Subject + lines.append("## Subject") + for subj in data.get("subject", []): + lines.append(f"- **Name**: `{subj.get('name', '')}`") + digests = format_digests(subj.get("digests", {})) + if digests: + lines.append(f"- **Digests**: `{digests}`") + lines.append("") + + # Predicate + pred = data.get("predicate", {}) + lines.append("## Predicate\n") + lines.append("### Build Type") + lines.append(f"- `{pred.get('buildType', '')}`\n") + + lines.append("### Builder") + builder = pred.get("builder", {}) + lines.append(f"- **ID**: `{builder.get('id', '')}`\n") + + # Invocation + invocation = pred.get("invocation", {}) + lines.append("### Invocation\n") + config = invocation.get("configSource", {}) + lines.append("#### Config Source") + lines.append(f"- **URI**: `{config.get('uri', '')}`") + lines.append(f"- **Entry Point**: `{config.get('entryPoint', '')}`") + digests = format_digests(config.get("digests", {})) + if digests: + lines.append(f"- **Digests**: `{digests}`") + lines.append("") + + env = invocation.get("environment", {}) + lines.append("#### Environment") + lines.append(f"- **Build URL**: `{env.get('build_url', '')}`") + lines.append(f"- **Job URL**: `{env.get('job_url', '')}`") + lines.append(f"- **Node Name**: `{env.get('node_name', '')}`\n") + + # Metadata + metadata = pred.get("metadata", {}) + lines.append("### Metadata") + lines.append(f"- **Build Invocation ID**: `{metadata.get('buildInvocationId', '')}`") + lines.append(f"- **Build Started On**: `{metadata.get('buildStartedOn', '')}`") + lines.append(f"- **Build Finished On**: `{metadata.get('buildFinishedOn', '')}`") + lines.append(f"- **Reproducible**: `{str(metadata.get('reproducible', ''))}`\n") + + completeness = metadata.get("completeness", {}) + lines.append("#### Completeness") + lines.append(f"- **Parameters Complete**: `{str(completeness.get('parametersComplete', ''))}`") + lines.append(f"- **Environment Complete**: `{str(completeness.get('environmentComplete', ''))}`") + lines.append(f"- **Materials Complete**: `{str(completeness.get('materialsComplete', ''))}`\n") + + # Materials + lines.append("### Materials") + for mat in pred.get("materials", []): + lines.append(f"- **URI**: `{mat.get('uri', '')}`") + digests = format_digests(mat.get("digests", {})) + if digests: + lines.append(f"- **Digests**: `{digests}`") + + with open('JenkinsSLSA.md', 'w') as f: + f.write('\n'.join(lines)) + +if __name__ == "__main__": + main() diff --git a/examples/jenkins-slsa/pom.xml b/examples/jenkins-slsa/pom.xml new file mode 100644 index 0000000..e2e9338 --- /dev/null +++ b/examples/jenkins-slsa/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + com.jfrog.evidence + jenkins-slsa-evidence-integration + 1.0 + + + 21 + 21 + + UTF-8 + UTF-8 + + 3.13.0 + 3.5.2 + 3.2.5 + + 5.10.2 + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven.plugins.compiler.version} + + ${maven.compiler.source} + ${maven.compiler.target} + + + + org.apache.maven.plugins + maven-surefire-plugin + ${maven.plugins.surefire.version} + + + + + diff --git a/examples/jenkins-slsa/src/main/java/com/jfrog/evidence/HelloWorld.java b/examples/jenkins-slsa/src/main/java/com/jfrog/evidence/HelloWorld.java new file mode 100644 index 0000000..5d3657d --- /dev/null +++ b/examples/jenkins-slsa/src/main/java/com/jfrog/evidence/HelloWorld.java @@ -0,0 +1,7 @@ +package com.jfrog.evidence; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("This is a sample Java application for Jenkins SLSA Evidence Integration."); + } +}