From 564e420c5c87b1cfeca3e20ca144a3e962935816 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Mon, 30 Jun 2025 16:48:27 +0530 Subject: [PATCH 1/6] Added Support for Jenkins SLSA Integration --- examples/jenkins-slsa/Jenkinsfile | 86 ++++++++++++++++++ examples/jenkins-slsa/Readme.md | 81 +++++++++++++++++ examples/jenkins-slsa/json-to-md.py | 88 +++++++++++++++++++ examples/jenkins-slsa/pom.xml | 53 +++++++++++ .../java/com/jfrog/evidence/HelloWorld.java | 7 ++ 5 files changed, 315 insertions(+) create mode 100644 examples/jenkins-slsa/Jenkinsfile create mode 100644 examples/jenkins-slsa/Readme.md create mode 100644 examples/jenkins-slsa/json-to-md.py create mode 100644 examples/jenkins-slsa/pom.xml create mode 100644 examples/jenkins-slsa/src/main/java/com/jfrog/evidence/HelloWorld.java diff --git a/examples/jenkins-slsa/Jenkinsfile b/examples/jenkins-slsa/Jenkinsfile new file mode 100644 index 0000000..bf4c70b --- /dev/null +++ b/examples/jenkins-slsa/Jenkinsfile @@ -0,0 +1,86 @@ +pipeline { + agent any + tools { + jfrog 'jfrog-cli-latest' + maven 'maven' + } + environment { + PACKAGE_REPO_NAME = 'maven-libs-release-local' + MARKDOWN_FILE_NAME = 'JenkinsSLSA.md' + PREDICATE_FILE_NAME = 'decoded-payload.json' + PREDICATE_TYPE = 'http://slsa.dev/provenance/v1' + } + stages { + stage('Checkout') { + steps { + git branch: 'master', url: 'https://github.com/VigneshC-Jfrog/jenkins-slsa-evidence-example.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' + // Retrieve Maven project artifactId and version for use in later steps + env.PACKAGE_NAME = sh(script: "mvn help:evaluate -Dexpression=project.artifactId -q -DforceStdout", returnStdout: true).trim() + env.PACKAGE_VERSION = sh(script: "mvn help:evaluate -Dexpression=project.version -q -DforceStdout", returnStdout: true).trim() + } + } + } + } + + post { + success { + provenanceRecorder artifactFilter: 'target/*.jar', targetDirectory: 'build/slsa' + script { + def slsaDir = '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}/" + } + 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: "decoded-payload.json", text: prettyJson + echo "Decoded payload saved to decoded-payload.json" + sh 'python3 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..91d446a --- /dev/null +++ b/examples/jenkins-slsa/Readme.md @@ -0,0 +1,81 @@ +# 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" + ``` + +## 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..3dee41b --- /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('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."); + } +} From 32b28a264cdc5809f0971b72ccba2e2ddb05dbb0 Mon Sep 17 00:00:00 2001 From: Vignesh C <62242171+VigneshC-Jfrog@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:02:37 +0530 Subject: [PATCH 2/6] Update json-to-md.py --- examples/jenkins-slsa/json-to-md.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/jenkins-slsa/json-to-md.py b/examples/jenkins-slsa/json-to-md.py index 3dee41b..7124506 100644 --- a/examples/jenkins-slsa/json-to-md.py +++ b/examples/jenkins-slsa/json-to-md.py @@ -14,7 +14,7 @@ def format_digests(digests): return "" def main(): - with open('decoded-payload.json', 'r') as f: + with open('examples/jenkins-slsa/decoded-payload.json', 'r') as f: data = json.load(f) lines = [] From 10f5b27cbf0f9242383e71969cde05c93c181d65 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Tue, 1 Jul 2025 10:51:55 +0530 Subject: [PATCH 3/6] Updated Jenkinsfile --- examples/jenkins-slsa/Jenkinsfile | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/jenkins-slsa/Jenkinsfile b/examples/jenkins-slsa/Jenkinsfile index bf4c70b..f4eba19 100644 --- a/examples/jenkins-slsa/Jenkinsfile +++ b/examples/jenkins-slsa/Jenkinsfile @@ -5,15 +5,16 @@ pipeline { maven 'maven' } environment { + PROJECT_WORKING_DIR = 'examples/jenkins-slsa' PACKAGE_REPO_NAME = 'maven-libs-release-local' MARKDOWN_FILE_NAME = 'JenkinsSLSA.md' - PREDICATE_FILE_NAME = 'decoded-payload.json' + PREDICATE_FILE_NAME = '${PROJECT_WORKING_DIR}/decoded-payload.json' PREDICATE_TYPE = 'http://slsa.dev/provenance/v1' } stages { stage('Checkout') { steps { - git branch: 'master', url: 'https://github.com/VigneshC-Jfrog/jenkins-slsa-evidence-example.git', credentialsId: 'github' + git branch: 'jenkins-slsa', url: 'https://github.com/VigneshC-Jfrog/Evidence-Examples.git', credentialsId: 'github' } } @@ -39,10 +40,10 @@ pipeline { --repo-resolve-snapshots=maven-libs-snapshot \ --repo-deploy-releases=maven-libs-release \ --repo-deploy-snapshots=maven-libs-snapshot' - jf 'mvn clean install' + 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 -Dexpression=project.artifactId -q -DforceStdout", returnStdout: true).trim() - env.PACKAGE_VERSION = sh(script: "mvn help:evaluate -Dexpression=project.version -q -DforceStdout", returnStdout: true).trim() + 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() } } } @@ -50,9 +51,9 @@ pipeline { post { success { - provenanceRecorder artifactFilter: 'target/*.jar', targetDirectory: 'build/slsa' + provenanceRecorder artifactFilter: '${PROJECT_WORKING_DIR}/target/*.jar', targetDirectory: '${PROJECT_WORKING_DIR}/build/slsa' script { - def slsaDir = 'build/slsa' + 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) { @@ -63,9 +64,9 @@ pipeline { 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: "decoded-payload.json", text: prettyJson - echo "Decoded payload saved to decoded-payload.json" - sh 'python3 json-to-md.py' + 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'), From 8b7afbd1f9014981e02549ac6fcdc78d03425ecc Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Tue, 1 Jul 2025 15:06:08 +0530 Subject: [PATCH 4/6] Updated Jenkinsfile --- examples/jenkins-slsa/Jenkinsfile | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/jenkins-slsa/Jenkinsfile b/examples/jenkins-slsa/Jenkinsfile index f4eba19..8b4378c 100644 --- a/examples/jenkins-slsa/Jenkinsfile +++ b/examples/jenkins-slsa/Jenkinsfile @@ -58,6 +58,7 @@ pipeline { def jsonlFile = jsonlFiles.find { it } if (!jsonlFile) { echo "No .jsonl file found in ${slsaDir}/" + return } echo "Found JSONL file: ${jsonlFile}" def jsonlText = readFile(jsonlFile) From 9c99dac2a9cfa6be9a32ed97ea2bf545e0dc4d6e Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Wed, 2 Jul 2025 10:12:15 +0530 Subject: [PATCH 5/6] Support for Jenkins SLSA Evidence Integration --- examples/jenkins-slsa/Readme.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/jenkins-slsa/Readme.md b/examples/jenkins-slsa/Readme.md index 91d446a..2b9b67b 100644 --- a/examples/jenkins-slsa/Readme.md +++ b/examples/jenkins-slsa/Readme.md @@ -72,6 +72,10 @@ Trigger the pipeline in Jenkins. The pipeline will: 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) From c86665b4c15548670917d8e1d9c9c1235f323378 Mon Sep 17 00:00:00 2001 From: Vignesh C Date: Wed, 2 Jul 2025 10:12:15 +0530 Subject: [PATCH 6/6] Support for Jenkins SLSA Evidence Integration --- examples/jenkins-slsa/Jenkinsfile | 2 +- examples/jenkins-slsa/{Readme.md => README.md} | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) rename examples/jenkins-slsa/{Readme.md => README.md} (91%) diff --git a/examples/jenkins-slsa/Jenkinsfile b/examples/jenkins-slsa/Jenkinsfile index 8b4378c..151fe05 100644 --- a/examples/jenkins-slsa/Jenkinsfile +++ b/examples/jenkins-slsa/Jenkinsfile @@ -14,7 +14,7 @@ pipeline { stages { stage('Checkout') { steps { - git branch: 'jenkins-slsa', url: 'https://github.com/VigneshC-Jfrog/Evidence-Examples.git', credentialsId: 'github' + git branch: 'jenkins-slsa', url: 'https://github.com/jfrog/Evidence-Examples.git', credentialsId: 'github' } } diff --git a/examples/jenkins-slsa/Readme.md b/examples/jenkins-slsa/README.md similarity index 91% rename from examples/jenkins-slsa/Readme.md rename to examples/jenkins-slsa/README.md index 91d446a..2b9b67b 100644 --- a/examples/jenkins-slsa/Readme.md +++ b/examples/jenkins-slsa/README.md @@ -72,6 +72,10 @@ Trigger the pipeline in Jenkins. The pipeline will: 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)