test(auth): harden Task 15 OIDC smoke evidence
This commit is contained in:
@@ -29,6 +29,98 @@ task13_sha256_text() {
|
||||
fi
|
||||
}
|
||||
|
||||
task13_record_image_evidence() {
|
||||
local reference="$1" role="$2" image_id verified_id repo_digests
|
||||
[[ "$role" =~ ^[a-z0-9-]+$ ]] || task13_fail "invalid image evidence role"
|
||||
image_id="$(docker image inspect --format '{{.Id}}' "$reference")"
|
||||
[[ "$image_id" =~ ^sha256:[0-9a-f]{64}$ ]] \
|
||||
|| task13_fail "Docker image identity was not resolved"
|
||||
verified_id="$(docker image inspect --format '{{.Id}}' "$image_id")"
|
||||
[[ "$verified_id" == "$image_id" ]] \
|
||||
|| task13_fail "Docker image identity changed during evidence capture"
|
||||
if ! repo_digests="$({ docker image inspect --format '{{json .RepoDigests}}' "$image_id"; } | node -e '
|
||||
const fs = require("node:fs");
|
||||
let value = JSON.parse(fs.readFileSync(0, "utf8"));
|
||||
if (value === null) value = [];
|
||||
if (!Array.isArray(value)) process.exit(1);
|
||||
const digests = [];
|
||||
for (const item of value) {
|
||||
if (typeof item !== "string" || !/@sha256:[0-9a-f]{64}$/.test(item)) process.exit(1);
|
||||
digests.push(item.slice(item.lastIndexOf("@") + 1));
|
||||
}
|
||||
process.stdout.write(JSON.stringify([...new Set(digests)].sort()));
|
||||
')"; then
|
||||
task13_fail "Docker repository digest evidence was invalid"
|
||||
return 1
|
||||
fi
|
||||
printf '%s\t%s\t%s\n' "$image_id" "$role" "$repo_digests" >>"$TASK13_IMAGE_EVIDENCE_RECORDS"
|
||||
}
|
||||
|
||||
task13_record_project_image_evidence() {
|
||||
local container_id image_id count=0
|
||||
while IFS= read -r container_id; do
|
||||
[[ -n "$container_id" ]] || continue
|
||||
image_id="$(docker container inspect --format '{{.Image}}' "$container_id")"
|
||||
task13_record_image_evidence "$image_id" compose-runtime
|
||||
count=$((count + 1))
|
||||
done < <(docker container ls -aq --filter "label=com.docker.compose.project=$TASK13_PROJECT")
|
||||
[[ "$count" -gt 0 ]] || task13_fail "no Compose runtime images were available for evidence capture"
|
||||
}
|
||||
|
||||
task13_write_image_evidence() {
|
||||
local image_id verified_id output_dir temporary
|
||||
while IFS= read -r image_id; do
|
||||
[[ -n "$image_id" ]] || continue
|
||||
verified_id="$(docker image inspect --format '{{.Id}}' "$image_id")"
|
||||
[[ "$verified_id" == "$image_id" ]] \
|
||||
|| task13_fail "Docker image identity was unavailable before cleanup"
|
||||
done < <(cut -f1 "$TASK13_IMAGE_EVIDENCE_RECORDS" | LC_ALL=C sort -u)
|
||||
|
||||
output_dir="$(dirname "$TASK13_IMAGE_EVIDENCE_OUTPUT")"
|
||||
mkdir -p "$output_dir"
|
||||
temporary="$(mktemp "$output_dir/.unified-docker-images.XXXXXX")"
|
||||
if ! node - "$TASK13_IMAGE_EVIDENCE_RECORDS" "$TASK13_SOURCE_COMMIT" "$TASK13_RUN_ID" >"$temporary" <<'NODE'
|
||||
const fs = require("node:fs");
|
||||
const [recordsFile, sourceCommit, runId] = process.argv.slice(2);
|
||||
if (!/^[0-9a-f]{40}$/.test(sourceCommit) || !/^[0-9A-Za-z-]+$/.test(runId)) process.exit(1);
|
||||
const images = new Map();
|
||||
for (const line of fs.readFileSync(recordsFile, "utf8").split("\n").filter(Boolean)) {
|
||||
const fields = line.split("\t");
|
||||
if (fields.length !== 3) process.exit(1);
|
||||
const [id, role, encodedDigests] = fields;
|
||||
if (!/^sha256:[0-9a-f]{64}$/.test(id) || !/^[a-z0-9-]+$/.test(role)) process.exit(1);
|
||||
const repoDigests = JSON.parse(encodedDigests);
|
||||
if (!Array.isArray(repoDigests)
|
||||
|| repoDigests.some((digest) => typeof digest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(digest))) {
|
||||
process.exit(1);
|
||||
}
|
||||
const current = images.get(id) ?? { id, roles: new Set(), repo_digests: new Set() };
|
||||
current.roles.add(role);
|
||||
for (const digest of repoDigests) current.repo_digests.add(digest);
|
||||
images.set(id, current);
|
||||
}
|
||||
if (images.size === 0) process.exit(1);
|
||||
const document = {
|
||||
gate: "unified-deployment-smoke",
|
||||
source_commit: sourceCommit,
|
||||
run_id: runId,
|
||||
images: [...images.values()].sort((left, right) => left.id.localeCompare(right.id)).map((image) => ({
|
||||
id: image.id,
|
||||
roles: [...image.roles].sort(),
|
||||
repo_digests: [...image.repo_digests].sort(),
|
||||
})),
|
||||
};
|
||||
process.stdout.write(`${JSON.stringify(document, null, 2)}\n`);
|
||||
NODE
|
||||
then
|
||||
rm -f "$temporary"
|
||||
task13_fail "could not serialize sanitized Docker image evidence"
|
||||
return 1
|
||||
fi
|
||||
chmod 0600 "$temporary"
|
||||
mv "$temporary" "$TASK13_IMAGE_EVIDENCE_OUTPUT"
|
||||
}
|
||||
|
||||
task13_registry_filesystem_fingerprint() {
|
||||
python3 - "$1" <<'PY'
|
||||
import hashlib
|
||||
@@ -1470,6 +1562,7 @@ task13_prepare_bad_candidate() {
|
||||
TASK13_BAD_CANDIDATE_ID="$(docker image inspect --format '{{.Id}}' "$TASK13_BAD_CANDIDATE_IMAGE")"
|
||||
[[ "$TASK13_BAD_CANDIDATE_ID" =~ ^sha256:[0-9a-f]{64}$ ]] \
|
||||
|| task13_fail "bad candidate image identity was not resolved"
|
||||
task13_record_image_evidence "$TASK13_BAD_CANDIDATE_IMAGE" rollback-candidate
|
||||
task13_run_logged "prove bad candidate exits" docker run \
|
||||
--name "$TASK13_BAD_CANDIDATE_CONTAINER" \
|
||||
--label "io.thothii.task13.run=$TASK13_RUN_ID" \
|
||||
@@ -1934,6 +2027,67 @@ task13_self_test_transaction_image_cleanup() {
|
||||
rm -f "$calls" "$foreign_error"
|
||||
}
|
||||
|
||||
task13_self_test_image_evidence() (
|
||||
local fixture records output
|
||||
fixture="$(mktemp -d "${TMPDIR:-/tmp}/thothii-task15-image-evidence.XXXXXX")"
|
||||
records="$fixture/records.tsv"
|
||||
output="$fixture/images.json"
|
||||
trap 'rm -rf "$fixture"' EXIT
|
||||
: >"$records"
|
||||
TASK13_RUN_ID="task15-image-run"
|
||||
TASK13_SOURCE_COMMIT="0123456789abcdef0123456789abcdef01234567"
|
||||
TASK13_IMAGE_EVIDENCE_RECORDS="$records"
|
||||
TASK13_IMAGE_EVIDENCE_OUTPUT="$output"
|
||||
TASK13_PROJECT="task15-image-project"
|
||||
|
||||
docker() {
|
||||
case "$*" in
|
||||
"container ls -aq --filter label=com.docker.compose.project=task15-image-project")
|
||||
printf '%s\n' container-one container-two
|
||||
;;
|
||||
"container inspect --format {{.Image}} container-one")
|
||||
printf '%s\n' 'sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'
|
||||
;;
|
||||
"container inspect --format {{.Image}} container-two")
|
||||
printf '%s\n' 'sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'
|
||||
;;
|
||||
"image inspect --format {{.Id}} fixture-candidate@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
|
||||
printf '%s\n' 'sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
|
||||
;;
|
||||
"image inspect --format {{.Id}} sha256:"*)
|
||||
printf '%s\n' "${*: -1}"
|
||||
;;
|
||||
"image inspect --format {{json .RepoDigests}} sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||
printf '%s\n' '["fixture/core@sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"]'
|
||||
;;
|
||||
"image inspect --format {{json .RepoDigests}} sha256:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
|
||||
printf '%s\n' '[]'
|
||||
;;
|
||||
"image inspect --format {{json .RepoDigests}} sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")
|
||||
printf '%s\n' '["fixture/candidate@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc"]'
|
||||
;;
|
||||
*) task13_fail "unexpected image evidence Docker command" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
task13_record_project_image_evidence
|
||||
task13_record_image_evidence \
|
||||
'fixture-candidate@sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc' \
|
||||
'rollback-candidate'
|
||||
task13_write_image_evidence
|
||||
unset -f docker
|
||||
|
||||
node - "$output" <<'NODE'
|
||||
const manifest = JSON.parse(require("node:fs").readFileSync(process.argv[2], "utf8"));
|
||||
if (manifest.source_commit !== "0123456789abcdef0123456789abcdef01234567"
|
||||
|| manifest.run_id !== "task15-image-run" || manifest.images.length !== 3) process.exit(1);
|
||||
const ids = manifest.images.map((image) => image.id);
|
||||
if (new Set(ids).size !== 3 || ids.some((id) => !/^sha256:[0-9a-f]{64}$/.test(id))) process.exit(1);
|
||||
if (!manifest.images.some((image) => image.roles.includes("rollback-candidate")
|
||||
&& image.repo_digests[0] === "sha256:cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc")) process.exit(1);
|
||||
NODE
|
||||
)
|
||||
|
||||
task13_self_test_rollback_fixture_contract() {
|
||||
[[ "${TASK13_BAD_CANDIDATE_BEHAVIOR:-}" == stopped ]] \
|
||||
|| task13_fail "rollback candidate is not declared as guaranteed stopped"
|
||||
@@ -2181,6 +2335,7 @@ task13_self_test() {
|
||||
task13_self_test_cleanup_ownership
|
||||
task13_self_test_image_cleanup_ownership
|
||||
task13_self_test_transaction_image_cleanup
|
||||
task13_self_test_image_evidence
|
||||
task13_self_test_rollback_fixture_contract
|
||||
task13_self_test_runtime_binding_fixture
|
||||
task13_self_test_server_runtime_binding_fixture
|
||||
@@ -2202,6 +2357,7 @@ task13_self_test_case() {
|
||||
runtime-bindings) task13_self_test_runtime_binding_fixture ;;
|
||||
server-bindings) task13_self_test_server_runtime_binding_fixture ;;
|
||||
cleanup) task13_self_test_stopped_project_containers ;;
|
||||
image-evidence) task13_self_test_image_evidence ;;
|
||||
timeout-group) task13_self_test_timeout_process_group ;;
|
||||
timeout-nested) task13_self_test_nested_timeout_process_group ;;
|
||||
timeout-public) task13_self_test_public_timeout_contract ;;
|
||||
@@ -2258,6 +2414,7 @@ task13_fixtures_only() {
|
||||
}
|
||||
|
||||
task13_initialize() {
|
||||
local source_status
|
||||
umask 077
|
||||
TASK13_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)"
|
||||
TASK13_TMP_PARENT="$(cd "${TMPDIR:-/tmp}" && pwd -P)"
|
||||
@@ -2269,6 +2426,14 @@ task13_initialize() {
|
||||
trap 'task13_cleanup $?' EXIT
|
||||
trap 'exit 130' INT TERM HUP
|
||||
TASK13_RUN_ID="$(date -u +%Y%m%d%H%M%S)-$$-${RANDOM:-0}"
|
||||
TASK13_SOURCE_COMMIT="$(git -C "$TASK13_ROOT" rev-parse --verify HEAD)"
|
||||
[[ "$TASK13_SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]] || task13_fail "source commit was not resolved"
|
||||
source_status="$(git -C "$TASK13_ROOT" status --porcelain --untracked-files=normal \
|
||||
| sed -e '/^?? \.playwright-cli\/$/d' -e '/^?? \.thothctl\/$/d')"
|
||||
[[ -z "$source_status" ]] || task13_fail "source must be clean for image traceability"
|
||||
TASK13_IMAGE_EVIDENCE_RECORDS="$TASK13_TMP/image-evidence.tsv"
|
||||
: >"$TASK13_IMAGE_EVIDENCE_RECORDS"
|
||||
TASK13_IMAGE_EVIDENCE_OUTPUT="$TASK13_ROOT/.artifacts/task-15/unified-docker-images.json"
|
||||
TASK13_PROFILE="local"
|
||||
TASK13_INSTALLATION="$TASK13_TMP/thothii-installation.yaml"
|
||||
TASK13_PROJECT="thothii-$(task13_sha256_text "$TASK13_INSTALLATION" | cut -c1-12)"
|
||||
@@ -2339,6 +2504,8 @@ task13_smoke_main() {
|
||||
task13_build_tht
|
||||
task13_configure_local_authentication
|
||||
task13_start_stack
|
||||
task13_record_project_image_evidence
|
||||
task13_record_image_evidence "$TASK13_CORE_IMAGE" fixture-runtime
|
||||
task13_assert_project_ownership
|
||||
task13_assert_built_image_ownership
|
||||
task13_assert_runtime
|
||||
@@ -2348,6 +2515,8 @@ task13_smoke_main() {
|
||||
task13_registry_lifecycle
|
||||
fi
|
||||
task13_update_rollback
|
||||
task13_record_project_image_evidence
|
||||
task13_write_image_evidence
|
||||
printf 'Task 13 %s deployment smoke passed.\n' "$mode"
|
||||
}
|
||||
|
||||
@@ -2360,10 +2529,14 @@ task13_server_smoke_main() {
|
||||
task13_build_tht
|
||||
task13_configure_server_oidc_authentication
|
||||
task13_start_server_stack
|
||||
task13_record_project_image_evidence
|
||||
task13_record_image_evidence "$TASK13_CORE_IMAGE" fixture-runtime
|
||||
task13_assert_project_ownership
|
||||
task13_assert_built_image_ownership
|
||||
task13_assert_server_runtime
|
||||
task13_assert_server_oidc_restore_verification
|
||||
task13_record_project_image_evidence
|
||||
task13_write_image_evidence
|
||||
printf 'Task 13 Linux server deployment smoke passed.\n'
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user