168 lines
5.7 KiB
Bash
Executable File
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}")
|