Add APK security reports

This commit is contained in:
Joachim Schlöffel
2026-08-24 10:09:00 +02:00
parent 479886d37d
commit 8d98ccf093
6 changed files with 255 additions and 0 deletions
+23
View File
@@ -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
+15
View File
@@ -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
+44
View File
@@ -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/<package>/<architecture>/
├── 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.
+5
View File
@@ -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"
+1
View File
@@ -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
+167
View File
@@ -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}")