diff --git a/.github/workflows/generate-vulnerability-report.yaml b/.github/workflows/generate-vulnerability-report.yaml new file mode 100644 index 0000000..20726be --- /dev/null +++ b/.github/workflows/generate-vulnerability-report.yaml @@ -0,0 +1,65 @@ +name: Generate Vulnerability Report + +on: + workflow_dispatch: + inputs: + imageTag: + description: 'Image Tag to scan' + required: true + default: 'latest' + type: string + +permissions: + contents: read + issues: write + +jobs: + run-scan: + runs-on: ubuntu-latest + environment: dockerhub + steps: + - uses: actions/checkout@v6 + + - uses: actions/setup-node@v6 + with: + node-version: 26 + + - name: Login to Docker Hub + uses: docker/login-action@v4 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Get sha256 digest of requested tag + id: digest + env: + IMAGE_TAG: ${{ inputs.imageTag }} + run: | + digest=$(docker buildx imagetools inspect "openintegrationengine/engine:${IMAGE_TAG}" --raw \ + | jq -r '.manifests[] | select(.platform.os == "linux" and .platform.architecture == "amd64") | .digest') + + echo "Got digest: $digest" + echo "imageDigest=$digest" >> "$GITHUB_OUTPUT" + echo "startTime=$(date -u +"%Y-%m-%dT%H:%M:%SZ")" >> "$GITHUB_OUTPUT" + + - name: Docker Scout + id: docker-scout + uses: docker/scout-action@v1 + with: + command: cves + image: openintegrationengine/engine@${{ steps.digest.outputs.imageDigest }} + platform: linux/amd64 + only-severities: critical,high + ignore-base: true + sarif-file: reports/sarif.json + + - name: Render report + working-directory: ./reports + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + IMAGE_TAG: ${{ inputs.imageTag }} + IMAGE_DIGEST: ${{ steps.digest.outputs.imageDigest }} + START_TIME: ${{ steps.digest.outputs.startTime }} + run: | + node render-report.ts "$IMAGE_TAG" "$IMAGE_DIGEST" "$START_TIME" + gh issue create --title "OpenIntegrationEngine ${IMAGE_TAG} vulnerabilities" --body-file report.md diff --git a/.github/workflows/render-docker-templates.yaml b/.github/workflows/render-docker-templates.yaml new file mode 100644 index 0000000..7647a0e --- /dev/null +++ b/.github/workflows/render-docker-templates.yaml @@ -0,0 +1,63 @@ +name: Render Docker templates + +on: + workflow_dispatch: + pull_request: + paths: + - releases.yaml + - render_docker_templates.sh + - templates/** + - dockerfiles/** + - .github/workflows/render-docker-templates.yaml + +jobs: + render-dockerfiles: + runs-on: ubuntu-latest + env: + GOMPLATE_VERSION: 4.3.2 + GOMPLATE_SHA512: ed77ea781cc2b63fbb325dbb29bbe3041f5158ccaec555fdc79b6ecca1b4c48fb6be2fee6612469c715b77fb9179be6e263ee1d58773232dfc346249afe660ab + GOMPLATE_DIR: ${{ github.workspace }}/.cache/gomplate + GOMPLATE_BIN: ${{ github.workspace }}/.cache/gomplate/gomplate + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Check for duplicate slugs + run: | + duplicates=$(yq -o=json -I=0 '.versions | map(.slug) | group_by(.) | map(select(length > 1) | .[0])' releases.yaml) + if [ "$duplicates" != "[]" ]; then + echo "::error file=releases.yaml::Duplicate version slugs: $duplicates" + exit 1 + fi + + - name: Cache gomplate binary + id: cache + uses: actions/cache@v5 + with: + path: ${{ env.GOMPLATE_DIR }} + key: | + gomplate-${{ env.GOMPLATE_VERSION }}-${{ env.GOMPLATE_SHA512 }} + + - name: Download gomplate + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: | + mkdir -p "${GOMPLATE_DIR}" + curl -fsSL --output "${GOMPLATE_BIN}" "https://github.com/hairyhenderson/gomplate/releases/download/v$GOMPLATE_VERSION/gomplate_linux-amd64" + echo "${{ env.GOMPLATE_SHA512 }} ${GOMPLATE_BIN}" | sha512sum --check --status + chmod +x "${GOMPLATE_BIN}" + + - name: Add gomplate to PATH + shell: bash + run: echo "${GOMPLATE_DIR}" >> "${GITHUB_PATH}" + + - name: Render Dockerfiles + run: ./render_docker_templates.sh + + - name: Check rendered files are up to date + run: | + git add --intent-to-add dockerfiles + if ! git diff --exit-code -- dockerfiles; then + echo "::error::dockerfiles/ is out of date. Run ./render_docker_templates.sh and commit the result." + exit 1 + fi diff --git a/.github/workflows/update-dockerhub-description.yaml b/.github/workflows/update-dockerhub-description.yaml new file mode 100644 index 0000000..84a0a44 --- /dev/null +++ b/.github/workflows/update-dockerhub-description.yaml @@ -0,0 +1,25 @@ +name: Update description in Docker Hub + +on: + workflow_dispatch: + push: + branches: + - main + paths: + - README.md + +concurrency: dockerhub-description + +jobs: + update-description: + runs-on: ubuntu-latest + environment: dockerhub + steps: + - uses: actions/checkout@v6 + + - name: Docker Hub Description + uses: peter-evans/dockerhub-description@v5 + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + repository: openintegrationengine/engine diff --git a/.gitignore b/.gitignore index 62c8935..d9c56ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ -.idea/ \ No newline at end of file +.idea/ +reports/sarif.json +reports/report.md diff --git a/reports/local.sh b/reports/local.sh new file mode 100755 index 0000000..3c354f4 --- /dev/null +++ b/reports/local.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# +# SPDX-License-Identifier: MPL-2.0 +# SPDX-FileCopyrightText: 2026 BrightCodeCompany OÜ +# + +set -euo pipefail + +# Scan output and the report are written next to this script +cd "$(dirname "$0")" + +imageRepo=openintegrationengine/engine +imageTag=${1:-latest} + +digest=$(docker buildx imagetools inspect "$imageRepo:$imageTag" --raw \ + | jq -r '.manifests[] | select(.platform.os == "linux" and .platform.architecture == "amd64") | .digest') + +echo "Got digest: $digest" +startTime=$(date -u +"%Y-%m-%dT%H:%M:%SZ") + +docker scout cves "$imageRepo@$digest" --ignore-base --only-severity critical,high --format sarif -o sarif.json + +# Requires at least Node 26 +node render-report.ts "$imageTag" "$digest" "$startTime" diff --git a/reports/render-report.ts b/reports/render-report.ts new file mode 100644 index 0000000..0b5fc65 --- /dev/null +++ b/reports/render-report.ts @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 BrightCodeCompany OÜ + +import fs from 'fs' + +const severityToColor = (severity: string) => { + switch (severity.toLowerCase()) { + case 'critical': + return '🔴' + case 'high': + return '🟠' + case 'medium': + return '🟡' + case 'low': + return '⚪' + } +} + +const getReportJson = () => { + try { + const report = fs.readFileSync('sarif.json', 'utf8') + return JSON.parse(report) + } catch (err) { + console.error(err) + process.exit(1) + } +} + +const writeReportToFile = (markdown: string) => { + try { + fs.writeFileSync('report.md', markdown) + } catch (err) { + console.error(err) + process.exit(1) + } +} + +const renderCve = (cve: object) => { + const pUrl = cve.properties.purls[0] + const packageName = pUrl.replace("pkg:", "").split("@")[0] + const installedVersion = pUrl.split("@")[1] + const severity = cve.properties.cvssV3_severity + const sevScore = cve.properties.cvssV3 + const fixedVersion = cve.properties.fixed_version + return `| [${cve.id}](${cve.helpUri}) | ${severityToColor(severity)} | ${sevScore} | \`${packageName}\` | \`${installedVersion}\` | \`${fixedVersion}\` |` +} + +const main = (imageTag: string, imageDigest: string, startTime: Instant) => { + const report = getReportJson().runs[0] + + const sortedCve = report.tool.driver.rules.sort((a: object, b: object) => { + return b.properties.cvssV3 - a.properties.cvssV3 + }) + + let cveStrings: string[] = [] + let cvssMap: { [severity: string]: number } = {} + for (const cve of sortedCve) { + const cveString = renderCve(cve) + + cvssMap[cve.properties.cvssV3_severity] = (cvssMap[cve.properties.cvssV3_severity] || 0) + 1 + cveStrings.push(cveString) + } + const driver = report.tool.driver + + const markdown = ` +## Scan + +| | | +|-------------|-----------------------------------------------------------------| +| **Target** | \`${imageTag}\` | +| **Digest** | \`${imageDigest}\` | +| **Scanner** | \`${driver.fullName} ${driver.version}\` | +| **Timestamp** | \`${startTime.toString()}\` | +| **Filter** | \`severity ≥ High; base-image vulns excluded\` | + +**Totals:** 🔴 Critical \`${cvssMap['CRITICAL'] || 0}\` · 🟠 High \`${cvssMap['HIGH'] || 0}\` · 🟡 Medium \`${cvssMap['MEDIUM'] || 0}\` · ⚪ Low \`${cvssMap['LOW'] || 0}\` + +## Findings + +| CVE | Sev | CVSS | Package | Installed | Fixed in | +|------|-----|------|---------|-----------|----------| +${cveStrings.join('\n')} +` + writeReportToFile(markdown) +} + +const args = process.argv.slice(2) +console.log(args) + +const imageTag = `openintegrationengine/engine:${args[0]}` +const imageDigest = args[1] +const startInstant = Temporal.Instant.from(args[2]) + +main(imageTag, imageDigest, startInstant)