diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..eb8bc22 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: "pip" + directory: "/examples/dependabot-alerts-example" + schedule: + interval: "daily" diff --git a/.github/workflows/dependabot-evidence-example.yml b/.github/workflows/dependabot-evidence-example.yml new file mode 100644 index 0000000..978f666 --- /dev/null +++ b/.github/workflows/dependabot-evidence-example.yml @@ -0,0 +1,102 @@ +name: dependabot-evidence-example +on: + workflow_dispatch: + +permissions: + id-token: write + contents: read + +jobs: + dependabot-evidence-example: + runs-on: ubuntu-latest + env: + REPO_NAME: 'dependabot-docker-local' + IMAGE_NAME: 'dependabot-docker-image' + BUILD_NAME: 'dependabot-evidence-eg' + VERSION: ${{ github.run_number }} + REGISTRY_DOMAIN: ${{ vars.REGISTRY_DOMAIN }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup JFrog CLI + uses: jfrog/setup-jfrog-cli@v4 + env: + JF_URL: ${{ vars.ARTIFACTORY_URL }} + JF_ACCESS_TOKEN: ${{ secrets.ARTIFACTORY_ACCESS_TOKEN }} + + - name: Log in to Artifactory Docker Registry + uses: docker/login-action@v3 + with: + registry: ${{ vars.ARTIFACTORY_URL }} + username: ${{ secrets.JF_USER }} + password: ${{ secrets.ARTIFACTORY_ACCESS_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build and Push Docker Image to Artifactory + run: | + docker build -f ./examples/dependabot-alerts-example/Dockerfile . --tag $REGISTRY_DOMAIN/$REPO_NAME/$IMAGE_NAME:$VERSION + jf rt docker-push $REGISTRY_DOMAIN/$REPO_NAME/$IMAGE_NAME:$VERSION $REPO_NAME --build-name=$BUILD_NAME --build-number=$VERSION + + - name: Get Artifact Details + run: | + ARTIFACT_NAME="$REGISTRY_DOMAIN/$REPO_NAME/$IMAGE_NAME:$VERSION" + echo "ARTIFACT_NAME=$ARTIFACT_NAME" >> $GITHUB_ENV + + IMAGE_ID=$(docker images --format "{{.ID}}" "$ARTIFACT_NAME") + echo "IMAGE_ID=$IMAGE_ID" >> $GITHUB_ENV + + IMAGE_SIZE=$(docker images --format "{{.Size}}" "$ARTIFACT_NAME" | sed 's/MB//' | awk '{print $1 * 1024 * 1024}') + echo "IMAGE_SIZE=$IMAGE_SIZE" >> $GITHUB_ENV + + echo "SCAN_DATE="$(date -u +"%Y-%m-%dT%H:%M:%SZ")"" >> $GITHUB_ENV + + - name: Fetch Dependabot Vulnerability Snapshot + id: dependabot_snapshot + env: + GH_TOKEN: ${{ secrets.TOKEN_GIT }} # GitHub Token with 'security_events: read' permission is required + OWNER: ${{ github.repository_owner }} + REPO: ${{ github.event.repository.name }} + run: | + gh api "repos/${OWNER}/${REPO}/dependabot/alerts?state=open" \ + --jq '[.[] | + { + packageName: .dependency.package.name, + ecosystem: .dependency.package.ecosystem, + vulnerableVersionRange: .security_vulnerability.vulnerable_version_range, + patchedVersion: (try .security_vulnerability.first_patched_version.identifier // "N/A"), + severity: .security_vulnerability.severity, + ghsaId: .security_advisory.ghsa_id, + cveId: (.security_advisory.cve_id // "N/A"), + advisoryUrl: .html_url, + summary: .security_advisory.summary, + detectedAt: .created_at + } + ]' > result.json + + jq -n --argjson data "$(cat result.json)" '{ data: $data }' > dependabot.json + + - name: Generate and Save Dependabot Markdown Report + run: | + python ./examples/dependabot-alerts-example/markdown_helper.py \ + "dependabot.json" \ + "dependabot_report.md" \ + "$ARTIFACT_NAME" \ + "$SCAN_DATE" \ + "$IMAGE_ID" \ + "$IMAGE_SIZE" + + - name: Create Dependabot Evidence + run: | + jf evd create \ + --package-name $IMAGE_NAME \ + --package-version $VERSION \ + --package-repo-name $REPO_NAME \ + --key "${{ secrets.TEST_PRVT_KEY }}" \ + --key-alias ${{ vars.TEST_PUB_KEY_ALIAS }} \ + --predicate ./dependabot.json \ + --predicate-type http://Github.com/Dependabot/static-analysis \ + --markdown dependabot_report.md \ No newline at end of file diff --git a/examples/dependabot-alerts-example/Dockerfile b/examples/dependabot-alerts-example/Dockerfile new file mode 100644 index 0000000..083aa10 --- /dev/null +++ b/examples/dependabot-alerts-example/Dockerfile @@ -0,0 +1,9 @@ +FROM python:3.7-slim-buster + +WORKDIR /app + +COPY ./examples/dependabot-alerts-example/requirements.txt . + +RUN pip install --no-cache-dir -r requirements.txt + +CMD ["ansible", "--version"] \ No newline at end of file diff --git a/examples/dependabot-alerts-example/README.md b/examples/dependabot-alerts-example/README.md new file mode 100644 index 0000000..a2cfeda --- /dev/null +++ b/examples/dependabot-alerts-example/README.md @@ -0,0 +1,104 @@ +# Dependabot Vulnerability Alerts Evidence Example Workflow + +The GitHub Actions workflow, named dependabot-evidence-example.yml, demonstrates how to automate the collection of Dependabot vulnerability alerts and attach them as signed evidence to a Docker image within JFrog Artifactory. + +## Overview +The workflow builds a Docker image, fetches open Dependabot vulnerability alerts for the repository, pushes the Docker image to JFrog Artifactory, and attaches the Dependabot alerts as signed evidence to the Docker image package. This workflow's primary goal is to automate the collection of security scan results from Dependabot and associate them directly with the deployed artifact in Artifactory, enhancing traceability and compliance for security posture in your CI/CD pipeline. + +## Prerequisites +- JFrog CLI 2.65.0 or above (installed automatically in the workflow) +- Artifactory configured as a Docker registry +- GitHub repository variables: Configure the following variables in your GitHub repository settings + (Settings > Secrets and variables > Actions > Variables) + - `REGISTRY_DOMAIN` (Artifactory Docker registry domain, e.g. `mycompany.jfrog.io`) + - `ARTIFACTORY_URL` (Artifactory base URL) + - `TEST_PUB_KEY_ALIAS` (Key alias for verifying evidence) +- GitHub repository secrets: Configure the following secrets in your GitHub repository settings + (Settings > Secrets and variables > Actions > Repository secrets) + - `ARTIFACTORY_ACCESS_TOKEN` (Artifactory access token) + - `JF_USER` (Artifactory username) + - `TEST_PRVT_KEY` (Private key for signing evidence) + - `TOKEN_GIT` (A GitHub Token with "security_events: read" permission to access Dependabot alerts via the GitHub API) + +## Environment Variables Used +- `REGISTRY_DOMAIN` - Docker registry domain +- `REPO_NAME` - Docker repository name +- `IMAGE_NAME` - Docker image name +- `VERSION` - Image version +- `BUILD_NAME` - Name for the build info + +## Workflow Steps +1. **Checkout Repository** + - Checks out the source code for the build context. +2. **Setup JFrog CLI** + - Install and Setup the JFrog CLI using the official GitHub Action. +3. **Log in to Artifactory Docker Registry** + - Authenticates Docker with Artifactory for pushing the image. +4. **Set up Docker Buildx** + - Prepares Docker Buildx for advanced build and push operations. +5. **Build and Push Docker Image to Artifactory** + - Builds the Docker image using the provided Dockerfile and tags it for the Artifactory registry. + - Pushes the tagged Docker image to the Artifactory Docker registry using JFrog CLI. +8. **Fetch Dependabot Vulnerability Snapshot** + - Fetchs the snapshot of open Dependabot vulnerability alerts for the repository and outputs the results in JSON format. +9. **Create Dependabot Evidence Using JFrog CLI** + - Attaches the Dependabot vulnerability snapshot as signed evidence to the Docker image package in Artifactory. + +## Example Dependabot Vulnerability Alert Data + +The Fetch Dependabot Vulnerability Snapshot step retrieves Dependabot alerts and transforms them into a structured JSON format. +- advisoryUrl: Link to the security advisory. +- cveId: Common Vulnerabilities and Exposures identifier (e.g., CVE-2020-1734). +- detectedAt: Timestamp when the vulnerability was detected. +- ecosystem: The package ecosystem (e.g., pip). +- ghsaId: GitHub Security Advisory ID (e.g., GHSA-h39q-95q5-9jfp). +- packageName: The name of the vulnerable package (e.g., ansible). +- patchedVersion: The version where the vulnerability is patched (e.g., 2.9.11, or N/A if not specified). +- severity: The severity level (e.g., high, medium, low). +- summary: A brief summary of the vulnerability. +- vulnerableVersionRange: The version range affected by the vulnerability. + +## Key Commands Used + +- **Build and Push Docker Image to Artifactory** + ```bash + docker build -f ./examples/dependabot-alerts-example/Dockerfile . --tag $REGISTRY_DOMAIN/$REPO_NAME/$IMAGE_NAME:$VERSION + jf rt docker-push $REGISTRY_DOMAIN/$REPO_NAME/$IMAGE_NAME:$VERSION $REPO_NAME --build-name=$BUILD_NAME --build-number=$VERSION + ``` +- **Fetch Dependabot Vulnerability Snapshot** + ```bash + gh api "repos/${OWNER}/${REPO}/dependabot/alerts?state=open" \ + --jq '[.[] | + { + packageName: .dependency.package.name, + ecosystem: .dependency.package.ecosystem, + vulnerableVersionRange: .security_vulnerability.vulnerable_version_range, + patchedVersion: (try .security_vulnerability.first_patched_version.identifier // "N/A"), + severity: .security_vulnerability.severity, + ghsaId: .security_advisory.ghsa_id, + cveId: (.security_advisory.cve_id // "N/A"), + advisoryUrl: .html_url, + summary: .security_advisory.summary, + detectedAt: .created_at + } + ]' > result.json + + jq -n --argjson data "$(cat result.json)" '{ data: $data }' > dependabot.json + ``` +- **Attach Evidence:** + ```bash + jf evd create \ + --package-name $IMAGE_NAME \ + --package-version $VERSION \ + --package-repo-name $REPO_NAME \ + --key "${{ secrets.TEST_PRVT_KEY }}" \ + --key-alias ${{ vars.TEST_PUB_KEY_ALIAS }} \ + --predicate ./dependabot.json \ + --predicate-type http://Github.com/Dependabot/static-analysis + ``` + +## References +- [Dependabot Documentation](https://docs.github.com/en/rest/dependabot) +- [JFrog Evidence Management](https://jfrog.com/help/r/jfrog-artifactory-documentation/evidence-management) +- [JFrog CLI Documentation](https://jfrog.com/getcli/) + diff --git a/examples/dependabot-alerts-example/markdown_helper.py b/examples/dependabot-alerts-example/markdown_helper.py new file mode 100644 index 0000000..c4ead36 --- /dev/null +++ b/examples/dependabot-alerts-example/markdown_helper.py @@ -0,0 +1,113 @@ +import json +import sys + +def generate_dependabot_markdown_report(json_file_path, artifact_name, scan_date, image_id, image_size): + try: + with open(json_file_path, 'r') as f: + data = json.load(f) + except FileNotFoundError: + return f"Error: The file '{json_file_path}' was not found. Please ensure it exists." + except json.JSONDecodeError: + return f"Error: Could not decode JSON from '{json_file_path}'. Please verify the file's content." + + markdown_output = f"# Dependabot Vulnerability Report\n\n" + + markdown_output += f""" +**Artifact Name:** `{artifact_name}` + +**Scan Date:** `{scan_date}` + +**Image ID:** `{image_id}` + +**Image Size:** `{image_size}` + + +""" + + alerts_data = data.get("data", []) + alerts_found = bool(alerts_data) + + severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "unknown": 0} + + for alert in alerts_data: + severity = alert.get("severity", "unknown").lower() + if severity in severity_counts: + severity_counts[severity] += 1 + else: + severity_counts["unknown"] += 1 + + markdown_output += "---\n\n" + markdown_output += "## Overview of Vulnerabilities\n\n" + markdown_output += "| Severity | Count |\n" + markdown_output += "| ------ | ------ |\n" + markdown_output += f"| CRITICAL | {severity_counts['critical']} |\n" + markdown_output += f"| HIGH | {severity_counts['high']} |\n" + markdown_output += f"| MEDIUM | {severity_counts['medium']} |\n" + markdown_output += f"| LOW | {severity_counts['low']} |\n" + markdown_output += f"| UNKNOWN | {severity_counts['unknown']} |\n\n" + + markdown_output += "---\n\n" + + markdown_output += "## Detected Vulnerabilities by Package\n\n" + + if not alerts_found: + markdown_output += "No Dependabot alerts were found in the provided JSON.\n" + else: + for alert in alerts_data: + package_name = alert.get("packageName", "N/A") + summary = alert.get("summary", "No summary provided.") + ecosystem = alert.get("ecosystem", "N/A") + cve_id = alert.get("cveId", "N/A") + ghsa_id = alert.get("ghsaId", "N/A") + severity = alert.get("severity", "N/A").capitalize() + vulnerable_range = alert.get("vulnerableVersionRange", "N/A") + patched_version = alert.get("patchedVersion", "N/A") + advisory_url = alert.get("advisoryUrl", "N/A") + detected_at = alert.get("detectedAt", "N/A") + + markdown_output += f"### Vulnerability: **{summary}**\n" + markdown_output += f"- **Package**: `{package_name}` (Ecosystem: `{ecosystem}`)\n" + markdown_output += f"- **Severity**: **{severity}**\n" + + if cve_id and cve_id != "N/A": + markdown_output += f"- **CVE ID**: `{cve_id}`\n" + if ghsa_id and ghsa_id != "N/A": + markdown_output += f"- **GHSA ID**: `{ghsa_id}`\n" + + markdown_output += f"- **Vulnerable Version Range**: `{vulnerable_range}`\n" + markdown_output += f"- **First Patched Version**: `{patched_version}`\n" + markdown_output += f"- **Detected At**: `{detected_at}`\n" + + if advisory_url and advisory_url != "N/A": + markdown_output += f"- **Advisory URL**: <{advisory_url}>\n" + markdown_output += "\n---\n\n" + + return markdown_output + +if __name__ == "__main__": + if len(sys.argv) != 7: + print("Usage: python markdown_helper.py ") + sys.exit(1) + + json_file_path = sys.argv[1] + output_markdown_path = sys.argv[2] + artifact_name = sys.argv[3] + scan_date = sys.argv[4] + image_id = sys.argv[5] + image_size = sys.argv[6] + + markdown_report = generate_dependabot_markdown_report( + json_file_path, + artifact_name, + scan_date, + image_id, + image_size + ) + + try: + with open(output_markdown_path, 'w') as outfile: + outfile.write(markdown_report) + print(f"Dependabot vulnerability report successfully generated and saved to '{output_markdown_path}'") + except IOError as e: + print(f"Error: Could not write the report to '{output_markdown_path}'. Reason: {e}") + sys.exit(1) \ No newline at end of file diff --git a/examples/dependabot-alerts-example/requirements.txt b/examples/dependabot-alerts-example/requirements.txt new file mode 100644 index 0000000..b9f0773 --- /dev/null +++ b/examples/dependabot-alerts-example/requirements.txt @@ -0,0 +1 @@ +ansible==2.9.9 \ No newline at end of file