diff --git a/examples/gitlab-provenance/.gitlab-ci.yml b/examples/gitlab-provenance/.gitlab-ci.yml new file mode 100644 index 0000000..16ec62d --- /dev/null +++ b/examples/gitlab-provenance/.gitlab-ci.yml @@ -0,0 +1,69 @@ +image: node:18 + +variables: + RUNNER_GENERATE_ARTIFACTS_METADATA: "true" + REPO_NAME: "evidence-npm" + PREDICATE_FILE: "./artifacts-metadata.json" + PREDICATE_TYPE: "http://slsa.dev/provenance/v1" + MARKDOWN_FILE: "GitLabSLSA.md" + PROJECT_WORKING_DIR: "." + +stages: + - build_and_publish + - create_md_file_and_attach_evidence + +build_and_publish: + stage: build_and_publish + before_script: + - apt-get update + - apt-get install -y curl python3 python3-pip + - curl -fL https://install-cli.jfrog.io | sh + - jf config add --url ${ARTIFACTORY_URL} --access-token ${ARTIFACTORY_ACCESS_TOKEN} --interactive=false + script: + - jf npmc --repo-resolve evidence-npm --repo-deploy evidence-npm + - jf npm publish + - jf npm pack + - export PACKAGE_NAME=$(node -p "require('./package.json').name") + - export PACKAGE_VERSION=$(node -p "require('./package.json').version") + - echo "PACKAGE_NAME=$PACKAGE_NAME" >> build.env + - echo "PACKAGE_VERSION=$PACKAGE_VERSION" >> build.env + artifacts: + paths: + - "*.tgz" + reports: + dotenv: build.env + only: + - main + when: manual + +create_md_file_and_attach_evidence: + stage: create_md_file_and_attach_evidence + before_script: + - apt-get update + - apt-get install -y curl python3 python3-pip jq + - curl -fL https://install-cli.jfrog.io | sh + - jf config add --url ${ARTIFACTORY_URL} --access-token ${ARTIFACTORY_ACCESS_TOKEN} --interactive=false + script: + # Extract predicate from the artifacts-metadata.json file + - | + if [ -f "${PREDICATE_FILE}" ]; then + echo "Found artifacts metadata file: ${PREDICATE_FILE}" + + # Extract predicate object from the JSON + predicateJson=$(jq '.predicate' "${PREDICATE_FILE}") + + # Write predicate to a separate file for evidence creation + echo "$predicateJson" > "${PROJECT_WORKING_DIR}/predicate.json" + echo "Predicate object extracted and saved to ${PROJECT_WORKING_DIR}/predicate.json" + + # Update PREDICATE_FILE variable to point to the extracted predicate + export PREDICATE_FILE="${PROJECT_WORKING_DIR}/predicate.json" + else + echo "No artifacts metadata file found: ${PREDICATE_FILE}" + fi + - python3 json-to-md.py + - jf evd create --package-name="${PACKAGE_NAME}" --package-version="${PACKAGE_VERSION}" --package-repo-name="${REPO_NAME}" --key="${PRIVATE_KEY}" --key-alias="${PRIVATE_KEY_ALIAS}" --predicate="${PREDICATE_FILE}" --predicate-type="${PREDICATE_TYPE}" --markdown="${MARKDOWN_FILE}" + dependencies: + - build_and_publish + needs: + - build_and_publish \ No newline at end of file diff --git a/examples/gitlab-provenance/README.md b/examples/gitlab-provenance/README.md new file mode 100644 index 0000000..6642af4 --- /dev/null +++ b/examples/gitlab-provenance/README.md @@ -0,0 +1,163 @@ +# **GitLab Provenance & SLSA Evidence Example** + +This repository provides a working example of a GitLab CI/CD pipeline that automates npm builds, generates SLSA provenance, and attaches the resulting provenance evidence to the npm package in **JFrog Artifactory**. + +This pipeline is an essential pattern for DevSecOps, creating a traceable, compliant, and secure software supply chain. + +### **Key Features** + +* **Automated Build & Push**: Builds an npm project and publishes artifacts to Artifactory. +* **SLSA Provenance Generation**: Uses GitLab Runner to automatically generate SLSA provenance metadata for the build artifacts. +* **Evidence Generation**: Creates an artifacts-metadata.json predicate file containing SLSA provenance. +* **Markdown Report**: Includes a helper script to generate a human-readable Markdown summary from the SLSA JSON results. +* **Signed Evidence Attachment**: Attaches the provenance results to the corresponding npm package version in Artifactory using jf evd create, cryptographically signing it for integrity. +* **SLSA**: [SLSA Provenance Specification](https://slsa.dev/spec/v1.1/provenance) + +### **Workflow** + +The following diagram illustrates the sequence of operations performed by the GitLab CI/CD pipeline. + +```mermaid +flowchart TD + subgraph subGraph0["
"] + C["Setup JFrog CLI"] + D["Configure npm Repositories"] + E["Build npm Project & Generate SLSA Provenance"] + F["Extract Package Name & Version"] + end + subgraph subGraph1["
"] + H["Setup Python"] + I["Convert Provenance JSON to Markdown"] + J["Attach Evidence to npm Package"] + end + A["Manual Pipeline Trigger"] --> B["build_and_publish Stage"] + B --> C + C --> D + D --> E + E --> F + F --> G["create_md_file_and_attach_evidence Stage"] + G --> H + H --> I + I --> J +``` + +--- + +### **1. Prerequisites** + +Before running this pipeline, you must have: + +* JFrog CLI 2.65.0 or above (installed automatically in the pipeline) +* An Artifactory repository of type npm (e.g., evidence-npm). +* A private key and a corresponding key alias configured in your JFrog Platform for signing evidence. +* The following GitLab CI/CD variables: + * `ARTIFACTORY_URL` (Artifactory base URL, e.g. `https://mycompany.jfrog.io`) + * `PRIVATE_KEY_ALIAS` (Key alias for signing evidence) +* The following GitLab CI/CD secrets: + * `ARTIFACTORY_ACCESS_TOKEN` (Artifactory access token) + * `PRIVATE_KEY` (Private key for signing evidence) + +### Environment Variables Used + +* `RUNNER_GENERATE_ARTIFACTS_METADATA` - Enables GitLab Runner to generate SLSA provenance metadata for the build artifacts. + +### **2. Configuration** + +To use this pipeline, you must configure the following GitLab CI/CD Variables. + +#### **GitLab Variables** + +Navigate to Settings > CI/CD > Variables and create the following variables: + +| Variable Name | Description | Example Value | Protected | Masked | +| :---- | :---- | :---- | :---- | :---- | +| ARTIFACTORY_URL | The base URL of your JFrog Platform instance. | https://mycompany.jfrog.io | No | No | +| PRIVATE_KEY_ALIAS | The alias for the public key in JFrog Platform used to verify the evidence signature. | my-signing-key-alias | No | No | +| ARTIFACTORY_ACCESS_TOKEN | A valid JFrog Access Token with permissions to read, write, and annotate in your target repository. | - | Yes | Yes | +| PRIVATE_KEY | The private key used to sign the evidence. This key corresponds to the alias configured in JFrog Platform. | - | Yes | Yes | + +#### **Pipeline Environment Variables** + +You can also customize the pipeline's behavior by modifying the variables block in the .gitlab-ci.yml file: + +| Variable Name | Description | Default Value | +| :---- | :---- | :---- | +| REPO_NAME | The name of the target npm repository in Artifactory. | evidence-npm | +| PREDICATE_FILE | Path to SLSA provenance JSON file. | ./artifacts-metadata.json | +| PREDICATE_TYPE | Predicate type URL for SLSA. | http://slsa.dev/provenance/v1 | +| MARKDOWN_FILE | Path to the generated Markdown file from provenance. | GitLabSLSA.md | + +--- + +### **3. Usage** + +This pipeline is triggered manually. + +1. Navigate to the **CI/CD** tab of your GitLab repository. +2. In the left sidebar, click on **Pipelines**. +3. Click the **Run pipeline** button. +4. Select the branch you want to run the pipeline on (default is main). +5. Click the green **Run pipeline** button. + +Once the pipeline completes successfully, you can navigate to your repository in Artifactory (evidence-npm) and view the npm artifact. Under the **Evidence** tab for the latest version, you will find the signed SLSA provenance results. + +### **How It Works: A Step-by-Step Breakdown** + +1. **build_and_publish Stage**: The pipeline begins by setting up the JFrog CLI, configuring npm repositories, and building the npm project using `jf npm publish` and `jf npm pack`. +2. **Extract Package Information**: The pipeline extracts the package name and version from the npm project using Node.js commands to read package.json. +3. **Generate SLSA Provenance**: GitLab Runner automatically generates SLSA provenance metadata for the build artifacts and outputs the findings into a structured artifacts-metadata.json file. +4. **create_md_file_and_attach_evidence Stage**: The second stage sets up JFrog CLI and Python, then converts the provenance JSON to a human-readable Markdown file using the `json-to-md.py` script. +5. **Attach Signed Evidence**: The final step uses the `jf evd create` command to attach both the JSON provenance and Markdown report as evidence to the specific npm package version in Artifactory. The evidence is signed using the provided PRIVATE_KEY, ensuring its authenticity and integrity. + +### **Key Commands Used** + +* **Configure JFrog CLI:** +```bash +jf config add --url ${ARTIFACTORY_URL} --access-token ${ARTIFACTORY_ACCESS_TOKEN} --interactive=false +``` + +* **Configure npm Repositories:** +```bash +jf npmc --repo-resolve evidence-npm --repo-deploy evidence-npm +``` + +* **Build and Publish npm Artifact:** +```bash +jf npm publish +jf npm pack +``` + +* **Extract npm Coordinates:** +```bash +node -p "require('./package.json').name" +node -p "require('./package.json').version" +``` + +* **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 "${REPO_NAME}" \ + --key "${PRIVATE_KEY}" \ + --key-alias "${PRIVATE_KEY_ALIAS}" \ + --predicate "${PREDICATE_FILE}" \ + --predicate-type "${PREDICATE_TYPE}" \ + --markdown "${MARKDOWN_FILE}" +``` + +### **Limitation** + +**Note:** The current pipeline and evidence attachment process expects a single npm artifact (tgz) is produced per build. It does **not** support multiple subjects or multiple packages 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) +* [GitLab SLSA Provenance Generation](https://docs.gitlab.com/ci/pipeline_security/#slsa-provenance-generation) +* [JFrog Evidence Management](https://jfrog.com/help/r/jfrog-artifactory-documentation/evidence-management) +* [JFrog CLI Documentation](https://jfrog.com/getcli/) \ No newline at end of file diff --git a/examples/gitlab-provenance/index.js b/examples/gitlab-provenance/index.js new file mode 100644 index 0000000..ae9e3a9 --- /dev/null +++ b/examples/gitlab-provenance/index.js @@ -0,0 +1 @@ +console.log("Hello, World!"); \ No newline at end of file diff --git a/examples/gitlab-provenance/json-to-md.py b/examples/gitlab-provenance/json-to-md.py new file mode 100644 index 0000000..be8defb --- /dev/null +++ b/examples/gitlab-provenance/json-to-md.py @@ -0,0 +1,77 @@ +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('./predicate.json', 'r') as f: + pred = json.load(f) + + lines = [] + lines.append("# SLSA Provenance Predicate") + lines.append("") + lines.append("## Predicate\n") + + # Build Definition + build_def = pred.get("buildDefinition", {}) + lines.append("### Build Definition") + lines.append(f"- **Build Type**: `{build_def.get('buildType', '')}`\n") + + # External Parameters + ext_params = build_def.get("externalParameters", {}) + lines.append("#### External Parameters") + lines.append(f"- **Entry Point**: `{ext_params.get('entryPoint', '')}`") + lines.append(f"- **Source**: `{ext_params.get('source', '')}`") + lines.append("") + + # Internal Parameters + int_params = build_def.get("internalParameters", {}) + lines.append("#### Internal Parameters") + for k, v in int_params.items(): + lines.append(f"- **{k}**: `{v}`") + lines.append("") + + # Resolved Dependencies + lines.append("#### Resolved Dependencies") + for dep in build_def.get("resolvedDependencies", []): + lines.append(f"- **URI**: `{dep.get('uri', '')}`") + digest = format_digests(dep.get("digest", {})) + if digest: + lines.append(f"- **Digest**: `{digest}`") + lines.append("") + + # Run Details + run_details = pred.get("runDetails", {}) + lines.append("### Run Details") + + # Builder + builder = run_details.get("builder", {}) + lines.append(f"- **Builder ID**: `{builder.get('id', '')}`") + version = builder.get("version", {}) + for k, v in version.items(): + lines.append(f"- **{k}**: `{v}`") + lines.append("") + + # Metadata + metadata = run_details.get("metadata", {}) + lines.append("#### Metadata") + lines.append(f"- **Invocation ID**: `{metadata.get('invocationID', '')}`") + lines.append(f"- **Started On**: `{metadata.get('startedOn', '')}`") + lines.append(f"- **Finished On**: `{metadata.get('finishedOn', '')}`") + lines.append("") + + with open('GitLabSLSA.md', 'w') as f: + f.write('\n'.join(lines)) + +if __name__ == "__main__": + main() diff --git a/examples/gitlab-provenance/package.json b/examples/gitlab-provenance/package.json new file mode 100644 index 0000000..f795547 --- /dev/null +++ b/examples/gitlab-provenance/package.json @@ -0,0 +1,12 @@ +{ + "name": "gitlab-provenance-evidence-integration", + "version": "1.0.0", + "description": "GitLab CI/CD pipeline with SLSA provenance evidence integration", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "keywords": ["gitlab", "provenance", "slsa", "evidence", "ci-cd"], + "author": "", + "license": "ISC" +} \ No newline at end of file