diff --git a/examples/gitlab-sbom/.gitlab-ci.yml b/examples/gitlab-sbom/.gitlab-ci.yml new file mode 100644 index 0000000..72d0fed --- /dev/null +++ b/examples/gitlab-sbom/.gitlab-ci.yml @@ -0,0 +1,53 @@ +image: docker:stable + +services: + - docker:dind + +variables: + REPO_NAME: "docker-sample" + BUILD_NAME: "gitlab-evidence-docker-build" + BUILD_NUMBER: "${CI_PIPELINE_ID}" + PACKAGE_NAME: "gitlab-evidence-app" + PACKAGE_VERSION: "${CI_COMMIT_SHORT_SHA}" + PREDICATE_FILE: "./gl-sbom-report.cdx.json" + PREDICATE_TYPE: "https://cyclonedx.org/schema" + DOCKER_IMAGE_NAME_WITH_TAG: ${REGISTRY_URL}/${REPO_NAME}/${PACKAGE_NAME}:${CI_COMMIT_SHORT_SHA} + MARKDOWN_FILE: "GitLab_SBOM.md" + +stages: + - build_and_push + - container_scanning + - create_md_file_and_attach_evidence + +build_and_push: + stage: build_and_push + before_script: + - apk update + - apk add go curl + - curl -fL https://install-cli.jfrog.io | sh + - jf config add --url ${ARTIFACTORY_URL} --access-token ${ARTIFACTORY_ACCESS_TOKEN} --interactive=false + script: + - docker build -f ./Dockerfile -t $DOCKER_IMAGE_NAME_WITH_TAG ./ + - jf rt docker-push ${DOCKER_IMAGE_NAME_WITH_TAG} $REPO_NAME --build-name=$BUILD_NAME --build-number=${BUILD_NUMBER} + - jf rt build-publish ${BUILD_NAME} ${BUILD_NUMBER} + +include: + - template: Jobs/Container-Scanning.gitlab-ci.yml + +container_scanning: + stage: container_scanning + variables: + CS_IMAGE: ${DOCKER_IMAGE_NAME_WITH_TAG} + CS_REGISTRY_USER: ${REGISTRY_USER} + CS_REGISTRY_PASSWORD: ${REGISTRY_PASSWORD} + +create_md_file_and_attach_evidence: + stage: create_md_file_and_attach_evidence + before_script: + - apk update + - apk add python3 py3-pip go curl + - curl -fL https://install-cli.jfrog.io | sh + - jf config add --url ${ARTIFACTORY_URL} --access-token ${ARTIFACTORY_ACCESS_TOKEN} --interactive=false + script: + - 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}" \ No newline at end of file diff --git a/examples/gitlab-sbom/Dockerfile b/examples/gitlab-sbom/Dockerfile new file mode 100644 index 0000000..ba286d5 --- /dev/null +++ b/examples/gitlab-sbom/Dockerfile @@ -0,0 +1,18 @@ +# Use Alpine Linux 3.18 as the base image +FROM alpine:3.18 +# +## Set the working directory +WORKDIR /app +# +## Copy requirements.txt if needed (uncomment if using Python/Ansible) +COPY ./requirements.txt . +# +## Install Python3, pip, and Ansible +RUN apk update && \ + apk add --no-cache python3 py3-pip ansible +# +## (Optional) Install Python dependencies +RUN pip3 install --no-cache-dir -r requirements.txt +# +## Default command (prints Ansible version) +CMD ["ansible", "--version"] \ No newline at end of file diff --git a/examples/gitlab-sbom/README.md b/examples/gitlab-sbom/README.md new file mode 100644 index 0000000..aebb856 --- /dev/null +++ b/examples/gitlab-sbom/README.md @@ -0,0 +1,77 @@ +# GitLab SBOM Evidence Example + +This project demonstrates how to automate Docker image builds, generate SBOM (Software Bill of Materials) reports, convert them to Markdown, and attach the signed SBOM evidence to the Docker image in JFrog Artifactory using GitLab CI/CD and JFrog CLI. + +## Overview + +The pipeline builds a Docker image, generates a CycloneDX SBOM, converts the SBOM JSON to Markdown, pushes the image to Artifactory, and attaches the signed SBOM as evidence to the image package. This enables traceability and compliance for your container images in CI/CD. + +## Prerequisites + +- JFrog CLI 2.65.0 or above (installed automatically in the pipeline) +- Artifactory configured as a Docker registry +- The following GitLab CI/CD variables: + - `REGISTRY_URL` (Artifactory Docker registry domain, e.g. `mycompany.jfrog.io`) + - `ARTIFACTORY_URL` (Artifactory base URL) + - `ARTIFACTORY_ACCESS_TOKEN` (Artifactory access token) + - `REGISTRY_USER` (Docker registry user) + - `REGISTRY_PASSWORD` (Docker registry password) + - `PRIVATE_KEY` (Private key for signing evidence) + - `PRIVATE_KEY_ALIAS` (Key alias for signing evidence) + +## Environment Variables Used + +- `REPO_NAME` - Docker repository name +- `BUILD_NAME` - Build name for Artifactory +- `BUILD_NUMBER` - Build number (uses GitLab pipeline ID) +- `PACKAGE_NAME` - Docker image name +- `PACKAGE_VERSION` - Docker image tag (uses Git commit short SHA) +- `PREDICATE_FILE` - Path to SBOM JSON file +- `PREDICATE_TYPE` - Predicate type URL for SBOM +- `DOCKER_IMAGE_NAME_WITH_TAG` - Full Docker image name with tag +- `MARKDOWN_FILE` - Path to the generated Markdown file from SBOM + +## Pipeline Stages + +1. **Build and Push Docker Image** + - Builds the Docker image using the provided Dockerfile and pushes it to Artifactory. +2. **Container Scanning** + - Scans the pushed Docker image for vulnerabilities. +3. **Generate Markdown from SBOM and Attach Evidence** + - Converts the CycloneDX SBOM JSON to Markdown. + - Attaches the SBOM (JSON and Markdown) as signed evidence to the Docker image package in Artifactory. + - +## Example Usage + +Trigger the pipeline in GitLab CI/CD. The pipeline will: + +- Build and push the Docker image +- Generate and convert the SBOM +- Push the image to Artifactory +- Attach the SBOM as evidence + +## Key Commands Used + +- **Build Docker Image:** + ```bash + docker build -f ./examples/gitlab-sbom/Dockerfile -t $DOCKER_IMAGE_NAME_WITH_TAG ./examples/gitlab-sbom + ``` +- **Push Docker Image:** + ```bash + jf rt docker-push $DOCKER_IMAGE_NAME_WITH_TAG $REPO_NAME --build-name=$BUILD_NAME --build-number=$BUILD_NUMBER + ``` +- **Convert SBOM 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}" + ``` + +## References + +- [Gitlab Container Scanning](https://docs.gitlab.com/user/application_security/container_scanning/) +- [CycloneDX SBOM Specification](https://cyclonedx.org/) +- [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-sbom/json-to-md.py b/examples/gitlab-sbom/json-to-md.py new file mode 100644 index 0000000..4c2a7a3 --- /dev/null +++ b/examples/gitlab-sbom/json-to-md.py @@ -0,0 +1,52 @@ +import json + +def json_to_md(json_path, md_path): + with open(json_path, 'r') as f: + data = json.load(f) + + name = data.get('metadata', {}).get('component', {}).get('name', 'N/A') + timestamp = data.get('metadata', {}).get('timestamp', 'N/A') + tools = data.get('metadata', {}).get('tools', {}).get('components', []) + components = data.get('components', []) + dependencies = data.get('dependencies', []) + + with open(md_path, 'w') as f: + f.write(f"# SBOM Summary\n\n") + f.write(f"**Component Name:** {name}\n\n") + f.write(f"**Timestamp:** {timestamp}\n\n") + f.write(f"## Tools Used\n") + if tools: + for tool in tools: + tool_name = tool.get('name', 'Unknown Tool') + tool_version = tool.get('version', 'Unknown Version') + f.write(f"- {tool_name} (version: {tool_version})\n") + else: + f.write("No tool information found.\n") + f.write(f"\n## Components\n") + if components: + f.write("| bom-ref | name | version |\n") + f.write("|---|---|---|\n") + for comp in components: + bom_ref = comp.get('bom-ref', 'N/A').replace('|', '\\|') + comp_name = comp.get('name', 'N/A').replace('|', '\\|') + comp_version = comp.get('version', 'N/A').replace('|', '\\|') + f.write(f"| {bom_ref} | {comp_name} | {comp_version} |\n") + else: + f.write("No components found.\n") + + f.write(f"\n## Dependencies\n") + if dependencies: + f.write("| Reference | DependsOn |\n") + f.write("|---|---|\n") + for dep in dependencies: + ref = dep.get('ref', 'N/A').replace('|', '\\|') + dependson = dep.get('dependsOn', []) + dependson_str = ', '.join(d.replace('|', '\\|') for d in dependson) if dependson else '' + f.write(f"| {ref} | {dependson_str} |\n") + else: + f.write("No dependencies found.\n") + print(f"Markdown file generated at: {md_path}") + + +if __name__ == "__main__": + json_to_md('./gl-sbom-report.cdx.json', 'GitLab_SBOM.md') \ No newline at end of file diff --git a/examples/gitlab-sbom/requirements.txt b/examples/gitlab-sbom/requirements.txt new file mode 100644 index 0000000..f414682 --- /dev/null +++ b/examples/gitlab-sbom/requirements.txt @@ -0,0 +1 @@ +ansible==2.9.9