#!/usr/bin/env bash
set -Eeuo pipefail

ACTION="${1:-setup}"
LAB_ROOT="${2:-/tmp/soria-glpi-security-${USER:-student}}"
MARKER_NAME=".soria-glpi-security-maintenance-project"

fail() { printf 'ERROR: %s\n' "$*" >&2; exit 1; }

resolve_safe_root() {
  case "$LAB_ROOT" in
    /tmp/soria-glpi-*|"${HOME}"/soria-glpi-*) ;;
    *) fail "refused lab root: $LAB_ROOT" ;;
  esac
  [ "$LAB_ROOT" != "/tmp" ] || fail "refused root: /tmp"
  [ "$LAB_ROOT" != "$HOME" ] || fail "refused root: $HOME"
}

require_marker() { [ -f "$LAB_ROOT/$MARKER_NAME" ] || fail "missing marker: $LAB_ROOT/$MARKER_NAME"; }
csv_rows() { awk 'END { print (NR > 0 ? NR - 1 : 0) }' "$1"; }

assert_unique_column() {
  local file="$1" column="$2" label="$3"
  awk -F',' -v col="$column" -v label="$label" '
    NR == 1 { next }
    $col == "" { printf "empty %s at line %d\n", label, NR > "/dev/stderr"; exit 1 }
    seen[$col]++ { printf "duplicate %s: %s\n", label, $col > "/dev/stderr"; exit 1 }
  ' "$file" || fail "$label uniqueness validation failed"
}

setup_lab() {
  if [ -e "$LAB_ROOT" ]; then
    require_marker
    rm -rf "$LAB_ROOT/refs" "$LAB_ROOT/policy" "$LAB_ROOT/input" "$LAB_ROOT/output" "$LAB_ROOT/evidence"
  else
    mkdir -p "$LAB_ROOT"
    : > "$LAB_ROOT/$MARKER_NAME"
  fi

  mkdir -p "$LAB_ROOT/refs" "$LAB_ROOT/policy" "$LAB_ROOT/input" "$LAB_ROOT/output" "$LAB_ROOT/evidence"

  cat > "$LAB_ROOT/refs/profiles.csv" <<'CSV'
profile_code,name,max_scope,super_admin,purge,setup_write,logs_read,force_2fa
SELF_SERVICE,Self-Service,ENTITY,no,no,no,no,no
TECH_L1,Technicien L1,ENTITY,no,no,no,no,yes
TECH_L2,Technicien L2,ENTITY,no,no,no,yes,yes
ASSET_MANAGER,Gestionnaire de parc,ENTITY,no,no,no,yes,yes
SERVICE_MANAGER,Responsable support,ENTITY,no,no,no,yes,yes
AUDITOR,Auditeur,ENTITY,no,no,no,yes,yes
PLATFORM_ADMIN,Administrateur plateforme,GLOBAL,no,no,yes,yes,yes
BREAK_GLASS,Compte de secours,GLOBAL,yes,yes,yes,yes,yes
CSV

  cat > "$LAB_ROOT/refs/entities.csv" <<'CSV'
entity_code,parent_code,name
SORIA,,SORIA Distribution
ORLEANS,SORIA,Siège Orléans
TOURS,SORIA,Agence Tours
LAB,SORIA,Laboratoire
CSV

  cat > "$LAB_ROOT/policy/access-policy.csv" <<'CSV'
control_code,description,required
NO_DAILY_SUPER_ADMIN,Le profil super-admin ne doit pas être utilisé au quotidien,yes
BREAK_GLASS_MFA,Le compte de secours impose la 2FA,yes
BREAK_GLASS_OWNER,Le compte de secours possède un propriétaire nommé,yes
RECURSION_JUSTIFIED,Toute autorisation récursive possède une justification,yes
INACTIVE_DISABLED,Un compte inactif depuis plus de 90 jours est désactivé,yes
QUARTERLY_REVIEW,Les habilitations privilégiées sont revues au moins tous les 90 jours,yes
SEPARATE_AUDITOR,L'auditeur ne possède aucun droit d'écriture,yes
CSV

  cat > "$LAB_ROOT/input/authorizations.csv" <<'CSV'
auth_id,user_id,profile_code,entity_code,recursive,dynamic,active,last_login_days,last_review_days,owner,justification
AUTH-001,alice,TECH_L1,ORLEANS,no,yes,yes,2,40,Support Manager,Support utilisateurs Orléans
AUTH-002,bob,TECH_L2,SORIA,yes,yes,yes,4,35,Support Manager,Escalade multi-sites
AUTH-003,carol,ASSET_MANAGER,SORIA,yes,no,yes,8,65,IT Manager,Gestion du parc multi-sites
AUTH-004,david,AUDITOR,SORIA,yes,no,yes,12,20,DPO,Audit trimestriel en lecture seule
AUTH-005,svc-glpi,PLATFORM_ADMIN,SORIA,yes,no,yes,0,15,Platform Owner,Administration technique contrôlée
AUTH-006,break-glass,BREAK_GLASS,SORIA,yes,no,yes,120,10,CISO,Urgence documentée et coffre-fort
AUTH-007,legacy-admin,BREAK_GLASS,SORIA,yes,no,yes,180,180,,Ancien compte administrateur
AUTH-008,erin,SELF_SERVICE,TOURS,no,yes,yes,1,30,HR,Portail utilisateur Tours
CSV

  cat > "$LAB_ROOT/input/security-controls.csv" <<'CSV'
user_id,mfa_enabled,credential_vaulted,interactive_daily_use,review_ticket
alice,no,no,no,REV-2026-Q3-001
bob,yes,no,no,REV-2026-Q3-002
carol,yes,no,no,REV-2026-Q3-003
david,yes,no,no,REV-2026-Q3-004
svc-glpi,yes,yes,no,REV-2026-Q3-005
break-glass,yes,yes,no,REV-2026-Q3-006
legacy-admin,no,no,yes,
erin,no,no,no,REV-2026-Q3-008
CSV

  cat > "$LAB_ROOT/policy/backup-policy.csv" <<'CSV'
component,required,encrypted,offsite,retention_days
DATABASE,yes,yes,yes,30
FILES,yes,yes,yes,30
CONFIG,yes,yes,yes,30
ENCRYPTION_KEY,yes,yes,yes,365
PLUGIN_INVENTORY,yes,no,yes,365
CSV

  cat > "$LAB_ROOT/input/backups.csv" <<'CSV'
backup_id,component,file_name,created_at,encrypted,offsite,checksum_present,retention_days
BKP-20260804-DATABASE,DATABASE,glpi-db-20260804.sql.zst,2026-08-04T18:00:00Z,yes,yes,yes,30
BKP-20260804-FILES,FILES,glpi-files-20260804.tar.zst,2026-08-04T18:05:00Z,yes,yes,yes,30
BKP-20260804-CONFIG,CONFIG,glpi-config-20260804.tar.zst,2026-08-04T18:06:00Z,yes,yes,yes,30
BKP-20260804-KEY,ENCRYPTION_KEY,glpi-key-20260804.age,2026-08-04T18:07:00Z,yes,yes,yes,365
BKP-20260804-PLUGINS,PLUGIN_INVENTORY,glpi-plugins-20260804.csv,2026-08-04T18:08:00Z,no,yes,yes,365
CSV

  cat > "$LAB_ROOT/input/restore-drill.csv" <<'CSV'
drill_id,source_version,target_version,target_database_empty,database_restored,files_restored,config_restored,key_restored,integrity_check,application_login,rto_minutes,evidence_ticket
RST-001,11.0.4,11.0.4,yes,yes,yes,yes,yes,yes,yes,42,CHG-RESTORE-001
CSV

  cat > "$LAB_ROOT/input/update-plan.csv" <<'CSV'
change_id,current_version,target_version,maintenance_window,backup_verified,restore_drill_recent,target_directory_empty,db_update_command,plugins_suspended,rollback_plan,approval
CHG-UPD-001,11.0.4,11.0.5,2026-08-09T06:00:00Z,yes,yes,yes,php bin/console db:update,yes,Restore database and files from BKP-20260804,approved
CSV

  cat > "$LAB_ROOT/input/plugins.csv" <<'CSV'
plugin_code,source,installed_version,target_compatible,business_owner,active,last_review_days,backup_before_change,decision_expected
FORMCREATOR,marketplace,2.14.14,yes,Service Manager,yes,25,yes,KEEP
FIELDS,marketplace,1.21.0,yes,Asset Manager,yes,40,yes,KEEP
LEGACYREPORT,manual,0.9.2,no,,yes,220,no,DISABLE_BEFORE_UPDATE
CSV

  cat > "$LAB_ROOT/input/health-checks.csv" <<'CSV'
check_code,domain,value,unit,warning_threshold,critical_threshold,direction
DISK_FREE,system,18,percent,20,10,minimum
DB_BACKUP_AGE,backup,2,hours,24,48,maximum
CRON_DELAY,automation,3,minutes,5,15,maximum
MAIL_QUEUE,notification,7,items,20,50,maximum
ERROR_LOG_RATE,logs,14,events_per_hour,10,30,maximum
INVENTORY_AGE,assets,9,percent_over_7d,10,20,maximum
SLA_BREACH,service,4,percent,5,10,maximum
CSV

  cat > "$LAB_ROOT/input/audit-events.csv" <<'CSV'
event_id,event_type,actor,age_minutes,severity,evidence
EVT-001,FAILED_LOGIN_BURST,legacy-admin,15,high,12 failed logins
EVT-002,PROFILE_CHANGE,svc-glpi,120,medium,TECH_L1 permission updated
EVT-003,PLUGIN_ERROR,LEGACYREPORT,30,high,PHP compatibility error
EVT-004,BACKUP_SUCCESS,backup-job,90,info,all components checksummed
CSV

  cat > "$LAB_ROOT/input/project-domains.csv" <<'CSV'
domain,required_evidence,status
GOVERNANCE,entities+profiles+catalogue,PASS
INVENTORY,taxonomy+agent+reconciliation,PASS
SERVICE_DESK,tickets+sla+knowledge,PASS
AUTOMATION,rules+queues+contracts+kpis,PASS
SECURITY,access+restore+update+monitoring,REVIEW
CSV

  cat > "$LAB_ROOT/policy/operations-runbook.md" <<'MD'
# Runbook de maintenance GLPI

## Avant changement

- vérifier la sauvegarde de la base, des fichiers, de la configuration et de la clé de chiffrement ;
- confirmer un test de restauration récent dans un environnement isolé ;
- exporter l'inventaire et la compatibilité des plugins ;
- approuver la fenêtre, les critères d'arrêt et le rollback ;
- suspendre les plugins conformément à la procédure de la version visée.

## Mise à jour

- extraire la version cible dans un répertoire vide ;
- préserver les chemins FHS et `downstream.php` si utilisés ;
- exécuter `php bin/console db:update` ;
- vérifier intégrité, authentification, actions automatiques, collecteur et inventaire ;
- ne jamais restaurer une base dans une base partiellement migrée ou non vide.

## Après changement

- réactiver uniquement les plugins compatibles et précédemment actifs ;
- contrôler logs, files, cron, espace disque et erreurs applicatives ;
- documenter la version, les preuves, les écarts et la décision de clôture.
MD

  (
    cd "$LAB_ROOT"
    sha256sum refs/*.csv policy/*.csv policy/*.md input/*.csv > evidence/source-checksums-before.sha256
  )

  cat > "$LAB_ROOT/README.txt" <<'TXT'
Commands:
  setup     create the offline GLPI security and maintenance dossier
  status    show source counts and generated outputs
  run       evaluate access, backups, restore, update, plugins, monitoring and project readiness
  validate  verify findings, evidence boundaries, checksums and final readiness
  reset     delete this marker-protected lab root
TXT

  printf 'GLPI security, maintenance and final project laboratory ready: %s\n' "$LAB_ROOT"
}

show_status() {
  require_marker
  printf 'Authorizations: %s\n' "$(csv_rows "$LAB_ROOT/input/authorizations.csv")"
  printf 'Backup components: %s\n' "$(csv_rows "$LAB_ROOT/input/backups.csv")"
  printf 'Plugins: %s\n' "$(csv_rows "$LAB_ROOT/input/plugins.csv")"
  printf 'Health checks: %s\n' "$(csv_rows "$LAB_ROOT/input/health-checks.csv")"
  printf 'Project domains: %s\n' "$(csv_rows "$LAB_ROOT/input/project-domains.csv")"
  test -f "$LAB_ROOT/output/final-readiness.csv" && printf 'Final readiness: generated\n' || printf 'Final readiness: not generated\n'
}

run_reference() {
  require_marker
  mkdir -p "$LAB_ROOT/output" "$LAB_ROOT/evidence"

  assert_unique_column "$LAB_ROOT/refs/profiles.csv" 1 "profile code"
  assert_unique_column "$LAB_ROOT/refs/entities.csv" 1 "entity code"
  assert_unique_column "$LAB_ROOT/input/authorizations.csv" 1 "authorization id"
  assert_unique_column "$LAB_ROOT/input/backups.csv" 1 "backup id"
  assert_unique_column "$LAB_ROOT/input/plugins.csv" 1 "plugin code"
  assert_unique_column "$LAB_ROOT/input/health-checks.csv" 1 "health check code"

  awk -F',' -v OFS=',' '
    FILENAME==ARGV[1] { if (FNR>1) { profile[$1]=1; super_admin[$1]=$4; purge[$1]=$5; setup_write[$1]=$6; logs_read[$1]=$7; force_2fa[$1]=$8 } next }
    FILENAME==ARGV[2] { if (FNR>1) { mfa[$1]=$2; vaulted[$1]=$3; daily[$1]=$4; review_ticket[$1]=$5 } next }
    FILENAME==ARGV[3] && FNR==1 { print "auth_id,user_id,profile_code,entity_code,risk,decision,reasons"; next }
    {
      risk="LOW"; decision="KEEP"; reasons="NONE"
      if (!($3 in profile)) { print "unknown profile: "$3 > "/dev/stderr"; exit 1 }
      if ($7=="yes" && $8+0>90 && $3!="BREAK_GLASS") { risk="HIGH"; decision="DISABLE"; reasons="INACTIVE_OVER_90_DAYS" }
      if ($3=="BREAK_GLASS" && mfa[$2]!="yes") { risk="CRITICAL"; decision="DISABLE"; reasons=(reasons=="NONE"?"MFA_MISSING":reasons"+MFA_MISSING") }
      if ($3=="BREAK_GLASS" && vaulted[$2]!="yes") { risk="CRITICAL"; decision="DISABLE"; reasons=(reasons=="NONE"?"NOT_VAULTED":reasons"+NOT_VAULTED") }
      if ($3=="BREAK_GLASS" && $10=="") { risk="CRITICAL"; decision="DISABLE"; reasons=(reasons=="NONE"?"OWNER_MISSING":reasons"+OWNER_MISSING") }
      if ($5=="yes" && $11=="") { risk="HIGH"; decision="REVIEW"; reasons=(reasons=="NONE"?"RECURSION_UNJUSTIFIED":reasons"+RECURSION_UNJUSTIFIED") }
      if ((super_admin[$3]=="yes" || setup_write[$3]=="yes") && $9+0>90) { risk="HIGH"; decision="REVIEW"; reasons=(reasons=="NONE"?"PRIVILEGED_REVIEW_OVERDUE":reasons"+PRIVILEGED_REVIEW_OVERDUE") }
      if (super_admin[$3]=="yes" && daily[$2]=="yes") { risk="CRITICAL"; decision="DISABLE"; reasons=(reasons=="NONE"?"DAILY_SUPER_ADMIN_USE":reasons"+DAILY_SUPER_ADMIN_USE") }
      print $1,$2,$3,$4,risk,decision,reasons
    }
  ' "$LAB_ROOT/refs/profiles.csv" "$LAB_ROOT/input/security-controls.csv" "$LAB_ROOT/input/authorizations.csv" > "$LAB_ROOT/output/access-review.csv" || fail "access review failed"

  awk -F',' -v OFS=',' '
    NR==FNR { if (FNR>1) { required[$1]=$2; encrypted[$1]=$3; offsite[$1]=$4; retention[$1]=$5 } next }
    FNR==1 { next }
    {
      found[$2]=1
      result="PASS"; reasons="NONE"
      if (required[$2]!="yes") { result="REVIEW"; reasons="UNEXPECTED_COMPONENT" }
      if (encrypted[$2]=="yes" && $5!="yes") { result="FAIL"; reasons=(reasons=="NONE"?"NOT_ENCRYPTED":reasons"+NOT_ENCRYPTED") }
      if (offsite[$2]=="yes" && $6!="yes") { result="FAIL"; reasons=(reasons=="NONE"?"NOT_OFFSITE":reasons"+NOT_OFFSITE") }
      if ($7!="yes") { result="FAIL"; reasons=(reasons=="NONE"?"CHECKSUM_MISSING":reasons"+CHECKSUM_MISSING") }
      if ($8+0 < retention[$2]+0) { result="FAIL"; reasons=(reasons=="NONE"?"RETENTION_TOO_SHORT":reasons"+RETENTION_TOO_SHORT") }
      rows[$2]=$2","result","reasons
    }
    END {
      print "component,result,reasons"
      for (c in required) {
        if (!(c in found)) print c,"FAIL","MISSING_COMPONENT"
        else print rows[c]
      }
    }
  ' "$LAB_ROOT/policy/backup-policy.csv" "$LAB_ROOT/input/backups.csv" | sort > "$LAB_ROOT/output/backup-evaluation.csv"

  awk -F',' -v OFS=',' '
    NR==1 { print "drill_id,result,rto_minutes,reasons"; next }
    {
      result="PASS"; reasons="NONE"
      if ($2!=$3) { result="FAIL"; reasons="VERSION_MISMATCH" }
      if ($4!="yes") { result="FAIL"; reasons=(reasons=="NONE"?"TARGET_DB_NOT_EMPTY":reasons"+TARGET_DB_NOT_EMPTY") }
      for (i=5;i<=10;i++) if ($i!="yes") { result="FAIL"; reasons=(reasons=="NONE"?"RESTORE_STEP_MISSING":reasons"+RESTORE_STEP_MISSING") }
      if ($11+0>60) { result="FAIL"; reasons=(reasons=="NONE"?"RTO_EXCEEDED":reasons"+RTO_EXCEEDED") }
      if ($12=="") { result="FAIL"; reasons=(reasons=="NONE"?"EVIDENCE_MISSING":reasons"+EVIDENCE_MISSING") }
      print $1,result,$11,reasons
    }
  ' "$LAB_ROOT/input/restore-drill.csv" > "$LAB_ROOT/output/restore-evaluation.csv"

  awk -F',' -v OFS=',' '
    NR==1 { print "change_id,result,reasons"; next }
    {
      result="PASS"; reasons="NONE"
      if ($4=="") { result="FAIL"; reasons="WINDOW_MISSING" }
      for (i=5;i<=7;i++) if ($i!="yes") { result="FAIL"; reasons=(reasons=="NONE"?"PREREQUISITE_MISSING":reasons"+PREREQUISITE_MISSING") }
      if ($8!="php bin/console db:update") { result="FAIL"; reasons=(reasons=="NONE"?"DB_UPDATE_COMMAND_INVALID":reasons"+DB_UPDATE_COMMAND_INVALID") }
      if ($9!="yes") { result="FAIL"; reasons=(reasons=="NONE"?"PLUGINS_NOT_SUSPENDED":reasons"+PLUGINS_NOT_SUSPENDED") }
      if ($10=="" || $11!="approved") { result="FAIL"; reasons=(reasons=="NONE"?"GOVERNANCE_INCOMPLETE":reasons"+GOVERNANCE_INCOMPLETE") }
      print $1,result,reasons
    }
  ' "$LAB_ROOT/input/update-plan.csv" > "$LAB_ROOT/output/update-evaluation.csv"

  awk -F',' -v OFS=',' '
    NR==1 { print "plugin_code,decision,risk,reasons"; next }
    {
      decision="KEEP"; risk="LOW"; reasons="NONE"
      if ($4!="yes") { decision="DISABLE_BEFORE_UPDATE"; risk="HIGH"; reasons="TARGET_INCOMPATIBLE" }
      if ($5=="") { decision="DISABLE_BEFORE_UPDATE"; risk="HIGH"; reasons=(reasons=="NONE"?"OWNER_MISSING":reasons"+OWNER_MISSING") }
      if ($7+0>90) { decision="DISABLE_BEFORE_UPDATE"; risk="HIGH"; reasons=(reasons=="NONE"?"REVIEW_OVERDUE":reasons"+REVIEW_OVERDUE") }
      if ($8!="yes") { decision="DISABLE_BEFORE_UPDATE"; risk="HIGH"; reasons=(reasons=="NONE"?"BACKUP_NOT_CONFIRMED":reasons"+BACKUP_NOT_CONFIRMED") }
      if (decision!=$9) { print "unexpected plugin decision for "$1 > "/dev/stderr"; exit 1 }
      print $1,decision,risk,reasons
    }
  ' "$LAB_ROOT/input/plugins.csv" > "$LAB_ROOT/output/plugin-governance.csv" || fail "plugin governance failed"

  awk -F',' -v OFS=',' '
    NR==1 { print "check_code,domain,value,result,action"; next }
    {
      result="PASS"; action="NONE"
      if ($7=="minimum") {
        if ($3+0 <= $6+0) { result="CRITICAL"; action="IMMEDIATE_REMEDIATION" }
        else if ($3+0 < $5+0) { result="WARN"; action="PLAN_REMEDIATION" }
      } else {
        if ($3+0 >= $6+0) { result="CRITICAL"; action="IMMEDIATE_REMEDIATION" }
        else if ($3+0 > $5+0) { result="WARN"; action="PLAN_REMEDIATION" }
      }
      print $1,$2,$3,result,action
    }
  ' "$LAB_ROOT/input/health-checks.csv" > "$LAB_ROOT/output/monitoring-findings.csv"

  awk -F',' -v OFS=',' '
    NR==1 { print "event_id,event_type,severity,decision"; next }
    {
      decision="RETAIN"
      if ($5=="high") decision="OPEN_INCIDENT"
      else if ($5=="medium") decision="REVIEW_CHANGE"
      else if ($5=="info") decision="ARCHIVE_EVIDENCE"
      print $1,$2,$5,decision
    }
  ' "$LAB_ROOT/input/audit-events.csv" > "$LAB_ROOT/output/audit-decisions.csv"

  access_critical=$(awk -F',' 'NR>1 && $5=="CRITICAL" {c++} END{print c+0}' "$LAB_ROOT/output/access-review.csv")
  backup_fail=$(awk -F',' 'NR>1 && $2=="FAIL" {c++} END{print c+0}' "$LAB_ROOT/output/backup-evaluation.csv")
  restore_fail=$(awk -F',' 'NR>1 && $2=="FAIL" {c++} END{print c+0}' "$LAB_ROOT/output/restore-evaluation.csv")
  update_fail=$(awk -F',' 'NR>1 && $2=="FAIL" {c++} END{print c+0}' "$LAB_ROOT/output/update-evaluation.csv")
  plugin_high=$(awk -F',' 'NR>1 && $3=="HIGH" {c++} END{print c+0}' "$LAB_ROOT/output/plugin-governance.csv")
  monitoring_warn=$(awk -F',' 'NR>1 && ($4=="WARN" || $4=="CRITICAL") {c++} END{print c+0}' "$LAB_ROOT/output/monitoring-findings.csv")

  cat > "$LAB_ROOT/output/remediation-plan.csv" <<CSV
finding_id,domain,priority,owner,action,evidence_required
REM-001,ACCESS,P1,CISO,Disable legacy-admin and preserve audit trail,Access review ticket and account disabled evidence
REM-002,PLUGIN,P1,Platform Owner,Disable LEGACYREPORT before update and document replacement,Plugin state export and compatibility decision
REM-003,MONITORING,P2,Platform Owner,Increase free disk above 20 percent,Filesystem report before and after cleanup
REM-004,LOGS,P2,Service Manager,Investigate application error rate,Problem record and corrective change
CSV

  overall="READY"
  if [ "$access_critical" -gt 0 ] || [ "$backup_fail" -gt 0 ] || [ "$restore_fail" -gt 0 ] || [ "$update_fail" -gt 0 ] || [ "$plugin_high" -gt 0 ] || [ "$monitoring_warn" -gt 0 ]; then
    overall="READY_WITH_ACTIONS"
  fi

  {
    printf 'domain,status,findings,evidence\n'
    printf 'GOVERNANCE,PASS,0,entities+profiles+catalogue\n'
    printf 'INVENTORY,PASS,0,taxonomy+agent+reconciliation\n'
    printf 'SERVICE_DESK,PASS,0,tickets+sla+knowledge\n'
    printf 'AUTOMATION,PASS,0,rules+queues+contracts+kpis\n'
    printf 'SECURITY,%s,%s,access+restore+update+plugins+monitoring\n' "$overall" "$((access_critical + backup_fail + restore_fail + update_fail + plugin_high + monitoring_warn))"
    printf 'OVERALL,%s,%s,final dossier with controlled remediation\n' "$overall" "$((access_critical + backup_fail + restore_fail + update_fail + plugin_high + monitoring_warn))"
  } > "$LAB_ROOT/output/final-readiness.csv"

  {
    printf '# Rapport final GLPI — sécurité, maintenance et projet PME\n\n'
    printf -- '- habilitations critiques : %s\n' "$access_critical"
    printf -- '- composants backup en échec : %s\n' "$backup_fail"
    printf -- '- tests de restauration en échec : %s\n' "$restore_fail"
    printf -- '- plans de mise à jour en échec : %s\n' "$update_fail"
    printf -- '- plugins à risque élevé : %s\n' "$plugin_high"
    printf -- '- contrôles de supervision hors cible : %s\n' "$monitoring_warn"
    printf -- '- décision finale : %s\n\n' "$overall"
    printf 'Le laboratoire ne contacte aucune instance GLPI, ne restaure aucune base, ne met à jour aucun logiciel et ne modifie aucun compte réel.\n'
  } > "$LAB_ROOT/evidence/final-report.md"

  (
    cd "$LAB_ROOT"
    sha256sum refs/*.csv policy/*.csv policy/*.md input/*.csv > evidence/source-checksums-after.sha256
    sha256sum output/*.csv evidence/final-report.md > evidence/output-checksums.sha256
  )

  diff -u "$LAB_ROOT/evidence/source-checksums-before.sha256" "$LAB_ROOT/evidence/source-checksums-after.sha256" > "$LAB_ROOT/evidence/source-immutability.diff" || fail "source fixtures changed during run"

  printf 'Reference security, restore, update, plugin, monitoring and final project evaluation completed.\n'
}

validate_reference() {
  require_marker
  for file in \
    output/access-review.csv output/backup-evaluation.csv output/restore-evaluation.csv \
    output/update-evaluation.csv output/plugin-governance.csv output/monitoring-findings.csv \
    output/audit-decisions.csv output/remediation-plan.csv output/final-readiness.csv \
    evidence/final-report.md evidence/source-checksums-before.sha256 \
    evidence/source-checksums-after.sha256 evidence/output-checksums.sha256 \
    evidence/source-immutability.diff
  do
    [ -f "$LAB_ROOT/$file" ] || fail "missing output: $file"
  done

  [ "$(csv_rows "$LAB_ROOT/input/authorizations.csv")" -eq 8 ] || fail "expected 8 authorizations"
  [ "$(csv_rows "$LAB_ROOT/input/backups.csv")" -eq 5 ] || fail "expected 5 backup components"
  [ "$(csv_rows "$LAB_ROOT/input/plugins.csv")" -eq 3 ] || fail "expected 3 plugins"
  [ "$(csv_rows "$LAB_ROOT/input/health-checks.csv")" -eq 7 ] || fail "expected 7 health checks"

  grep -Fq 'AUTH-007,legacy-admin,BREAK_GLASS,SORIA,CRITICAL,DISABLE' "$LAB_ROOT/output/access-review.csv" || fail "legacy admin critical finding missing"
  grep -Fq 'AUTH-006,break-glass,BREAK_GLASS,SORIA,LOW,KEEP,NONE' "$LAB_ROOT/output/access-review.csv" || fail "controlled break-glass result missing"
  [ "$(awk -F',' 'NR>1 && $2=="FAIL" {c++} END{print c+0}' "$LAB_ROOT/output/backup-evaluation.csv")" -eq 0 ] || fail "backup evaluation should pass"
  grep -Fq 'RST-001,PASS,42,NONE' "$LAB_ROOT/output/restore-evaluation.csv" || fail "restore drill result missing"
  grep -Fq 'CHG-UPD-001,PASS,NONE' "$LAB_ROOT/output/update-evaluation.csv" || fail "update plan result missing"
  grep -Fq 'LEGACYREPORT,DISABLE_BEFORE_UPDATE,HIGH' "$LAB_ROOT/output/plugin-governance.csv" || fail "plugin block decision missing"
  grep -Fq 'DISK_FREE,system,18,WARN,PLAN_REMEDIATION' "$LAB_ROOT/output/monitoring-findings.csv" || fail "disk warning missing"
  grep -Fq 'ERROR_LOG_RATE,logs,14,WARN,PLAN_REMEDIATION' "$LAB_ROOT/output/monitoring-findings.csv" || fail "log warning missing"
  grep -Fq 'EVT-001,FAILED_LOGIN_BURST,high,OPEN_INCIDENT' "$LAB_ROOT/output/audit-decisions.csv" || fail "security incident decision missing"
  [ "$(csv_rows "$LAB_ROOT/output/remediation-plan.csv")" -eq 4 ] || fail "expected 4 remediation actions"
  grep -Fq 'OVERALL,READY_WITH_ACTIONS' "$LAB_ROOT/output/final-readiness.csv" || fail "final readiness decision missing"
  grep -Fq 'ne contacte aucune instance GLPI' "$LAB_ROOT/evidence/final-report.md" || fail "proof boundary missing"

  [ ! -s "$LAB_ROOT/evidence/source-immutability.diff" ] || fail "source immutability diff is not empty"
  (
    cd "$LAB_ROOT"
    sha256sum -c evidence/source-checksums-after.sha256 >/dev/null
    sha256sum -c evidence/output-checksums.sha256 >/dev/null
  ) || fail "checksum verification failed"

  ! grep -RniE --exclude='*.sh' --exclude='source-checksums-*' --exclude='output-checksums.sha256' '(password[[:space:]]*=|token[[:space:]]*=|private[_-]?key|https?://[^/[:space:]]+@)' "$LAB_ROOT" >/dev/null || fail "credential-like value detected"

  printf 'Validation successful: least privilege, backup, restore, update, plugins, monitoring and final project evidence are correct.\n'
}

reset_lab() {
  require_marker
  rm -rf "$LAB_ROOT"
  printf 'GLPI security, maintenance and final project laboratory removed: %s\n' "$LAB_ROOT"
}

resolve_safe_root
case "$ACTION" in
  setup) setup_lab ;;
  status) show_status ;;
  run) run_reference ;;
  validate) validate_reference ;;
  reset) reset_lab ;;
  *) fail "unknown action: $ACTION" ;;
esac
