From b8979fc686c4947927693cafee19ba2aac265ead Mon Sep 17 00:00:00 2001 From: Nandagopal Nambiar Date: Wed, 11 Jun 2025 13:15:54 +0530 Subject: [PATCH] Adding support for Trivy evidence integration --- .github/workflows/trivy-evidence-example.yml | 62 +++++++++ examples/trivy-verify-example/Dockerfile | 10 ++ examples/trivy-verify-example/README.md | 91 ++++++++++++++ .../trivy_json_to_markdown_helper.py | 119 ++++++++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 .github/workflows/trivy-evidence-example.yml create mode 100644 examples/trivy-verify-example/Dockerfile create mode 100644 examples/trivy-verify-example/README.md create mode 100644 examples/trivy-verify-example/trivy_json_to_markdown_helper.py diff --git a/.github/workflows/trivy-evidence-example.yml b/.github/workflows/trivy-evidence-example.yml new file mode 100644 index 0000000..866fd12 --- /dev/null +++ b/.github/workflows/trivy-evidence-example.yml @@ -0,0 +1,62 @@ +name: trivy-evidence-example + +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + package-docker-image-with-trivy-evidence: + runs-on: ubuntu-latest + env: + REGISTRY_URL: ${{ vars.REGISTRY_DOMAIN }} + REPO_NAME: 'docker-trivy-repo' + IMAGE_NAME: 'docker-trivy-image' + VERSION: ${{ github.run_number }} + BUILD_NAME: 'trivy-docker-build' + + steps: + - name: Install jfrog cli + uses: jfrog/setup-jfrog-cli@v4 + env: + JF_URL: ${{ vars.ARTIFACTORY_URL }} + JF_ACCESS_TOKEN: ${{ secrets.ARTIFACTORY_ACCESS_TOKEN }} + + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build Docker Image + run: | + docker build . --file ./examples/trivy-verify-example/Dockerfile --tag $REGISTRY_URL/$REPO_NAME/$IMAGE_NAME:$VERSION + - name: Run Trivy + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.REGISTRY_URL }}/${{ env.REPO_NAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }} + severity: HIGH,CRITICAL + format: json + output: trivy-results.json + + - name: Convert Trivy JSON Output to Markdown + run: python ./examples/trivy-verify-example/trivy_json_to_markdown_helper.py trivy-results.json + + - name: Push Docker Image to Artifactory + run: | + echo "Pushing Docker image to Artifactory..." + jf rt docker-push $REGISTRY_URL/$REPO_NAME/$IMAGE_NAME:$VERSION $REPO_NAME --build-name=$BUILD_NAME --build-number=${{ github.run_number }} + - name: Publish Build Info + run: | + jf rt build-publish $BUILD_NAME ${{ github.run_number }} + - name: Attach Evidence Using JFrog CLI + run: | + jf evd create \ + --package-name $IMAGE_NAME \ + --package-version $VERSION \ + --package-repo-name $REPO_NAME \ + --key "${{ secrets.TRIVY_TEST_PKEY }}" \ + --key-alias ${{ vars.TRIVY_TEST_KEY }} \ + --predicate ./trivy-results.json \ + --predicate-type http://aquasec.com/trivy/security-scan \ + --markdown trivy-results.md + echo "Trivy evidence attached to package" \ No newline at end of file diff --git a/examples/trivy-verify-example/Dockerfile b/examples/trivy-verify-example/Dockerfile new file mode 100644 index 0000000..7522cc3 --- /dev/null +++ b/examples/trivy-verify-example/Dockerfile @@ -0,0 +1,10 @@ +# Use the official lightweight Python image +FROM python:3.9-slim + +# Set the working directory +WORKDIR /app + +# Add a simple script that prints a message +RUN echo 'print("Hello from Docker!")' > hello.py + +CMD ["python", "hello.py"] \ No newline at end of file diff --git a/examples/trivy-verify-example/README.md b/examples/trivy-verify-example/README.md new file mode 100644 index 0000000..79f1092 --- /dev/null +++ b/examples/trivy-verify-example/README.md @@ -0,0 +1,91 @@ +# Trivy Security Scan Evidence Example + +This example demonstrates how to automate Trivy security scanning for Docker images and attach the scan results as +signed evidence to the image in JFrog Artifactory using GitHub Actions and JFrog CLI. + +## Overview + +The workflow builds a Docker image, scans it with Trivy for vulnerabilities, pushes the image to Artifactory, and +attaches the Trivy scan results as evidence to the image package. This enables traceability and compliance for security +scanning in your CI/CD pipeline. + +## Prerequisites + +- JFrog CLI 2.65.0 or above (installed automatically in the workflow) +- Artifactory configured as a Docker registry +- The following GitHub repository variables: + - `REGISTRY_DOMAIN` (Artifactory Docker registry domain, e.g. `mycompany.jfrog.io`) + - `ARTIFACTORY_URL` (Artifactory base URL) + - `TRIVY_TEST_KEY` (Key alias for signing evidence) +- The following GitHub repository secrets: + - `ARTIFACTORY_ACCESS_TOKEN` (Artifactory access token) + - `TRIVY_TEST_PKEY` (Private key for signing evidence) + +## Environment Variables Used + +- `REGISTRY_DOMAIN` - Docker registry domain + +## Workflow Steps + +1. **Install JFrog CLI** + - Installs the JFrog CLI using the official GitHub Action. +2. **Checkout Repository** + - Checks out the source code for the build context. +3. **Build Docker Image** + - Builds the Docker image using the provided Dockerfile and tags it for the Artifactory registry. +4. **Run Trivy Security Scan** + - Scans the built Docker image for vulnerabilities using Trivy and outputs the results in JSON format. +5. **Generate Custom Markdown For Trivy Results** + - (Optional) Converts the Trivy JSON scan results to markdown format for better readability using a python script + with predefined static markdown template. +6. **Push Docker Image to Artifactory** + - Pushes the tagged Docker image to the Artifactory Docker registry using JFrog CLI. +7. **Publish Build Info** + - Publishes build information to Artifactory for traceability. +8. **Attach Trivy Evidence Using JFrog CLI** + - Attaches the Trivy scan results as signed evidence to the Docker image package in Artifactory. + +## Example Usage + +You can trigger the workflow manually from the GitHub Actions tab. The workflow will: + +- Build and scan the Docker image +- Push the image to Artifactory +- Attach the Trivy scan results as evidence + +## Key Commands Used + +- **Build Docker Image:** + ```bash + docker build . --file ./examples/trivy-verify-example/Dockerfile --tag $REGISTRY_URL/$REPO_NAME/$IMAGE_NAME:$VERSION + ``` +- **Run Trivy Scan:** + ```yaml + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.REGISTRY_URL }}/${{ env.REPO_NAME }}/${{ env.IMAGE_NAME }}:${{ env.VERSION }} + severity: HIGH,CRITICAL + format: json + output: trivy-results.json + ``` +- **Push Docker Image:** + ```bash + jf rt docker-push $REGISTRY_URL/$REPO_NAME/$IMAGE_NAME:$VERSION $REPO_NAME --build-name=$BUILD_NAME --build-number=${{ github.run_number }} + ``` +- **Attach Evidence:** + ```bash + jf evd create \ + --package-name $IMAGE_NAME \ + --package-version $VERSION \ + --package-repo-name $REPO_NAME \ + --key "${{ secrets.TRIVY_TEST_PKEY }}" \ + --key-alias ${{ vars.TRIVY_TEST_KEY }} \ + --predicate ./trivy-results.json \ + --predicate-type http://aquasec.com/trivy/security-scan + ``` + +## References + +- [Trivy Documentation](https://aquasecurity.github.io/trivy/) +- [JFrog Evidence Management](https://jfrog.com/help/r/jfrog-artifactory-documentation/evidence-management) +- [JFrog CLI Documentation](https://jfrog.com/getcli/) diff --git a/examples/trivy-verify-example/trivy_json_to_markdown_helper.py b/examples/trivy-verify-example/trivy_json_to_markdown_helper.py new file mode 100644 index 0000000..47612eb --- /dev/null +++ b/examples/trivy-verify-example/trivy_json_to_markdown_helper.py @@ -0,0 +1,119 @@ +import json +import sys + + +def count_severity(vulnerabilities): + severity_counts = {'CRITICAL': 0, 'HIGH': 0, 'MEDIUM': 0, 'LOW': 0, 'UNKNOWN': 0} + for vuln in vulnerabilities: + severity = vuln['Severity'].upper() + if severity in severity_counts: + severity_counts[severity] += 1 + else: + severity_counts['UNKNOWN'] += 1 + return severity_counts + + +def generate_markdown_report(trivy_output): + artifact = trivy_output['ArtifactName'] + artifact_type = trivy_output['ArtifactType'] + created_at = trivy_output['CreatedAt'] + image_id = trivy_output.get('Metadata', {}).get('ImageID', 'N/A') + image_size = trivy_output.get('Metadata', {}).get('Size', 'N/A') + + if 'Results' in trivy_output and len(trivy_output['Results']) > 0: + os_info = trivy_output['Results'][0].get('Target', 'N/A').split() + os_name = os_info[-2] # Expected format "OS version" + os_version = os_info[-1] + else: + os_name = 'N/A' + os_version = 'N/A' + + all_vulnerabilities = [] + for result in trivy_output['Results']: + all_vulnerabilities.extend(result.get('Vulnerabilities', [])) + + severity_counts = count_severity(all_vulnerabilities) + + markdown_report = f""" +## Trivy Scan Report: {artifact} + +**Artifact Name:** `{artifact}` + +**Artifact Type:** `{artifact_type}` + +**Scan Date:** `{created_at}` + +**Operating System:** `{os_name} {os_version}` + +**Image ID:** `{image_id}` + +**Image Size:** `{image_size}` + +--- +### Overview of Vulnerabilities +| Severity | Count | +| :--------- | :---- | +| CRITICAL | {severity_counts.get('CRITICAL', 0)} | +| HIGH | {severity_counts.get('HIGH', 0)} | +| MEDIUM | {severity_counts.get('MEDIUM', 0)} | +| LOW | {severity_counts.get('LOW', 0)} | +| UNKNOWN | {severity_counts.get('UNKNOWN', 0)} | +--- +### Detected Vulnerabilities by Package +This section lists all detected vulnerabilities, categorized by the type of package (OS or language-specific) and then by individual packages. +""" + + for result in trivy_output['Results']: + package_class = result['Class'] + target = result['Target'] + + if package_class == 'os-pkgs': + markdown_report += f""" +#### OS Packages (`os-pkgs`) +**Target:** `{target}` +| Vulnerability ID | Package | Installed Version | Severity | Description | Status | +| :--------------- | :--------- | :---------------- | :------- | :-------------------------------------------- | :---------- | +""" + for vuln in result['Vulnerabilities']: + markdown_report += f"| {vuln['VulnerabilityID']} | {vuln['PkgName']} | {vuln['InstalledVersion']} | {vuln['Severity']} | {vuln['Description']} | {vuln['Status']} |\n" + + elif package_class == 'lang-pkgs': + markdown_report += f""" +#### Language-specific Packages (`lang-pkgs`) +**Target:** `{target}` +| Vulnerability ID | Package | Installed Version | Fixed Version | Severity | Description | Status | +| :--------------- | :--------- | :---------------- | :------------ | :------- | :-------------------------------------------- | :---------- | +""" + for vuln in result['Vulnerabilities']: + fixed_version = vuln.get('FixedVersion', 'N/A') + markdown_report += f"| {vuln['VulnerabilityID']} | {vuln['PkgName']} | {vuln['InstalledVersion']} | {fixed_version} | {vuln['Severity']} | {vuln['Description']} | {vuln['Status']} |\n" + + markdown_report += "\n---" + return markdown_report + + +def main(input_file): + # Read JSON input from a file + with open(input_file, 'r') as file: + trivy_output = json.load(file) + + # Generate the Markdown report + markdown_report = generate_markdown_report(trivy_output) + + # Define the output file path + output_file = 'trivy-results.md' + + # Write the Markdown report to a file + with open(output_file, 'w') as file: + file.write(markdown_report) + + print(f"Markdown report generated successfully and saved to {output_file}!") + + +if __name__ == '__main__': + if len(sys.argv) != 2: + print("Usage: python trivy_json_to_markdown_helper.py ") + sys.exit(1) + + input_file = sys.argv[1] + main(input_file)