diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index 9efe6ac..772ad4f 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -36,10 +36,16 @@ jobs: CARGO_HOME: "/cache/public-alpine-packages/${{ matrix.alpine_registry_branch }}/greptimedb/cargo" RUSTUP_HOME: "/cache/public-alpine-packages/${{ matrix.alpine_registry_branch }}/greptimedb/rustup" CARGO_TARGET_DIR: "/cache/public-alpine-packages/${{ matrix.alpine_registry_branch }}/greptimedb/target" + TRIVY_VERSION: "0.74.0" + TRIVY_SHA256: "2ae6fe3ee734b7fdf11335663e18c75ea12dccc76062f09f164a3b0f8be4371a" steps: - name: Prepare Environment run: | apk add --no-cache --update abuild-rootbld alpine-sdk atools-apkbuild-lint bash ca-certificates curl doas git jq nodejs rsync shellcheck sudo tar + trivy_archive="trivy_${TRIVY_VERSION}_Linux-64bit.tar.gz" + curl -fsSL -o "/tmp/${trivy_archive}" "https://github.com/aquasecurity/trivy/releases/download/v${TRIVY_VERSION}/${trivy_archive}" + echo "${TRIVY_SHA256} /tmp/${trivy_archive}" | sha256sum -c - + tar -xzf "/tmp/${trivy_archive}" -C /usr/local/bin trivy mkdir -p "$ALPINE_APK_REPO_CACHE" "$CARGO_HOME" "$RUSTUP_HOME" "$CARGO_TARGET_DIR" chown -R 1000:1000 /cache/public-alpine-packages/${{ matrix.alpine_registry_branch }} @@ -101,6 +107,23 @@ jobs: if: github.event_name != 'schedule' run: scripts/apk/ci-smoke.sh + - name: Generate security reports + if: github.event_name != 'schedule' && env.ALPINE_SKIP_BUILD != '1' + run: | + scripts/apk/security-report.sh + if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + find packages/security-reports -name summary.md -type f -print | sort | while IFS= read -r summary; do + cat "$summary" + done >> "$GITHUB_STEP_SUMMARY" + fi + + - name: Upload security reports + if: github.event_name != 'schedule' && env.ALPINE_SKIP_BUILD != '1' && steps.artifact-upload.outputs.available == 'true' + uses: actions/upload-artifact@v3 + with: + name: apk-security-reports-${{ matrix.alpine_registry_branch }} + path: packages/security-reports/ + - name: Runtime test if: github.event_name != 'schedule' && env.ALPINE_SKIP_BUILD != '1' run: scripts/apk/runtime-test.sh diff --git a/README.md b/README.md index eea9da1..e13a3c8 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ apk:packages List built Alpine packages and dependencies apk:publish-check Run lint, multi-arch build, package listing, and smoke test apk:publish-gitea Publish built packages to the Gitea Alpine package registry apk:outdated Check packaged upstream versions and release summaries +apk:security-report Generate Trivy and CycloneDX reports for built packages apk:shell Open an Alpine package build shell in Docker apk:smoke Install-test built packages from the local repository in Docker apk:test-install Test README installation instructions in a fresh Alpine container @@ -234,6 +235,20 @@ APK_UPDATE_CHECK_MODE=fail mise run apk:outdated When an update is available, the command also lists compact release-note summaries for every release between the packaged and latest versions. +### Security Reports and Roadmap + +The [project review and development roadmap](docs/project-review.md) tracks +release-reliability goals. Generate advisory Trivy and CycloneDX reports for +built packages with: + +```sh +ALPINE_PACKAGE=seaweedfs mise run apk:security-report +``` + +Reports are written to `packages/security-reports/` and are uploaded by CI for +each selected package build. Local runs scan the newest compatible package set +available in the signed local repository. + ### Gitea Workflow `.gitea/workflows/build.yml` builds and publishes `v3.24`. Pull requests diff --git a/docs/project-review.md b/docs/project-review.md new file mode 100644 index 0000000..bcf2320 --- /dev/null +++ b/docs/project-review.md @@ -0,0 +1,44 @@ +# Project Review and Development Roadmap + +## Current State + +This repository packages four upstream services for Alpine 3.24, with source +builds, binary repackages, OpenRC integration, Docker-based local builds, and +CI publication to the Gitea Alpine registry. Package selection avoids rebuilding +unchanged versions already present in the registry. + +The main release risks are missing retained build artifacts, permissive duplicate +publish handling, rolling toolchain inputs, and limited end-to-end service +validation. Documentation and test defaults must remain synchronized with each +`APKBUILD` version. + +## Prioritized Goals + +1. **Reproducible, auditable releases.** Split build and publish work so publish + consumes a retained APK artifact plus a checksum manifest; verify registry + contents after upload and reject mismatched duplicate versions. +2. **Build security visibility.** Generate an installed-rootfs CycloneDX SBOM + and advisory Trivy report for every package and architecture selected by CI. + Retain reports as Gitea workflow artifacts. +3. **Service-level confidence.** Add bounded readiness checks for Gitea, + LavinMQ, and GreptimeDB; retain SeaweedFS role startup coverage. +4. **Automation correctness.** Add fixture-based tests for package selection, + APKINDEX parsing, metadata/version drift, and registry error paths. +5. **Developer feedback and cost.** Record build durations/cache outcomes, + keep package-scoped validation, and add a scheduled update-report workflow. + +## Security Reports + +`mise run apk:security-report` scans the installed root filesystem for each +selected local package. It writes advisory reports under: + +```text +packages/security-reports/// +├── trivy.json +├── sbom.cdx.json +└── summary.md +``` + +The scan includes the package, its selected subpackages, and resolved runtime +dependencies from the signed local repository. Findings do not block a build yet; report generation, +package installation, and report validation do. diff --git a/mise.toml b/mise.toml index a9d53b8..1328f67 100644 --- a/mise.toml +++ b/mise.toml @@ -3,6 +3,7 @@ act = "latest" docker-cli = "latest" jq = "latest" shellcheck = "latest" +trivy = "latest" [tasks."apk:build"] description = "Build the Alpine package in Docker for the default architecture" @@ -69,6 +70,10 @@ run = "scripts/apk/list-packages.sh" description = "Check packaged upstream versions against latest GitHub releases" run = "scripts/apk/outdated.sh" +[tasks."apk:security-report"] +description = "Generate Trivy and CycloneDX reports for built selected packages" +run = "scripts/apk/security-report.sh" + [tasks."apk:publish-check"] description = "Run lint, multi-arch build, package listing, and smoke test" run = "scripts/apk/publish-check.sh" diff --git a/scripts/apk/publish-check.sh b/scripts/apk/publish-check.sh index 2c64c46..f53cdf7 100755 --- a/scripts/apk/publish-check.sh +++ b/scripts/apk/publish-check.sh @@ -10,3 +10,4 @@ mise run apk:lint-shell mise run apk:build-all mise run apk:packages mise run apk:smoke +mise run apk:security-report diff --git a/scripts/apk/security-report.sh b/scripts/apk/security-report.sh new file mode 100755 index 0000000..8580739 --- /dev/null +++ b/scripts/apk/security-report.sh @@ -0,0 +1,167 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# shellcheck source=scripts/apk/package-lib.sh +. "${repo_root}/scripts/apk/package-lib.sh" + +repo_name="${ALPINE_REPO_NAME:-local}" +alpine_version="${ALPINE_VERSION:-3.24}" +arches="${ALPINE_SECURITY_ARCHES:-${ALPINE_ARCHES:-x86_64}}" +reports_dir="${APK_SECURITY_REPORTS_DIR:-${repo_root}/packages/security-reports}" +trivy_bin="${TRIVY_BIN:-trivy}" + +require_command() { + command -v "$1" >/dev/null 2>&1 || { + printf 'missing required command: %s\n' "$1" >&2 + exit 2 + } +} + +platform_for_arch() { + case "$1" in + x86_64) printf '%s\n' 'linux/amd64' ;; + aarch64) printf '%s\n' 'linux/arm64' ;; + *) printf 'unsupported Alpine architecture: %s\n' "$1" >&2; return 2 ;; + esac +} + +install_rootfs() { + local rootfs="$1" + local arch="$2" + shift 2 + local -a specs=("$@") + local -a apk_args=(--root "${rootfs}/installed" --initdb --no-scripts --arch "${arch}") + local key_dir repo_file + + key_dir="${rootfs}/keys" + repo_file="${rootfs}/repositories" + mkdir -p "${key_dir}" + cp /etc/apk/keys/*.pub "${key_dir}/" + cp "${repo_root}/packages/${repo_name}/${arch}"/*.rsa.pub "${key_dir}/" + { + printf 'https://dl-cdn.alpinelinux.org/alpine/v%s/main\n' "${alpine_version}" + printf 'https://dl-cdn.alpinelinux.org/alpine/v%s/community\n' "${alpine_version}" + printf 'file://%s/packages/%s\n' "${repo_root}" "${repo_name}" + } > "${repo_file}" + + if [[ "$(id -u)" != 0 ]]; then + apk_args+=(--usermode) + fi + apk "${apk_args[@]}" --keys-dir "${key_dir}" --repositories-file "${repo_file}" \ + add --no-cache "${specs[@]}" +} + +install_rootfs_docker() { + local rootfs="$1" + local arch="$2" + shift 2 + local platform + + platform="$(platform_for_arch "${arch}")" + docker run --rm --platform "${platform}" \ + --user "$(id -u):$(id -g)" \ + -e REPO_NAME="${repo_name}" \ + -e ALPINE_ARCH="${arch}" \ + -e ALPINE_VERSION="${alpine_version}" \ + -v "${repo_root}:/work:ro" \ + -v "${rootfs}:/scan" \ + "alpine:${alpine_version}" \ + sh -ec ' + mkdir -p /scan/keys + cp /etc/apk/keys/*.pub /scan/keys/ + cp /work/packages/"$REPO_NAME"/"$ALPINE_ARCH"/*.rsa.pub /scan/keys/ + { + printf "https://dl-cdn.alpinelinux.org/alpine/v%s/main\\n" "$ALPINE_VERSION" + printf "https://dl-cdn.alpinelinux.org/alpine/v%s/community\\n" "$ALPINE_VERSION" + printf "file:///work/packages/%s\\n" "$REPO_NAME" + } > /scan/repositories + apk --root /scan/installed --usermode --initdb --no-scripts --arch "$ALPINE_ARCH" \ + --keys-dir /scan/keys --repositories-file /scan/repositories \ + add --no-cache "$@" + ' security-report "$@" +} + +write_summary() { + local report="$1" + local summary="$2" + local package_name="$3" + local arch="$4" + local total + + total="$(jq '[.Results[]?.Vulnerabilities[]?] | length' "${report}")" + { + printf '# %s (%s) security report\n\n' "${package_name}" "${arch}" + printf 'Installed APK rootfs scanned with Trivy. Findings are advisory.\n\n' + printf '| Severity | Count |\n| --- | ---: |\n' + for severity in CRITICAL HIGH MEDIUM LOW UNKNOWN; do + printf '| %s | %s |\n' "${severity}" \ + "$(jq --arg severity "${severity}" '[.Results[]?.Vulnerabilities[]? | select(.Severity == $severity)] | length' "${report}")" + done + printf '\nTotal vulnerabilities: %s\n' "${total}" + } > "${summary}" +} + +print_report() { + local report_dir="$1" + local package_name="$2" + local arch="$3" + + printf '\nSecurity reports for %s (%s):\n' "${package_name}" "${arch}" + printf ' Trivy: file://%s/trivy.json\n' "${report_dir}" + printf ' SBOM: file://%s/sbom.cdx.json\n' "${report_dir}" + printf ' Summary: file://%s/summary.md\n' "${report_dir}" + cat "${report_dir}/summary.md" + printf '\n' +} + +require_command "${trivy_bin}" +require_command jq + +if ! command -v apk >/dev/null 2>&1; then + require_command docker +fi + +tmpdir="$(mktemp -d)" +cleanup() { + rm -rf "${tmpdir}" +} +trap cleanup EXIT + +while IFS= read -r package_dir; do + apk_validate_package_dir "${package_dir}" + package_name="$(apk_package_name "${package_dir}")" + + for arch in ${arches}; do + apk_package_supports_arch "${package_dir}" "${arch}" || continue + repo_dir="${repo_root}/packages/${repo_name}/${arch}" + if [[ ! -f "${repo_dir}/APKINDEX.tar.gz" ]] || ! compgen -G "${repo_dir}/*.rsa.pub" >/dev/null; then + printf 'missing signed local repository for %s: %s\n' "${package_name}" "${repo_dir}" >&2 + exit 1 + fi + + mapfile -t specs < <( + apk_package_versioned_names "${package_dir}" "${arch}" | sed 's/=.*//' + ) + specs=(alpine-baselayout "${specs[@]}") + report_dir="${reports_dir}/${package_name}/${arch}" + rootfs="${tmpdir}/${package_name}-${arch}" + mkdir -p "${report_dir}" "${rootfs}" + + printf 'Generating security reports for %s (%s)\n' "${package_name}" "${arch}" + if command -v apk >/dev/null 2>&1; then + install_rootfs "${rootfs}" "${arch}" "${specs[@]}" + else + install_rootfs_docker "${rootfs}" "${arch}" "${specs[@]}" + fi + + "${trivy_bin}" filesystem --distro "alpine/${alpine_version}" --scanners vuln --format json --no-progress \ + --output "${report_dir}/trivy.json" "${rootfs}/installed" + "${trivy_bin}" filesystem --distro "alpine/${alpine_version}" --scanners vuln --format cyclonedx --no-progress \ + --output "${report_dir}/sbom.cdx.json" "${rootfs}/installed" + jq -e 'type == "object" and has("ArtifactName")' "${report_dir}/trivy.json" >/dev/null + jq -e '.bomFormat == "CycloneDX"' "${report_dir}/sbom.cdx.json" >/dev/null + write_summary "${report_dir}/trivy.json" "${report_dir}/summary.md" "${package_name}" "${arch}" + print_report "${report_dir}" "${package_name}" "${arch}" + done +done < <(apk_package_dirs "${repo_root}")