# kb_web_guard_check_v1.0.ps1 # Purpose : Verify that the web-published registry matches the NAS (set & SHA). # Target : Windows PowerShell 5.1 # Encoding: Script = UTF-8 with BOM # Behavior: No writes; exits 0 if OK, 1 if mismatches. # Usage : # powershell -NoProfile -ExecutionPolicy Bypass -File .\kb_web_guard_check_v1.0.ps1 ` # -NasRoot "\\DS-918\chatgpt\ChatGPT-Gouvernance-Projets\_registry" ` # -WebBase "https://repo_gpt.telki.fr/kb" [-Deep] [-TimeoutSec 20] # # Notes: # - Compares LOCAL computed manifest vs WEB manifest_sha256.json # - With -Deep, it also downloads each web file and recomputes its SHA to ensure integrity [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$NasRoot, [Parameter(Mandatory=$true)] [string]$WebBase, [string[]]$Extensions = @(".txt",".json"), [switch]$Deep, [int]$TimeoutSec = 20 ) function Write-I($m){ Write-Host $m -ForegroundColor Cyan } function Write-OK($m){ Write-Host "[OK] $m" -ForegroundColor Green } function Write-NOK($m){ Write-Host "[NOK] $m" -ForegroundColor Red } # Normalize NAS root path to pure filesystem path $RootNorm = $null try { $rp = Resolve-Path -LiteralPath $NasRoot } catch { $rp = $null } if($rp -ne $null -and $rp.Provider -and $rp.Provider.Name -eq 'FileSystem' -and $rp.ProviderPath){ $RootNorm = $rp.ProviderPath } elseif($rp -ne $null -and $rp.Path){ $RootNorm = ($rp.Path -replace '^[^:]+::','') } else { $RootNorm = ($NasRoot -replace '^[^:]+::','') } $RootTrim = $RootNorm.TrimEnd('\','/') # Prepare web base URI try { $baseUri = New-Object System.Uri(($WebBase.TrimEnd('/') + '/')) } catch { Write-NOK "Invalid WebBase URL: $WebBase" exit 2 } # --- LOCAL: build manifest on the fly (memory only) --- $all = Get-ChildItem -LiteralPath $RootTrim -Recurse -File -ErrorAction SilentlyContinue $localRows = @() foreach($f in $all){ $ext = [System.IO.Path]::GetExtension($f.FullName).ToLowerInvariant() if($Extensions -contains $ext){ if($f.Name -ieq 'manifest_sha256.json'){ continue } if($f.Name -match '\.sha256$'){ continue } $sha = (Get-FileHash -LiteralPath $f.FullName -Algorithm SHA256).Hash.ToUpperInvariant() # compute relative path $rel = $f.FullName if($rel.Length -ge $RootTrim.Length -and $rel.Substring(0,$RootTrim.Length).ToLower() -eq $RootTrim.ToLower()){ $rel = $rel.Substring($RootTrim.Length).TrimStart('\','/') } else { # fallback $rel = Split-Path -Leaf -Path $f.FullName } $rel = ($rel -replace '\\','/') $localRows += [pscustomobject]@{ path=$rel; sha256=$sha; size=$f.Length } } } # --- WEB: fetch and parse manifest --- $webManifestUrl = (New-Object System.Uri($baseUri, "manifest_sha256.json")).ToString() try { $resp = Invoke-WebRequest -Uri $webManifestUrl -UseBasicParsing -TimeoutSec $TimeoutSec $webJson = $resp.Content | ConvertFrom-Json } catch { Write-NOK "Failed to fetch/parse web manifest: $webManifestUrl :: $($_.Exception.Message)" exit 2 } $webRows = @() if($webJson -and $webJson.files){ foreach($r in $webJson.files){ $webRows += [pscustomobject]@{ path=($r.path); sha256=($r.sha256.ToUpper()); size=($r.size) } } } # --- Compare sets --- # Build dictionaries $dictLocal = @{} foreach($r in $localRows){ $dictLocal[$r.path] = $r } $dictWeb = @{} foreach($r in $webRows){ $dictWeb[$r.path] = $r } $missingOnWeb = New-Object System.Collections.ArrayList $extraOnWeb = New-Object System.Collections.ArrayList $shaMismatch = New-Object System.Collections.ArrayList $matchCount = 0 # Local -> Web foreach($k in $dictLocal.Keys){ if(-not $dictWeb.ContainsKey($k)){ [void]$missingOnWeb.Add($k) } else { if($dictLocal[$k].sha256 -ne $dictWeb[$k].sha256){ [void]$shaMismatch.Add($k) } else { $matchCount++ } } } # Web -> Local foreach($k in $dictWeb.Keys){ if(-not $dictLocal.ContainsKey($k)){ [void]$extraOnWeb.Add($k) } } Write-I "== SUMMARY (manifest vs local) ==" Write-Host ("Local count : " + $dictLocal.Count) Write-Host ("Web count : " + $dictWeb.Count) Write-Host ("Matches : " + $matchCount) Write-Host ("MissingWeb : " + $missingOnWeb.Count) Write-Host ("ExtraWeb : " + $extraOnWeb.Count) Write-Host ("ShaDiff : " + $shaMismatch.Count) if($missingOnWeb.Count -gt 0){ Write-Host "" Write-NOK "Missing on WEB:" $missingOnWeb | ForEach-Object { " - $_" } } if($extraOnWeb.Count -gt 0){ Write-Host "" Write-NOK "Present on WEB but not local:" $extraOnWeb | ForEach-Object { " - $_" } } if($shaMismatch.Count -gt 0){ Write-Host "" Write-NOK "SHA mismatch:" $shaMismatch | ForEach-Object { " - $_" } } # --- Deep verification (optional) --- $deepMismatch = New-Object System.Collections.ArrayList if($Deep){ Write-I "" Write-I "== DEEP VERIFY (download each web file and recompute SHA) ==" foreach($k in $dictWeb.Keys){ $webPath = $k try { $u = (New-Object System.Uri($baseUri, $webPath)).ToString() $tmp = Join-Path $env:TEMP ("kb_guard_" + [System.Guid]::NewGuid().ToString("N")) Invoke-WebRequest -Uri $u -UseBasicParsing -TimeoutSec $TimeoutSec -OutFile $tmp $sha = (Get-FileHash -LiteralPath $tmp -Algorithm SHA256).Hash.ToUpperInvariant() Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue } catch { [void]$deepMismatch.Add($webPath + " (download-fail)") continue } # Compare to web manifest SHA (and to local if present) $ok = $true if($sha -ne $dictWeb[$k].sha256){ $ok=$false } if($dictLocal.ContainsKey($k) -and $sha -ne $dictLocal[$k].sha256){ $ok=$false } if(-not $ok){ [void]$deepMismatch.Add($webPath + " (sha mismatch)") } } Write-Host ("Deep mismatches : " + $deepMismatch.Count) if($deepMismatch.Count -gt 0){ $deepMismatch | ForEach-Object { " - $_" } } } # Exit code if($missingOnWeb.Count -eq 0 -and $extraOnWeb.Count -eq 0 -and $shaMismatch.Count -eq 0 -and $deepMismatch.Count -eq 0){ Write-OK "WEB GUARD: OK" exit 0 } else { Write-NOK "WEB GUARD: NOK" exit 1 }