# gov_profile_launcher_v1.4.ps1 - PS 5.1-safe param() function Ensure-Dir([string]$p){ if($p -and !(Test-Path $p)){ New-Item -ItemType Directory -Force -Path $p | Out-Null } } function Ask-YesNo([string]$q){ while($true){ $a=Read-Host ($q+' (O/N)'); if($a -in @('O','o','Y','y')){return $true}; if($a -in @('N','n')){return $false} } } function FileSha([string]$p){ if(Test-Path $p){ (Get-FileHash -Algorithm SHA256 -LiteralPath $p).Hash } else { '' } } # === Canonique: recalcul SHA du bloc [SYNC_MANIFEST] (CRLF, TrimEnd, pas de newline final) function Recalc-ManifestShaObject([string]$raw){ $m = [regex]::Match($raw,'(?ms)^\[SYNC_MANIFEST\]\s*(.*?)(?=^\[|\Z)') if(-not $m.Success){ return @{'ok'=$false;'msg'='Section [SYNC_MANIFEST] absente.'} } $blk = $m.Groups[1].Value $infile = [regex]::Match($blk,'(?mi)^\s*MANIFEST_SHA256\s*:\s*([0-9a-f]+)\s*$').Groups[1].Value.ToLower() $coreLines=@() foreach($ln in ($blk -split "`r?`n")){ if($ln -match '^\s*MANIFEST_SHA256\s*:'){ continue } $coreLines += $ln.TrimEnd() } $core = [string]::Join("`r`n",$coreLines) $sha = [BitConverter]::ToString([Security.Cryptography.SHA256]::Create().ComputeHash([Text.Encoding]::UTF8.GetBytes($core))).Replace('-','').ToLower() return @{'ok'=$true;'infile'=$infile;'recalc'=$sha} } # === Découverte dynamique des profils function Discover-Profiles(){ $root="\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry" $dir = Join-Path $root 'scripts' if(!(Test-Path $dir)){ return @() } $files = Get-ChildItem -LiteralPath $dir -Filter 'rebuild_bootpack_full_*.ps1' -File $list = @() foreach($f in $files){ $n = $f.BaseName # ex: rebuild_bootpack_full_hub_v1.1 $name = $n -replace '^rebuild_bootpack_full_','' $name = ($name -split '_v')[0] if([string]::IsNullOrWhiteSpace($name)){ continue } $label = $name.ToUpper() $obj = New-Object psobject -Property @{ Label=$label; Script=$f.FullName } $list += $obj } # Tri alphabétique pour menu stable $list | Sort-Object Label } function Choose-Profile(){ $profiles = Discover-Profiles if($profiles.Count -eq 0){ Write-Host "[ERR] Aucun profil trouvé (scripts rebuild_bootpack_full_*.ps1)."; return $null } Write-Host "=== Profils détectés ===" for($i=0; $i -lt $profiles.Count; $i++){ ($i+1).ToString().PadLeft(2,' ') + ") " + $profiles[$i].Label | Write-Host } while($true){ $c = Read-Host "Numéro de profil" if([int]::TryParse($c, [ref]([int]$null))){ $idx=[int]$c-1; if($idx -ge 0 -and $idx -lt $profiles.Count){ return $profiles[$idx] } } Write-Host "Sélection invalide." } } function Run-Selected([string]$mode){ $sel = Choose-Profile if($null -eq $sel){ return } $root="\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry" $logs=Join-Path $root 'logs'; Ensure-Dir $logs $ts=Get-Date -Format 'yyyyMMdd_HHmmss' $log=Join-Path $logs ('run_'+$sel.Label+'_'+$mode+'_'+$ts+'.txt') if(!(Test-Path $sel.Script)){ Write-Host "[ERR] Script introuvable: "+$sel.Script; return } if($mode -eq 'Write'){ if(-not (Ask-YesNo ("Confirmer l'écriture réelle pour "+$sel.Label))){ Write-Host "Annulé."; return } } $arg=@('-NoProfile','-ExecutionPolicy','Bypass','-File',$sel.Script); if($mode -eq 'Preview'){$arg+='-Preview'}else{$arg+='-Write'} & 'powershell.exe' @arg 2>&1 | Tee-Object -FilePath $log | Out-Null if(Test-Path $log){ Get-Content -LiteralPath $log -Tail 60; 'Log -> '+$log | Write-Host } else { Write-Host "[WARN] Log non trouvé: $log" } } function Audit-Manifest(){ $bp="\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry\bootpack\bootpack.txt" if(!(Test-Path $bp)){ Write-Host "[ERR] BootPack introuvable: $bp"; return } $raw = Get-Content -LiteralPath $bp -Raw -Encoding UTF8 $res = Recalc-ManifestShaObject $raw if(-not $res.ok){ Write-Host "[ERR] "+$res.msg; return } 'InFile = '+$res.infile | Write-Host 'Recalc = '+$res.recalc | Write-Host 'STATUS = '+($(if($res.infile -eq $res.recalc){'MATCH'}else{'DIFF'})) | Write-Host } function Fix-Manifest(){ $root="\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry" $fix =Join-Path $root 'scripts\fix_manifest_sha_v1.1.ps1' if(!(Test-Path $fix)){ Write-Host "[ERR] fix_manifest_sha_v1.1.ps1 introuvable."; return } $logs=Join-Path $root 'logs'; Ensure-Dir $logs $ts=Get-Date -Format 'yyyyMMdd_HHmmss'; $log=Join-Path $logs ('run_fix_manifest_'+$ts+'.txt') & 'powershell.exe' -NoProfile -ExecutionPolicy Bypass -File $fix -Write 2>&1 | Tee-Object -FilePath $log | Out-Null Get-Content -LiteralPath $log -Tail 30; 'Log -> '+$log | Write-Host } function Sync-Report([switch]$Export){ $logp="\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry\logs\registry_activity.log" if(!(Test-Path $logp)){ Write-Host "[INFO] Aucun log d'activité encore."; return } $lines = Get-Content -LiteralPath $logp -Encoding UTF8 $count = $lines.Count $start = if($count -gt 20){ $count-20 } else { 0 } $slice = $lines[$start..($count-1)] if($Export){ $rep=Join-Path (Split-Path $logp -Parent) '..\reports'; $rep=[IO.Path]::GetFullPath($rep); Ensure-Dir $rep $ts=Get-Date -Format 'yyyyMMdd_HHmmss' $out=Join-Path $rep ('sync_report_'+$ts+'.txt') $header="=== SYNC REPORT export - "+(Get-Date -Format s)+" ===" $header | Out-File -LiteralPath $out -Encoding utf8 -Append $slice | Out-File -LiteralPath $out -Encoding utf8 -Append 'Export -> '+$out | Write-Host } else { Write-Host "=== SYNC REPORT (20 derniers) ===" foreach($l in $slice){ $l } } } function Effective-Policy(){ $bp="\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry\bootpack\bootpack.txt" if(!(Test-Path $bp)){ Write-Host "[ERR] BootPack introuvable."; return } $raw = Get-Content -LiteralPath $bp -Raw -Encoding UTF8 $proj = [regex]::Match($raw,'(?mi)^\s*PROJECT:\s*(.+)$').Groups[1].Value.Trim() $rootp= [regex]::Match($raw,'(?mi)^\s*ROOT:\s*(.+)$').Groups[1].Value.Trim() $policy= [regex]::Match($raw,'(?ms)^\[POLICY\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value.Trim() $profRef=[regex]::Match($raw,'(?ms)^\[PROJECT_PROFILE_REF\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value.Trim() Write-Host "=== EFFECTIVE POLICY ===" "PROJECT : $proj" | Write-Host "ROOT : $rootp" | Write-Host "POLICY (L0) :" | Write-Host foreach($ln in ($policy -split "`r?`n")){ " - "+$ln.Trim() | Write-Host } "PROFILE REF :" | Write-Host foreach($ln in ($profRef -split "`r?`n")){ " "+$ln | Write-Host } } function Audit-8Checks(){ $bp="\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry\bootpack\bootpack.txt" if(!(Test-Path $bp)){ Write-Host "[ERR] BootPack introuvable."; return } $raw = Get-Content -LiteralPath $bp -Raw -Encoding UTF8 $ok=@{}; $msg=@{} $ok.C1=$true; $msg.C1="BootPack présent" $pol=[regex]::Match($raw,'(?ms)^\[POLICY\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $need=@('GOV_SCRIPT_GATE v1.4','SAFE-WRITE v1.1','TXT-ONLY v1.0','SYNC-MEM-ARCHIVE-RULE v1.0','SYNC-GUARD v1.1') $have=$pol -split "`r?`n" | % { $_.Trim() } | ? {$_ -ne ''} $miss=@(); foreach($r in $need){ if(-not ($have -contains $r)){ $miss+=$r } } $ok.C2=($miss.Count -eq 0); $msg.C2=$(if($ok.C2){"OK"}else{"Manque: "+($miss -join ' ; ')}) $kbblk=[regex]::Match($raw,'(?ms)^\[BUG_KB_JSON_POINTER\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $kbPath=[regex]::Match($kbblk,'(?mi)^\s*Path:\s*(.+)$').Groups[1].Value.Trim() $kbShaIn=[regex]::Match($kbblk,'(?mi)^\s*SHA256:\s*([0-9A-Fa-f]+)$').Groups[1].Value.Trim().ToLower() $kbShaReal=(FileSha $kbPath).ToLower() $ok.C3=($kbShaReal -ne '' -and $kbShaIn -eq $kbShaReal); $msg.C3=$(if($ok.C3){"OK"}else{"SHA pointer="+$kbShaIn+" vs real="+$kbShaReal}) $kbEntriesIn=[regex]::Match($kbblk,'(?mi)^\s*Entries:\s*([0-9]+)$').Groups[1].Value # compteur robuste $kbCount=-1 if(Test-Path $kbPath){ $rawKB = Get-Content -LiteralPath $kbPath -Raw -Encoding UTF8 $i=$rawKB.LastIndexOf(']}'); if($i -gt 0){ $rawKB=$rawKB.Substring(0,$i+2) } try{ $kbCount=(($rawKB|ConvertFrom-Json).entries).Count } catch { $kbCount=([regex]::Matches($rawKB,'"id"\s*:')).Count } } $ok.C4=($kbCount -ge 0 -and [string]$kbCount -eq [string]$kbEntriesIn); $msg.C4=$(if($ok.C4){"OK"}else{"Entries pointer="+$kbEntriesIn+" vs real="+$kbCount}) $man=[regex]::Match($raw,'(?ms)^\[SYNC_MANIFEST\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $snapPath=[regex]::Match($man,'(?mi)^\s*SNAPSHOT_FILE\s*:\s*(.+)$').Groups[1].Value.Trim() $snapShaIn=[regex]::Match($man,'(?mi)^\s*SNAPSHOT_SHA256\s*:\s*([0-9A-Fa-f]+|MISSING)$').Groups[1].Value.Trim().ToLower() $snapRealExist=Test-Path $snapPath $snapShaReal=$(if($snapRealExist){ (FileSha $snapPath).ToLower() } else { 'missing' }) $ok.C5=($(if($snapRealExist){ $snapShaReal -eq $snapShaIn.ToLower() } else { $snapShaIn -eq 'missing' })) $msg.C5=$(if($ok.C5){"OK"}else{"SNAPSHOT "+$(if($snapRealExist){"hash="+$snapShaReal}else{"absent"})+" vs manifest="+$snapShaIn}) $prf=[regex]::Match($raw,'(?ms)^\[PROJECT_PROFILE_REF\]\s*(.*?)(?=^\[|\Z)').Groups[1].Value $pf=[regex]::Match($prf,'(?mi)^\s*PROFILE_FILE\s*:\s*(.+)$').Groups[1].Value.Trim() $pfShaIn=[regex]::Match($prf,'(?mi)^\s*PROFILE_SHA256\s*:\s*([0-9A-Fa-f:]+)$').Groups[1].Value.Trim().ToLower() $pfExist=Test-Path $pf $pfShaReal=$(if($pfExist){ (FileSha $pf).ToLower() } else { 'missing' }) $ok.C6=($pfExist -and $pfShaIn -eq $pfShaReal); if(-not $pfExist -and ($pfShaIn -like 'planned:*')){$ok.C6=$true} $msg.C6=$(if($ok.C6){"OK"}else{"PROFILE "+$(if($pfExist){"hash="+$pfShaReal}else{"absent"})+" vs ref="+$pfShaIn}) $res=Recalc-ManifestShaObject $raw $ok.C7=($res.ok -and $res.infile -eq $res.recalc) $msg.C7=$(if($ok.C7){"OK"}else{"hash file="+$res.infile+" vs recalc="+$res.recalc}) $lines = New-Object System.Collections.ArrayList foreach($ln in ($man -split "`r?`n")){ if($ln -match '^\s{2,}\\\\'){ [void]$lines.Add($ln.Trim()) } } $mismatch=@(); $missing=@() foreach($l in $lines){ $sp=$l.Split('=') if($sp.Count -lt 2){ continue } $path=$sp[0].Trim(); $shaIn=$sp[1].Trim().ToLower() $exists=Test-Path $path if(-not $exists){ if($shaIn -ne 'missing'){ $missing+=$path }; continue } $shaReal=(FileSha $path).ToLower() if($shaIn -ne $shaReal){ $mismatch+=("$path -> $shaIn vs $shaReal") } } $ok.C8=($mismatch.Count -eq 0 -and $missing.Count -eq 0) if($ok.C8){ $msg.C8="OK" } else { $m=""; if($mismatch.Count -gt 0){ $m += "Mismatch: "+($mismatch -join ' | ') } if($missing.Count -gt 0){ if($m -ne ''){ $m+=' ; ' }; $m += "Missing: "+($missing -join ' | ') } $msg.C8=$m } Write-Host "=== AUDIT FIL - 8 CHECKS ===" foreach($k in 'C1','C2','C3','C4','C5','C6','C7','C8'){ ($k+' = '+($(if($ok.$k){'PASS'}else{'FAIL'}))+' - '+$msg.$k) | Write-Host } } function Show-Menu(){ Clear-Host Write-Host "=== GOV PROFILE LAUNCHER v1.4 ===" Write-Host "1) Lancer profil - Preview" Write-Host "2) Lancer profil - Write (commit)" Write-Host "3) Audit MANIFEST (recalc vs fichier)" Write-Host "4) Corriger MANIFEST_SHA (SAFE-WRITE)" Write-Host "5) SYNC REPORT (20 derniers)" Write-Host "6) EXPORT SYNC REPORT (TXT)" Write-Host "7) AFFICHER EFFECTIVE POLICY (L0+profil)" Write-Host "8) AUDIT FIL - 8 CHECKS" Write-Host "0) Quitter" Write-Host "" } while($true){ Show-Menu $c=Read-Host "Choix" switch($c){ '1' { Run-Selected -mode 'Preview' ; Pause } '2' { Run-Selected -mode 'Write' ; Pause } '3' { Audit-Manifest ; Pause } '4' { Fix-Manifest ; Pause } '5' { Sync-Report ; Pause } '6' { Sync-Report -Export ; Pause } '7' { Effective-Policy ; Pause } '8' { Audit-8Checks ; Pause } '0' { return } default { Write-Host "Choix invalide." ; Start-Sleep -Seconds 1 } } }