#Requires -Version 5.1 <# .SYNOPSIS Vector 1 - Retira los canarios implantados y limpia la evidencia local. .DESCRIPTION Lee el manifiesto de implantacion y elimina SOLO los ficheros que cumplen las tres condiciones: 1. figuran como plantados y verificados en el manifiesto, 2. siguen existiendo, 3. su sha256 coincide con el registrado. Si el hash no coincide, NO se borra nada: se marca como 'manual_review'. Un canario modificado puede ser evidencia o sintoma de algo, y borrarlo a ciegas destruiria informacion. Es imprescindible ejecutarlo al cerrar el ejercicio: dejar ficheros marcados en los endpoints contaminaria el baseline de DLP/FIM de los siguientes ejercicios. Metodos soportados: local, admin-share (UNC) y winrm (comprobacion y borrado en el propio host remoto, sin asumir shares administrativos). Modo conservador: simula por defecto, requiere -Execute para borrar. .EXAMPLE .\Remove-Canaries.ps1 -ScopePath ..\scope\scope.vector1.json Muestra que se borraria, sin tocar nada. .EXAMPLE .\Remove-Canaries.ps1 -ScopePath ..\scope\scope.vector1.json -Execute #> [CmdletBinding()] param( [string]$ScopePath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'scope\scope.vector1.json'), [string]$RunId = 'latest', [switch]$Execute ) . (Join-Path (Split-Path -Parent $PSScriptRoot) 'lib\Common.ps1') . (Join-Path (Split-Path -Parent $PSScriptRoot) 'lib\Canary.ps1') $startedUtc = (Get-Date).ToUniversalTime() $cfg = Get-RTCanaryConfig -Path $ScopePath $retireId = New-RTRunId -Prefix ($cfg.exercise_id + '-RETIRE') $logRoot = New-RTDirectory -Path (Expand-RTPath -Path $cfg.log_root) $manifestRoot = Expand-RTPath -Path $cfg.manifest_root $logPath = Join-Path $logRoot ("retire-$retireId.log") $manifestPath = Join-Path $manifestRoot ("retire-$retireId.jsonl") $acting = [bool]$Execute Write-RTLog -LogPath $logPath -Message "================================================================" Write-RTLog -LogPath $logPath -Message "Vector 1 - Retirada de canarios. run_id=$retireId" Write-RTLog -LogPath $logPath -Message "Operador: $env:COMPUTERNAME\$env:USERNAME Modo: $(if ($acting) { 'BORRADO REAL' } else { 'SIMULACION' })" if (-not $acting) { Write-RTLog -LogPath $logPath -Level DRYRUN -Message "MODO SIMULACION: no se borra nada. Anade -Execute para retirar los canarios." } # -------------------------------------------------------------------------- # Manifiestos de implantacion # -------------------------------------------------------------------------- $plantManifests = @(Get-ChildItem -LiteralPath $manifestRoot -Filter 'plant-*.jsonl' -File -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending) if ($RunId -ne 'latest') { $plantManifests = @($plantManifests | Where-Object { $_.Name -like "*$RunId*" }) } if ($plantManifests.Count -eq 0) { throw "No hay manifiestos de implantacion en $manifestRoot (run: $RunId)." } $entries = @() foreach ($m in $plantManifests) { $entries += @(Get-RTManifestEntries -ManifestPath $m.FullName | Where-Object { $_.type -eq 'plant' -and $_.verified }) } # Clave de deduplicacion: identificador Y ruta de plantado. # # El canary_id lleva el EQUIPO pero NO el usuario. En un equipo compartido, dos usuarios # generan los MISMOS identificadores (CN-...-01 .. -08) en rutas distintas. Deduplicar # solo por identificador hacia que una entrada pisara a la otra: la del segundo usuario # desaparecia de la lista y sus ficheros se quedaban para siempre en su perfil. # Medido en laboratorio: retirados=0, fallos=8. # # Se conserva la PRIMERA aparicion de cada clave: los manifiestos vienen ordenados de mas # nuevo a mas antiguo, asi que la primera es la mas reciente y su hash es el vigente. $byId = @{} foreach ($e in $entries) { $clave = ([string]$e.canary_id) + '|' + ([string]$e.planted_path).ToLowerInvariant() if (-not $byId.ContainsKey($clave)) { $byId[$clave] = $e } } Write-RTLog -LogPath $logPath -Message "Canarios a retirar: $($byId.Count) (de $($entries.Count) entradas de plantado)" $counter = [ordered]@{ total = $byId.Count; removed = 0; missing = 0; manual_review = 0; failed = 0 } foreach ($id in @($byId.Keys)) { $e = $byId[$id] $method = [string]$e.method $path = [string]$e.planted_path $expected = [string]$e.sha256 $status = 'unknown' if (-not $acting) { Write-RTLog -LogPath $logPath -Level DRYRUN -Message ("[SIMULACION] borrar {0} ({1}, {2})" -f $path, $method, [string]$e.canary_id) continue } try { switch ($method) { 'winrm' { $status = Invoke-Command -ComputerName ([string]$e.host) -ErrorAction Stop -ArgumentList $path, $expected -ScriptBlock { param($p, $exp) if (-not (Test-Path -LiteralPath $p)) { return 'missing' } $h = (Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash if ($h -ne $exp) { return 'mismatch' } Remove-Item -LiteralPath $p -Force return 'removed' } } default { if (-not (Test-Path -LiteralPath $path)) { $status = 'missing' } elseif ((Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash -ne $expected) { $status = 'mismatch' } else { Remove-Item -LiteralPath $path -Force -ErrorAction Stop $status = 'removed' } } } } catch { $status = 'error' Write-RTLog -LogPath $logPath -Level ERROR -Message "Fallo retirando $path : $($_.Exception.Message)" } switch ($status) { 'removed' { $counter.removed++ } 'missing' { $counter.missing++ } 'mismatch' { $counter.manual_review++ } default { $counter.failed++ } } $level = switch ($status) { 'removed' { 'ACTION' } 'missing' { 'WARN' } 'mismatch' { 'ERROR' } default { 'ERROR' } } Write-RTLog -LogPath $logPath -Level $level -Message ("{0,-10} {1} [{2}]" -f $status, $path, $method) Add-RTManifestEntry -ManifestPath $manifestPath -Entry ([ordered]@{ type = 'retire' retire_id = $retireId canary_id = [string]$e.canary_id host = [string]$e.host method = $method path = $path sha256 = $expected status = $status ts_utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }) } $finishedUtc = (Get-Date).ToUniversalTime() $summary = [ordered]@{ retire_id = $retireId exercise_id = $cfg.exercise_id operator = "$env:COMPUTERNAME\$env:USERNAME" acting = $acting started_utc = $startedUtc.ToString('yyyy-MM-ddTHH:mm:ssZ') finished_utc = $finishedUtc.ToString('yyyy-MM-ddTHH:mm:ssZ') counters = $counter manifest = if ($acting) { $manifestPath } else { $null } } $summary | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath (Join-Path $logRoot "retire-summary-$retireId.json") -Encoding UTF8 Write-RTLog -LogPath $logPath -Message "RESUMEN: retirados=$($counter.removed) ausentes=$($counter.missing) revision_manual=$($counter.manual_review) fallos=$($counter.failed)" Write-RTLog -LogPath $logPath -Message "================================================================" Write-Host '' Write-Host ('Modo: {0}' -f $(if ($acting) { 'BORRADO REAL' } else { 'SIMULACION (nada borrado)' })) -ForegroundColor $(if ($acting) { 'Cyan' } else { 'Magenta' }) Write-Host ("Retirados: {0} Ausentes: {1} Revision manual: {2} Fallos: {3}" -f ` $counter.removed, $counter.missing, $counter.manual_review, $counter.failed) if ($counter.manual_review -gt 0) { Write-Host '' Write-Host 'REQUIEREN REVISION MANUAL (hash distinto al registrado; NO se han borrado):' -ForegroundColor Red foreach ($id in @($byId.Keys)) { $e = $byId[$id] $stillThere = $false try { $stillThere = Test-Path -LiteralPath ([string]$e.planted_path) } catch { } if ($stillThere -and [string]$e.method -ne 'winrm') { if ((Get-FileHash -LiteralPath ([string]$e.planted_path) -Algorithm SHA256).Hash -ne [string]$e.sha256) { Write-Host (" - {0} ({1})" -f $e.planted_path, $id) -ForegroundColor Red } } } } if (-not $acting) { Write-Host 'Anade -Execute para retirar los canarios.' -ForegroundColor Yellow }