diff --git a/README.md b/README.md index 763b686..b618d3c 100644 --- a/README.md +++ b/README.md @@ -29,3 +29,15 @@ Both inputs are required so the consumer is explicit about source. Wire them to ## Versioning The repo follows lightweight tags: `v1`, `v1.0.0`, etc. Consumers pin to `@v1` for in-major updates, or `@` for full pinning. + +## `scan` — SBOM, osv-scanner and the triage report (v2) + +`uses: https://git.janhouse.lv/janhouse/sbom-tools-action/scan@v2` uploads the +repo's source SBOM to Dependency-Track, runs osv-scanner over the repo, and +reports to dtrack-panel which images the repo builds and its +`osv-scanner.toml` `[[IgnoredVulns]]`. The panel's auto-triage then marks +those NOT_AFFECTED in Dependency-Track — for the CI project and for the app's +own dependency tree in the running images — and closes CI-project findings for +dependencies that never reach the image (dev/build-time). Inputs and an example +are in `scan/action.yml`. Authenticates with the same +`DEPENDENCY_TRACK_API_KEY` the upload uses; no extra secret. diff --git a/scan/action.yml b/scan/action.yml new file mode 100644 index 0000000..289a7b8 --- /dev/null +++ b/scan/action.yml @@ -0,0 +1,164 @@ +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