Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 62 additions & 0 deletions .github/workflows/trivy-evidence-example.yml
Original file line number Diff line number Diff line change
@@ -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"
10 changes: 10 additions & 0 deletions examples/trivy-verify-example/Dockerfile
Original file line number Diff line number Diff line change
@@ -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"]
91 changes: 91 additions & 0 deletions examples/trivy-verify-example/README.md
Original file line number Diff line number Diff line change
@@ -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/)
119 changes: 119 additions & 0 deletions examples/trivy-verify-example/trivy_json_to_markdown_helper.py
Original file line number Diff line number Diff line change
@@ -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 <input_file>")
sys.exit(1)

input_file = sys.argv[1]
main(input_file)