From bf996103f5de92b9e39d81fb4956cc3aa26c5a52 Mon Sep 17 00:00:00 2001 From: Philipp Horstenkamp Date: Sun, 13 Sep 2026 00:12:26 +0200 Subject: [PATCH] feat: initial commit of cve-differential action --- .gitignore | 15 ++++ README.md | 81 +++++++++++++++++ action.yaml | 186 ++++++++++++++++++++++++++++++++++++++ checksums.txt | 22 +++++ cve-diff.js | 241 ++++++++++++++++++++++++++++++++++++++++++++++++++ markdown.tpl | 67 ++++++++++++++ renovate.json | 39 ++++++++ 7 files changed, 651 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 action.yaml create mode 100644 checksums.txt create mode 100755 cve-diff.js create mode 100644 markdown.tpl create mode 100644 renovate.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..363f8b8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +# OS / Editor files +.DS_Store +*.swp +*~ +.idea/ +.vscode/ + +# Scanned artifacts / temp reports +sboms/ +cve-reports/ +*.spdx.json +cve-*.json +cve-delta.md +cve-summary.md +node_modules/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..cb56cb8 --- /dev/null +++ b/README.md @@ -0,0 +1,81 @@ +# CVE Differential (`cve-differential`) + +A self-contained Gitea and GitHub Actions composite action that extracts multi-platform SBOM attestations from container images, runs vulnerability scans with [Trivy](https://github.com/aquasecurity/trivy), and produces a set-based CVE delta report between a head image and a target baseline image. + +--- + +## ✨ Features + +- **Multi-Platform Support**: Automatically detects and extracts SPDX SBOM attestations for `linux/amd64` and `linux/arm64`. +- **Fast Attestation Extraction**: Uses `docker buildx imagetools inspect` to extract SBOMs directly from registry manifest indices in seconds without downloading layer blobs. +- **Graceful Fallbacks**: If SBOM attestations or baseline images are missing, automatically falls back to generating SBOMs directly with Trivy. +- **Zero-Dependency Diff Engine**: Built with native Node.js (`node:fs`, `node:path`) — no Python or external packages required on runner environments. +- **Set-Based Differential Analysis**: Categorizes vulnerabilities into: + - 🔴 **Introduced Vulnerabilities** (newly added in this branch / commit) + - 🟢 **Fixed Vulnerabilities** (present in target baseline, resolved in this branch) + - ⚪ **Unchanged Vulnerabilities** (pre-existing in target baseline) +- **Secure by Default**: Verified Trivy binary downloads against static SHA256 checksum lockfiles and single-step unified caching (binary + vulnerability database). + +--- + +## 📥 Inputs + +| Input | Description | Required | Default | +| :--- | :--- | :---: | :--- | +| `head-image` | Head container image reference to scan (e.g. `registry.example.com/repo:git-sha`) | **Yes** | — | +| `target-image` | Target baseline container image reference to compare against (e.g. `registry.example.com/repo:latest`) | **Yes** | — | +| `target-branch` | Target branch name for report headings | No | `main` | +| `trivy-version` | Trivy version to install and verify | No | `0.74.0` | +| `output-delta` | Output path for the CVE delta Markdown report | No | `cve-delta.md` | +| `output-summary` | Output path for the full vulnerability Markdown summary | No | `cve-summary.md` | + +--- + +## 📤 Outputs + +| Output | Description | +| :--- | :--- | +| `delta-report` | Path to the generated CVE delta Markdown report | +| `summary-report` | Path to the full vulnerability Markdown summary report | + +--- + +## 🚀 Usage Example + +```yaml +jobs: + build-and-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Build and Push Head Container Image + uses: docker/build-push-action@v5 + with: + context: . + push: true + sbom: true + platforms: linux/amd64,linux/arm64 + tags: registry.example.com/my-app:git-${{ github.sha }} + + - name: Compare Vulnerabilities (Head vs Target) + uses: Philipp/cve-differential@main + with: + head-image: registry.example.com/my-app:git-${{ github.sha }} + target-image: registry.example.com/my-app:latest + target-branch: ${{ github.base_ref || 'main' }} + + - name: Publish CVE Delta to Job Summary + if: always() + run: | + if [ -f cve-delta.md ]; then + cat cve-delta.md >> "$GITHUB_STEP_SUMMARY" + fi +``` + +--- + +## 📄 License + +MIT diff --git a/action.yaml b/action.yaml new file mode 100644 index 0000000..3386b8c --- /dev/null +++ b/action.yaml @@ -0,0 +1,186 @@ +name: Trivy Multi-Arch CVE Delta Comparison +description: Downloads and verifies Trivy, extracts multi-platform SBOMs, and computes a consolidated set-based CVE delta report + +inputs: + head-image: + description: Head container image reference (e.g. host/server/gitea:git-12345678) + required: true + target-image: + description: Target baseline container image reference (e.g. host/server/gitea:latest) + required: true + target-branch: + description: Target branch name for delta heading (e.g. main) + required: false + default: main + trivy-version: + description: Trivy binary version to install and verify + required: false + default: 0.74.0 # trivy-version + output-delta: + description: Output markdown file for CVE delta report + required: false + default: cve-delta.md + output-summary: + description: Output markdown file for full vulnerability summary + required: false + default: cve-summary.md + +outputs: + delta-report: + description: Path to CVE delta markdown report + value: ${{ inputs.output-delta }} + summary-report: + description: Path to full vulnerability markdown report + value: ${{ inputs.output-summary }} + +runs: + using: composite + steps: + - name: Prepare Cache Keys + id: vars + shell: bash + run: echo "date=$(date -u +%Y-%m-%d)" >> "$GITHUB_OUTPUT" + + - name: Cache Trivy (Binary & Vulnerability DB) + uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0 + with: + path: | + ~/.cache/trivy-bin + ~/.cache/trivy + key: trivy-${{ inputs.trivy-version }}-${{ runner.os }}-${{ runner.arch }}-${{ steps.vars.outputs.date }} + restore-keys: | + trivy-${{ inputs.trivy-version }}-${{ runner.os }}-${{ runner.arch }}- + + - name: Install and Verify Trivy + shell: bash + run: | + mkdir -p "$HOME/.cache/trivy-bin" "$HOME/.local/bin" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + + ARCH=$(uname -m) + case "$ARCH" in + x86_64) T_ARCH="64bit" ;; + aarch64|arm64) T_ARCH="ARM64" ;; + *) echo "Unsupported architecture: $ARCH"; exit 1 ;; + esac + tarball="trivy_${{ inputs.trivy-version }}_Linux-${T_ARCH}.tar.gz" + checksums_file="${{ github.action_path }}/checksums.txt" + + if [ -f "$HOME/.cache/trivy-bin/${tarball}" ] && (cd "$HOME/.cache/trivy-bin" && grep " ${tarball}$" "$checksums_file" | sha256sum -c -); then + echo "Cached Trivy archive verified against lockfile." + else + echo "Archive missing or checksum mismatch; downloading..." + curl -sfLO "https://github.com/aquasecurity/trivy/releases/download/v${{ inputs.trivy-version }}/${tarball}" --output-dir "$HOME/.cache/trivy-bin" + (cd "$HOME/.cache/trivy-bin" && grep " ${tarball}$" "$checksums_file" | sha256sum -c -) + fi + + tar -xzf "$HOME/.cache/trivy-bin/${tarball}" -C "$HOME/.local/bin" trivy + chmod +x "$HOME/.local/bin/trivy" + + - name: Extract Multi-Platform SBOMs + shell: bash + run: | + export PATH="$HOME/.local/bin:$PATH" + mkdir -p sboms/head sboms/target + + cleanup_file() { + local f="$1" + if [ ! -s "$f" ] || [ "$(cat "$f" 2>/dev/null)" = "null" ]; then + rm -f "$f" + fi + } + + # Extract platform SBOMs for head image + echo "Extracting platform SBOMs for head image: ${{ inputs.head-image }}..." + docker buildx imagetools inspect "${{ inputs.head-image }}" \ + --format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sboms/head/sbom-amd64.spdx.json 2>/dev/null || true + cleanup_file sboms/head/sbom-amd64.spdx.json + + docker buildx imagetools inspect "${{ inputs.head-image }}" \ + --format '{{ json (index .SBOM "linux/arm64").SPDX }}' > sboms/head/sbom-arm64.spdx.json 2>/dev/null || true + cleanup_file sboms/head/sbom-arm64.spdx.json + + # If single-arch, extract root .SPDX + if [ ! -f sboms/head/sbom-amd64.spdx.json ] && [ ! -f sboms/head/sbom-arm64.spdx.json ]; then + docker buildx imagetools inspect "${{ inputs.head-image }}" \ + --format '{{ json .SBOM.SPDX }}' > sboms/head/sbom-amd64.spdx.json 2>/dev/null || true + cleanup_file sboms/head/sbom-amd64.spdx.json + fi + + # Fallback to generating head SBOM with Trivy if attestation missing + if [ ! -f sboms/head/sbom-amd64.spdx.json ] && [ ! -f sboms/head/sbom-arm64.spdx.json ]; then + echo "Head image lacks SBOM attestation; generating with Trivy..." + trivy image --format spdx-json --output sboms/head/sbom-amd64.spdx.json "${{ inputs.head-image }}" || true + cleanup_file sboms/head/sbom-amd64.spdx.json + fi + + # Extract platform SBOMs for target image + echo "Extracting platform SBOMs for target image: ${{ inputs.target-image }}..." + docker buildx imagetools inspect "${{ inputs.target-image }}" \ + --format '{{ json (index .SBOM "linux/amd64").SPDX }}' > sboms/target/sbom-amd64.spdx.json 2>/dev/null || true + cleanup_file sboms/target/sbom-amd64.spdx.json + + docker buildx imagetools inspect "${{ inputs.target-image }}" \ + --format '{{ json (index .SBOM "linux/arm64").SPDX }}' > sboms/target/sbom-arm64.spdx.json 2>/dev/null || true + cleanup_file sboms/target/sbom-arm64.spdx.json + + if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ]; then + docker buildx imagetools inspect "${{ inputs.target-image }}" \ + --format '{{ json .SBOM.SPDX }}' > sboms/target/sbom-amd64.spdx.json 2>/dev/null || true + cleanup_file sboms/target/sbom-amd64.spdx.json + fi + + # Fallback to generating target SBOM with Trivy if attestation missing + if [ ! -f sboms/target/sbom-amd64.spdx.json ] && [ ! -f sboms/target/sbom-arm64.spdx.json ]; then + echo "Target image lacks SBOM attestation; attempting to generate with Trivy..." + trivy image --format spdx-json --output sboms/target/sbom-amd64.spdx.json "${{ inputs.target-image }}" || true + cleanup_file sboms/target/sbom-amd64.spdx.json + fi + + # Expose extracted head SBOMs to workspace root for workflow artifact uploads + [ -f sboms/head/sbom-amd64.spdx.json ] && cp sboms/head/sbom-amd64.spdx.json sbom-amd64.spdx.json || true + [ -f sboms/head/sbom-arm64.spdx.json ] && cp sboms/head/sbom-arm64.spdx.json sbom-arm64.spdx.json || true + + - name: Scan Multi-Platform SBOMs and Generate Delta + shell: bash + run: | + export PATH="$HOME/.local/bin:$PATH" + mkdir -p cve-reports/head cve-reports/target + + # Scan each available head SBOM + for f in sboms/head/sbom-*.spdx.json; do + [ -f "$f" ] || continue + arch=$(basename "$f" .spdx.json | sed 's/sbom-//') + echo "Scanning head SBOM for architecture: $arch" + trivy sbom --format json --output "cve-reports/head/cve-${arch}.json" "$f" || true + done + + # Scan each available target SBOM + for f in sboms/target/sbom-*.spdx.json; do + [ -f "$f" ] || continue + arch=$(basename "$f" .spdx.json | sed 's/sbom-//') + echo "Scanning target SBOM for architecture: $arch" + trivy sbom --format json --output "cve-reports/target/cve-${arch}.json" "$f" || true + done + + # Run multi-arch set-based diff + node "${{ github.action_path }}/cve-diff.js" \ + "cve-reports/target" \ + "cve-reports/head" \ + "${{ inputs.target-branch }}" > "${{ inputs.output-delta }}" || true + + # Generate full markdown summary report for primary platform + primary_sbom="" + if [ -f sboms/head/sbom-amd64.spdx.json ]; then + primary_sbom="sboms/head/sbom-amd64.spdx.json" + elif [ -f sboms/head/sbom-arm64.spdx.json ]; then + primary_sbom="sboms/head/sbom-arm64.spdx.json" + fi + + if [ -n "$primary_sbom" ]; then + trivy sbom \ + --format template \ + --template "@${{ github.action_path }}/markdown.tpl" \ + --output "${{ inputs.output-summary }}" \ + "$primary_sbom" || true + fi diff --git a/checksums.txt b/checksums.txt new file mode 100644 index 0000000..1fec5b3 --- /dev/null +++ b/checksums.txt @@ -0,0 +1,22 @@ +b8e8834a90b0f28c172498a96d1b91876eb2f09867f02aa314df9a862aa86227 trivy_0.74.0_FreeBSD-64bit.tar.gz +cea47f6624cab9123a6bebc2f6586f3f82b8cd3165fc0ecd21677ba8a4ca0f5c trivy_0.74.0_Linux-32bit.deb +bec3a7aed02d60b6bb2996bb63897f604464c2db0b2b29dfc3cc7ce9959c8aaf trivy_0.74.0_Linux-32bit.rpm +048af0c9597a8e275f60bb9e0543748dbc76e22bdd52456b84ea0fa0cd2a078a trivy_0.74.0_Linux-32bit.tar.gz +cf1e32ec8d4d8823e023096a28cadb14f5b5123ce03f201fb633c5b76aa712dd trivy_0.74.0_Linux-64bit.deb +2331b82ec0ce5529a09fad8ccbfe03987ec9277ff4fbd6e96c6cb0aff8103477 trivy_0.74.0_Linux-64bit.rpm +2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a trivy_0.74.0_Linux-64bit.tar.gz +5665eaa443bfb117c5c4c373254f0ba9e2888985e6233588ce98bb0960f558c3 trivy_0.74.0_Linux-ARM.deb +bdd73ea298ec538ffae3351614a3adefc8c48599fce7943e6376fcfa040c0e85 trivy_0.74.0_Linux-ARM.rpm +99adbe319fa685cb4bff04dd0077e31cb94828338cab5db7f78fe6dcecca8ec1 trivy_0.74.0_Linux-ARM.tar.gz +14fae2604eab589fd544224ef56b8b8a44266872c43b259bd36cfde81428acc8 trivy_0.74.0_Linux-ARM64.deb +bb753f2dacd22417ac7709fdd910a0b017f4bb43330eac8b51c2a90403ae2fd3 trivy_0.74.0_Linux-ARM64.rpm +b94ce1976bbf3c15b514b605ee88be7c6d94a29be2302847ff01cb794d47aad5 trivy_0.74.0_Linux-ARM64.tar.gz +16a1e36b6d405951b36f1e4b31bb7655f8551f1581934fcb2e6e5bdf8c46f6c9 trivy_0.74.0_Linux-PPC64LE.deb +cc7183af95dbab1ae1400fca17b35ca8f9591676a1042144994698cad9cac5a6 trivy_0.74.0_Linux-PPC64LE.rpm +c5aabfe8789d0d43f578ef2d01174884c8ff0f2106c627a220a43f7bf9054959 trivy_0.74.0_Linux-PPC64LE.tar.gz +f8fe56cee9a502e82f05e45465943e3bc6e75de05530ffc7bddf93e65b0de5bb trivy_0.74.0_Linux-s390x.deb +4fb2d5bd1ab5e61917e73cd2936daeae1c5ab9af024321a222437d824bac5579 trivy_0.74.0_Linux-s390x.rpm +516cfe38eaede79c178088265657aad12be6c155271ac68ff728a141961ef197 trivy_0.74.0_Linux-s390x.tar.gz +472816f6888dda689d075c30254d4210b4d1035acf365aa72332f584c2f60485 trivy_0.74.0_macOS-64bit.tar.gz +1caada5e0e2091909357c7525d3aa76f4b660b13821bc143b190c7483e31cc11 trivy_0.74.0_macOS-ARM64.tar.gz +94c40e0696e4b907a74b7b2e1438d5d72ebaca83115817407f568a002d520842 trivy_0.74.0_windows-64bit.zip diff --git a/cve-diff.js b/cve-diff.js new file mode 100755 index 0000000..71628ef --- /dev/null +++ b/cve-diff.js @@ -0,0 +1,241 @@ +#!/usr/bin/env node + +/** + * Compare two Trivy vulnerability JSON reports (supporting multi-arch) and produce a set-based Markdown diff. + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +const SEVERITY_ORDER = ['CRITICAL', 'HIGH', 'MEDIUM', 'LOW', 'UNKNOWN']; + +const SEVERITY_BADGES = { + CRITICAL: '🔴 CRITICAL', + HIGH: '🟠 HIGH', + MEDIUM: '🟡 MEDIUM', + LOW: '🔵 LOW', + UNKNOWN: '⚪ UNKNOWN', +}; + +/** + * Loads one or more JSON reports (path, comma-separated list, or directory) and merges findings with arch tracking. + * @param {string} arg + * @returns {Map} + */ +function loadReportsForArg(arg) { + const cves = new Map(); + const filePaths = []; + + for (const part of arg.split(',').map((s) => s.trim())) { + if (!part || !fs.existsSync(part)) continue; + const stat = fs.statSync(part); + if (stat.isDirectory()) { + const entries = fs.readdirSync(part).sort(); + for (const entry of entries) { + if (entry.endsWith('.json')) { + filePaths.push(path.join(part, entry)); + } + } + } else if (stat.isFile()) { + filePaths.push(part); + } + } + + for (const filePath of filePaths) { + let stat; + try { + stat = fs.statSync(filePath); + } catch { + continue; + } + if (stat.size === 0) continue; + + const baseName = path.basename(filePath).toLowerCase(); + let arch = 'all'; + if (baseName.includes('amd64')) { + arch = 'amd64'; + } else if (baseName.includes('arm64') || baseName.includes('aarch64')) { + arch = 'arm64'; + } + + let data; + try { + const content = fs.readFileSync(filePath, 'utf8'); + data = JSON.parse(content); + } catch { + continue; + } + + if (!data || typeof data !== 'object' || !Array.isArray(data.Results)) { + continue; + } + + for (const result of data.Results) { + const target = result.Target || result.Type || 'App'; + const vulns = Array.isArray(result.Vulnerabilities) ? result.Vulnerabilities : []; + for (const v of vulns) { + const vid = v.VulnerabilityID || ''; + const pkg = v.PkgName || ''; + if (!vid || !pkg) continue; + + const key = `${vid}:::${pkg}`; + if (!cves.has(key)) { + cves.set(key, { ...v, _target: target, _archs: new Set([arch]) }); + } else { + cves.get(key)._archs.add(arch); + } + } + } + } + + return cves; +} + +/** + * Formats a vulnerability record into a Markdown table row. + * @param {object} v + * @returns {string} + */ +function formatVulnRow(v) { + const sev = v.Severity || 'UNKNOWN'; + const badge = SEVERITY_BADGES[sev] || `⚪ ${sev}`; + const vid = v.VulnerabilityID || ''; + const url = v.PrimaryURL || ''; + const link = url ? `[${vid}](${url})` : vid; + const pkg = v.PkgName || ''; + + const archs = v._archs || new Set(['all']); + let archBadge; + if ((archs.has('amd64') && archs.has('arm64')) || archs.has('all')) { + archBadge = '`all`'; + } else { + archBadge = `\`${Array.from(archs).sort().join(', ')}\``; + } + + const installed = v.InstalledVersion || '-'; + const fixed = v.FixedVersion || '-'; + let title = (v.Title || v.Description || '').replace(/\n/g, ' ').trim(); + if (title.length > 80) { + title = title.slice(0, 77) + '...'; + } + + return `| ${badge} | ${link} | \`${pkg}\` | ${archBadge} | ${installed} | ${fixed} | ${title} |`; +} + +/** + * Compares base and branch vulnerabilities and formats a Markdown diff. + * @param {Map} baseCves + * @param {Map} branchCves + * @param {string} baseName + * @returns {string} + */ +function renderMarkdownDiff(baseCves, branchCves, baseName = 'main') { + if (baseCves.size === 0) { + return [ + `### 🔄 Vulnerability Delta (vs \`${baseName}\`)`, + '', + '> [!NOTE]', + `> No baseline scan from \`${baseName}\` was available to compare against.`, + '', + ].join('\n'); + } + + const baseKeys = new Set(baseCves.keys()); + const branchKeys = new Set(branchCves.keys()); + + const newKeys = []; + let unchangedCount = 0; + for (const key of branchKeys) { + if (!baseKeys.has(key)) { + newKeys.push(key); + } else { + unchangedCount++; + } + } + + const resolvedKeys = []; + for (const key of baseKeys) { + if (!branchKeys.has(key)) { + resolvedKeys.push(key); + } + } + + function sortVulns(a, b) { + const aSev = a.Severity || 'UNKNOWN'; + const bSev = b.Severity || 'UNKNOWN'; + const aIdx = SEVERITY_ORDER.indexOf(aSev) !== -1 ? SEVERITY_ORDER.indexOf(aSev) : 99; + const bIdx = SEVERITY_ORDER.indexOf(bSev) !== -1 ? SEVERITY_ORDER.indexOf(bSev) : 99; + if (aIdx !== bIdx) return aIdx - bIdx; + const aPkg = a.PkgName || ''; + const bPkg = b.PkgName || ''; + return aPkg.localeCompare(bPkg); + } + + const newVulns = newKeys.map((k) => branchCves.get(k)).sort(sortVulns); + const resolvedVulns = resolvedKeys.map((k) => baseCves.get(k)).sort(sortVulns); + + const lines = [ + `### 🔄 Vulnerability Delta (vs \`${baseName}\`)`, + '', + '| Metric | Count |', + '| :--- | :--- |', + `| 🚨 **New Vulnerabilities** | ${newVulns.length > 0 ? `🚨 **${newVulns.length}**` : '0'} |`, + `| ✅ **Resolved Vulnerabilities** | ${resolvedVulns.length > 0 ? `✅ **${resolvedVulns.length}**` : '0'} |`, + `| ⚪ **Unchanged Vulnerabilities** | ${unchangedCount} |`, + '', + `#### 🚨 New Vulnerabilities Introduced (${newVulns.length})`, + ]; + + if (newVulns.length > 0) { + lines.push('| Severity | CVE | Package | Arch | Installed | Fixed In | Title |'); + lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); + for (const v of newVulns) { + lines.push(formatVulnRow(v)); + } + } else { + lines.push('✅ No new vulnerabilities introduced in this build.'); + } + lines.push(''); + + lines.push(`#### ✅ Resolved / Removed Vulnerabilities (${resolvedVulns.length})`); + if (resolvedVulns.length > 0) { + lines.push('| Severity | CVE | Package | Arch | Previously Installed | Fixed In | Title |'); + lines.push('| :--- | :--- | :--- | :--- | :--- | :--- | :--- |'); + for (const v of resolvedVulns) { + lines.push(formatVulnRow(v)); + } + } else { + lines.push('⚪ No existing vulnerabilities were resolved.'); + } + lines.push(''); + + return lines.join('\n'); +} + +function main() { + const args = process.argv.slice(2); + if (args.length < 2) { + console.error('Usage: cve-diff.js [base_name]'); + process.exit(1); + } + + const baseArg = args[0]; + const branchArg = args[1]; + const baseName = args[2] || 'main'; + + const baseCves = loadReportsForArg(baseArg); + const branchCves = loadReportsForArg(branchArg); + + const output = renderMarkdownDiff(baseCves, branchCves, baseName); + console.log(output); +} + +if (require.main === module) { + main(); +} + +module.exports = { + loadReportsForArg, + formatVulnRow, + renderMarkdownDiff, +}; diff --git a/markdown.tpl b/markdown.tpl new file mode 100644 index 0000000..45bab8e --- /dev/null +++ b/markdown.tpl @@ -0,0 +1,67 @@ +### 🛡️ Security Scan Report + +{{- range . }} +{{- $targetName := .Target }} +{{- if eq $targetName "" }} + {{- if eq .Type "gobinary" }} + {{- $targetName = "Go binary" }} + {{- else if .Type }} + {{- $targetName = .Type }} + {{- else if .Class }} + {{- $targetName = .Class }} + {{- else }} + {{- $targetName = "Application / Package" }} + {{- end }} +{{- end }} + +{{- $hasFindings := false }} + +{{- if .Vulnerabilities }} +{{- $hasFindings = true }} +#### Target: `{{ $targetName }}` (Vulnerabilities) + +| Severity | CVE | Package | Installed | Fixed In | Title | +| :--- | :--- | :--- | :--- | :--- | :--- | +{{- $vulns := .Vulnerabilities }} +{{- range $sev := list "CRITICAL" "HIGH" "MEDIUM" "LOW" "UNKNOWN" }} +{{- range $vuln := $vulns }} +{{- if eq $vuln.Severity $sev }} +| {{ if eq $vuln.Severity "CRITICAL" }}🔴 CRITICAL{{ else if eq $vuln.Severity "HIGH" }}🟠 HIGH{{ else if eq $vuln.Severity "MEDIUM" }}🟡 MEDIUM{{ else if eq $vuln.Severity "LOW" }}🔵 LOW{{ else }}⚪ {{ $vuln.Severity }}{{ end }} | [{{ $vuln.VulnerabilityID }}]({{ $vuln.PrimaryURL }}) | `{{ $vuln.PkgName }}` | {{ $vuln.InstalledVersion }} | {{ default "-" $vuln.FixedVersion }} | {{ $vuln.Title }} | +{{- end }} +{{- end }} +{{- end }} +{{- end }} + +{{- if .Misconfigurations }} +{{- $hasFindings = true }} +#### Target: `{{ $targetName }}` (Misconfigurations) + +| Severity | Rule ID | Title | Message | Resolution | +| :--- | :--- | :--- | :--- | :--- | +{{- $misconfs := .Misconfigurations }} +{{- range $sev := list "CRITICAL" "HIGH" "MEDIUM" "LOW" "UNKNOWN" }} +{{- range $m := $misconfs }} +{{- if eq $m.Severity $sev }} +| {{ if eq $m.Severity "CRITICAL" }}🔴 CRITICAL{{ else if eq $m.Severity "HIGH" }}🟠 HIGH{{ else if eq $m.Severity "MEDIUM" }}🟡 MEDIUM{{ else if eq $m.Severity "LOW" }}🔵 LOW{{ else }}⚪ {{ $m.Severity }}{{ end }} | [{{ $m.ID }}]({{ $m.PrimaryURL }}) | {{ $m.Title }} | {{ $m.Message }} | {{ default "-" $m.Resolution }} | +{{- end }} +{{- end }} +{{- end }} +{{- end }} + +{{- if .Secrets }} +{{- $hasFindings = true }} +#### Target: `{{ $targetName }}` (Secrets) + +| Severity | Rule ID | Title | Lines | +| :--- | :--- | :--- | :--- | +{{- range .Secrets }} +| {{ if eq .Severity "CRITICAL" }}🔴 CRITICAL{{ else if eq .Severity "HIGH" }}🟠 HIGH{{ else if eq .Severity "MEDIUM" }}🟡 MEDIUM{{ else if eq .Severity "LOW" }}🔵 LOW{{ else }}⚪ {{ .Severity }}{{ end }} | {{ .RuleID }} | {{ .Title }} | L{{ .StartLine }}-L{{ .EndLine }} | +{{- end }} +{{- end }} + +{{- if not $hasFindings }} +#### Target: `{{ $targetName }}` +✅ No security issues found. +{{- end }} + +{{- end }} diff --git a/renovate.json b/renovate.json new file mode 100644 index 0000000..4e9d111 --- /dev/null +++ b/renovate.json @@ -0,0 +1,39 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:recommended" + ], + "customManagers": [ + { + "customType": "regex", + "fileMatch": [ + "^action\\.ya?ml$" + ], + "matchStrings": [ + "default:\\s*(?[0-9]+\\.[0-9]+\\.[0-9]+)\\s*#\\s*trivy-version" + ], + "depNameTemplate": "aquasecurity/trivy", + "datasourceTemplate": "github-releases", + "versioningTemplate": "semver" + } + ], + "packageRules": [ + { + "matchDatasources": [ + "github-releases" + ], + "matchPackageNames": [ + "aquasecurity/trivy" + ], + "postUpgradeTasks": { + "commands": [ + "curl -sfL https://github.com/aquasecurity/trivy/releases/download/v{{{newVersion}}}/trivy_{{{newVersion}}}_checksums.txt -o checksums.txt" + ], + "fileFilters": [ + "checksums.txt" + ], + "executionMode": "update" + } + } + ] +}