Files
alpine-packages/scripts/apk/security-report.sh
T

168 lines
5.7 KiB
Bash
Executable File

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