name: 'SBOM + triage report' description: > Source SBOM to Dependency-Track, osv-scanner over the repo, and a report to dtrack-panel (the images this repo builds + its osv-scanner.toml decisions) so the panel's auto-triage can apply them to the CI project and the running images. author: 'janhouse' # Usage (a repo's .gitea/workflows/sbom.yml): # - uses: actions/checkout@v4 # - uses: https://git.janhouse.lv/janhouse/sbom-tools-action/scan@v2 # with: # project: Janhouse-Web # images: git.janhouse.lv/private-docker/janhouse-cms # dtrack-url: ${{ secrets.DEPENDENCY_TRACK_URL }} # dtrack-api-key: ${{ secrets.DEPENDENCY_TRACK_API_KEY }} # # osv-scanner.toml [[IgnoredVulns]] are NOT_AFFECTED decisions: dtrack-panel # writes them to Dependency-Track for this repo's own dependencies (the CI # project, and the app's dependency tree in the images). A reason may start # with a CycloneDX justification: "code_not_reachable: we never parse XML". # They don't expire unless ignoreUntil says so — re-check them when the code # that uses the package changes (see the repo's AGENTS.md). inputs: project: description: 'Dependency-Track project name for this repo (the CI project)' required: true project-version: description: 'Dependency-Track project version' default: 'latest' images: description: 'Images this repo builds, comma or newline separated, without tag (git.janhouse.lv/janhouse/app)' default: '' container-image: description: 'Optional: also include this image''s OS packages in the CI SBOM (merged)' default: '' container-skip-files: description: 'With container-image: trivy --skip-files glob for the app dir already covered by the lockfile scan' default: '/app/**' path: description: 'Directory to scan' default: '.' dtrack-url: description: 'Dependency-Track base URL (secrets.DEPENDENCY_TRACK_URL)' required: true dtrack-api-key: description: 'Dependency-Track API key with BOM upload (secrets.DEPENDENCY_TRACK_API_KEY)' required: true panel-url: description: 'dtrack-panel CI report URL (default: /panel-ci/report)' default: '' trivy-version: # 0.70.0 is the newest Trivy emitting CycloneDX 1.6 (see dtrack-docker-scanner's Dockerfile) default: 'v0.70.0' cyclonedx-version: default: 'v0.27.2' osv-scanner-version: default: 'v2.7.0' runs: using: composite steps: - name: Cache SBOM tools id: cache uses: actions/cache@v4 with: path: ~/.local/sbom-tools key: sbom-scan-${{ runner.os }}-trivy-${{ inputs.trivy-version }}-cdx-${{ inputs.cyclonedx-version }}-osv-${{ inputs.osv-scanner-version }} - name: Install Trivy, CycloneDX CLI, osv-scanner if: steps.cache.outputs.cache-hit != 'true' shell: bash env: TRIVY_VERSION: ${{ inputs.trivy-version }} CDX_VERSION: ${{ inputs.cyclonedx-version }} OSV_VERSION: ${{ inputs.osv-scanner-version }} run: | set -euo pipefail d=~/.local/sbom-tools; mkdir -p "$d" curl -sSfL "https://raw.githubusercontent.com/aquasecurity/trivy/${TRIVY_VERSION}/contrib/install.sh" | sh -s -- -b "$d" "${TRIVY_VERSION}" curl -sSfL "https://github.com/CycloneDX/cyclonedx-cli/releases/download/${CDX_VERSION}/cyclonedx-linux-x64" -o "$d/cyclonedx-cli" curl -sSfL "https://github.com/google/osv-scanner/releases/download/${OSV_VERSION}/osv-scanner_linux_amd64" -o "$d/osv-scanner" chmod +x "$d"/* - name: Add tools to PATH shell: bash run: echo "$HOME/.local/sbom-tools" >> "$GITHUB_PATH" - name: Source SBOM (lockfiles) shell: bash env: SCAN_PATH: ${{ inputs.path }} CONTAINER_IMAGE: ${{ inputs.container-image }} CONTAINER_SKIP: ${{ inputs.container-skip-files }} run: | set -euo pipefail trivy fs "$SCAN_PATH" --quiet --timeout 20m --format cyclonedx --output source-sbom.json if [ -n "$CONTAINER_IMAGE" ]; then trivy image "$CONTAINER_IMAGE" --quiet --timeout 20m --skip-files "$CONTAINER_SKIP" \ --format cyclonedx --output container-sbom.json cyclonedx-cli merge --input-files container-sbom.json source-sbom.json \ --output-file sbom.json --output-format json else cp source-sbom.json sbom.json fi echo "SBOM: $(jq '.components | length' sbom.json) components" - name: Upload SBOM to Dependency-Track shell: bash env: DTRACK_URL: ${{ inputs.dtrack-url }} DTRACK_API_KEY: ${{ inputs.dtrack-api-key }} PROJECT: ${{ inputs.project }} PROJECT_VERSION: ${{ inputs.project-version }} run: | set -euo pipefail tag=$(git describe --exact-match --tags 2>/dev/null || git rev-parse --short HEAD) jq -n --arg n "$PROJECT" --arg v "$PROJECT_VERSION" --arg t "$tag" --rawfile bom <(base64 -w0 sbom.json) \ '{projectName: $n, projectVersion: $v, autoCreate: true, isLatestProjectVersion: true, projectTags: [{name: $t}], bom: $bom}' > payload.json curl -fsS -X PUT "${DTRACK_URL%/}/api/v1/bom" \ -H "X-Api-Key: ${DTRACK_API_KEY}" -H "Content-Type: application/json" \ --data-binary @payload.json echo; echo "Uploaded ${PROJECT} @ ${PROJECT_VERSION} (${tag})" - name: osv-scanner (informational) shell: bash continue-on-error: true env: SCAN_PATH: ${{ inputs.path }} run: | # Exit 1 = vulnerabilities found; this step only reports. Go call # analysis runs when the Go toolchain is on the runner. osv-scanner scan source -r "$SCAN_PATH" --format table || true - name: Report to dtrack-panel (images + osv-scanner.toml) shell: bash continue-on-error: true env: DTRACK_URL: ${{ inputs.dtrack-url }} DTRACK_API_KEY: ${{ inputs.dtrack-api-key }} PANEL_URL: ${{ inputs.panel-url }} PROJECT: ${{ inputs.project }} IMAGES: ${{ inputs.images }} SCAN_PATH: ${{ inputs.path }} REPO: ${{ github.repository }} COMMIT: ${{ github.sha }} run: | set -euo pipefail url=${PANEL_URL:-${DTRACK_URL%/}/panel-ci/report} images=$(printf '%s' "$IMAGES" | tr ',' '\n' | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | grep -v '^$' | jq -R . | jq -s .) tomls='[]' while IFS= read -r f; do tomls=$(jq --arg p "$f" --rawfile c "$f" '. + [{path: $p, content: $c}]' <<<"$tomls") done < <(find "$SCAN_PATH" -name osv-scanner.toml -not -path '*/node_modules/*' -not -path '*/.git/*' | head -20) jq -n --arg repo "$REPO" --arg commit "$COMMIT" --arg project "$PROJECT" \ --argjson images "$images" --argjson tomls "$tomls" \ '{repo: $repo, commit: $commit, project: $project, images: $images, osvScannerToml: $tomls}' > report.json echo "Reporting $(jq '.images | length' report.json) image(s), $(jq '.osvScannerToml | length' report.json) osv-scanner.toml file(s) to ${url}" curl -fsS -X POST "$url" -H "X-Api-Key: ${DTRACK_API_KEY}" -H "Content-Type: application/json" \ --data-binary @report.json -o response.json echo "Applied on the panel's next sync. Still open (not triaged):" jq -r '.open[] | " \(.project): \(.n) \(.severity)"' response.json || true