Inventariar un servidor de archivos con PowerShell ── auditoría de capacidad y permisos de acceso (ACL)

· Actualizado el: · · PowerShell, Windows, Servidor de archivos, ACL, Permisos de acceso, Mejora operativa, Seguridad, Script

«El espacio libre del servidor de archivos ha bajado del 10 %» ── el trabajo que empieza con esta alerta suele ser deprimente. No se sabe qué está consumiendo la capacidad. No se puede decidir si una carpeta de hace diez años que nadie toca se puede borrar. Y ni siquiera se puede responder de inmediato quién puede acceder a dónde, o si quedan permisos de alguien que ya dejó la empresa. Una investigación que consiste en abrir propiedades una por una en el Explorador de archivos nunca termina cuando el recurso compartido pesa varios terabytes.

Cada vez que me pongo manos a la obra con este tipo de encargo, pienso lo mismo: ordenar un servidor de archivos no es una “técnica de borrado”, sino una “técnica de inventario”. Si se pregunta “¿puedo borrar esto?” sin cifras ni listados, el departamento nunca va a asentir. En cambio, si se presenta una tabla del tipo “solo esta carpeta ocupa 800 GB, de los cuales 620 GB corresponden a archivos sin actualizar desde hace más de tres años”, la conversación avanza de inmediato.

Este artículo está dirigido a personal de sistemas y de operaciones de pequeñas y medianas empresas, y organiza el procedimiento práctico para inventariar con PowerShell la capacidad y los permisos de acceso (ACL) de un servidor de archivos, hasta volcarlo en un informe CSV. El criterio se mantiene constante en todo momento: “primero leer, los cambios al final, siempre con copia de seguridad y -WhatIf”.

1. La conclusión, primero

  • La auditoría de capacidad se basa en Get-ChildItem -Recurse + Measure-Object -Sum. No agregue todo de una vez: calcule por carpeta de primer nivel para identificar “dónde está el peso”. 12
  • No ignore los accesos denegados: regístrelos. En lugar de detener el recorrido con -ErrorAction SilentlyContinue, use -ErrorVariable para anotar siempre los lugares denegados. Un lugar que no se pudo contar no es “cero”, es “desconocido”. 3
  • Determine los archivos antiguos según LastWriteTime. En muchos entornos, la actualización de la fecha de último acceso de NTFS está deshabilitada de forma predeterminada (o gestionada por el sistema), por lo que no es un dato confiable para decidir. 45
  • Extraiga “candidatos” a archivos duplicados con Get-FileHash (SHA256 por defecto). La regla de oro para ahorrar E/S es filtrar primero por tamaño y calcular el hash después. 6
  • El inventario de ACL se hace con la propiedad Access de Get-Acl. Vuelque a CSV IdentityReference (quién), FileSystemRights (qué) e IsInherited (heredado o directo), y use AreAccessRulesProtected para detectar los lugares donde se cortó la herencia. 78
  • En este artículo se inventarían los permisos NTFS. El acceso efectivo a través de un recurso compartido (SMB) se determina combinando los permisos del recurso compartido con la ACL de NTFS. Liste el lado del recurso compartido por separado con Get-SmbShareAccess y consúltelo junto con el inventario de NTFS. 9
  • Especifique siempre la codificación de caracteres del informe CSV. El valor predeterminado de Export-Csv en Windows PowerShell 5.1 es ASCII, lo que corrompe los caracteres japoneses, y el valor predeterminado de PowerShell 7 es UTF-8 sin BOM, que a veces no combina bien con el comportamiento predeterminado de Excel. 1011
  • Los cambios (Set-Acl) solo después de completar el inventario y la confirmación con el departamento. Haga copia de seguridad con icacls /save, confirme el objetivo con -WhatIf y luego aplique. 1213

Si sigue todo el procedimiento de este artículo, al final tendrá estos cuatro entregables. Primero mostramos cómo quedan.

Entregable Capítulo donde se genera Columnas Qué información aporta
Ranking de capacidad por carpeta Capítulo 2 Folder / SizeGB / FileCount Qué carpeta pesa más. De aquí salen los números necesarios para frases como “el recurso compartido pesa ◯ GB en total, de los cuales el departamento de ventas ocupa ◯ GB”
old-files.csv Capítulo 3 FullName / LastWriteTime / SizeMB Lista de archivos sin actualizar desde hace más de 3 años. Como está ordenada de forma descendente por SizeMB, con las primeras decenas de filas ya se ve “dónde está el peso relevante”
duplicate-candidates.csv Capítulo 3 Hash / Path Grupos de archivos con contenido idéntico. Al ordenar por la columna Hash, los que comparten contenido quedan agrupados
acl-report.csv Capítulo 4 Path / Identity / Rights / Type / IsInherited / Inheritance / InheritanceBroken Registro de quién tiene qué permiso en qué ubicación. Cada ACE ocupa una fila

Además de esto, se generan archivos de texto que registran los lugares que no se pudieron recorrer (denied-paths.txt, old-files-uninspected.txt, hash-uninspected.txt, acl-uninspected.txt). Si alguno de ellos no está vacío, los cuatro entregables anteriores son “informes con huecos”. Entréguelos indicando explícitamente ese hecho.

La frase del comienzo, “solo esta carpeta ocupa 800 GB, de los cuales 620 GB corresponden a archivos sin actualizar desde hace más de tres años”, se construye cruzando el primer y el segundo entregable. La segunda cifra se obtiene simplemente releyendo old-files.csv y sumando.

# Calcula, a partir de old-files.csv, el tamaño total de los archivos sin actualizar hace más de 3 años
$old   = Import-Csv .\old-files.csv
$sumMB = ($old | ForEach-Object { [double]$_.SizeMB } | Measure-Object -Sum).Sum
'Sin actualizar hace 3+ años: {0} archivos / {1:N1} GB' -f $old.Count, ($sumMB / 1024)

2. Auditoría de capacidad ── convertir en números dónde está el peso

Lo primero que hay que hacer es calcular el tamaño total de cada carpeta situada directamente bajo la raíz del recurso compartido. Enumere los archivos de forma recursiva con Get-ChildItem y sume Length con Measure-Object. 12

$root = 'D:\share'   # Se asume ejecución en el propio servidor de archivos. Las precauciones con UNC están en el capítulo 6
$denied = @()

# Agregar por carpeta de primer nivel ── primero conviene "identificar dónde está el peso" antes que agregarlo todo de una vez
# Para que también quede en $denied el fallo de enumerar la propia raíz, se aplica la misma opción al Get-ChildItem exterior
$report = foreach ($dir in Get-ChildItem -LiteralPath $root -Directory `
    -ErrorAction SilentlyContinue -ErrorVariable +denied) {
    # SilentlyContinue para no detenerse ante un acceso denegado, pero acumulando la denegación en $denied
    # La enumeración se envía directo por pipeline a Measure-Object (si se recibe en una variable, se
    # retienen en memoria todos los FileInfo, lo que resulta pesado en carpetas con millones de archivos)
    $stats = Get-ChildItem -LiteralPath $dir.FullName -Recurse -File `
        -ErrorAction SilentlyContinue -ErrorVariable +denied |
        Measure-Object -Property Length -Sum
    [PSCustomObject]@{
        Folder    = $dir.Name
        SizeGB    = [math]::Round([double]$stats.Sum / 1GB, 2)
        FileCount = $stats.Count
    }
}
# Los archivos ubicados directamente bajo la raíz también se cuentan como una fila (de lo contrario quedan fuera del agregado por carpeta)
$rootStats = Get-ChildItem -LiteralPath $root -File `
    -ErrorAction SilentlyContinue -ErrorVariable +denied |
    Measure-Object -Property Length -Sum
if ($rootStats.Count -gt 0) {
    $report += [PSCustomObject]@{
        Folder    = '(raíz directa)'
        SizeGB    = [math]::Round([double]$rootStats.Sum / 1GB, 2)
        FileCount = $rootStats.Count
    }
}

$report | Sort-Object SizeGB -Descending | Format-Table -AutoSize

# Conserve siempre los "lugares que no se pudieron contar" ── si esto no está vacío, el agregado está incompleto
$denied | ForEach-Object { $_.TargetObject } | Sort-Object -Unique |
    Set-Content -Path .\denied-paths.txt

Hay dos puntos clave. Primero, -ErrorAction SilentlyContinue no es un interruptor que “hace como si el error no hubiera ocurrido”. Solo suprime la visualización y continúa la ejecución; el error en sí sigue produciéndose. 3 Por eso lo recogemos con -ErrorVariable. Al anteponer + al nombre de la variable, en lugar de sobrescribirla se le va anexando contenido, lo que permite reunir en una sola variable las denegaciones de todo el bucle. 3 Las carpetas con acceso denegado quedan fuera del agregado, por lo que en el informe aparecen más pequeñas de lo que realmente son. Si denied-paths.txt no está vacío, indíquelo siempre en el informe. Si quiere profundizar más en el diseño del manejo de errores, puede consultar el artículo publicado simultáneamente “Diseño de manejo de errores y reintentos en PowerShell”.

Segundo, el problema de los niveles profundos. En un recurso compartido que lleva años en operación, casi con seguridad existen rutas que superan los 260 caracteres (MAX_PATH), y con algunas herramientas la enumeración falla en esos puntos. 14 Si el resultado del recorrido presenta huecos que no tienen explicación, sospeche primero de la longitud de la ruta. El panorama completo de esta limitación y cómo abordarla está reunido en “MAX_PATH y las trampas de las rutas y nombres de archivo en Windows”.

3. Archivos antiguos y candidatos a duplicados ── listar antes que borrar

3.1. Elaborar una lista de “archivos sin tocar hace 3 años”

Una vez identificados los lugares que más pesan, el siguiente paso es reunir el material para los “candidatos a eliminar”. El criterio que se usa es LastWriteTime (fecha de última modificación).

$cutoff = (Get-Date).AddYears(-3)

# Lista de archivos sin actualizar desde hace más de 3 años. No se borra nada ── primero se prepara el material para mostrar al departamento
# Los lugares donde falló la enumeración también se registran con -ErrorVariable, para revelarlos después
Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable oldEnumErrors |
    Where-Object LastWriteTime -lt $cutoff |
    Select-Object FullName, LastWriteTime,
        @{ Name = 'SizeMB'; Expression = { [math]::Round($_.Length / 1MB, 2) } } |
    Sort-Object SizeMB -Descending |
    Export-Csv -Path .\old-files.csv -NoTypeInformation -Encoding utf8BOM   # En 5.1, use UTF8 (ambos llevan BOM)

# Los lugares que no se pudieron enumerar también se revelan siempre, para que la lista no parezca "completa" sin serlo
$oldEnumErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\old-files-uninspected.txt

Es tentador pensar que “usando la fecha de último acceso (LastAccessTime) también se podría saber que ‘ni siquiera se ha leído’”, pero es una trampa. En NTFS, actualizar la fecha de último acceso afecta al rendimiento, por lo que su activación o desactivación se controla mediante el comando fsutil behavior y el valor de registro NtfsDisableLastAccessUpdate 4, y desde Windows Vista está deshabilitada de forma predeterminada (en versiones recientes, desde Windows 10 queda gestionada por el sistema, y en los servidores permanece deshabilitada). 5 Es decir, es normal que existan archivos que en realidad se han abierto pero cuya fecha permanece antigua. En el material explicativo del inventario, use LastWriteTime —”la fecha en que se modificó el contenido por última vez”— y deje explícito su significado; así la conversación con el departamento no se complica.

3.2. Presentar “candidatos” a archivos duplicados

En las carpetas compartidas se acumulan en grandes cantidades archivos del tipo “versión final”, “versión final_corregida” o “copia de ~”. Si el contenido es idéntico se puede determinar con Get-FileHash. El algoritmo predeterminado es SHA256, y si los valores de hash coinciden se puede considerar que el contenido de los archivos es idéntico. 6

# Calcular el hash de todos los archivos es costoso en E/S. Primero se filtra solo a los "archivos del mismo tamaño"
# Los lugares donde falló la enumeración (acceso denegado, etc.) también se registran con -ErrorVariable, para revelarlos después
$candidates = Get-ChildItem -LiteralPath $root -Recurse -File `
    -ErrorAction SilentlyContinue -ErrorVariable enumErrors |
    Group-Object -Property Length |
    Where-Object { $_.Count -ge 2 -and [long]$_.Name -gt 0 } |   # Se excluye el tamaño 0
    ForEach-Object { $_.Group }

# Solo se calcula el hash de los candidatos filtrados, y los grupos coincidentes se emiten como "candidatos" a duplicado
$candidates |
    Get-FileHash -ErrorAction SilentlyContinue -ErrorVariable hashErrors |   # El valor predeterminado es SHA256
    Group-Object -Property Hash |
    Where-Object Count -ge 2 |
    ForEach-Object { $_.Group } |
    Select-Object Hash, Path |
    Export-Csv -Path .\duplicate-candidates.csv -NoTypeInformation -Encoding utf8BOM   # En 5.1, use UTF8

# Las carpetas que no se pudieron enumerar y los archivos cuyo hash no se pudo
# calcular (por bloqueo, falta de permiso de lectura, etc.) se registran siempre
# como "no revisados" en una lista
# (si se descartan en silencio, no se pueden distinguir de "cero duplicados")
@($enumErrors) + @($hashErrors) | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\hash-uninspected.txt

Los llamamos deliberadamente “candidatos” porque decidir cuál conservar es un problema del negocio, no de la técnica. Aunque el contenido sea idéntico, “el original del departamento A” y “la copia de referencia del departamento B” pueden tener un significado distinto. El alcance de este script llega hasta tener el listado para conversar con los responsables, sin borrar nada de forma mecánica. Las propiedades de la herramienta del hash en sí (qué garantiza y qué no) están organizadas en “Procedimiento práctico para identificar el formato a partir de una cadena de hash”.

3.3. Poner en cuarentena antes de borrar ── el procedimiento “cuarentena → observación → eliminación”

Aunque el departamento confirme el listado, no se pasa directamente al borrado. El contenido de “cuarentena → observación → eliminación” que aparece en la tabla de criterios del capítulo 7 consiste en los siguientes pasos.

  1. Decida el destino de la cuarentena. Cree una carpeta con la fecha en el nombre en el mismo servidor. A ser posible, use el mismo volumen que el origen (el motivo se explica más adelante). Limite los permisos a los administradores y a los responsables. Sin embargo, restringir solo el permiso de la carpeta no afecta a los archivos que hay dentro. Esto se debe a que, al mover dentro del mismo volumen, los permisos originales se conservan tal cual, por lo que el procedimiento completo incluye devolver cada archivo movido a la herencia predeterminada (esto también se explica más adelante).
  2. Mueva conservando la jerarquía original. Si se mueve todo de forma plana, los archivos con el mismo nombre chocan entre sí, y al momento de devolverlos ya no se sabrá cuál era la ubicación original.
  3. Conserve un registro de los movimientos. Sin una tabla de correspondencia entre la ruta original y la ruta de destino, no se puede responder a “¿a dónde fue a parar ese archivo?”. El registro debe contener únicamente lo que realmente se movió. Separe en archivos distintos el resultado de la simulación y lo que no se pudo mover: si se mezclan, en el momento de la recuperación se terminará buscando ubicaciones que no existen.
  4. Defina el período de observación. Es más práctico ajustarlo al ciclo del negocio —por ejemplo, “hasta que pase un cierre de trimestre”— en lugar de fijarlo en algo como “un mes”. Si llega una consulta, se revisa el registro y se devuelve el archivo.
  5. Una vez transcurrido el período, elimine los archivos. Justo antes de eliminar, comparta el registro una vez más con el departamento correspondiente.
# Mueve a la carpeta de cuarentena el listado ya confirmado por el departamento (old-files.csv con solo las filas objetivo)
$root       = 'D:\share'
$quarantine = 'D:\quarantine\2026-07'
$list       = Import-Csv .\old-files-approved.csv   # Las columnas son las mismas que en old-files.csv
$dryRun     = $true       # Primero deje $true para confirmar el objetivo; si no hay problemas, cambie a $false para la ejecución real
$planned    = @()   # Resultado de la simulación. Se emite en un archivo distinto al registro de la ejecución real
$moved      = @()   # Lo que realmente se movió (para el recuento; el registro real está en el archivo de abajo)
$failed     = @()   # Lo que no se movió. Permanece en su ubicación original
$needsAcl   = @()   # Lo que se movió pero no se pudo restringir en permisos

# El archivo de salida usa un nombre distinto en cada ejecución. Con un nombre fijo,
# en el momento en que se procese el siguiente lote se sobrescribiría el registro anterior,
# y ya no se podrían devolver los archivos que se movieron entonces
$runId    = Get-Date -Format 'yyyyMMdd-HHmmss'
$ledger   = ".\quarantine-log-$runId.csv"        # Registro para la recuperación. Se anexa en cada movimiento
$planFile = ".\quarantine-plan-$runId.csv"
$failFile = ".\quarantine-failed-$runId.csv"
$aclFile  = ".\quarantine-needs-acl-$runId.csv"
$enc      = 'utf8BOM'                            # En 5.1, use 'UTF8'

# Rutas absolutas usadas para determinar el rango. Igualando el final con \,
# se evita que D:\share2 se considere incorrectamente dentro de D:\share
$rootFull       = [System.IO.Path]::GetFullPath($root).TrimEnd('\') + '\'
$quarantineFull = [System.IO.Path]::GetFullPath($quarantine).TrimEnd('\') + '\'

foreach ($row in $list) {
    $src = $row.FullName
    if (-not (Test-Path -LiteralPath $src)) { continue }   # Se omite lo que ya no existe

    # Este CSV lo edita una persona, y puede colarse alguna fila que apunte fuera de $root.
    # El Substring de abajo funciona en silencio incluso en ese caso, y pondría en cuarentena
    # el archivo de una ubicación completamente distinta. Por eso, antes de continuar, se
    # resuelve la ruta real y se comprueba que esté dentro del ámbito auditado
    $srcFull = (Resolve-Path -LiteralPath $src).ProviderPath
    if (-not $srcFull.StartsWith($rootFull, [StringComparison]::OrdinalIgnoreCase)) {
        $failed += [PSCustomObject]@{ Source = $src; Destination = ''
                                      Error  = "Apunta fuera del ámbito auditado ($root)" }
        continue
    }

    # Se mueve conservando la jerarquía original (evita choques de archivos con el mismo nombre y el "no se puede devolver")
    $relative = $srcFull.Substring($rootFull.Length)
    # Join-Path no resuelve '..'. Tras plegar la ruta con GetFullPath,
    # se comprueba que quede dentro del destino de cuarentena
    $dest     = [System.IO.Path]::GetFullPath((Join-Path $quarantine $relative))
    if (-not $dest.StartsWith($quarantineFull, [StringComparison]::OrdinalIgnoreCase)) {
        $failed += [PSCustomObject]@{ Source = $src; Destination = $dest
                                      Error  = "Sale fuera de la cuarentena ($quarantine)" }
        continue
    }

    if ($dryRun) {
        # Las filas de la simulación no deben mezclarse con $moved. Si se escriben en el mismo
        # registro, con solo volver a simular después de la ejecución real, el registro válido
        # quedaría sobrescrito por una lista de "destinos que no existen"
        $planned += [PSCustomObject]@{ Source = $src; Destination = $dest }
        continue
    }

    try {
        New-Item -ItemType Directory -Path (Split-Path -Parent $dest) -Force -ErrorAction Stop | Out-Null

        # El error de Move-Item no es terminante por defecto. Sin -ErrorAction Stop,
        # un bloqueo, un acceso denegado o una colisión harían que se pasara a la
        # siguiente línea, dejando escrito en el registro "movido" cuando en
        # realidad el archivo sigue en su ubicación original
        Move-Item -LiteralPath $srcFull -Destination $dest -ErrorAction Stop
    }
    catch {
        # El archivo que falló aquí no se movió. Sigue en su ubicación original
        $failed += [PSCustomObject]@{ Source = $src; Destination = $dest; Error = $_.Exception.Message }
        continue
    }

    # A partir de aquí, el archivo ya está en $dest. Pase lo que pase después,
    # el hecho de que "está en $dest" debe quedar siempre en el registro. Si esto
    # se metiera en el lado de failed, el registro se leería como "ubicación
    # original" cuando el archivo real está en la cuarentena, la peor de las
    # discrepancias, y al reejecutar Test-Path $src daría falso, así que nunca
    # más se volvería a recoger.
    #
    # Y no se debe esperar a salir del bucle para escribir todo junto. Si se corta
    # la alimentación a mitad de camino, ningún archivo movido quedaría registrado.
    # Se anexa uno por uno
    $record = [PSCustomObject]@{ Source = $srcFull; Destination = $dest; MovedAt = Get-Date }
    $record | Export-Csv -Path $ledger -NoTypeInformation -Encoding $enc -Append
    $moved += $record

    # Al mover dentro del mismo volumen, los permisos originales se conservan tal cual (se explica más adelante).
    # Se devuelve a la herencia predeterminada para que solo apliquen los permisos de la carpeta de cuarentena
    try {
        & icacls $dest /reset /q
        if ($LASTEXITCODE -ne 0) { throw "icacls /reset falló (exit $LASTEXITCODE)" }
    }
    catch {
        # Se movió, pero no se pudo restringir el permiso. Queda en la cuarentena con el permiso amplio original.
        # Esto también se anexa uno por uno
        $aclRow = [PSCustomObject]@{ Destination = $dest; Error = $_.Exception.Message }
        $aclRow | Export-Csv -Path $aclFile -NoTypeInformation -Encoding $enc -Append
        $needsAcl += $aclRow
    }
}

if ($dryRun) {
    $planned | Export-Csv -Path $planFile -NoTypeInformation -Encoding $enc
    Write-Host "Simulación: $($planned.Count) elementos son objetivo. No se ha movido nada ($planFile)"
    return
}

$failed | Export-Csv -Path $failFile -NoTypeInformation -Encoding $enc

# Lo que no se pudo restringir en permisos no se deja pasar en silencio. No inicie el período de observación hasta que esto llegue a 0
if ($needsAcl.Count -gt 0) {
    Write-Warning "Hay $($needsAcl.Count) archivos en cuarentena con permisos sin restringir. Revise $aclFile y resuélvalo"
}
Write-Host "Movidos: $($moved.Count) / Fallidos: $($failed.Count) / Permisos pendientes: $($needsAcl.Count)"
Write-Host "Registro: $ledger"

Antes de mover nada, confirme que la ruta esté realmente dentro del ámbito auditado. Lo que lee este script es old-files-approved.csv ── un archivo que una persona abre, del que borra filas y al que a veces añade líneas a mano. Aunque se cuele aquí una línea con un FullName que apunte fuera de $root, $src.Substring($root.Length) no lanza ninguna excepción. Como solo cuenta caracteres y corta el principio, construye sin inmutarse el destino de movimiento de un archivo completamente ajeno. Reutilizar un CSV antiguo, olvidar copiar una ruta, añadir un objetivo “de paso” ── todo esto ocurre en la práctica. Por el mismo motivo, Join-Path no resuelve .., así que una línea con separadores relativos puede dar como resultado un $dest que apunte fuera de la carpeta de cuarentena.

Por eso, tras convertir la ruta a su forma real con Resolve-Path, se confirma que quede dentro de $root, y $dest también se pliega con GetFullPath para comprobar que quede dentro de $quarantine. Añadir \ al final de las rutas de comparación evita que D:\share2 se considere incorrectamente dentro de D:\share. Las líneas fuera de rango no se mueven: se dejan en $failed para que se corrija el CSV. Estas dos comprobaciones existen para que, también desde el código, quede garantizado que la cuarentena “primero decide el objetivo y después mueve”.

El motivo de intercalar icacls /reset dentro del script es una cuestión de permisos. Como indica claramente la documentación oficial, un objeto hereda por defecto los permisos del padre de destino, pero la única excepción es “mover a otra carpeta dentro del mismo volumen”: solo en ese caso se conservan los permisos originales. 15

Es decir, la forma recomendada en el paso 1 —”mover al mismo volumen que el original”— es también una forma en la que los permisos de la carpeta de cuarentena no afectan a los archivos que contiene. Un archivo que estaba abierto al grupo compartido del departamento sigue siendo legible por ese grupo después de moverlo, incluso si la carpeta de cuarentena está restringida solo a los administradores. Aquí es donde se produce el peor fallo posible en este trabajo: “no se ha borrado, pero tampoco se ha puesto en cuarentena de verdad”. icacls <ruta> /reset reemplaza la ACL por la ACL de herencia predeterminada, así que si se ejecuta inmediatamente después del movimiento, el resultado es que solo aplican los permisos de la carpeta de cuarentena. 13

Si se mueve a otro volumen (otra unidad u otro servidor), en ese momento se hereda el permiso de la carpeta de destino. 15 En este caso no hace falta /reset, pero se da por sentado que el permiso de la propia carpeta de cuarentena ya se restringió de antemano. Sea cual sea el camino, es más seguro tomar unos cuantos archivos con Get-Acl después del movimiento y confirmar que los permisos son los esperados.

Preste atención también al tratamiento de los casos en los que el movimiento tuvo éxito pero falló el /reset. En ese momento, el archivo ya está en la cuarentena, pero conserva el permiso amplio original. Si esto se descarta del registro como “fallo”, quedará constancia de que sigue en su ubicación original, y como además al reejecutar la ruta original ya no existe, no se volverá a recoger. Es la peor forma posible: queda en la cuarentena, con el permiso amplio, sin figurar en ningún registro. Por eso el script anterior lo anota en el registro en cuanto se completa el movimiento, y emite en un CSV aparte lo que no se pudo restringir en permisos. Si el número no es cero, no inicie el período de observación hasta haberlo resuelto.

No use un nombre de archivo fijo para el registro. La cuarentena no termina en una sola pasada; se repite muchas veces, por departamento y por trimestre. Si se escribe con el nombre fijo quarantine-log.csv, en el momento en que se ejecute el segundo lote desaparece el registro del primero. Los archivos que se movieron entonces siguen estando en la cuarentena, pero no queda ningún registro de su procedencia ── no se pueden recuperar. Por eso el script anterior añade -$runId en cada ejecución. Para devolverlos, se leen todos con Import-Csv .\quarantine-log-*.csv.

El registro se anexa archivo por archivo. Si se hace un Export-Csv en bloque al salir del bucle, en caso de que la sesión se corte o el servidor se reinicie a mitad de camino, quedaría un estado en el que ningún archivo movido está registrado. Como el movimiento sí ocurrió realmente, esto no es un estado “previo al trabajo”, sino uno de “solo falta el registro”. Escribiendo uno por uno con -Append, se puede recuperar hasta donde se llegó, sin importar dónde se detuvo. Es más lento porque se reabre el archivo cada vez, pero el registro es un lugar donde importa más la fiabilidad que la velocidad.

Los archivos que genera el script están separados según su función ($runId es la hora de ejecución, por ejemplo 20260718-143052).

Archivo Contenido Cuándo se genera
quarantine-plan-$runId.csv Lista de lo que está previsto mover En la simulación ($dryRun = $true)
quarantine-log-$runId.csv Lo que realmente se movió. Es el registro que se usa para devolver los archivos. Se anexa uno por uno En la ejecución real
quarantine-failed-$runId.csv Lo que no se pudo mover. Permanece en su ubicación original En la ejecución real
quarantine-needs-acl-$runId.csv Lo que se movió pero no se pudo restringir en permisos Solo cuando corresponde

4. Inventario de permisos de acceso (ACL) ── convertir en tabla quién puede hacer qué

Junto con la capacidad, el otro objeto del inventario son los permisos. A partir de aquí la terminología aumenta de golpe, así que primero organizamos las relaciones.

Otra representación del mismo contenidoDescriptor de seguridadSe asigna a cada archivo o carpetaEs lo que obtiene Get-AclPropietarioSe registra mediante un SIDDACLLista de a quién se permite o deniega quéEs lo que se ve en la propiedad Access de Get-AclSACLConfiguración de auditoría. No se trata en este artículoACE ── una línea de la DACLIdentityReference: quién (SID o nombre resuelto)FileSystemRights: qué puede hacerAccessControlType: permiso o denegaciónIsInherited: heredado o asignado directamenteACEACE ...SDDLFormato que representa todo este descriptor como una sola cadena de texto

Dicho en palabras, es una estructura anidada: dentro del descriptor de seguridad está la DACL, dentro de la DACL se alinean las ACE, y a quien apunta cada ACE es un SID. Lo que en la práctica se llama “ACL” se refiere casi siempre a esta DACL. Lea las tablas y los CSV que aparecen más adelante pensando que son, línea por línea, las ACE más internas volcadas una a una. El SDDL es una representación en cadena de texto del mismo contenido, y en la etapa de inventario no hace falta leerlo.

Con esa base, vamos al tema. Get-Acl obtiene el descriptor de seguridad de un archivo o carpeta, y a través de la propiedad Access se puede leer la lista de entradas de control de acceso (ACE) de la DACL. 7

# Se inventaría la ACL de las carpetas hasta 2 niveles desde la raíz del recurso compartido
# El "tronco" del diseño de permisos se concentra en los niveles superficiales, así que primero se revisa ahí
$aclErrors = @()   # -ErrorVariable con + es de tipo anexado, así que se reinicializa en cada ejecución para no arrastrar la anterior
$targets = @(Get-Item -LiteralPath $root `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors) +
           @(Get-ChildItem -LiteralPath $root -Directory -Recurse -Depth 1 `
                 -ErrorAction SilentlyContinue -ErrorVariable +aclErrors)

$aclReport = foreach ($t in $targets) {
    # También se registra el fallo de obtención (imposible leer, borrado durante el recorrido, etc.) para revelarlo después
    $acl = Get-Acl -LiteralPath $t.FullName -ErrorAction SilentlyContinue -ErrorVariable +aclErrors
    if (-not $acl) { continue }
    if (@($acl.Access).Count -eq 0) {
        # Las carpetas con DACL vacía (cero entradas explícitas) también se conservan como fila, para que la información de herencia cortada no desaparezca del registro
        [PSCustomObject]@{
            Path = $t.FullName; Identity = '(sin entradas)'; Rights = $null; Type = $null
            IsInherited = $null; Inheritance = $null
            InheritanceBroken = $acl.AreAccessRulesProtected
        }
        continue
    }
    foreach ($ace in $acl.Access) {
        [PSCustomObject]@{
            Path        = $t.FullName
            Identity    = $ace.IdentityReference    # Quién (usuario/grupo)
            Rights      = $ace.FileSystemRights     # Qué puede hacer
            Type        = $ace.AccessControlType    # Allow / Deny
            IsInherited = $ace.IsInherited          # Heredado del padre o asignado directamente
            # Distingue si el mismo permiso aplica "solo a esta carpeta" o "también a los archivos/carpetas de abajo"
            Inheritance = "$($ace.InheritanceFlags)/$($ace.PropagationFlags)"
            InheritanceBroken = $acl.AreAccessRulesProtected  # Si en esta carpeta se cortó la herencia
        }
    }
}
$aclReport | Export-Csv -Path .\acl-report.csv -NoTypeInformation -Encoding utf8BOM   # En 5.1, use UTF8

# Los objetivos donde no se pudo obtener la ACL también se revelan siempre como un hueco del registro (mismo criterio que en las demás auditorías)
$aclErrors | ForEach-Object { $_.TargetObject } |
    Set-Content -Path .\acl-uninspected.txt

Al abrir este CSV, filtre con los siguientes criterios para buscar anomalías.

Columna a revisar Señal de anomalía Trasfondo típico
Identity Aparece directamente el nombre de una persona Acumulación de “permiso a esta persona, por ahora”. Se pudre con traslados y bajas
Identity Se muestra como un SID sin resolver, tipo S-1-5-21-... Queda el permiso de una cuenta ya eliminada (primer candidato a limpiar)
IsInherited = False Hay ACE de asignación directa dispersas en niveles profundos Asignaciones puntuales improvisadas. Excepciones que se salen del diseño
InheritanceBroken = True Carpeta con la herencia cortada Una medida pasada del tipo “no quiero que se vea esta carpeta en particular”. Objetivo prioritario del inventario
Type = Deny Existe una ACE de denegación En igualdad de condiciones, la denegación tiene prioridad sobre el permiso. Sin embargo, como las ACE explícitas se evalúan antes que las heredadas, también hay casos en los que un permiso explícito gana a una denegación heredada. Revise el origen de la ACE y, en última instancia, confirme el acceso efectivo

Una entrada con IsInherited en False es “un permiso que alguien añadió a mano en esa carpeta”, y una carpeta con AreAccessRulesProtected en True es “una isla a la que no llegan los cambios de permisos del padre”. 78 La mayoría de los problemas de permisos se concentran en estos dos casos, así que empiece a registrarlos por ahí. Conviene dejar constancia también de una limitación: como este ejemplo es un recorrido superficial con -Depth 1, las carpetas que cortaron la herencia en niveles más profundos quedan fuera del objetivo, y tampoco se genera ningún error. Si quiere investigar exhaustivamente todas las herencias cortadas, aunque lleve tiempo, quite el límite de -Depth y ejecute el mismo script, o vuelva a ejecutarlo señalando como raíz la carpeta del departamento que sospeche. Por otro lado, Get-Acl también puede mostrar el descriptor de seguridad en el formato de cadena SDDL 7, pero para fines de inventario basta con la tabla de la propiedad Access. No es tarde para profundizar en la lectura del SDDL una vez que se encuentre con un fenómeno que esta tabla no pueda explicar.

Una advertencia práctica más. Especifique siempre la codificación de caracteres del CSV. Export-Csv de Windows PowerShell 5.1 emite en ASCII por defecto, lo que corrompe los caracteres no ASCII. 10 El valor predeterminado de la serie PowerShell 7 es utf8NoBOM (UTF-8 sin BOM) 11, y en algunos entornos, al abrirlo directamente con Excel, aparece con caracteres corruptos. Si el archivo se va a entregar a un departamento, lo más seguro es orientarse a UTF-8 con BOM, por ejemplo con -Encoding UTF8BOM (en 5.1, -Encoding UTF8).

5. Cómo hacer los cambios ── icacls frente a Set-Acl, copias de seguridad y -WhatIf

Solo tras completar el inventario y la confirmación con el departamento comienza la fase de cambios. Aquí hay dos familias de herramientas.

Uso Herramienta Motivo
Investigación e informes Get-Acl Como el resultado es un objeto, es fácil filtrarlo y convertirlo a CSV 7
Copia de seguridad previa al cambio icacls /save Guarda la ACL en un archivo y con /restore se puede devolver tal cual 13
Asignación/eliminación de permisos habitual icacls /grant, /remove Se resuelve en una sola línea, /t la aplica de forma recursiva, y también permite reactivar la herencia (/inheritancelevel) 13
Cambios condicionales complejos Set-Acl Permite construir las reglas por código. Compatible con -WhatIf 12
Reconstrucción de una ACL dañada icacls /reset Reemplaza por la ACL de herencia predeterminada (impacto grande. Último recurso) 13

Sea cual sea la herramienta que use, el orden es fijo: copia de seguridad → -WhatIf (o confirmación del objetivo) → aplicación → verificación posterior.

# 1. Antes de cambiar nada, guarde la ACL en un archivo (/t para todo lo que hay debajo, /c para continuar aunque haya errores)
icacls "D:\share\sales" /save "C:\aclbackup\sales-acl.txt" /t /c
# No avance al cambio si la copia de seguridad no se completó. Confirme el éxito de icacls por su código de salida
if ($LASTEXITCODE -ne 0) {
    throw "Falló la copia de seguridad de la ACL (icacls ExitCode=$LASTEXITCODE). Se cancela el trabajo de cambio"
}

# 2. Si el cambio se hace con Set-Acl: obtener → editar reglas → aplicar, en tres pasos
$path = 'D:\share\sales\estimate'
$acl  = Get-Acl -LiteralPath $path

# Regla que otorga permiso de modificación al grupo de ventas (heredado por subcarpetas y archivos)
$rule = New-Object System.Security.AccessControl.FileSystemAccessRule(
    'CONTOSO\SalesTeam', 'Modify', 'ContainerInherit,ObjectInherit', 'None', 'Allow')
# La asignación adicional se hace con AddAccessRule. SetAccessRule reemplaza las reglas Allow
# existentes del mismo usuario/grupo, por lo que los permisos detallados ya configurados para
# ese grupo pueden desaparecer en silencio
$acl.AddAccessRule($rule)

# 3. Primero confirme con -WhatIf "dónde se aplicará" y luego avance a la ejecución real
Set-Acl -LiteralPath $path -AclObject $acl -WhatIf
# Si no hay problemas: Set-Acl -LiteralPath $path -AclObject $acl

Set-Acl funciona “aplicando como modelo el descriptor de seguridad obtenido con Get-Acl” 12, así que si se confunde el objetivo entre la obtención y la aplicación, se produce un reemplazo completo e involuntario de la ACL. El hábito, poco vistoso pero eficaz, de mantener la ruta de obtención y la de aplicación en la misma variable, como en el ejemplo anterior, y de intercalar -WhatIf antes de aplicar, evita accidentes. La operación de cortar la herencia (SetAccessRuleProtection) también se puede hacer dentro del mismo marco 12, pero, como se vio en el capítulo 4, una herencia cortada supone un coste de gestión futuro, así que limite las cortes nuevas a los lugares donde realmente sean necesarias.

El diseño necesario para convertir este conjunto de trabajos en un script de ejecución periódica (SupportsShouldProcess, cómo dejar rastro de auditoría, el registro en el Programador de tareas) puede reutilizar tal cual el patrón tratado en “Aplicaciones prácticas de scripts de PowerShell ── automatizar de forma segura la investigación de registros, el archivado y la generación de informes”.

6. Dónde ejecutar ── el recorrido a través de UNC y la cuenta de ejecución

Por último, hablemos de “dónde se ejecuta y con qué cuenta”, algo que influye en el resultado más de lo que parece.

  • Como principio, los recorridos a gran escala deben ejecutarse en el propio servidor. En una enumeración recursiva a través de una ruta UNC (\\fs01\share), la obtención de metadatos de cada archivo implica un viaje de ida y vuelta por la red, así que en volúmenes de cientos de miles de archivos el tiempo requerido cambia en un orden de magnitud. Si quiere hacerlo desde una estación de administración, lo más práctico es ejecutar el script en el lado del servidor mediante PowerShell Remoting y recoger solo el CSV con los resultados. Consulte el procedimiento en “Introducción a PowerShell Remoting (WinRM)”, publicado simultáneamente.
  • El resultado es una instantánea de los permisos de la cuenta con la que se ejecuta. Los lugares inaccesibles quedan fuera tanto de la enumeración como de Get-Acl. Realice la investigación con una cuenta con permisos de administrador y, aun así, incluya en el informe los lugares que sigan denegados (denied-paths.txt del capítulo 2) como “lugares no investigados”.
  • Si se ejecuta a través de UNC, también hay que tener cuidado con las trampas de las credenciales y la sesión. Las letras de unidad son propias de cada sesión de inicio de sesión, así que una Z: asignada durante un inicio de sesión interactivo no es visible desde un trabajo del Programador de tareas. Además, el error 1219 (ERROR_SESSION_CREDENTIAL_CONFLICT) es un error de diseño según el cual no se permite que el mismo usuario se conecte al mismo servidor (o recurso compartido) usando varios nombres de usuario distintos. 16 Si intenta conectarse al mismo servidor de archivos con dos tipos de credenciales —por ejemplo, “solo el recurso compartido de investigación con la cuenta de administrador, y el resto con mi propia cuenta”— la segunda conexión fallará por este motivo. Si va a investigar a través de UNC, unifique las conexiones existentes en una sola credencial antes de ejecutar el script. El panorama completo de esta trampa está reunido en “Trampas de las unidades de red y las rutas UNC”.
  • El tiempo requerido se mide en su propio entorno, no con “cifras ajenas”. El resultado varía enormemente según el tipo de disco, el número de archivos, el análisis en tiempo real del antivirus y la profundidad de los niveles, así que lo más seguro es elegir una carpeta de departamento representativa, medirla antes de lanzar el proceso completo, y extrapolar al número total de archivos del recurso compartido. Si se hace la misma medición tanto a través de UNC como en el propio servidor, también se puede demostrar con números si conviene “ejecutarlo en el servidor”.
# Se elige una carpeta de departamento representativa, se mide la velocidad real del recorrido y sobre eso se hace la estimación del total
# (si se elige una carpeta sin ningún archivo, la división final falla; elija un lugar con datos reales)
$sample = 'D:\share\sales'
$start  = Get-Date
$count  = (Get-ChildItem -LiteralPath $sample -Recurse -File `
    -ErrorAction SilentlyContinue | Measure-Object).Count
$sec    = ((Get-Date) - $start).TotalSeconds
'{0} archivos enumerados en {1:N1} s → {2:N1} s por cada 10 000 archivos' -f `
    $count, $sec, ($sec / $count * 10000)

Esta estimación funciona para los procesos centrados en la enumeración (el agregado de capacidad del capítulo 2, el listado de archivos antiguos del capítulo 3 y la obtención de ACL del capítulo 4). Solo el cálculo de hash del capítulo 3 tiene un tiempo determinado por el número total de bytes, no por el número de archivos, así que estímelo aparte.

7. Reglas prácticas (tabla de decisión)

Punto Opciones Criterio de decisión
Alcance de la auditoría de capacidad Todo de una vez / por carpeta de primer nivel Primero se identifica el peso en niveles superficiales, y solo los lugares que pesan se profundizan 12
Tratamiento de errores Detener / Ignorar / registrar y continuar SilentlyContinue + ErrorVariable para “continuar registrando todo”. Las zonas denegadas se dejan explícitas en el informe 3
Criterio de archivo antiguo LastAccessTime / LastWriteTime La fecha de acceso no se adopta porque en muchos entornos su actualización está deshabilitada. Se acuerda con el departamento la fecha de modificación más un período 45
Detección de duplicados Hash de todos los archivos / filtrar por tamaño y luego calcular el hash El hash es costoso en E/S. Solo se calcula para los grupos del mismo tamaño 6
Profundidad de la auditoría de ACL Todas las carpetas / niveles superficiales + lugares con herencia cortada Si la herencia está viva, basta con mirar al padre. El foco está en IsInherited=False y Protected=True 78
Herramienta para cambiar permisos icacls / Set-Acl icacls para cambios habituales y copias de seguridad. Set-Acl+-WhatIf para procesos por lotes con condiciones 1312
Ejecución de la eliminación Borrado inmediato / cuarentena → observación → eliminación Tras la confirmación del departamento sobre el listado, primero se mueve a la carpeta de cuarentena y, si no hay problemas tras un período, se elimina

8. Resumen

  • Ordenar un servidor de archivos empieza por el inventario, no por el borrado. Genere en CSV el conjunto de cuatro entregables —ranking de capacidad por carpeta de primer nivel, lista de archivos antiguos, candidatos a duplicados y registro de ACL— y úselo como material para conversar con el departamento.
  • El recorrido no se detiene gracias a SilentlyContinue, pero a cambio se registran siempre con ErrorVariable los lugares denegados. Un lugar que no se pudo contar no es “cero”, es “desconocido”.
  • El criterio de antigüedad se basa en LastWriteTime. La fecha de último acceso de NTFS no es confiable, porque en muchos entornos su actualización está deshabilitada.
  • Los duplicados se filtran primero por tamaño y luego con Get-FileHash (SHA256 por defecto). Una coincidencia es un “candidato”, y la decisión de cuál conservar se toma con el área de negocio.
  • La ACL se vuelca a CSV desde la propiedad Access de Get-Acl, y se revisan con prioridad las asignaciones directas a personas, las entradas que quedan solo como SID y la herencia cortada (AreAccessRulesProtected). No hace falta profundizar en el SDDL.
  • Los cambios siguen el orden: copia de seguridad con icacls /save → -WhatIf o confirmación del objetivo → aplicación → verificación posterior. Los recorridos a gran escala se ejecutan en el propio servidor (o mediante Remoting), con permisos de administrador.

Artículos relacionados

Áreas de consultoría relacionadas

KomuraSoft LLC ofrece apoyo en la creación de scripts para inventariar la capacidad y los permisos de un servidor de archivos, investigaciones asociadas a la revisión del diseño de permisos, y la automatización de informes periódicos (incluida la operación con el Programador de tareas). También son bienvenidas las consultas desde la etapa de “primero quiero convertir la situación actual en números”.

Referencias

  1. Microsoft Learn, Get-ChildItem. Sobre la enumeración de elementos con Get-ChildItem, la recursión con -Recurse y la limitación de profundidad con -Depth, y sobre que al usar -Recurse se recomienda especificar el objetivo con -LiteralPath para evitar la interpretación de comodines.  2 3

  2. Microsoft Learn, Measure-Object. Sobre que Measure-Object puede calcular la suma, el máximo, el mínimo y el promedio del tamaño de archivo con -Property Length -Sum, entre otros, y sobre el ejemplo de agregación de archivos de un directorio combinándolo con Get-ChildItem.  2 3

  3. Microsoft Learn, about_CommonParameters. Sobre que -ErrorAction SilentlyContinue suprime la visualización del error y continúa la ejecución, que -ErrorVariable almacena los registros de error en la variable indicada, y que anteponer + al nombre de la variable hace que se anexe en lugar de sobrescribir.  2 3 4

  4. Microsoft Learn, fsutil behavior. Sobre que la activación o desactivación de la actualización de la fecha de último acceso (Last Access Time) de NTFS se controla con el parámetro disablelastaccess de fsutil behavior y el valor de registro NtfsDisableLastAccessUpdate, y sobre que la desactivación de la actualización está pensada para mejorar la velocidad de acceso a archivos/directorios.  2 3

  5. Microsoft Learn, [MS-FSA]: Appendix A: Product Behavior. Sobre que desde Windows Vista la actualización de la fecha de último acceso de NTFS/ReFS está deshabilitada por defecto, que desde Windows 10 v1803 queda gestionada por el sistema, y que en los sistemas servidor la actualización de la fecha de último acceso siempre está deshabilitada.  2 3

  6. Microsoft Learn, Get-FileHash. Sobre que el algoritmo predeterminado de Get-FileHash es SHA256, que si el valor de hash de dos archivos coincide se puede considerar que su contenido es idéntico, y que el valor de hash no cambia aunque se modifique el nombre o la extensión del archivo.  2 3

  7. Microsoft Learn, Get-Acl. Sobre que Get-Acl obtiene el descriptor de seguridad de un archivo o recurso, que por defecto muestra la lista de entradas de control de acceso de la DACL (Access), y que también se puede obtener en formato SDDL (propiedad Sddl).  2 3 4 5 6

  8. Microsoft Learn, ObjectSecurity.AreAccessRulesProtected Property. Sobre que la propiedad AreAccessRulesProtected indica si la DACL del descriptor de seguridad está protegida (no recibe herencia del padre).  2 3

  9. Microsoft Learn, Get-SmbShareAccess. Sobre que Get-SmbShareAccess es un cmdlet que obtiene la ACL de un recurso compartido SMB (las entidades de seguridad a las que se otorgó acceso al recurso compartido y sus permisos/derechos de permitir o denegar). 

  10. Microsoft Learn, about_Character_Encoding. Sobre que en Windows PowerShell (5.1) la codificación predeterminada varía según el cmdlet y Export-Csv crea el archivo en ASCII, y sobre que desde PowerShell 6 el valor predeterminado se unificó a utf8NoBOM.  2

  11. Microsoft Learn, Export-Csv. Sobre que Export-Csv crea un archivo CSV con las propiedades de cada objeto como columnas, que en la serie PowerShell 7 el valor predeterminado de -Encoding es UTF8NoBOM, y que se pueden especificar explícitamente otros valores como UTF8BOM.  2

  12. Microsoft Learn, Set-Acl. Sobre que Set-Acl cambia la ACL del objetivo usando como modelo el descriptor de seguridad pasado en AclObject, que es compatible con -WhatIf/-Confirm, sobre el procedimiento de crear una FileSystemAccessRule y agregarla con AddAccessRule(), y sobre el ejemplo de deshabilitar la herencia con SetAccessRuleProtection() (con la opción de conservar las reglas heredadas existentes).  2 3 4 5

  13. Microsoft Learn, icacls. Sobre que /reset “reemplaza la ACL del archivo objetivo por la ACL de herencia predeterminada” y que /q suprime el mensaje de éxito.  2 3 4 5 6

  14. Microsoft Learn, Maximum Path Length Limitation. Sobre que el límite MAX_PATH de la API de Windows es de 260 caracteres, y que la restricción solo se relaja en muchas funciones Win32 cuando coinciden tanto el valor de registro LongPathsEnabled como la declaración longPathAware del lado de la aplicación. 

  15. Microsoft Learn, Permissions on copying and moving files and folders. Sobre que por defecto un objeto hereda los permisos de acceso del padre de destino o de creación, que la única excepción es el movimiento a otra carpeta dentro del mismo volumen, en cuyo caso se conservan los permisos de acceso originales, y que al copiar o mover a otro volumen se heredan los permisos de acceso de la carpeta de destino.  2

  16. Microsoft Learn, System Error Codes (1000-1299). Sobre que el error 1219 (ERROR_SESSION_CREDENTIAL_CONFLICT) es el error que indica que “no se permite que el mismo usuario realice varias conexiones al mismo servidor o recurso compartido usando distintos nombres de usuario”. 

Artículos recientes con las mismas etiquetas para profundizar en temas cercanos.

Estas páginas sitúan el tema en un contexto más amplio de servicios y decisiones.

El artículo está directamente relacionado con los siguientes servicios.

Preguntas frecuentes

Preguntas habituales en las consultas sobre el tema del artículo.

¿Cómo se calcula el tamaño total por carpeta con PowerShell?
Enumere los archivos con Get-ChildItem usando -Recurse y sume con la propiedad -Sum de Measure-Object sobre Length. En la práctica es mejor no agregarlo todo de golpe: primero calcule por cada carpeta de primer nivel bajo la raíz del recurso compartido, para identificar 'dónde está el peso'. Combine esto con -ErrorAction SilentlyContinue y -ErrorVariable, y registre siempre los lugares que no se pudieron contar por acceso denegado. Si se ignora el error en silencio, los lugares que quedaron fuera del cálculo parecerán tener 'tamaño cero'.
¿Puedo usar LastAccessTime (fecha de último acceso) para determinar si un archivo es antiguo?
No se lo recomiendo. En NTFS, por motivos de rendimiento, la actualización de la fecha de último acceso suele estar deshabilitada de forma predeterminada (o gestionada por el sistema) en muchos entornos, por lo que puede ocurrir que un archivo se haya leído realmente sin que su fecha se actualice. Use LastWriteTime (fecha de última modificación) como criterio para el inventario y preséntelo al departamento como 'tiempo transcurrido desde el último cambio de contenido'. No tome la decisión de archivar de forma mecánica: haga que los responsables revisen el listado antes de continuar.
¿Qué información entrega Get-Acl? ¿Es necesario leer el SDDL?
Get-Acl obtiene el descriptor de seguridad de un archivo o carpeta, y a través de la propiedad Access se puede leer 'quién' (IdentityReference), 'qué' (FileSystemRights), 'si permite o deniega' (AccessControlType) y 'si es heredado o asignado directamente' (IsInherited). Con AreAccessRulesProtected también se puede saber si la herencia está cortada en esa carpeta. También existe el formato de cadena SDDL, pero para fines de inventario es más fácil compartir con los responsables si se exporta la propiedad Access a un CSV; no hace falta profundizar en el SDDL.
¿Los cambios de ACL deben hacerse con Set-Acl de PowerShell o con icacls?
Como criterio general, use Get-Acl —que trata los datos como objetos— para la investigación y los informes, e icacls como primera opción para el trabajo real de cambios. icacls permite guardar la ACL en un archivo con /save y restaurarla tal cual con /restore, lo que facilita la copia de seguridad y la reversión antes de un cambio. Si usa Set-Acl, el proceso tiene tres pasos: obtener con Get-Acl, editar las reglas y aplicar, y conviene confirmar el objetivo con -WhatIf antes de ejecutar. En cualquiera de los dos casos, el principio es avanzar al cambio solo después de completar el inventario y la confirmación con el departamento correspondiente.
¿Se puede investigar un servidor de archivos a través de una ruta UNC?
Si el volumen es pequeño no hay problema, pero un recorrido completo de cientos de miles de archivos a través de la red resulta mucho más lento. Si es posible, es más eficiente ejecutar directamente en el servidor de archivos (o mediante PowerShell Remoting) y llevarse solo el CSV con los resultados. Además, el resultado del recorrido depende de los permisos de la cuenta con la que se ejecuta: las carpetas inaccesibles quedan fuera de la enumeración, así que ejecute con una cuenta de administrador y, aun así, registre los lugares que sigan denegados como 'lugares no investigados' en el informe.

Perfil del autor

Página de presentación del autor del artículo.

Go Komura

Representante de KomuraSoft LLC

Especializado en desarrollo de software para Windows, consultoría técnica e investigación de fallos, sobre todo en proyectos con sistemas existentes y errores difíciles de reproducir.

Volver al blog