#!/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}")