<# ================================================================================ VERIFICADOR DEL PRÁCTICO · TGSIT Módulo 3 Javier Baz · BIOS Instituto de Informática v1.0 — agosto 2026 QUÉ HACE Mira el equipo del alumno y dice qué tareas quedaron hechas. Se corre parado al lado del alumno: en 20 segundos tenés el tablero en pantalla y tildás. ⚠️ SOLO LEE. No crea, no borra, no modifica nada del equipo. Lo único que escribe es un archivo temporal en %TEMP% para leer la política de seguridad, y lo borra enseguida. Podés correrlo con confianza sobre la máquina de cualquiera. LOS DOS MOMENTOS (y por qué son dos) La restitución BORRA LA EVIDENCIA: si el alumno ya borró los usuarios y la carpeta, no queda nada que mirar. Por eso se corre dos veces: 1. -Modo Trabajo ANTES de que restituya → ¿hizo las tareas? 2. -Modo Restitucion DESPUÉS de que restituya → ¿dejó todo como estaba? Las dos notas cuentan. Un alumno puede hacer todo bien y no restituir: eso se ve y se califica aparte. CÓMO SE USA .\verificar-m3.ps1 (modo Trabajo) .\verificar-m3.ps1 -Modo Restitucion .\verificar-m3.ps1 -Codigo JP01 -Servicio MapsBroker .\verificar-m3.ps1 -Guardar (deja un .txt al lado) O directamente doble clic en verificar-m3.bat, que lo eleva solo. REQUIERE PowerShell como administrador. Sin elevación funciona igual, pero algunas comprobaciones (política de seguridad, puntos de restauración) no se pueden leer y aparecen como "no se pudo verificar" en vez de dar un resultado falso. ⚠️ ESTE ARCHIVO SE ARMA, NO SE EDITA Sale de Herramientas/tgsit/armar-verificador.py, que une el motor comun (Examenes/TGSIT-comun/) con las tareas de este modulo. Si editas ACA, el proximo armado se lo lleva puesto: el cambio va a la pieza que corresponda. ================================================================================ #> [CmdletBinding()] param( [ValidateSet('Trabajo','Restitucion')] [string]$Modo = 'Trabajo', [string]$Codigo = '', # Dos datos sueltos que algunos módulos necesitan y otros no. Viven acá # porque PowerShell exige que param() sea lo primero del archivo y las # tareas no pueden agregarle parámetros. Lo que NO puede vivir acá es su # valor por omisión: 'C:\Proyecto' era la carpeta del módulo 3, y el motor # es la pieza compartida. Cada módulo pone el suyo en su archivo de tareas. [string]$Servicio = '', [string]$Carpeta = '', [switch]$Guardar, # Clave del docente (hoja Config, celda B7). Si se pasa, el resultado se # carga solo en la planilla. Sin ella, el script solo muestra en pantalla. [string]$ClaveDocente = '', [string]$Url = 'https://script.google.com/macros/s/AKfycbzj1b77-6MnvBjAESQTXE676EjrXt5s5OEh_276FHISMctMCfgUIVWpvXYSECgGXLFK/exec' ) $ErrorActionPreference = 'Continue' # ── QUE LOS ACENTOS SE VEAN ─────────────────────────────────────────────────── # El archivo esta en UTF-8 y PowerShell lo lee bien; la que no puede mostrarlos # es la CONSOLA, que arranca en la codepage vieja (850/437) y escupe «I2?3» en # lugar de «I2·3». Esto lo arregla solo para esta ventana: no toca ninguna # configuracion del equipo, y al cerrarla vuelve todo como estaba. try { [Console]::OutputEncoding = [Text.Encoding]::UTF8 } catch { } # ── SOLO EN LA CONSOLA ──────────────────────────────────────────────────────── # En PowerShell ISE el script no pregunta nada: mide sin codigo, muestra la tabla # y no deja ningun registro, sin avisar. Paso en el ensayo del practico final. # Mejor frenar a la vista que andar a medias en silencio. if ($Host.Name -ne 'ConsoleHost') { Write-Host '' Write-Host ' ================================================================' -ForegroundColor Red Write-Host ' ESTE PROGRAMA NO ES LA CONSOLA' -ForegroundColor Red Write-Host '' Write-Host (' Estás en: ' + $Host.Name) -ForegroundColor Gray Write-Host ' Acá el verificador no puede preguntarte nada.' -ForegroundColor Gray Write-Host '' Write-Host ' Cerrá esta ventana y abrí la consola correcta:' -ForegroundColor Yellow Write-Host ' clic derecho en Inicio -> Terminal (Administrador)' -ForegroundColor Yellow Write-Host ' Ahí pegá el mismo comando.' -ForegroundColor Yellow Write-Host ' ================================================================' -ForegroundColor Red Write-Host '' return } # ── SIDs, no nombres ────────────────────────────────────────────────────────── # El grupo Administradores se llama "Administrators" en inglés y "Administradores" # en español. Lo que NO cambia nunca es el SID. Comparar por nombre es el error # clásico que hace fallar un script en la máquina de al lado. $SID_ADMINS = 'S-1-5-32-544' $SID_USUARIOS = 'S-1-5-32-545' $ALUMNOS_ESPERADOS = @('ana','luis','pedro') # ── estado ──────────────────────────────────────────────────────────────────── $script:filas = @() $script:puntos = 0 $script:posible = 0 # ── DONDE VIVEN LOS ARCHIVOS LOCALES ────────────────────────────────────────── # Antes salian de la carpeta del script ($PSCommandPath). Eso anda con el # pendrive, pero NO cuando el script corre en memoria (irm | iex): ahi no hay # archivo en disco, $PSCommandPath viene vacio y Join-Path se cae. # # ProgramData y no TEMP: es de la maquina y no del usuario, no se limpia sola, y # sobrevive aunque entre otro usuario entre una corrida y la siguiente. function Carpeta-Datos { $d = Join-Path $env:ProgramData 'TGSIT' if (-not (Test-Path $d)) { try { $null = New-Item -ItemType Directory -Path $d -Force -ErrorAction Stop } catch { $d = $env:TEMP } # sin permisos: mejor TEMP que perder el dato } return $d } # ── PREGUNTAR LO QUE FALTE ──────────────────────────────────────────────────── # Pegado en una consola no hay .bat que le pase las respuestas al script, asi # que las pide el mismo. Solo pregunta lo que NO vino por parametro: si lo # corres con -Codigo JP01, no te lo vuelve a preguntar. function Interactivo { [Environment]::UserInteractive -and $Host.Name -eq 'ConsoleHost' } function Preguntar { param([string]$Texto, [string]$PorDefecto = '') if (-not (Interactivo)) { return $PorDefecto } # Read-Host ya agrega ": " al final. Sin sacarle el nuestro, en pantalla # salia "Servicio (Enter para omitir)::" con dos puntos dobles. $r = Read-Host (' ' + $Texto.TrimEnd(':')) if ([string]::IsNullOrWhiteSpace($r)) { return $PorDefecto } return $r.Trim() } # La clave se pide TAPADA y por Read-Host a proposito. Tipearla como parametro # la deja escrita en ConsoleHost_history.txt del perfil de esa maquina, y en la # maquina de un alumno eso queda ahi despues de que te fuiste. Lo que se # contesta a un Read-Host no entra en ese historial. function Preguntar-Clave { if (-not (Interactivo)) { return '' } $sec = Read-Host ' Clave del docente (si sos alumno, dejala vacía y Enter)' -AsSecureString if (-not $sec -or $sec.Length -eq 0) { return '' } $b = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($sec) try { return [Runtime.InteropServices.Marshal]::PtrToStringAuto($b) } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($b) } } # ── ELEGIR AL ALUMNO DE LA LISTA, EN VEZ DE TIPEARLO ────────────────────────── # Tipear el codigo es donde se cuela el error que nadie ve: JP01 en lugar de # JP02 son dos alumnos reales, la planilla acepta los dos y la nota queda # cargada en el que no era. Eligiendo un numero de una lista eso no puede pasar. # # Necesita la clave del docente porque la lista trae nombres. Sin clave —o sin # red— cae a pedir el codigo tipeado, que es como funcionaba antes: nunca deja # al docente sin poder trabajar. function Elegir-Alumno { param([string]$Clave) if (-not (Interactivo)) { return '' } if (-not $Clave) { return (Preguntar 'Código del alumno (Enter para omitir):') } Write-Host ' Buscando la lista del módulo…' -ForegroundColor DarkGray $r = $null try { $cuerpo = (@{ accion = 'padron'; claveDocente = $Clave } | ConvertTo-Json -Compress) $r = Invoke-RestMethod -Uri $Url -Method Post -Body $cuerpo ` -ContentType 'application/json' -TimeoutSec 30 -ErrorAction Stop } catch { $r = $null } if (-not $r -or -not $r.ok -or -not $r.alumnos -or $r.alumnos.Count -eq 0) { $motivo = if ($r -and $r.estado) { $r.estado } else { 'sin respuesta' } Write-Host (" No se pudo traer la lista ($motivo). Escribí el código a mano.") -ForegroundColor Yellow return (Preguntar 'Código del alumno (Enter para omitir):') } Write-Host '' $i = 0 foreach ($a in $r.alumnos) { $i++ # Se muestra lo que YA tiene cargado: asi ves de un vistazo a quien le # falta, y no lo corres dos veces sin darte cuenta. $ya = if ($a.verificado) { ' [' + $a.verificado + ']' } else { '' } Write-Host (" {0,2}. {1,-8} {2}{3}" -f $i, $a.codigo, $a.nombre, $ya) -ForegroundColor Gray } Write-Host '' $op = Preguntar ('Número del alumno (1-' + $i + ', Enter para escribir el código):') if ($op -match '^\d+$' -and [int]$op -ge 1 -and [int]$op -le $i) { $elegido = $r.alumnos[[int]$op - 1] Write-Host (' -> ' + $elegido.nombre + ' (' + $elegido.codigo + ')') -ForegroundColor Green return $elegido.codigo } return (Preguntar 'Código del alumno (Enter para omitir):') } function Agregar { param( [string]$Tarea, [ValidateSet('OK','MAL','NA')] [string]$Estado, [string]$Detalle = '', [int]$Vale = 1 ) $script:filas += [pscustomobject]@{ Tarea = $Tarea; Estado = $Estado; Detalle = $Detalle; Vale = $Vale } if ($Estado -ne 'NA') { $script:posible += $Vale if ($Estado -eq 'OK') { $script:puntos += $Vale } } } function EsAdmin { $id = [Security.Principal.WindowsIdentity]::GetCurrent() (New-Object Security.Principal.WindowsPrincipal($id)).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator) } function NombreDeSID { param([string]$Sid) try { (New-Object System.Security.Principal.SecurityIdentifier($Sid)).Translate( [System.Security.Principal.NTAccount]).Value } catch { $Sid } } # La carpeta del ejercicio. El valor por omisión vive ACÁ y no en el param() del # motor, que es la pieza compartida: 'C:\Proyecto' es del módulo 3 y de nadie # más. Si se pasa -Carpeta por línea de comandos, gana la que se pasó. if (-not $Carpeta) { $Carpeta = 'C:\Proyecto' } # ══════════════════════════════════════════════════════════════════════════════ # COMPROBACIONES # Cada una sabe qué esperar en cada modo. En Trabajo se espera que la tarea # esté hecha; en Restitución, que ya no quede rastro. # ══════════════════════════════════════════════════════════════════════════════ function Ver-Usuarios { $existentes = @() foreach ($n in $ALUMNOS_ESPERADOS) { if (Get-LocalUser -Name $n -ErrorAction SilentlyContinue) { $existentes += $n } } if ($Modo -eq 'Trabajo') { $falta = $ALUMNOS_ESPERADOS | Where-Object { $_ -notin $existentes } if ($existentes.Count -eq 3) { Agregar 'I2·1 · Las tres cuentas' 'OK' 'ana, luis y pedro existen' } else { Agregar 'I2·1 · Las tres cuentas' 'MAL' ("faltan: " + ($falta -join ', ')) } } else { if ($existentes.Count -eq 0) { Agregar 'REST · Usuarios borrados' 'OK' 'no quedó ninguno de los tres' } else { Agregar 'REST · Usuarios borrados' 'MAL' ("todavía están: " + ($existentes -join ', ')) } } return $existentes } function Ver-Pedro { if ($Modo -ne 'Trabajo') { return } $miembros = @() try { $miembros = Get-LocalGroupMember -SID $SID_ADMINS -ErrorAction Stop | ForEach-Object { ($_.Name -split '\\')[-1].ToLower() } } catch { Agregar 'I2·1 · Pedro es administrador' 'NA' 'no se pudo leer el grupo' return } $grupo = NombreDeSID $SID_ADMINS if ($miembros -contains 'pedro') { Agregar 'I2·1 · Pedro es administrador' 'OK' "está en $grupo" } else { Agregar 'I2·1 · Pedro es administrador' 'MAL' "no está en $grupo" } # Y el control inverso, que es la mitad que se olvida: los alumnos NO deben # ser administradores. Poner a todos en Administradores "para que ande" es # justamente lo que la tarea enseña a no hacer. # # Ojo: esto SOLO se evalúa si las cuentas existen. Si no las creó, no puede # ganar el punto por "no ponerlas en Administradores" — no hacer nada no es # respetar el mínimo privilegio. $creados = @('ana','luis') | Where-Object { Get-LocalUser -Name $_ -ErrorAction SilentlyContinue } if ($creados.Count -eq 0) { Agregar 'I2·1 · Ana y Luis NO son admins' 'NA' 'las cuentas no existen' } else { $colados = $creados | Where-Object { $miembros -contains $_ } if ($colados.Count -eq 0) { Agregar 'I2·1 · Ana y Luis NO son admins' 'OK' 'mínimo privilegio respetado' } else { Agregar 'I2·1 · Ana y Luis NO son admins' 'MAL' ("quedaron como admin: " + ($colados -join ', ')) } } } function Ver-OpcionesDeCuenta { if ($Modo -ne 'Trabajo') { return } $luis = Get-LocalUser -Name 'luis' -ErrorAction SilentlyContinue if ($luis) { # Cuando se marca "debe cambiar la contraseña en el siguiente inicio", # Windows vacía PasswordLastSet. Es la huella que deja esa casilla. if ($null -eq $luis.PasswordLastSet) { Agregar 'I2·2 · Luis debe cambiar la clave' 'OK' 'PasswordLastSet vacío' } else { Agregar 'I2·2 · Luis debe cambiar la clave' 'MAL' "última vez: $($luis.PasswordLastSet)" } } else { Agregar 'I2·2 · Luis debe cambiar la clave' 'NA' 'no existe el usuario luis' } $pedro = Get-LocalUser -Name 'pedro' -ErrorAction SilentlyContinue if ($pedro) { if ($null -eq $pedro.PasswordExpires) { Agregar 'I2·2 · La clave de Pedro no caduca' 'OK' 'sin fecha de expiración' } else { Agregar 'I2·2 · La clave de Pedro no caduca' 'MAL' "caduca el $($pedro.PasswordExpires)" } } else { Agregar 'I2·2 · La clave de Pedro no caduca' 'NA' 'no existe el usuario pedro' } } function Ver-Horario { $luis = Get-LocalUser -Name 'luis' -ErrorAction SilentlyContinue if (-not $luis) { if ($Modo -eq 'Trabajo') { Agregar 'I2·3 · Horario de Luis' 'NA' 'no existe el usuario luis' } return } # No hay cmdlet para esto: hay que leer la salida de "net user". La línea # cambia de nombre según el idioma, así que se detecta por el CONTENIDO # (¿dice "All"/"Todos" o hay días listados?) y no por el rótulo. $salida = cmd /c "net user luis" 2>$null $linea = $salida | Where-Object { $_ -match '(?i)logon hours|horas de inicio' } if (-not $linea) { Agregar 'I2·3 · Horario de Luis' 'NA' 'no se pudo leer net user' return } $valor = ($linea -split ':\s{2,}',2)[-1].Trim() $sinRestriccion = $valor -match '(?i)^(all|todos)' if ($Modo -eq 'Trabajo') { if (-not $sinRestriccion -and $valor) { Agregar 'I2·3 · Horario de Luis' 'OK' $valor } else { Agregar 'I2·3 · Horario de Luis' 'MAL' 'sin restricción horaria' } } else { if ($sinRestriccion) { Agregar 'REST · Horario devuelto' 'OK' $valor } else { Agregar 'REST · Horario devuelto' 'MAL' "todavía restringido: $valor" } } } function Ver-Politica { # Dos vías: secedit exporta con claves en INGLÉS siempre (no depende del # idioma del Windows) pero necesita elevación. Si no la hay, se cae a # "net accounts", que sí depende del idioma y por eso se lee por posición. $largo = $null; $historial = $null; $via = '' $tmp = Join-Path $env:TEMP ("tgsit-sec-" + [guid]::NewGuid().ToString('N') + ".inf") $null = secedit /export /areas SECURITYPOLICY /cfg $tmp /quiet 2>$null if (Test-Path $tmp) { $via = 'secedit' Get-Content $tmp | ForEach-Object { if ($_ -match '^\s*MinimumPasswordLength\s*=\s*(\d+)') { $largo = [int]$matches[1] } if ($_ -match '^\s*PasswordHistorySize\s*=\s*(\d+)') { $historial = [int]$matches[1] } } Remove-Item $tmp -Force -ErrorAction SilentlyContinue } else { # Se lee por RÓTULO, no por posición. # # Antes se tomaba [3] como longitud y [4] como historial. Eso funciona # hasta que Windows agrega una línea o cambia el orden: ahí la posición # se corre y el script lee otra cosa —el umbral de bloqueo, digamos— # como si fuera la longitud mínima, y le marca MAL a un alumno que hizo # la tarea bien. # # El rótulo cambia con el idioma, así que se busca una palabra que no se # repita en la salida: "longitud"/"minimum password length" y # "historial"/"password history". Es la misma técnica que ya se usa unas # líneas más arriba para leer el horario de Luis con "net user". $via = 'net accounts' cmd /c "net accounts" 2>$null | ForEach-Object { $linea = $_ if ($linea -match ':\s{2,}(.+?)\s*$') { $v = $matches[1].Trim() if ($linea -match '(?i)longitud|minimum password length') { # Si no es un número no se inventa un cero: queda sin leer y # la tarea sale NA, que es recuperable. if ($v -match '^\d+$') { $largo = [int]$v } } elseif ($linea -match '(?i)historial|password history') { # Acá sí: "Ninguna" / "None" significa cero de verdad. $historial = if ($v -match '^\d+$') { [int]$v } else { 0 } } } } } if ($null -eq $largo) { Agregar 'I2·4 · Política de contraseñas' 'NA' 'no se pudo leer (¿sin elevación?)' return } if ($Modo -eq 'Trabajo') { if ($largo -ge 8 -and $historial -ge 3) { Agregar 'I2·4 · Política de contraseñas' 'OK' "largo=$largo historial=$historial (vía $via)" } else { # La vía va también en el MAL: si el alumno discute el resultado, # tenés que poder saber CÓMO se midió sin volver a la máquina. Agregar 'I2·4 · Política de contraseñas' 'MAL' "largo=$largo historial=$historial — se pedía 8 y 3 (vía $via)" } } else { # En restitución no se puede saber cuál era el valor original de ESE # equipo, así que no se califica: se muestra para que lo compares con # lo que el alumno anotó. Inventar un "valor correcto" sería mentir. Agregar 'REST · Política (informativo)' 'NA' "largo=$largo historial=$historial — comparar con lo anotado" } } function Ver-Permisos { $existe = Test-Path $Carpeta if ($Modo -eq 'Restitucion') { if (-not $existe) { Agregar 'REST · Carpeta borrada' 'OK' "$Carpeta ya no está" } else { Agregar 'REST · Carpeta borrada' 'MAL' "$Carpeta todavía existe" } return } if (-not $existe) { Agregar 'I2·5 · Permisos de la carpeta' 'MAL' "no existe $Carpeta" Agregar 'I2·6 · Tomar posesión' 'NA' 'sin carpeta no se puede verificar' return } $acl = $null try { $acl = Get-Acl $Carpeta -ErrorAction Stop } catch { Agregar 'I2·5 · Permisos de la carpeta' 'NA' 'no se pudo leer la ACL' return } # La herencia cortada es el paso que más se olvida, y es lo que hace que # "sacar un usuario" funcione de verdad. Por eso se verifica aparte. if ($acl.AreAccessRulesProtected) { Agregar 'I2·5 · Herencia cortada' 'OK' 'permisos explícitos' } else { Agregar 'I2·5 · Herencia cortada' 'MAL' 'la herencia sigue activa' } $reglas = $acl.Access | Where-Object { -not $_.IsInherited } function DerechosDe([string]$quien) { ($reglas | Where-Object { ($_.IdentityReference.Value -split '\\')[-1] -ieq $quien } | ForEach-Object { $_.FileSystemRights.ToString() }) -join ' + ' } $dAna = DerechosDe 'ana' $dLuis = DerechosDe 'luis' if ($dAna -match 'Modify|FullControl|Write') { Agregar 'I2·5 · Ana puede escribir' 'OK' $dAna } else { Agregar 'I2·5 · Ana puede escribir' 'MAL' $(if ($dAna) { $dAna } else { 'sin permiso explícito' }) } if ($dLuis -and $dLuis -notmatch 'Modify|FullControl|Write') { Agregar 'I2·5 · Luis solo lee' 'OK' $dLuis } elseif ($dLuis) { Agregar 'I2·5 · Luis solo lee' 'MAL' "tiene de más: $dLuis" } else { Agregar 'I2·5 · Luis solo lee' 'MAL' 'sin permiso explícito' } # ── tomar posesión (subcarpeta Privado) ── $priv = Join-Path $Carpeta 'Privado' if (Test-Path $priv) { try { $aclP = Get-Acl $priv -ErrorAction Stop $duenio = $aclP.Owner $corto = ($duenio -split '\\')[-1] $nAdmin = (NombreDeSID $SID_ADMINS) if ($duenio -ieq $nAdmin -or $corto -imatch 'administrador|administrator') { Agregar 'I2·6 · Tomar posesión' 'OK' "propietario: $duenio" } else { Agregar 'I2·6 · Tomar posesión' 'MAL' "propietario: $duenio" } } catch { # Que NO se pueda leer la ACL es en sí el estado "antes de tomar # posesión": la carpeta está, pero ni el admin entra. Agregar 'I2·6 · Tomar posesión' 'MAL' 'acceso denegado — todavía no tomó posesión' } } else { Agregar 'I2·6 · Tomar posesión' 'NA' 'no existe la subcarpeta Privado' } } function Ver-Registro { $ruta = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Run' $v = $null try { $v = (Get-ItemProperty -Path $ruta -Name 'PruebaTGSIT' -ErrorAction Stop).PruebaTGSIT } catch { } if ($Modo -eq 'Trabajo') { if ($v) { Agregar 'I1·6 · Valor en el Registro' 'OK' "PruebaTGSIT = $v" } else { Agregar 'I1·6 · Valor en el Registro' 'NA' 'no está (puede que ya lo haya revertido)' } } else { if ($v) { Agregar 'REST · Registro limpio' 'MAL' "PruebaTGSIT sigue ahí: $v" } else { Agregar 'REST · Registro limpio' 'OK' 'PruebaTGSIT ya no está' } } } function Ver-Restauracion { if ($Modo -ne 'Trabajo') { return } if (-not (EsAdmin)) { Agregar 'I1·4 · Punto de restauración' 'NA' 'requiere elevación' return } try { $ps = @(Get-ComputerRestorePoint -ErrorAction Stop) $mio = $ps | Where-Object { $_.Description -match '(?i)ANTES-(DIAG|ENTREGA|LAB)' } if ($mio) { $u = ($mio | Select-Object -Last 1).Description Agregar 'I1·4 · Punto de restauración' 'OK' $u } elseif ($ps.Count -gt 0) { Agregar 'I1·4 · Punto de restauración' 'MAL' "hay $($ps.Count) puntos, ninguno ANTES-*" } else { Agregar 'I1·4 · Punto de restauración' 'MAL' 'no hay ningún punto' } } catch { Agregar 'I1·4 · Punto de restauración' 'NA' 'no se pudo leer (¿protección desactivada?)' } } function Ver-Servicio { if ($Servicio) { $s = Get-Service -Name $Servicio -ErrorAction SilentlyContinue if (-not $s) { Agregar 'I1·9 · Tipo de inicio' 'NA' "el servicio $Servicio no existe acá" return } $w = Get-CimInstance Win32_Service -Filter "Name='$Servicio'" -ErrorAction SilentlyContinue $inicio = if ($w) { $w.StartMode } else { '?' } if ($Modo -eq 'Trabajo') { if ($inicio -imatch 'Disabled') { Agregar 'I1·9 · Tipo de inicio' 'OK' "$Servicio quedó Deshabilitado" } else { Agregar 'I1·9 · Tipo de inicio' 'MAL' "$Servicio está en $inicio" } } else { if ($inicio -inotmatch 'Disabled') { Agregar 'REST · Servicio restituido' 'OK' "$Servicio en $inicio" } else { Agregar 'REST · Servicio restituido' 'MAL' "$Servicio sigue Deshabilitado" } } } else { # Sin -Servicio no se puede calificar: no sabemos cuál eligió el alumno # ni cómo estaba antes. Se listan los deshabilitados para que decidas. $desh = @(Get-CimInstance Win32_Service -Filter "StartMode='Disabled'" -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name) $muestra = ($desh | Select-Object -First 6) -join ', ' Agregar 'I1·9 · Tipo de inicio' 'NA' "pasá -Servicio . Deshabilitados ahora: $muestra" } } function Ver-Inicio { if ($Modo -ne 'Trabajo') { return } # Lo que el Administrador de tareas deshabilita queda marcado acá. No en # todos los Windows es legible sin permisos, así que puede quedar en NA. $ruta = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run' try { $p = Get-ItemProperty -Path $ruta -ErrorAction Stop $apagados = @() $p.PSObject.Properties | Where-Object { $_.Name -notlike 'PS*' } | ForEach-Object { # El primer byte distinto de 2/6 marca "deshabilitado" if ($_.Value -is [byte[]] -and $_.Value.Length -gt 0 -and $_.Value[0] -ne 2 -and $_.Value[0] -ne 6) { $apagados += $_.Name } } if ($apagados.Count -gt 0) { Agregar 'I2·7 · Programa de inicio apagado' 'OK' ($apagados -join ', ') } else { Agregar 'I2·7 · Programa de inicio apagado' 'MAL' 'no hay ninguno deshabilitado' } } catch { Agregar 'I2·7 · Programa de inicio apagado' 'NA' 'no se pudo leer la lista' } } # ══════════════════════════════════════════════════════════════════════════════ # EL CONTRATO CON EL MOTOR # # El motor no conoce los nombres de estas funciones: llama a esta, y punto. # El orden es el de la consigna, que es el que el alumno siguio. # ══════════════════════════════════════════════════════════════════════════════ function Correr-Comprobaciones { # $null = porque Ver-Usuarios devuelve las cuentas que encontro, y sin esto # se irian a la salida mezcladas con el tablero. $null = Ver-Usuarios Ver-Pedro Ver-OpcionesDeCuenta Ver-Horario Ver-Politica Ver-Permisos Ver-Registro Ver-Restauracion Ver-Servicio Ver-Inicio } # Lo que este módulo necesita preguntar antes de medir. El motor la llama si # existe; el módulo 4 no la define, porque no pregunta nada. function Pedir-Datos-Del-Modulo { if (-not $script:Servicio) { Write-Host ' El servicio que deshabilitaste en la tarea 9 de la Integradora 1.' -ForegroundColor DarkGray Write-Host ' Si no lo anotaste, Enter y te lista los deshabilitados.' -ForegroundColor DarkGray # $script: porque se escribe una variable del script desde adentro de # una función: sin eso quedaría en una copia local y se perdería. $script:Servicio = Preguntar 'Servicio (Enter para omitir):' } } # ══════════════════════════════════════════════════════════════════════════════ # SALIDA # ══════════════════════════════════════════════════════════════════════════════ function Mostrar { $ancho = 78 Write-Host '' Write-Host ('=' * $ancho) -ForegroundColor DarkCyan Write-Host (' VERIFICADOR TGSIT · MÓDULO 3 — modo ' + $Modo.ToUpper()) -ForegroundColor Cyan Write-Host ('=' * $ancho) -ForegroundColor DarkCyan $enc = " Equipo: $env:COMPUTERNAME Fecha: " + (Get-Date -Format 'dd/MM/yyyy HH:mm') if ($Codigo) { $enc += " Alumno: $Codigo" } Write-Host $enc -ForegroundColor Gray if (-not (EsAdmin)) { Write-Host ' ⚠ Sin privilegios de administrador: algunas comprobaciones no se pueden hacer.' -ForegroundColor Yellow } Write-Host '' foreach ($f in $script:filas) { switch ($f.Estado) { 'OK' { $m = ' [ OK ] '; $c = 'Green' } 'MAL' { $m = ' [ ✗ ] '; $c = 'Red' } 'NA' { $m = ' [ -- ] '; $c = 'DarkGray' } } Write-Host ($m + $f.Tarea.PadRight(38)) -ForegroundColor $c -NoNewline Write-Host $f.Detalle -ForegroundColor DarkGray } Write-Host '' Write-Host ('-' * $ancho) -ForegroundColor DarkCyan $pct = if ($script:posible -gt 0) { [math]::Round($script:puntos / $script:posible * 100) } else { 0 } $col = if ($pct -ge 75) { 'Green' } elseif ($pct -ge 50) { 'Yellow' } else { 'Red' } Write-Host (" RESULTADO: $($script:puntos) de $($script:posible) comprobaciones ($pct%)") -ForegroundColor $col $na = @($script:filas | Where-Object { $_.Estado -eq 'NA' }).Count if ($na -gt 0) { Write-Host " ($na sin verificar — no cuentan ni a favor ni en contra)" -ForegroundColor DarkGray } Write-Host ('=' * $ancho) -ForegroundColor DarkCyan # ── LO QUE ESTE SCRIPT NO PUEDE VER ────────────────────────────────────── # Las integradoras son 18 tareas y acá se comprueban 10. Sin decirlo, un # "13 de 13" se lee como "está todo hecho", y ocho tareas no se miraron # nunca. Peor todavía para el alumno, que es el que se queda tranquilo. # # No son tareas menores: son las que dejan rastro en un papel o en la # cabeza, no en el disco. Por eso las mira Javier y no la maquina. Write-Host '' Write-Host ' ESTO NO LO PUEDE VER EL SCRIPT — lo revisa el docente:' -ForegroundColor Cyan foreach ($t in @( 'I1-1 abrir la consola de tres formas distintas', 'I1-2 la consola de trabajo armada a medida', 'I1-3 la misma consola, sin privilegios', 'I1-5 el respaldo del registro ANTES de tocarlo', 'I1-7 encontrar el error en el visor de eventos', 'I1-8 las dependencias de un servicio', 'I2-8 las verificaciones que hizo el propio alumno', 'I2-9 el parte de servicio escrito')) { Write-Host (' · ' + $t) -ForegroundColor DarkGray } Write-Host ' Un puntaje perfecto acá NO significa la integradora completa.' -ForegroundColor DarkGray Write-Host ('=' * $ancho) -ForegroundColor DarkCyan Write-Host '' } # ══════════════════════════════════════════════════════════════════════════════ # ENVÍO A LA PLANILLA # # Manda SOLO el dato objetivo: cuántas comprobaciones pasó el equipo. La nota # la confirmás vos en la planilla — el script no ve si escribió el parte de # servicio ni si entendió lo que hizo, y eso también se califica. # # Va con clave de docente porque el pedido sale de la máquina del alumno. # Si no hay red, el resultado queda en un archivo pendiente y se reintenta la # próxima vez que corras el verificador con conexión: nunca se pierde una nota # porque el wifi del instituto ande mal. # ══════════════════════════════════════════════════════════════════════════════ function Archivo-Pendientes { Join-Path (Carpeta-Datos) 'pendientes-verificador.jsonl' } function Enviar-Uno { param([hashtable]$Datos, [string]$Clave) # Se CLONA antes de agregar la clave: si se modificara el original, la # clave del docente terminaría escrita en el archivo de pendientes, que # puede quedar en la máquina del alumno. La clave no se guarda nunca. $envio = @{ accion = 'practico' } foreach ($k in $Datos.Keys) { $envio[$k] = $Datos[$k] } $envio['claveDocente'] = $Clave # POST, no GET: los datos van en el CUERPO del pedido y no en la dirección. # En GET la clave del docente quedaría escrita en los registros de ejecución # de Apps Script. Las páginas del portal no pueden hacer esto —el navegador # las frena por CORS— pero PowerShell sí. $json = ($envio | ConvertTo-Json -Compress) try { $r = Invoke-RestMethod -Uri $Url -Method Post -Body $json ` -ContentType 'application/json' ` -TimeoutSec 30 -ErrorAction Stop # La respuesta ya viene como objeto: doPost devuelve JSON puro, sin # el envoltorio cb(...) que necesitan las páginas. if ($r -is [string]) { if ($r -match '^\s*cb\((.*)\)\s*$') { return ($matches[1] | ConvertFrom-Json) } try { return ($r | ConvertFrom-Json) } catch { return $null } } return $r } catch { return $null } } function Guardar-Pendiente { param([hashtable]$Datos) try { ($Datos | ConvertTo-Json -Compress) | Add-Content -Path (Archivo-Pendientes) -Encoding utf8 Write-Host ' Sin conexión: el resultado quedó guardado y se reintenta la próxima vez.' -ForegroundColor Yellow } catch { Write-Host ' No se pudo guardar el pendiente. Anotá el resultado a mano.' -ForegroundColor Red } } function Reintentar-Pendientes { param([string]$Clave) $f = Archivo-Pendientes if (-not (Test-Path $f)) { return } $lineas = @(Get-Content $f -ErrorAction SilentlyContinue | Where-Object { $_.Trim() }) if (-not $lineas.Count) { return } Write-Host (" Hay $($lineas.Count) resultado(s) pendiente(s) de envios anteriores…") -ForegroundColor Cyan $quedan = @() foreach ($l in $lineas) { $obj = $null try { $obj = $l | ConvertFrom-Json } catch { continue } $h = @{} $obj.PSObject.Properties | ForEach-Object { if ($_.Name -ne 'claveDocente') { $h[$_.Name] = $_.Value } } $r = Enviar-Uno -Datos $h -Clave $Clave if ($r -and $r.ok) { Write-Host (" enviado: " + $h['codigo']) -ForegroundColor Green } else { $quedan += $l } } if ($quedan.Count) { $quedan | Set-Content -Path $f -Encoding utf8 } else { Remove-Item $f -Force -ErrorAction SilentlyContinue Write-Host ' todos los pendientes se enviaron.' -ForegroundColor Green } } function Enviar-Resultado { param([string]$Clave) if (-not $Clave) { Write-Host ' (Sin clave del docente no se carga en la planilla: solo se muestra acá.)' -ForegroundColor DarkGray Write-Host '' return } if (-not $Codigo) { Write-Host ' Falta el código del alumno: no se puede cargar en la planilla.' -ForegroundColor Yellow Write-Host '' return } Reintentar-Pendientes -Clave $Clave # 'origen' le dice a la planilla CUAL de los dos verificadores habla. Sin # esto los dos escribian en el mismo casillero y este le borraba la marca # al del practico final, ademas de precargar sus comprobaciones como si # fueran una nota. Las integradoras son un acuse de avance, no una nota. $datos = @{ codigo = $Codigo origen = 'integradoras' modo = $Modo.ToLower() puntos = $script:puntos total = $script:posible modulo = '3' } Write-Host ' Cargando en la planilla…' -ForegroundColor Cyan $r = Enviar-Uno -Datos $datos -Clave $Clave if ($r -and $r.ok) { # El NOMBRE va primero y en grande: es lo único que delata un código # bien tipeado pero del alumno equivocado. if ($r.nombre) { Write-Host (" ✓ Cargado a: " + $r.nombre + " (" + $r.codigo + ")") -ForegroundColor Green Write-Host (" " + $r.verificado) -ForegroundColor DarkGray } else { Write-Host (" ✓ Cargado: " + $r.verificado) -ForegroundColor Green } # Este verificador NO carga notas: deja constancia de lo que la maquina # comprobo, y nada mas. # # Aca habia un "Ya habia una nota cargada: no se pisa", heredado de # cuando los dos verificadores compartian la logica de precarga. Desde # que las integradoras dejaron de precargar nota, esa rama se disparaba # SIEMPRE — y decia que habia una nota aunque no hubiera ninguna. Write-Host ' Es un acuse de avance, no una nota: la del práctico la ponés vos.' -ForegroundColor DarkGray } elseif ($r -and $r.estado) { Write-Host (" No se cargo: " + $r.estado) -ForegroundColor Yellow # 'viejo' y 'otro modulo' no se arreglan reintentando: guardarlos seria # dejar un pendiente que falla en cada corrida y nunca se va. if ($r.estado -notmatch 'clave|padron|viejo|otro') { Guardar-Pendiente -Datos $datos } } else { Guardar-Pendiente -Datos $datos } Write-Host '' } function Guardar-Txt { $nombre = "verificacion-{0}-{1}-{2}.txt" -f ` $(if ($Codigo) { $Codigo } else { $env:COMPUTERNAME }), $Modo, (Get-Date -Format 'yyyyMMdd-HHmm') $destino = Join-Path (Carpeta-Datos) $nombre $lineas = @( "VERIFICADOR TGSIT M3 - modo $Modo", "Equipo: $env:COMPUTERNAME Fecha: $(Get-Date -Format 'dd/MM/yyyy HH:mm')", $(if ($Codigo) { "Alumno: $Codigo" } else { "Alumno: (sin codigo)" }), '' ) foreach ($f in $script:filas) { $lineas += ("[{0,-3}] {1} {2}" -f $f.Estado, $f.Tarea.PadRight(38), $f.Detalle) } $lineas += '' $lineas += "RESULTADO: $($script:puntos) de $($script:posible)" $lineas | Out-File -FilePath $destino -Encoding utf8 Write-Host " Guardado en: $destino" -ForegroundColor Cyan Write-Host '' } # ══════════════════════════════════════════════════════════════════════════════ # EL CANDADO # # Antes de mirar una sola cosa de la maquina se le pregunta a la planilla si la # verificacion esta abierta. El candado vive ALLA y no aca a proposito: este # archivo es texto que cualquiera puede leer y copiar, asi que una llave # escrita adentro seria una llave debajo del felpudo. # # FALLA CERRADO: si no hay respuesta, no se corre. Un candado que se abre # desconectando el cable de red no es un candado. El costo es real y esta # elegido a proposito: sin internet en el salon, no se verifica. # # Ojo: esto decide si el script ARRANCA. Que el resultado se pueda ESCRIBIR en # la planilla lo sigue decidiendo la clave del docente, que es otra cosa. # ══════════════════════════════════════════════════════════════════════════════ function Ventana-Abierta { try { $r = Invoke-RestMethod -Uri ($Url + '?accion=ventana&callback=cb') ` -Method Get -TimeoutSec 15 -ErrorAction Stop # doGet envuelve todo en cb(...), asi que llega como texto, no como objeto. if ($r -is [string]) { if ($r -match '(?s)^\s*cb\((.*)\)\s*$') { $r = $matches[1] | ConvertFrom-Json } else { return @{ ok = $false; motivo = 'respuesta ilegible' } } } if ($r.abierta) { return @{ ok = $true; modulo = $r.modulo } } return @{ ok = $false; motivo = 'cerrada' } } catch { return @{ ok = $false; motivo = 'sin respuesta de la planilla' } } } $ventana = Ventana-Abierta if (-not $ventana.ok) { Write-Host '' Write-Host ('=' * 78) -ForegroundColor DarkRed if ($ventana.motivo -eq 'cerrada') { Write-Host ' LA VERIFICACIÓN ESTÁ CERRADA' -ForegroundColor Red Write-Host '' Write-Host ' El docente la abre desde la planilla, en el menú' -ForegroundColor Gray Write-Host ' "Examen TGSIT" → "Abrir la verificación".' -ForegroundColor Gray } else { Write-Host ' NO SE PUDO CONSULTAR LA PLANILLA' -ForegroundColor Red Write-Host '' Write-Host (' Motivo: ' + $ventana.motivo + '.') -ForegroundColor Gray Write-Host ' Sin confirmar que la verificación esté abierta, este script' -ForegroundColor Gray Write-Host ' no corre. Revisá la conexión a internet de esta máquina.' -ForegroundColor Gray } Write-Host ('=' * 78) -ForegroundColor DarkRed Write-Host '' if ([Environment]::UserInteractive -and $Host.Name -eq 'ConsoleHost') { try { Write-Host ' Enter para cerrar…' -ForegroundColor DarkGray; $null = Read-Host } catch { } } exit 1 } # ── LO QUE NO VINO POR PARAMETRO, SE PREGUNTA ───────────────────────────────── # Esto es lo que reemplaza al .bat: pegado en una consola nadie le pasa los # datos al script, asi que los pide aca. Lo que si viniste pasando por parametro # no se vuelve a preguntar. if (Interactivo) { Write-Host '' Write-Host (' VERIFICADOR TGSIT · MÓDULO 3 (módulo activo: ' + $ventana.modulo + ')') -ForegroundColor Cyan Write-Host ' Solo LEE el equipo. No crea, no borra, no modifica nada.' -ForegroundColor DarkGray Write-Host '' if (-not $PSBoundParameters.ContainsKey('Modo')) { Write-Host ' 1 - TRABAJO (antes de restituir)' -ForegroundColor Gray Write-Host ' 2 - RESTITUCIÓN (después de restituir)' -ForegroundColor Gray $Modo = $(if ((Preguntar 'Opción [1]:' '1') -eq '2') { 'Restitucion' } else { 'Trabajo' }) Write-Host '' } # La clave va PRIMERO: con ella se puede traer la lista del modulo y # elegir al alumno en vez de tipearlo, que es donde se cuela el error. if (-not $ClaveDocente) { $ClaveDocente = Preguntar-Clave } if (-not $Codigo) { $Codigo = Elegir-Alumno -Clave $ClaveDocente } # Contrato OPCIONAL: si el módulo necesita preguntar algo suyo antes de # medir, define esta función en su archivo de tareas. Acá estaba escrito el # texto del módulo 3 —«el servicio que deshabilitaste en la tarea 9»—, o sea # que el motor sabía qué tareas tiene UN módulo. El 4 no pregunta nada. if (Get-Command Pedir-Datos-Del-Modulo -ErrorAction SilentlyContinue) { Pedir-Datos-Del-Modulo } Write-Host '' } # ══════════════════════════════════════════════════════════════════════════════ # CORRER # ══════════════════════════════════════════════════════════════════════════════ # Se mide en una vuelta que se puede repetir. En las integradoras repetir es # justamente lo que queremos: el alumno ve que algo fallo, lo arregla y vuelve a # medir. Antes para eso tenia que pegar el comando otra vez; ahora es un numero. while ($true) { $script:filas = @(); $script:puntos = 0; $script:posible = 0 # EL CONTRATO con el archivo de tareas del modulo. Antes aca estaba la # lista de las diez funciones del modulo 3 escrita a mano -Ver-Pedro, # Ver-Horario...-, o sea que el motor conocia por nombre las tareas de UN # modulo. El modulo 4 hubiera tenido que llamar Ver-Pedro a algo suyo. # Ahora cada modulo agrupa las suyas como quiera y expone esta funcion. Correr-Comprobaciones Mostrar Enviar-Resultado -Clave $ClaveDocente if ($Guardar) { Guardar-Txt } # Sin consola interactiva (una tarea programada, otro script) no hay a quien # preguntarle: se mide una vez y se termina, como siempre. if (-not (Interactivo)) { break } Write-Host ' ¿Y ahora?' -ForegroundColor Cyan Write-Host ' 1 medir de nuevo (arreglaste algo)' -ForegroundColor Gray if ($Modo -eq 'Trabajo') { Write-Host ' 2 ya restituí: medir la restitución' -ForegroundColor Gray } if ($ClaveDocente) { Write-Host ' Enter terminar' -ForegroundColor Gray } else { Write-Host ' Enter terminar y CERRAR esta consola' -ForegroundColor Gray } $op = '' try { $op = [string](Read-Host ' Opción') } catch { break } $op = $op.Trim() Write-Host '' if ($op -eq '1') { continue } if ($op -eq '2' -and $Modo -eq 'Trabajo') { $Modo = 'Restitucion'; continue } break } # ── Sin clave, la consola se CIERRA ─────────────────────────────────────────── # Esta consola corre como administrador. Si el alumno se levanta y la deja # abierta, el siguiente que se sienta tiene una consola elevada en esa maquina. # En el Practico Final se cierra al usar el vale; aca no hay vale, asi que la # señal es la otra: sin clave del docente, el que lo corrio es un alumno. # # Con la clave NO se cierra: sos vos, y cortarte la consola en la cara seria un # estorbo. Y desde el .bat tambien funciona: se cierra esta ventana y el .bat # sigue con su propio menu. if (-not $ClaveDocente -and (Interactivo)) { [Environment]::Exit(0) }