Blueforce-Linux/tests/check-specs.sh
emirhanmamak-blueforce ef102b9657 feat(field-os): provisioning layer, real ISO build, offline contracts
Adds the Blueforce Field OS provisioning stack on top of the existing fleet
architecture, without rewriting the working systems (installer, diagnostics,
Ansible, WireGuard/SSH/xRDP/RustDesk, Docker policies, monitoring, Docusaurus).

ISO build (the previous builder was a skeleton that produced no medium):
- boot layout is read from the base ISO at build time via
  `xorriso -report_el_torito as_mkisofs` and rebuilt with `-as mkisofs`;
  `-boot_image any replay` cannot work on Ubuntu live ISOs because the GRUB2
  MBR lives in the source image's first 16 sectors and the EFI system partition
  is an appended partition, neither of which is a file in the ISO9660 tree
- preflight now asserts El Torito boot metadata positively: xorriso exits 0 on
  a non-ISO input, which previously let an arbitrary file pass --check
- artifact produced and verified: dist/Blueforce-Field-OS-1.0.0-amd64.iso,
  6 481 917 952 bytes, SHA256 cb0cc56f...d431296, BIOS+UEFI entries preserved,
  /autoinstall.yaml at the media root, casper/minimal.squashfs byte-identical
  to the base
- recorded baseline deviation: the verified medium on the build host is the
  Desktop flavour, so the manifest carries a baseline_deviation block instead
  of hiding it; Server remains one flag away

Offline and identity:
- offline APT contract with concrete package requirements, pin format and an
  audit table; builds fail closed while pins are UNPINNED
- autoinstall keeps interactive-sections: [storage]: the operator confirms the
  target disk, and no automatic wipe is ever performed
- firstboot terminal wizard for the 8-digit dealer id, three-state lifecycle
  (PROVISIONED_OFFLINE -> ENROLLED -> READY) with fail-closed gates, so a
  missing internet link or WireGuard handshake cannot fail provisioning but can
  never produce a false READY

Fleet and migration:
- five read-only Ansible playbooks for Field OS version, provisioning,
  enrollment, remote channels and offline readiness, all wave-scoped with a
  separate production override
- Windows pre-migration inventory and data export extended to the full brief
  scope, read-only, never collecting credentials

Tests: tests/run-all.sh -> 8/8 pass, exit 0. Boot acceptance (UEFI, Legacy
BIOS, Secure Boot), air-gapped install, real enrollment endpoint and Windows
rollback remain LAB work and are listed in IMPLEMENTATION-REPORT.md.
2026-09-16 11:14:42 +03:00

166 lines
12 KiB
Bash
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# Validate repository specifications and Ansible safety contracts.
set -euo pipefail
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
PB="$REPO_ROOT/ansible/playbooks"
ROLES="$REPO_ROOT/ansible/roles"
FAIL=0
fail() { printf 'FAIL: %s\n' "$*" >&2; FAIL=1; }
pass() { printf 'OK: %s\n' "$*"; }
# The canonical documentation set is docs/00..NN. Adding a document is expected
# over time, so the set is derived from the tree instead of pinned to a literal:
# what must hold is that the numbering starts at 00 and has no gaps.
mapfile -t DOCS < <(printf '%s\n' "$REPO_ROOT"/docs/[0-9][0-9]-*.md | sort)
if [[ ${#DOCS[@]} -eq 0 ]]; then
fail "no numbered docs found under docs/"
else
highest=0
for doc in "${DOCS[@]}"; do
n="$(basename "$doc" | cut -c1-2)"
n=$((10#$n))
[[ $n -gt $highest ]] && highest=$n
done
for ((i = 0; i <= highest; i++)); do
printf -v number '%02d' "$i"
matches=("$REPO_ROOT"/docs/"$number"-*.md)
[[ -f ${matches[0]} ]] || fail "missing numbered doc: $number (numbering must be gapless)"
done
pass "${#DOCS[@]} numbered docs present (00..$(printf '%02d' "$highest"), gapless)"
fi
# Docusaurus yalnız numaralı kanonik dokümanları yayımlamalıdır; şablon site içeriği değildir.
SYNC_SCRIPT="$REPO_ROOT/docs-site/sync-docs.sh"
SITE_DOCS="$REPO_ROOT/docs-site/docs"
if [[ ! -x "$SYNC_SCRIPT" ]]; then
fail "docs-site sync script missing or not executable"
elif ! grep -Fq '[0-9][0-9]-*.md' "$SYNC_SCRIPT"; then
fail "docs-site sync script must select only numbered Markdown files"
elif ! grep -Fq '_TEMPLATE.md is never site content' "$SYNC_SCRIPT"; then
fail "docs-site sync script must explicitly exclude the template"
else
pass "docs-site sync contract selects numbered docs and excludes the template"
fi
if [[ -f "$SITE_DOCS/_TEMPLATE.md" ]]; then
fail "docs-site must not contain docs/_TEMPLATE.md"
else
pass "docs-site template is absent"
fi
# Each numbered doc must retain ordered sections 1 through 13 and at least one diagram.
mermaid_total=0
for doc in "${DOCS[@]}"; do
heading_list="$(grep -E '^## [0-9]+\.' "$doc" | sed -E 's/^## ([0-9]+)\..*/\1/' | paste -sd, -)"
if [[ "$heading_list" != '1,2,3,4,5,6,7,8,9,10,11,12,13' ]]; then
fail "non-sequential numbered headings in ${doc#$REPO_ROOT/}: ${heading_list:-none}"
fi
count="$(grep -c '^```mermaid$' "$doc" || true)"
if [[ "$count" -lt 1 ]]; then
fail "missing Mermaid block in ${doc#$REPO_ROOT/}"
fi
mermaid_total=$((mermaid_total + count))
done
# Every numbered doc carries at least one diagram. A fixed global total is
# deliberately not asserted: documents are added over time and a pinned sum turns
# a legitimate addition into a red test.
if [[ "$mermaid_total" -lt "${#DOCS[@]}" ]]; then
fail "Mermaid count ($mermaid_total) is below the document count (${#DOCS[@]}); every doc needs a diagram"
else
pass "${#DOCS[@]}/${#DOCS[@]} docs carry a diagram ($mermaid_total Mermaid blocks total)"
fi
EXPECTED_PLAYBOOKS=(bf-ping.yml bf-uptime.yml bf-reboot.yml bf-docker-status.yml bf-service-restart.yml bf-wireguard-restart.yml bf-rustdesk-restart.yml bf-rdp-restart.yml bf-collect-logs.yml bf-disk-check.yml bf-package-check.yml bf-security-updates-check.yml bf-deploy-update.yml bf-run-script.yml bf-gui-on.yml bf-gui-off.yml bf-fieldos-version.yml bf-provisioning-status.yml bf-enrollment-status.yml bf-remote-channels-check.yml bf-offline-ready-check.yml)
for pb in "${EXPECTED_PLAYBOOKS[@]}"; do
[[ -f "$PB/$pb" ]] && pass "playbook present: $pb" || fail "missing playbook: $pb"
done
for role in common wireguard docker monitoring; do
for file in tasks/main.yml defaults/main.yml README.md; do
[[ -f "$ROLES/$role/$file" ]] && pass "role file present: $role/$file" || fail "missing role file: $role/$file"
done
done
# Safety contracts from docs/09 and docs/10.
grep -q 'serial' "$PB/bf-reboot.yml" || fail 'bf-reboot.yml must batch hosts with serial'
grep -q 'any_errors_fatal: true' "$PB/bf-reboot.yml" || fail 'bf-reboot.yml must halt on errors'
grep -q 'release_wave' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must require a release wave'
grep -q 'wave_gate_confirmed' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must require a wave gate'
grep -q 'halt_on_fail' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must enforce halt_on_fail'
grep -q 'max_fail_percentage: 0' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must stop the wave on failure'
grep -q 'serial: 5' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must use the fixed batch size'
grep -q 'production_override' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must require a separate production override'
grep -q 'ansible_play_hosts_all' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must verify selected hosts against the release wave'
grep -q 'Run mandatory post-batch health gate' "$PB/bf-deploy-update.yml" || fail 'bf-deploy-update.yml must run a post-batch health gate'
for pb in bf-collect-logs.yml bf-gui-on.yml bf-run-script.yml bf-gui-off.yml; do
grep -q 'operation_wave' "$PB/$pb" || fail "$pb must require operation_wave"
grep -q 'operation_confirmed' "$PB/$pb" || fail "$pb must require operation_confirmed"
grep -q 'ansible_play_hosts_all' "$PB/$pb" || fail "$pb must verify its selected hosts"
grep -q 'production_override' "$PB/$pb" || fail "$pb must require a separate production override"
grep -q "operation_wave != 'production' or production_override | bool" "$PB/$pb" || fail "$pb must refuse production without production_override=true"
done
if grep -q '^ hosts: blueforce$' "$PB/bf-gui-off.yml"; then
fail 'bf-gui-off.yml must not target the blueforce group by default'
fi
grep -q '^operation_wave:' "$REPO_ROOT/ansible/inventory/group_vars/all.yml" || fail 'shared operation_wave variable missing'
grep -q '^operation_confirmed:' "$REPO_ROOT/ansible/inventory/group_vars/all.yml" || fail 'shared operation_confirmed variable missing'
for asset in config/firewall/blueforce-docker-user-restore.sh config/systemd/blueforce-docker-firewall.service config/systemd/docker.service.d/blueforce-docker-firewall.conf; do
[[ -f "$REPO_ROOT/$asset" ]] || fail "missing persistent Docker firewall asset: $asset"
done
grep -q 'systemctl enable "$DOCKER_POLICY_UNIT"' "$REPO_ROOT/scripts/install/installer/modules/13-firewall.sh" || fail 'firewall module must enable persistent Docker firewall unit'
grep -q 'docker.service.d/blueforce-docker-firewall.conf' "$REPO_ROOT/scripts/install/installer/modules/13-firewall.sh" || fail 'firewall module must install Docker restart coupling'
grep -q 'DOCKER_POLICY_BIN" --check' "$REPO_ROOT/scripts/install/installer/modules/13-firewall.sh" || fail 'firewall module check must verify Docker policy rules'
grep -q '13-firewall' "$REPO_ROOT/scripts/install/installer/modules/18-final-check.sh" || fail 'final check must include the firewall rule verification gate'
grep -q 'bash "$SCRIPT_DIR/$g.sh" --check' "$REPO_ROOT/scripts/install/installer/modules/18-final-check.sh" || fail 'final check must execute every gate in check mode'
grep -q 'ctorigdstport' "$REPO_ROOT/config/firewall/blueforce-docker-user-restore.sh" || fail 'Docker firewall must consume published-port allowlist rules'
grep -q 'iptables -C DOCKER-USER' "$REPO_ROOT/config/firewall/blueforce-docker-user-restore.sh" || fail 'Docker firewall check must verify every installed allowlist rule'
grep -q -- '-i br+ -j RETURN' "$REPO_ROOT/config/firewall/blueforce-docker-user-restore.sh" || fail 'Docker firewall must permit egress from user-defined bridge interfaces'
grep -q '^BindsTo=docker.service$' "$REPO_ROOT/config/systemd/blueforce-docker-firewall.service" || fail 'Docker firewall service must bind to Docker lifecycle'
grep -q '^PartOf=docker.service$' "$REPO_ROOT/config/systemd/blueforce-docker-firewall.service" || fail 'Docker firewall service must restart with Docker'
grep -q '^Wants=blueforce-docker-firewall.service$' "$REPO_ROOT/config/systemd/docker.service.d/blueforce-docker-firewall.conf" || fail 'Docker must start the firewall service on every start'
grep -q 'BF_WG_HANDSHAKE_MAX_AGE' "$REPO_ROOT/scripts/install/installer/modules/06-wireguard.sh" || fail 'WireGuard module must enforce handshake freshness'
grep -q 'configured ||' "$REPO_ROOT/scripts/install/installer/modules/09-rustdesk.sh" || fail 'RustDesk module must prove client configuration'
grep -q 'service_allowlist' "$PB/bf-service-restart.yml" || fail 'bf-service-restart.yml must enforce an allowlist'
grep -q 'script_allowed_dirs' "$PB/bf-run-script.yml" || fail 'bf-run-script.yml must enforce allowed script paths'
grep -Fq "collect_logs_dir is match('^/var/lib/blueforce/support-bundles" "$PB/bf-collect-logs.yml" || fail 'bf-collect-logs.yml must restrict bundles to the approved support directory'
if [[ "$(grep -Fc '| quote' "$PB/bf-collect-logs.yml")" -lt 2 ]]; then
fail 'bf-collect-logs.yml must shell-quote every generated bundle path'
fi
if grep -RniE --include='*.yml' '^[[:space:]]*[^#].*(image:[^#]*:latest([[:space:]#]|$)|watchtower)' "$PB" "$ROLES/docker"; then
fail "forbidden moving tag or Watchtower configuration found in deploy paths"
else
pass 'no moving tags or auto-updaters in deploy paths'
fi
if grep -qniE 'state:[[:space:]]*(present|latest)|apt-get .*(upgrade|dist-upgrade)[[:space:]]+-y|apt:.*name:' "$PB/bf-security-updates-check.yml"; then
fail 'bf-security-updates-check.yml must remain report-only'
else
pass 'security update check is report-only'
fi
# Field OS lifecycle and enrollment contracts must remain explicit and safe.
for asset in scripts/diagnostics/bf-enroll scripts/diagnostics/bf-enroll-now scripts/diagnostics/bf-enrollment-status scripts/diagnostics/bf-release scripts/diagnostics/bf-remote-status scripts/diagnostics/bf-check-local scripts/diagnostics/bf-check-enrollment scripts/diagnostics/bf-check-ready config/systemd/blueforce-enroll.service config/systemd/blueforce-enroll.timer; do
[[ -f "$REPO_ROOT/$asset" ]] && pass "Field OS asset present: $asset" || fail "missing Field OS asset: $asset"
done
grep -q -- '--offline' "$REPO_ROOT/scripts/install/blueforce-install.sh" || fail 'installer must support --offline'
grep -q 'PROVISIONED_OFFLINE' "$REPO_ROOT/scripts/install/blueforce-install.sh" || fail 'installer must record PROVISIONED_OFFLINE'
grep -q 'BF_ADMIN_PUBLIC_KEY' "$REPO_ROOT/scripts/install/installer/modules/07-ssh.sh" || fail 'SSH module must accept an explicit admin public key only before hardening'
grep -q 'systemctl is-active --quiet xrdp' "$REPO_ROOT/scripts/install/installer/modules/08-rdp.sh" || fail 'RDP check must require active xrdp'
if grep -Eq 'systemctl (stop|disable) (xrdp|xrdp-sesman)' "$REPO_ROOT/scripts/maintenance/bf-gui-off"; then fail 'bf-gui-off must never stop or disable xRDP'; else pass 'bf-gui-off preserves xRDP'; fi
grep -q -- '--data-binary "@\$REQUEST_FILE"' "$REPO_ROOT/scripts/diagnostics/bf-enroll" || fail 'enrollment curl must consume the request file'
grep -q -- '-H @<(printf' "$REPO_ROOT/scripts/diagnostics/bf-enroll" || fail 'enrollment Authorization header must use process substitution'
if grep -q 'Authorization: Bearer \*\*\*' "$REPO_ROOT/scripts/diagnostics/bf-enroll"; then fail 'enrollment must not send a redacted literal Authorization value'; fi
grep -q 'central_verification' "$REPO_ROOT/scripts/diagnostics/bf-remote-status" || fail 'remote status must require central verification evidence'
if grep -q 'bf-enrollment-status\|phase="' "$REPO_ROOT/scripts/diagnostics/bf-remote-status"; then fail 'remote status must not infer proof from persisted lifecycle phase'; fi
grep -q 'UNAPPROVED_SKELETON' "$REPO_ROOT/scripts/diagnostics/bf-release" || fail 'release report must reject skeleton manifests'
"$REPO_ROOT/tests/test-field-os-lifecycle.sh" && pass 'Field OS lifecycle shell tests pass' || fail 'Field OS lifecycle shell tests failed'
grep -q 'PROVISIONED_OFFLINE.*ENROLLED.*READY' "$REPO_ROOT/scripts/diagnostics/bf-enrollment-status" || fail 'status must define canonical lifecycle states'
grep -q 'blueforce-install.sh' "$REPO_ROOT/provisioning/firstboot/bf-firstboot" && grep -q -- '--offline' "$REPO_ROOT/provisioning/firstboot/bf-firstboot" || fail 'firstboot must invoke offline installer after dealer ID validation'
grep -q 'BF_FIRSTBOOT_TEST_MODE' "$REPO_ROOT/provisioning/firstboot/bf-firstboot" || fail 'firstboot must provide mutation-free test mode'
if [[ "$FAIL" -ne 0 ]]; then
printf 'check-specs: FAILED\n' >&2
exit 1
fi
printf 'check-specs: all passed\n'