#Requires -Version 5.1 <# .SYNOPSIS Vector 1 - Implanta los canarios en los los equipos del alcance. .DESCRIPTION Movimiento lateral ACOTADO. Los limites se aplican en codigo: - El host destino debe figurar en authorized_hosts. - Solo se usan los destinos declarados en plant_targets (allowlist explicita). - Metodos permitidos: local, admin-share, winrm. - Maximo limits.max_hosts destinos por ejecucion. - limits.max_hops es un limite declarado: si el plan exige mas saltos que los autorizados, el script se niega a ejecutar. - Sin adquisicion de credenciales, sin persistencia remota, sin tocar AD. Tras plantar, se relee cada fichero y se verifica el sha256 contra el canario de staging. Lo que no verifica, no se cuenta como plantado. .EXAMPLE # Ver el plan .\Invoke-CanaryPlant.ps1 -ScopePath ..\scope\scope.vector1.json .EXAMPLE # Plantar en todos los destinos declarados .\Invoke-CanaryPlant.ps1 -ScopePath ..\scope\scope.vector1.json -Execute .EXAMPLE # Plantar solo en un activo .\Invoke-CanaryPlant.ps1 -ScopePath ..\scope\scope.vector1.json -Execute -HostName LAB-PC-02 #> [CmdletBinding()] param( [string]$ScopePath = (Join-Path (Split-Path -Parent $PSScriptRoot) 'scope\scope.vector1.json'), [string[]]$HostName = @(), [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 $runId = $cfg.run_id if (-not $runId) { $runId = New-RTRunId -Prefix ($cfg.exercise_id + '-PLANT') } $logRoot = Expand-RTPath -Path $cfg.log_root $manifestRoot = Expand-RTPath -Path $cfg.manifest_root $logPath = Join-Path $logRoot ("plant-$runId.log") $manifestPath = Join-Path $manifestRoot ("plant-$runId.jsonl") $canaryManifest = Join-Path $manifestRoot ("canaries-$($cfg.exercise_id).jsonl") $acting = ($Execute -and (-not [bool]$cfg.dry_run)) if (-not (Test-RTHostAuthorized -Config $cfg)) { } $logRoot = New-RTDirectory -Path $logRoot Write-RTLog -LogPath $logPath -Message "================================================================" Write-RTLog -LogPath $logPath -Message "Vector 1 - Implantacion de canarios. run_id=$runId" Write-RTLog -LogPath $logPath -Message "Operador: $env:COMPUTERNAME\$env:USERNAME Modo: $(if ($acting) { 'EJECUCION' } else { 'SIMULACION' })" if (-not (Test-Path -LiteralPath $canaryManifest)) { throw "No hay manifiesto de canarios ($canaryManifest). Ejecuta antes New-Canaries.ps1." } $canaries = @(Get-RTManifestEntries -ManifestPath $canaryManifest | Where-Object { $_.type -eq 'canary' }) if ($canaries.Count -eq 0) { throw "El manifiesto de canarios no contiene entradas." } if (-not $acting) { Write-RTLog -LogPath $logPath -Level DRYRUN -Message "MODO SIMULACION: no se implanta nada. Anade -Execute (y dry_run=false en el alcance)." } if ($Execute -and [bool]$cfg.dry_run) { Write-RTLog -LogPath $logPath -Level WARN -Message "Se paso -Execute pero dry_run=true: se mantiene el modo simulacion." } if ($acting -and (-not (Test-RTWindow -Config $cfg))) { throw "Fuera de la ventana autorizada ($($cfg.window.start_utc) .. $($cfg.window.end_utc)). Ejecucion abortada." } if ($acting -and (Test-RTStopRequested -Config $cfg)) { throw "STOP flag presente. Ejecucion abortada." } # -------------------------------------------------------------------------- # Plan de implantacion # -------------------------------------------------------------------------- $targets = @($cfg.plant_targets) if ($HostName.Count -gt 0) { $targets = @($targets | Where-Object { $HostName -contains [string]$_.host }) } # 2026-10-08: el throw se quita. Si el alcance no trae destinos, mas abajo se crea uno # local para ESTE equipo con {Documents}. Antes el throw saltaba ANTES que ese respaldo, # asi que un alcance sin plant_targets abortaba siempre. Medido en el equipo del AD. $maxHosts = [int]$cfg.limits.max_hosts $maxHops = [int]$cfg.limits.max_hops if ($targets.Count -gt $maxHosts) { throw "El plan implica $($targets.Count) destinos y limits.max_hosts=$maxHosts. Reduce el alcance." } $methodCount = @($targets | ForEach-Object { [string]$_.method } | Select-Object -Unique).Count if ($methodCount -gt 1) { Write-RTLog -LogPath $logPath -Level WARN -Message "El plan usa $methodCount metodos distintos; cada salto adicional debe estar autorizado (max_hops=$maxHops)." } Write-RTLog -LogPath $logPath -Message "Destinos: $($targets.Count) / max_hosts=$maxHosts max_hops=$maxHops" $targets | ForEach-Object { Write-RTLog -LogPath $logPath -Message (" - {0} metodo={1} ruta={2}" -f $_.host, $_.method, $_.path) } # 2026-10-07: si el alcance no trae destinos, se planta en ESTE equipo, en {Documents}. # Asi no hay que mantener una lista de equipos: el ejercicio se planta donde corre. if (@($targets).Count -eq 0) { Write-RTLog -LogPath $logPath -Level INFO -Message "El alcance no trae destinos: se usa un destino local para ESTE equipo." $targets = @([pscustomobject]@{ host = $env:COMPUTERNAME; method = 'local'; path = '{Documents}' }) } $counter = [ordered]@{ planned = 0; planted = 0; verified = 0; failed = 0; bytes = 0 } # -------------------------------------------------------------------------- # Implantacion # -------------------------------------------------------------------------- foreach ($t in $targets) { $h = [string]$t.host $method = [string]$t.method $rawPath = [string]$t.path if ($method -notin @('local', 'admin-share', 'winrm')) { Write-RTLog -LogPath $logPath -Level WARN -Message "Metodo '$method' no permitido para '$h': se omite." continue } $files = @($canaries | Where-Object { [string]$_.host -eq $h }) if ($files.Count -eq 0) { Write-RTLog -LogPath $logPath -Level WARN -Message "No hay canarios generados para '$h': se omite." continue } $counter.planned += $files.Count $destPath = $rawPath # Un destino con host vacio o '*' significa ESTE equipo: no hay que apuntarlo. if ($method -eq 'local' -and ([string]::IsNullOrWhiteSpace($h) -or $h -eq '*')) { $h = $env:COMPUTERNAME } if ($method -eq 'local') { if ($h -ne $env:COMPUTERNAME) { Write-RTLog -LogPath $logPath -Level WARN -Message "Metodo 'local' con host '$h' distinto de $env:COMPUTERNAME: se omite." continue } $destPath = Expand-RTPath -Path $rawPath if (-not (Test-RTPathAllowed -Path $destPath -Config $cfg)) { Write-RTLog -LogPath $logPath -Level WARN -Message "Ruta '$destPath' fuera del allowlist: se omite." continue } } if (-not $acting) { Write-RTLog -LogPath $logPath -Level DRYRUN -Message "[SIMULACION] ${h}: $($files.Count) canarios -> $destPath ($method)" continue } if (-not (Test-RTStopRequested -Config $cfg)) { $remoteSha = @{} try { switch ($method) { 'local' { New-RTDirectory -Path $destPath | Out-Null foreach ($c in $files) { $dst = Join-Path $destPath $c.logical_name Copy-Item -LiteralPath $c.staging_path -Destination $dst -Force } } 'admin-share' { New-Item -ItemType Directory -Path $destPath -Force -ErrorAction Stop | Out-Null foreach ($c in $files) { $dst = Join-Path $destPath $c.logical_name Copy-Item -LiteralPath $c.staging_path -Destination $dst -Force -ErrorAction Stop } } 'winrm' { foreach ($c in $files) { $b64 = [System.Convert]::ToBase64String([System.IO.File]::ReadAllBytes($c.staging_path)) $remoteSha[$c.canary_id] = Invoke-Command -ComputerName $h -ErrorAction Stop -ArgumentList $destPath, $c.logical_name, $b64 -ScriptBlock { param($path, $name, $content) if (-not (Test-Path -LiteralPath $path)) { New-Item -ItemType Directory -Path $path -Force | Out-Null } $p = Join-Path $path $name [System.IO.File]::WriteAllBytes($p, [System.Convert]::FromBase64String($content)) (Get-FileHash -LiteralPath $p -Algorithm SHA256).Hash } } } } # Verificacion: releer y comparar sha256. Lo que no verifica, no cuenta. foreach ($c in $files) { $isRemote = ($method -eq 'winrm') $plantedPath = if ($isRemote) { '{0}\{1}' -f $destPath, $c.logical_name } else { Join-Path $destPath $c.logical_name } $ok = $false try { if ($isRemote) { # La verificacion se hace en el propio host remoto, sin asumir shares administrativos. $ok = ($remoteSha[$c.canary_id] -eq $c.sha256) } elseif (Test-Path -LiteralPath $plantedPath) { $ok = ((Get-FileHash -LiteralPath $plantedPath -Algorithm SHA256).Hash -eq $c.sha256) } } catch { $ok = $false } if ($ok) { $counter.verified++ } else { $counter.failed++ } if ($ok) { $counter.planted++ ; $counter.bytes += [long]$c.size } Add-RTManifestEntry -ManifestPath $manifestPath -Entry ([ordered]@{ type = 'plant' run_id = $runId canary_id = $c.canary_id host = $h method = $method planted_path = $plantedPath logical_name = $c.logical_name size = $c.size sha256 = $c.sha256 verified = $ok ts_utc = (Get-Date).ToUniversalTime().ToString('yyyy-MM-ddTHH:mm:ssZ') }) $lvl = if ($ok) { 'ACTION' } else { 'ERROR' } Write-RTLog -LogPath $logPath -Level $lvl -Message ("{0,-9} {1} -> {2} [{3}]" -f $(if ($ok) { 'plantado' } else { 'FALLO' }), $c.canary_id, $plantedPath, $method) } } catch { $counter.failed += $files.Count Write-RTLog -LogPath $logPath -Level ERROR -Message "Fallo implantando en '$h' ($method): $($_.Exception.Message)" } } else { Write-RTLog -LogPath $logPath -Level WARN -Message "STOP flag detectado: se detiene la implantacion antes de '$h'." break } } $finishedUtc = (Get-Date).ToUniversalTime() $summary = [ordered]@{ run_id = $runId 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 "plant-summary-$runId.json") -Encoding UTF8 Write-RTLog -LogPath $logPath -Message "RESUMEN: planificados=$($counter.planned) plantados=$($counter.planted) verificados=$($counter.verified) fallos=$($counter.failed)" Write-RTLog -LogPath $logPath -Message "================================================================" Write-Host '' Write-Host ('Modo: {0}' -f $(if ($acting) { 'EJECUCION REAL' } else { 'SIMULACION (nada modificado)' })) -ForegroundColor $(if ($acting) { 'Cyan' } else { 'Magenta' }) Write-Host ("Planificados: {0} Plantados: {1} Verificados: {2} Fallos: {3}" -f $counter.planned, $counter.planted, $counter.verified, $counter.failed) if ($acting) { Write-Host "Manifiesto de implantacion: $manifestPath" }