CI / Tests, lint (push) Successful in 18s
The CIS gate, the SBOM, the cosign signature and the matcher's verdict lived in NetOrk/base-os, the head of the chain. Every link below it -- base-python, engine, ui, satellite, the service images -- needs the same, so the tools move here, and every image repository pins them by commit. - image_tools: cis.py, cis_os.sh, sbom.py and verify.py from base-os. The image's own repository holds its cis.toml and packages.allow; both default to the working directory. - bin/sign.sh signs whatever platforms a reference holds: base-os's index of amd64 and arm64 (EXPECT_PLATFORMS) or a dev image's single manifest (image_tools/platforms.py). - bin/verify-base.sh checks every FROM image of a Dockerfile against cosign.pub before a build; one without a digest is refused (image_tools/dockerfile.py). - bin/install-cosign.sh installs cosign v3.1.3 for amd64 and arm64 runners, checked against the release checksums. - actions/verify-base, judge, sign wrap them for the workflows. - cosign.pub is the public half of the COSIGN_PRIVATE_KEY_B64 org secret, the key rotated on 2026-10-22.
172 lines
8.0 KiB
Bash
172 lines
8.0 KiB
Bash
#!/bin/sh
|
|
# cis_os.sh: the CIS controls image_tools/cis.py measures from inside the image.
|
|
#
|
|
# Runs as root in the image, fed on stdin (docker run -i --user 0 ... sh -s), so
|
|
# nothing has to be mounted from a runner whose paths the host daemon does not
|
|
# know. Reads nothing from outside but CIS_PACKAGES, the package names the image
|
|
# may hold. Prints one line per control:
|
|
#
|
|
# <id> TAB PASS|FAIL TAB <title> TAB <what fails>
|
|
#
|
|
# docker:* are the CIS Docker Benchmark 1.6.0 controls Trivy leaves to "Manual"
|
|
# and that can be seen from inside; debian:* are the CIS Debian Linux 12 v1.1.0
|
|
# controls that make sense in a container. Boot, partitions, kernel parameters,
|
|
# services, logging, audit and sshd belong to the host (infrastructure
|
|
# BAUSTANDARD §F) and are not measured here.
|
|
set -u
|
|
|
|
# check <id> <title> <function>: PASS when the function names no offender.
|
|
check() {
|
|
out=$("$3" 2>/dev/null | head -n 20 | tr '\n' ' ')
|
|
if [ -z "$out" ]; then
|
|
printf '%s\tPASS\t%s\t\n' "$1" "$2"
|
|
else
|
|
printf '%s\tFAIL\t%s\t%s\n' "$1" "$2" "${out% }"
|
|
fi
|
|
}
|
|
|
|
login_defs() { awk -v k="$1" '$1 == k { v = $2 } END { print v }' /etc/login.defs; }
|
|
|
|
# Shells one can log in with: /etc/shells without the nologin ones.
|
|
SHELLS=$(grep '^/' /etc/shells | grep -v nologin)
|
|
|
|
# --- CIS Docker Benchmark 1.6.0, section 4 -----------------------------------
|
|
|
|
extra_packages() {
|
|
dpkg-query -W -f '${Package}\n' | while read -r p; do
|
|
case " ${CIS_PACKAGES:-} " in *" $p "*) ;; *) echo "$p" ;; esac
|
|
done
|
|
}
|
|
|
|
setid_files() { find / -xdev -type f -perm /6000; }
|
|
|
|
unverified_sources() {
|
|
grep -rEil 'trusted=yes|Trusted: *yes|AllowUnauthenticated|AllowInsecureRepositories|AllowDowngradeToInsecureRepositories' /etc/apt
|
|
for f in /etc/apt/sources.list.d/*.sources; do
|
|
[ -e "$f" ] || continue
|
|
awk -v f="$f" 'BEGIN { RS = "" } index($0, "Signed-By:") == 0 { print f }' "$f"
|
|
done
|
|
grep -sE '^[[:space:]]*deb[[:space:]]' /etc/apt/sources.list /etc/apt/sources.list.d/*.list \
|
|
| grep -v 'signed-by='
|
|
}
|
|
|
|
check docker:4.3 "Unnecessary packages are not installed (packages.allow)" extra_packages
|
|
check docker:4.8 "setuid and setgid permissions are removed" setid_files
|
|
check docker:4.11 "Only verified packages are installed" unverified_sources
|
|
|
|
# --- CIS Debian Linux 12 v1.1.0 ----------------------------------------------
|
|
|
|
# in_range <key> <min> [max]: names the key unless min <= its value <= max.
|
|
in_range() {
|
|
v=$(login_defs "$1")
|
|
case $v in '' | *[!0-9]*) echo "$1=${v:-unset}"; return ;; esac
|
|
if [ "$v" -lt "$2" ] || [ "$v" -gt "${3:-$v}" ]; then echo "$1=$v"; fi
|
|
}
|
|
pass_max_days() { in_range PASS_MAX_DAYS 1 365; }
|
|
pass_min_days() { in_range PASS_MIN_DAYS 1; }
|
|
pass_warn_age() { in_range PASS_WARN_AGE 7; }
|
|
weak_hashing() {
|
|
v=$(login_defs ENCRYPT_METHOD)
|
|
case $v in SHA512 | YESCRYPT) ;; *) echo "ENCRYPT_METHOD=${v:-unset}" ;; esac
|
|
}
|
|
weak_umask() {
|
|
v=$(login_defs UMASK)
|
|
case $v in '' | *[!0-7]*) echo "UMASK=${v:-unset}"; return ;; esac
|
|
# 027 or stricter: group may not write, others get nothing
|
|
if [ $(((0$v >> 3) & 2)) -eq 0 ] || [ $((0$v & 7)) -ne 7 ]; then echo "UMASK=$v"; fi
|
|
}
|
|
|
|
other_uid0() { awk -F: '$3 == "0" && $1 != "root" { print $1 }' /etc/passwd; }
|
|
other_gid0_accounts() {
|
|
awk -F: '$4 == "0" && $1 !~ /^(root|sync|shutdown|halt|operator)$/ { print $1 }' /etc/passwd
|
|
}
|
|
other_gid0_groups() { awk -F: '$3 == "0" && $1 != "root" { print $1 }' /etc/group; }
|
|
root_unlocked() { awk -F: '$1 == "root" && $2 !~ /^[!*]/ { print "root" }' /etc/shadow; }
|
|
system_login_shells() {
|
|
min=$(login_defs UID_MIN)
|
|
echo "$SHELLS" | awk -F: -v min="${min:-1000}" '
|
|
NR == FNR { if ($0 != "") shell[$0] = 1; next }
|
|
$1 != "root" && ($3 + 0 < min + 0 || $3 == "65534") && ($7 in shell) { print $1 }
|
|
' - /etc/passwd
|
|
}
|
|
unlocked_without_shell() {
|
|
echo "$SHELLS" | awk -F: '
|
|
FILENAME == "-" { if ($0 != "") shell[$0] = 1; next }
|
|
FILENAME == "/etc/passwd" { if ($1 != "root" && !($7 in shell)) nologin[$1] = 1; next }
|
|
($1 in nologin) && $2 !~ /^[!*]/ { print $1 }
|
|
' - /etc/passwd /etc/shadow
|
|
}
|
|
|
|
# too_open <file> <max mode> <owner> <group regex>: names the file when it is
|
|
# more open than the mode allows or owned by someone else; a missing file is fine.
|
|
too_open() {
|
|
[ -e "$1" ] || return 0
|
|
# shellcheck disable=SC2046
|
|
set -- "$1" "$2" "$3" "$4" $(stat -Lc '%a %U %G' "$1")
|
|
if [ $((0$5 & ~0$2 & 07777)) -ne 0 ] || [ "$6" != "$3" ] || ! echo "$7" | grep -qxE "$4"; then
|
|
echo "$1=$5:$6:$7"
|
|
fi
|
|
}
|
|
perm_passwd() { too_open /etc/passwd 644 root root; }
|
|
perm_passwd_() { too_open /etc/passwd- 644 root root; }
|
|
perm_group() { too_open /etc/group 644 root root; }
|
|
perm_group_() { too_open /etc/group- 644 root root; }
|
|
perm_shadow() { too_open /etc/shadow 640 root 'root|shadow'; }
|
|
perm_shadow_() { too_open /etc/shadow- 640 root 'root|shadow'; }
|
|
perm_gshadow() { too_open /etc/gshadow 640 root 'root|shadow'; }
|
|
perm_gshadow_() { too_open /etc/gshadow- 640 root 'root|shadow'; }
|
|
perm_shells() { too_open /etc/shells 644 root root; }
|
|
perm_opasswd() { too_open /etc/security/opasswd 600 root root; }
|
|
|
|
world_writable() {
|
|
find / -xdev -type f -perm -0002
|
|
find / -xdev -type d -perm -0002 ! -perm -1000
|
|
}
|
|
unowned() { find / -xdev \( -nouser -o -nogroup \); }
|
|
|
|
not_shadowed() { awk -F: '$2 != "x" { print $1 }' /etc/passwd; }
|
|
empty_passwords() { awk -F: '$2 == "" { print $1 }' /etc/shadow; }
|
|
missing_groups() { awk -F: 'NR == FNR { g[$3] = 1; next } !($4 in g) { print $1 ":" $4 }' /etc/group /etc/passwd; }
|
|
shadow_members() {
|
|
awk -F: '
|
|
NR == FNR { if ($1 == "shadow") { gid = $3; if ($4 != "") print "shadow:" $4 } next }
|
|
gid != "" && $4 == gid { print $1 }
|
|
' /etc/group /etc/passwd
|
|
}
|
|
dup_uids() { cut -d: -f3 /etc/passwd | sort | uniq -d; }
|
|
dup_gids() { cut -d: -f3 /etc/group | sort | uniq -d; }
|
|
dup_users() { cut -d: -f1 /etc/passwd | sort | uniq -d; }
|
|
dup_groups() { cut -d: -f1 /etc/group | sort | uniq -d; }
|
|
|
|
check debian:5.4.1.1 "Password expiration is configured" pass_max_days
|
|
check debian:5.4.1.2 "Minimum password days are configured" pass_min_days
|
|
check debian:5.4.1.3 "Password expiration warning days are configured" pass_warn_age
|
|
check debian:5.4.1.4 "Strong password hashing algorithm is configured" weak_hashing
|
|
check debian:5.4.2.1 "root is the only UID 0 account" other_uid0
|
|
check debian:5.4.2.2 "root is the only GID 0 account" other_gid0_accounts
|
|
check debian:5.4.2.3 "group root is the only GID 0 group" other_gid0_groups
|
|
check debian:5.4.2.4 "root account access is controlled (locked)" root_unlocked
|
|
check debian:5.4.2.7 "System accounts do not have a valid login shell" system_login_shells
|
|
check debian:5.4.2.8 "Accounts without a valid login shell are locked" unlocked_without_shell
|
|
check debian:5.4.3.3 "Default user umask is configured" weak_umask
|
|
check debian:7.1.1 "Permissions on /etc/passwd are configured" perm_passwd
|
|
check debian:7.1.2 "Permissions on /etc/passwd- are configured" perm_passwd_
|
|
check debian:7.1.3 "Permissions on /etc/group are configured" perm_group
|
|
check debian:7.1.4 "Permissions on /etc/group- are configured" perm_group_
|
|
check debian:7.1.5 "Permissions on /etc/shadow are configured" perm_shadow
|
|
check debian:7.1.6 "Permissions on /etc/shadow- are configured" perm_shadow_
|
|
check debian:7.1.7 "Permissions on /etc/gshadow are configured" perm_gshadow
|
|
check debian:7.1.8 "Permissions on /etc/gshadow- are configured" perm_gshadow_
|
|
check debian:7.1.9 "Permissions on /etc/shells are configured" perm_shells
|
|
check debian:7.1.10 "Permissions on /etc/security/opasswd are configured" perm_opasswd
|
|
check debian:7.1.11 "World writable files and directories are secured" world_writable
|
|
check debian:7.1.12 "No files or directories without an owner and a group exist" unowned
|
|
check debian:7.2.1 "Accounts in /etc/passwd use shadowed passwords" not_shadowed
|
|
check debian:7.2.2 "/etc/shadow password fields are not empty" empty_passwords
|
|
check debian:7.2.3 "All groups in /etc/passwd exist in /etc/group" missing_groups
|
|
check debian:7.2.4 "The shadow group is empty" shadow_members
|
|
check debian:7.2.5 "No duplicate UIDs exist" dup_uids
|
|
check debian:7.2.6 "No duplicate GIDs exist" dup_gids
|
|
check debian:7.2.7 "No duplicate user names exist" dup_users
|
|
check debian:7.2.8 "No duplicate group names exist" dup_groups
|