#!/usr/bin/env bash
set -Eeuo pipefail
A=${1:-setup}; R=${2:-/tmp/soria-hardening-baseline-${USER:-student}}; M=.soria-hardening-baseline-recovery
x(){ echo "ERROR: $*" >&2; exit 1; }; okroot(){ case $R in /tmp/soria-hardening-*|"$HOME"/soria-hardening-*) ;; *) x "refused root $R";; esac; [[ $R != / && $R != /tmp && $R != "$HOME" ]]||x unsafe; }; mark(){ [[ -f $R/$M ]]||x marker; }; rows(){ awk 'END{print NR-1}' "$1"; }
refs(){ awk -F, 'FILENAME==ARGV[1]{if(FNR>1)a[$1];next}FNR>1&&!($2 in a){exit 1}' "$1" "$2"; }; rb(){ awk -F, 'NR>1&&$11==""{exit 1}' "$1"; }; sep(){ awk -F, 'NR>1&&$3=="yes"&&$8=="yes"{exit 1}' "$1"; }; no(){ local n=$1;shift; "$@" >/dev/null 2>&1&&x "negative passed: $n"; echo "Negative test blocked: $n"; }
man(){ d=$1;o=$2; echo relative_path,size_bytes,mode,uid,gid,sha256 >"$o"; while IFS= read -r -d '' f; do echo "${f#"$d"/},$(stat -c %s "$f"),$(stat -c %a "$f"),$(stat -c %u "$f"),$(stat -c %g "$f"),$(sha256sum "$f"|cut -d' ' -f1)"; done < <(find "$d" -type f -print0|sort -z)>>"$o"; }
setup(){
 [[ -e $R ]]&&{ mark;chmod -R u+w "$R" 2>/dev/null||:;rm -rf "$R"/{source,work,output,evidence,restore};}||{ mkdir -p "$R";:>"$R/$M";}; mkdir -p "$R"/{source/{inventory,baseline,change,backup/payload/{app/config,data,identity}},work,output,evidence,restore}
 cat >"$R/source/inventory/assets.csv" <<'E'
id,name,role,owner,crit,zone,mission
A1,Portal,linux-web,Commerce,H,dmz,orders
A2,ERP,linux-app,Apps,C,svc,process
A3,Identity,windows-identity,IAM,C,svc,auth
A4,Firewall,network-edge,Net,C,edge,flows
A5,Backup,backup-repository,BCP,C,bkp,restore
A6,Admin,admin-workstation,Infra,H,mgmt,admin
E
 cat >"$R/source/inventory/services.csv" <<'E'
id,asset,name,proto,port,exposure,required,before,action,why,test,rollback
S1,A1,https,tcp,443,external,yes,enabled,KEEP,portal,portal,enable
S2,A1,ssh,tcp,22,admin,yes,enabled,RESTRICT,admin,path,restore-rule
S3,A1,telnet,tcp,23,external,no,enabled,REMOVE,unused,https,restore-pkg
S4,A2,erp-api,tcp,8443,internal,yes,enabled,KEEP,business,orders,enable
S5,A2,ftp,tcp,21,internal,no,enabled,REMOVE,legacy,erp,restore-pkg
S6,A3,dns,udp,53,internal,yes,enabled,KEEP,identity,users,enable
S7,A3,kerberos,tcp,88,internal,yes,enabled,KEEP,auth,users,enable
S8,A4,wireguard,udp,51820,external,yes,enabled,KEEP,vpn,path,enable
S9,A4,web-admin,tcp,443,admin,yes,enabled,RESTRICT,vpn-only,deny-user,restore-rule
S10,A5,backup-ingest,tcp,9100,internal,yes,enabled,KEEP,backup,snapshot,enable
S11,A5,avahi,udp,5353,internal,no,enabled,DISABLE,unused,backup,enable
S12,A6,sample-app,tcp,3000,local,no,enabled,REMOVE,demo,admin,restore-pkg
E
 cat >"$R/source/inventory/software.csv" <<'E'
id,asset,name,required,before,action,why,test,rollback
W1,A1,nginx,yes,installed,KEEP,portal,portal,reinstall
W2,A1,telnetd,no,installed,REMOVE,unused,https,reinstall
W3,A2,erp-runtime,yes,installed,KEEP,erp,orders,restore
W4,A2,vsftpd,no,installed,REMOVE,legacy,erp,restore
W5,A3,dns-role,yes,installed,KEEP,identity,users,restore
W6,A4,wireguard-tools,yes,installed,KEEP,vpn,path,restore
W7,A5,avahi-daemon,no,installed,DISABLE,unused,backup,enable
W8,A6,sample-app,no,installed,REMOVE,demo,admin,restore
E
 cat >"$R/source/change/risk-requirements.csv" <<'E'
id,decision,asset,target,positive,negative,functional,limit
R1,REDUCE,A1,https+restricted-ssh,https,telnet-absent,portal,no-real-port
R2,REDUCE,A2,erp-no-ftp,erp,ftp-absent,orders,no-real-app
R3,REDUCE,A3,dns+kerberos,services,unknown-ref,users,no-gpo
R4,REDUCE,A4,vpn+restricted-admin,vpn,user-denied,path,no-tunnel
R5,REDUCE,A5,separate-repos-roles,isolated,bad-role,restore,no-immutability
R6,REDUCE,A6,no-demo,admin,demo-absent,admin,no-endpoint
E
 cat >"$R/source/baseline/role-controls.csv" <<'E'
role,id,control,target,verify,limit
linux-web,C1,exposure,https+ssh-restrict,compare,no-socket
linux-app,C2,protocols,erp-no-ftp,orders,no-app
windows-identity,C3,identity,dns+kerberos,users,no-windows
network-edge,C4,admin-path,vpn+restrict,path,no-firewall
backup-repository,C5,recovery,primary+isolated,manifests,no-immutability
admin-workstation,C6,reduction,no-demo,admin,no-endpoint
E
 cat >"$R/source/change/patch-candidates.csv" <<'E'
id,asset,advisory,priority,exposure,action,window,precheck,stop,test,rollback,evidence
C1,A1,WEB-08,URGENT,external,patch,emergency,snapshot,portal-fail,portal,restore-snapshot,E1
C2,A2,ERP-07,HIGH,internal,patch,maintenance,backup,schema-diff,orders,restore-pkg,E2
C3,A3,ID-07,HIGH,internal,patch,pilot,replica,auth-fail,identity,remove-update,E3
C4,A4,FW-06,MEDIUM,edge,firmware,maintenance,export,no-admin,vpn,restore-fw,E4
C5,A5,BKP-06,MEDIUM,backup,patch,maintenance,restore-point,catalog-fail,restore,restore-version,E5
E
 printf 'id,requirement\nP1,inventory\nP2,precheck\nP3,stop\nP4,function\nP5,rollback\nP6,evidence\n' >"$R/source/change/change-policy.csv"
 cat >"$R/source/backup/backup-roles.csv" <<'E'
id,account,prod_admin,primary_write,primary_delete,isolated_read,isolated_write,isolated_delete,policy_admin,restore_write,purpose
B1,writer,no,yes,no,no,no,no,no,no,primary
B2,replicator,no,no,no,yes,yes,no,no,no,isolate
B3,policy,no,no,no,no,no,no,yes,no,retention
B4,restore,no,no,no,yes,no,no,no,yes,recover
E
 cat >"$R/source/backup/repository-policy.csv" <<'E'
id,zone,writer,reader,retention,immutable,offsite,simulation,limit
primary,backup,B1,B2,30,no,no,writable,synthetic
isolated,isolated-backup,B2,B4,90,yes,yes,readonly-permissions,local-only
E
 printf 'environment=training\nservice=erp-api\n' >"$R/source/backup/payload/app/config/app.conf"; printf 'id,state\nO1,ok\nO2,ok\nO3,ok\n' >"$R/source/backup/payload/data/orders.csv"; printf 'id,state\nU1,on\nU2,on\nU3,on\n' >"$R/source/backup/payload/identity/users.csv"; chmod 640 "$R/source/backup/payload/app/config/app.conf";chmod 600 "$R/source/backup/payload/data/orders.csv";chmod 644 "$R/source/backup/payload/identity/users.csv";(cd "$R";find source -type f -print0|sort -z|xargs -0 sha256sum>evidence/source.sha256);chmod -R a-w "$R/source";echo "Lab ready: $R"
}
status(){ mark; for p in 'Assets inventory/assets' 'Services inventory/services' 'Software inventory/software' 'Requirements change/risk-requirements' 'Patches change/patch-candidates' 'Roles backup/backup-roles';do set -- $p;echo "$1: $(rows "$R/source/$2.csv")";done; [[ -f $R/output/final-decision.txt ]]&&cat "$R/output/final-decision.txt"||echo 'Decision: not generated'; }
run(){ mark;chmod -R u+w "$R"/{work,output,restore} 2>/dev/null||:;rm -rf "$R"/{work,output,restore};mkdir -p "$R"/{work,output,restore};refs "$R/source/inventory/assets.csv" "$R/source/inventory/services.csv"||x refs;refs "$R/source/inventory/assets.csv" "$R/source/inventory/software.csv"||x refs;rb "$R/source/change/patch-candidates.csv"||x rollback;sep "$R/source/backup/backup-roles.csv"||x roles
 cp "$R/source/inventory/assets.csv" "$R/output/initial-inventory.csv";cp "$R/source/inventory/services.csv" "$R/output/exposure-register.csv";cp "$R/source/baseline/role-controls.csv" "$R/output/role-baseline.csv";{ echo type,id,asset,name,action,why,test,rollback;awk -F, 'NR>1{OFS=",";print "service",$1,$2,$3,$9,$10,$11,$12}' "$R/source/inventory/services.csv";awk -F, 'NR>1{OFS=",";print "software",$1,$2,$3,$6,$7,$8,$9}' "$R/source/inventory/software.csv";} >"$R/output/reduction-plan.csv";cp "$R/source/change/patch-candidates.csv" "$R/output/patch-change-plan.csv"
 awk -F, 'BEGIN{OFS=","}NR==1{print "id","asset","precheck";next}{print $1,$2,$8}' "$R/source/change/patch-candidates.csv">"$R/output/prechecks.csv";awk -F, 'BEGIN{OFS=","}NR==1{print "id","asset","stop";next}{print $1,$2,$9}' "$R/source/change/patch-candidates.csv">"$R/output/stop-criteria.csv";awk -F, 'BEGIN{OFS=","}NR==1{print "id","asset","rollback","evidence";next}{print $1,$2,$11,$12}' "$R/source/change/patch-candidates.csv">"$R/output/rollback-plan.csv"
 awk -F, 'BEGIN{OFS=","}NR==1{print "id","name","state";next}{print $1,$3,$8}' "$R/source/inventory/services.csv">"$R/work/before.csv";awk -F, 'BEGIN{OFS=","}NR==1{print "id","name","state";next}{s="enabled";if($9=="REMOVE")s="removed";else if($9=="DISABLE")s="disabled";else if($9=="RESTRICT")s="restricted";print $1,$3,s}' "$R/source/inventory/services.csv">"$R/work/target.csv";cp "$R/work/before.csv" "$R/work/rollback-before.csv"
 awk -F, 'BEGIN{OFS=","}NR==1{print "id","account","result","reason";next}{print $1,$2,($3=="yes"&&$8=="yes")?"FAIL":"PASS","separated"}' "$R/source/backup/backup-roles.csv">"$R/output/backup-role-review.csv";awk -F, 'BEGIN{OFS=","}NR==1{print "id","zone","retention","immutable","offsite","simulation","claim","action";next}{print $1,$2,$5,$6,$7,$8,($1=="isolated")?"WITH_ACTIONS":"PARTIAL","validate-real-storage"}' "$R/source/backup/repository-policy.csv">"$R/output/repository-assurance.csv"
 mkdir -p "$R/work/repos"/{primary,isolated}/snap;cp -a "$R/source/backup/payload/." "$R/work/repos/primary/snap/";cp -a "$R/work/repos/primary/snap/." "$R/work/repos/isolated/snap/";chmod -R a-w "$R/work/repos/isolated";man "$R/work/repos/isolated/snap" "$R/output/backup-manifest.csv";mkdir -p "$R/restore/snap";cp -a "$R/work/repos/isolated/snap/." "$R/restore/snap/";man "$R/restore/snap" "$R/output/restore-manifest.csv";{ echo path,result,detail;tail -n+2 "$R/output/backup-manifest.csv"|while IFS=, read -r p z m u g h;do [[ $(grep "^$p," "$R/output/restore-manifest.csv") == "$p,$z,$m,$u,$g,$h" ]]&&echo "$p,MATCH,hash-size-mode-uid-gid"||echo "$p,MISMATCH,difference";done;} >"$R/output/restore-comparison.csv"
 echo 'The lab ne démontre pas un dépôt réellement immuable, off-site storage, application compatibility or production restoration.' >"$R/output/proof-boundaries.md";echo "RESTORE_WITH_ACTIONS. Les permissions en lecture seule du lab ne constituent pas une preuve d'immutabilité." >"$R/output/restore-report.md";echo RESTORE_WITH_ACTIONS >"$R/output/final-decision.txt";echo generated
}
validate(){ mark;for f in initial-inventory exposure-register role-baseline reduction-plan patch-change-plan prechecks stop-criteria rollback-plan backup-role-review repository-assurance backup-manifest restore-manifest restore-comparison;do [[ -f $R/output/$f.csv ]]||x "$f";done;(cd "$R";sha256sum -c evidence/source.sha256>/dev/null)||x source;[[ $(rows "$R/source/inventory/assets.csv") -eq 6 && $(rows "$R/source/inventory/services.csv") -eq 12 && $(rows "$R/source/inventory/software.csv") -eq 8 && $(rows "$R/source/change/risk-requirements.csv") -eq 6 && $(rows "$R/source/change/patch-candidates.csv") -eq 5 && $(rows "$R/source/backup/backup-roles.csv") -eq 4 ]]||x counts
 for q in 'https enabled' 'erp-api enabled' 'dns enabled' 'kerberos enabled' 'wireguard enabled' 'backup-ingest enabled' 'ssh restricted' 'web-admin restricted' 'telnet removed' 'ftp removed' 'sample-app removed' 'avahi disabled';do set -- $q;awk -F, -v n=$1 -v s=$2 'NR>1&&$2==n&&$3==s{f=1}END{exit !f}' "$R/work/target.csv"||x "$1 $2";done;[[ $(rows "$R/output/prechecks.csv") -eq 5 && $(rows "$R/output/stop-criteria.csv") -eq 5 && $(rows "$R/output/rollback-plan.csv") -eq 5 ]]||x change;grep -q '^isolated,isolated-backup,90,yes,yes,readonly-permissions,WITH_ACTIONS' "$R/output/repository-assurance.csv"||x assurance;! find "$R/work/repos/isolated" -type f -perm /222|grep -q .||x writable;cmp -s "$R/output/backup-manifest.csv" "$R/output/restore-manifest.csv"||x manifests;! grep -q MISMATCH "$R/output/restore-comparison.csv"||x restore;[[ $(rows "$R/output/backup-manifest.csv") -eq 3 && $(rows "$R/restore/snap/data/orders.csv") -eq 3 && $(rows "$R/restore/snap/identity/users.csv") -eq 3 ]]||x function;grep -q erp-api "$R/restore/snap/app/config/app.conf"||x config;cp "$R/work/target.csv" "$R/work/back.csv";cp "$R/work/rollback-before.csv" "$R/work/back.csv";cmp -s "$R/work/back.csv" "$R/work/before.csv"||x rollback
 n=$R/work/negative;rm -rf "$n";mkdir -p "$n";cp "$R/source/inventory/services.csv" "$n/s";chmod u+w "$n/s";echo 'X,AX,x,tcp,1,x,no,enabled,REMOVE,x,x,x'>>"$n/s";no refs refs "$R/source/inventory/assets.csv" "$n/s";cp "$R/source/change/patch-candidates.csv" "$n/p";chmod u+w "$n/p";awk -F, 'BEGIN{OFS=","}NR==2{$11=""}{print}' "$n/p">"$n/p2";no rollback rb "$n/p2";cp "$R/source/backup/backup-roles.csv" "$n/r";chmod u+w "$n/r";echo 'X,bad,yes,no,no,no,no,yes,no,no,x'>>"$n/r";no roles sep "$n/r";cp "$R/output/restore-manifest.csv" "$n/m";chmod u+w "$n/m";sed '2s/[^,]*$/0000/' "$n/m">"$n/m2";no manifest cmp -s "$R/output/backup-manifest.csv" "$n/m2";[[ $(cat "$R/output/final-decision.txt") == RESTORE_WITH_ACTIONS ]]||x decision;grep -q 'ne démontre pas un dépôt réellement immuable' "$R/output/proof-boundaries.md"||x boundary;grep -q 'permissions en lecture seule' "$R/output/restore-report.md"||x limit;echo validated
}
reset(){ mark;chmod -R u+w "$R" 2>/dev/null||:;rm -rf "$R";echo removed;};okroot;case $A in setup)setup;;status)status;;run)run;;validate)validate;;reset)reset;;*)x action;;esac
