Public/Clear-OldIISLog.ps1
|
function Clear-OldIISLog { <# .EXTERNALHELP TheCleaners-help.xml #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] [Alias('Clean-IISLog')] [OutputType('TheCleaners.CleanupResult')] param ( [Parameter()] [ValidateRange(1, [Int16]::MaxValue)] [Int16] $Days = 60, [Parameter()] [switch] $PassThru ) if (-not $PSBoundParameters.ContainsKey('WhatIf') -or -not $PSBoundParameters['WhatIf']) { $Exception = [System.NotSupportedException]::new('IIS cleanup is preview-only. Run Clear-OldIISLog -WhatIf. Removal is not available in this version.') $ErrorRecord = Get-TheCleanersErrorRecord -Exception $Exception -ErrorId 'IISCleanupPreviewOnly' -Category NotImplemented $PSCmdlet.ThrowTerminatingError($ErrorRecord) } if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) { $Exception = [System.PlatformNotSupportedException]::new('IIS log discovery requires Windows.') $ErrorRecord = Get-TheCleanersErrorRecord -Exception $Exception -ErrorId 'IISWindowsRequired' -Category NotImplemented $PSCmdlet.ThrowTerminatingError($ErrorRecord) } Write-Warning -Message 'IIS preview only: no files will be removed. Candidate discovery is experimental, not a deletion allowlist.' $CutoffUtc = (Get-Date).ToUniversalTime().AddDays(-$Days) $Roots = [System.Collections.Generic.List[object]]::new() $SeenRoots = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase) $FtpDiscoveryErrorRecords = [System.Collections.Generic.List[object]]::new() $DiscoveryErrorReported = $false $DiscoveryErrorIds = [System.Collections.Generic.List[string]]::new() $DiscoveryFailureResultReported = $false $IisProtectedPaths = @() try { $IisProtectedPaths = @(Get-TheCleanersIisProtectedPaths) } catch { $DiscoveryErrorReported = $true $DiscoveryFailureResultReported = $true $null = $DiscoveryErrorIds.Add('IISDiscoveryFailed') $ProtectionInventoryTarget = 'IIS protected-path inventory' $ErrorRecord = Get-TheCleanersErrorRecord -Exception $_.Exception -ErrorId 'IISDiscoveryFailed' -Category ReadError -TargetObject $ProtectionInventoryTarget $PSCmdlet.WriteError($ErrorRecord) if ($PassThru) { $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $ProtectionInventoryTarget -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Unknown' -ProtectionPathCount 0 -ProtectionPaths @() -DiscoverySource 'IIS protection inventory' -DisplayName 'IIS protection inventory' -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = 1 $Result.ErrorIds = @('IISDiscoveryFailed') $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } return } $WebAdministrationModule = Get-Module -Name 'WebAdministration' -ListAvailable | Select-Object -First 1 if ($null -ne $WebAdministrationModule) { try { $WebAdministrationDiscovery = [System.Management.Automation.PowerShell]::Create() try { $null = $WebAdministrationDiscovery.AddScript({ param ( [Parameter(Mandatory)] [string] $ModulePath ) $ErrorActionPreference = 'Stop' Import-Module -Name $ModulePath -Scope Local -ErrorAction Stop foreach ($Site in @(WebAdministration\Get-Website -ErrorAction Stop)) { $FtpLogConfiguration = $null $FtpConfigurationErrorMessage = $null $FtpBindings = @($Site.Bindings | Where-Object { [string]$_.Protocol -ieq 'ftp' }) if ($FtpBindings.Count -gt 0) { try { $SiteIdProperty = $Site.PSObject.Properties['Id'] if ($null -eq $SiteIdProperty -or [string]::IsNullOrWhiteSpace([string]$SiteIdProperty.Value)) { throw [System.InvalidOperationException]::new("The IIS FTP site '$($Site.Name)' has no numeric site identifier.") } $SiteId = [int64]::Parse([string]$SiteIdProperty.Value, [System.Globalization.CultureInfo]::InvariantCulture) $FtpFilter = "system.applicationHost/sites/site[@id='$SiteId']/ftpServer/logFile" $FtpLogConfiguration = @(WebAdministration\Get-WebConfiguration -Filter $FtpFilter -PSPath 'MACHINE/WEBROOT/APPHOST' -ErrorAction Stop | Select-Object -First 1) if ($FtpLogConfiguration.Count -eq 0) { throw [System.InvalidOperationException]::new("The IIS FTP log configuration was not returned for site '$($Site.Name)'.") } $FtpLogConfiguration = $FtpLogConfiguration[0] } catch { $FtpConfigurationErrorMessage = $_.Exception.Message } } [pscustomobject]@{ Name = $Site.Name Id = $Site.Id LogFile = $Site.LogFile Bindings = $Site.Bindings FtpLogConfiguration = $FtpLogConfiguration FtpConfigurationErrorMessage = $FtpConfigurationErrorMessage } } }).AddArgument($WebAdministrationModule.Path) $DiscoveredSites = @($WebAdministrationDiscovery.Invoke()) if ($WebAdministrationDiscovery.HadErrors) { $DiscoveryError = @($WebAdministrationDiscovery.Streams.Error | Select-Object -First 1) if ($DiscoveryError.Count -gt 0) { throw $DiscoveryError[0].Exception } throw [System.InvalidOperationException]::new('IIS website discovery failed in the isolated dependency runspace.') } } finally { $WebAdministrationDiscovery.Dispose() } foreach ($Site in $DiscoveredSites) { $ConfiguredRoot = [Environment]::ExpandEnvironmentVariables([string]$Site.LogFile.Directory) $SiteName = [string]$Site.Name $WebRootDefinition = $null if ([string]::IsNullOrWhiteSpace($ConfiguredRoot)) { Write-Verbose -Message "IIS site '$($Site.Name)' has no log directory." } else { $Format = [string]$Site.LogFile.LogFormat $WebDiscoveryErrorIds = [System.Collections.Generic.List[string]]::new() if ([string]::IsNullOrWhiteSpace($Format)) { $null = $WebDiscoveryErrorIds.Add('IISLogFormatUnavailable') } $LocalTimeRollover = $null $RolloverProperty = if ($null -eq $Site.LogFile) { $null } else { $Site.LogFile.PSObject.Properties['LocalTimeRollover'] } if ($null -ne $RolloverProperty -and $null -ne $RolloverProperty.Value) { $LocalTimeRollover = [bool]$RolloverProperty.Value } else { $null = $WebDiscoveryErrorIds.Add('IISLocalTimeRolloverUnavailable') } $WebRootDefinition = [pscustomobject]@{ Path = Join-Path -Path $ConfiguredRoot -ChildPath ('W3SVC{0}' -f $Site.Id) DisplayName = $SiteName Source = 'WebAdministration' Format = if ([string]::IsNullOrWhiteSpace($Format)) { $null } else { $Format } Service = 'W3SVC' LocalTimeRollover = $LocalTimeRollover DiscoveryErrorIds = $WebDiscoveryErrorIds } $Roots.Add($WebRootDefinition) } $FtpBindings = @($Site.Bindings | Where-Object { [string]$_.Protocol -ieq 'ftp' }) if ($FtpBindings.Count -eq 0) { continue } try { if (-not [string]::IsNullOrWhiteSpace([string]$Site.FtpConfigurationErrorMessage)) { throw [System.InvalidOperationException]::new($Site.FtpConfigurationErrorMessage) } if ($null -eq $Site.FtpLogConfiguration) { throw [System.InvalidOperationException]::new("The IIS FTP log configuration was unavailable for site '$SiteName'.") } $FtpConfiguredRoot = [Environment]::ExpandEnvironmentVariables([string]$Site.FtpLogConfiguration.Directory) if ([string]::IsNullOrWhiteSpace($FtpConfiguredRoot)) { if ([string]::IsNullOrWhiteSpace($env:SystemDrive)) { throw [System.InvalidOperationException]::new("Cannot determine the default IIS FTP log directory for site '$SiteName'.") } $FtpConfiguredRoot = Join-Path -Path $env:SystemDrive -ChildPath 'inetpub/logs/LogFiles' } $FtpFormat = [string]$Site.FtpLogConfiguration.LogFormat if ([string]::IsNullOrWhiteSpace($FtpFormat)) { throw [System.InvalidOperationException]::new("The IIS FTP logging format was unavailable for site '$SiteName'.") } $FtpLocalTimeRollover = $null $FtpRolloverProperty = $Site.FtpLogConfiguration.PSObject.Properties['LocalTimeRollover'] if ($null -ne $FtpRolloverProperty -and $null -ne $FtpRolloverProperty.Value) { $FtpLocalTimeRollover = [bool]$FtpRolloverProperty.Value } else { throw [System.InvalidOperationException]::new("The IIS FTP local-time rollover setting was unavailable for site '$SiteName'.") } $Roots.Add([pscustomobject]@{ Path = Join-Path -Path $FtpConfiguredRoot -ChildPath ('FTPSVC{0}' -f $Site.Id) DisplayName = "$SiteName FTP" Source = 'WebAdministration' Format = $FtpFormat Service = 'FTPSVC' LocalTimeRollover = $FtpLocalTimeRollover DiscoveryErrorIds = [System.Collections.Generic.List[string]]::new() }) } catch { $DiscoveryErrorReported = $true $DiscoveryFailureResultReported = $true $null = $DiscoveryErrorIds.Add('IISFtpDiscoveryFailed') if ($PassThru) { $FtpFailureRootPath = "IIS FTP log root unavailable for '$SiteName'" $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $FtpFailureRootPath -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource 'WebAdministration' -DisplayName "$SiteName FTP" -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = 1 $Result.ErrorIds = @('IISFtpDiscoveryFailed') $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } $ErrorRecord = Get-TheCleanersErrorRecord -Exception $_.Exception -ErrorId 'IISFtpDiscoveryFailed' -Category ReadError -TargetObject $SiteName $null = $FtpDiscoveryErrorRecords.Add($ErrorRecord) } } } catch { $DiscoveryErrorReported = $true $null = $DiscoveryErrorIds.Add('IISDiscoveryFailed') foreach ($RootDefinition in @($Roots | Where-Object { $_.Source -eq 'WebAdministration' })) { $null = $RootDefinition.DiscoveryErrorIds.Add('IISDiscoveryFailed') } $ErrorRecord = Get-TheCleanersErrorRecord -Exception $_.Exception -ErrorId 'IISDiscoveryFailed' -Category ReadError $PSCmdlet.WriteError($ErrorRecord) } } else { $DefaultRootDefinition = $null $DefaultDiscoveryErrorIds = [System.Collections.Generic.List[string]]::new() $null = $DefaultDiscoveryErrorIds.Add('IISLogFormatUnavailable') $null = $DefaultDiscoveryErrorIds.Add('IISLocalTimeRolloverUnavailable') if (-not [string]::IsNullOrWhiteSpace($env:SystemDrive)) { $DefaultRootDefinition = [pscustomobject]@{ Path = Join-Path -Path $env:SystemDrive -ChildPath 'inetpub/logs/LogFiles' DisplayName = 'Default IIS log root' Source = 'DefaultPath' Format = $null Service = 'W3SVC' LocalTimeRollover = $null DiscoveryErrorIds = $DefaultDiscoveryErrorIds } $Roots.Add($DefaultRootDefinition) } $RegistryRoot = $null $RegistryFormat = $null $RegistryLocalTimeRollover = $null try { try { $RegistrySettings = Get-ItemProperty -LiteralPath 'HKLM:\System\CurrentControlSet\Services\W3SVC\Parameters' -Name 'LogDir' -ErrorAction Stop $RegistryRoot = [Environment]::ExpandEnvironmentVariables([string]$RegistrySettings.LogDir) } catch { $OptionalRegistryLogDirIsAbsent = ( $_.Exception -is [System.Management.Automation.ItemNotFoundException] -or ($_.Exception -is [System.Management.Automation.PSArgumentException] -and $_.Exception.Message -match '^Property .+ does not exist') -or $_.FullyQualifiedErrorId -match 'PathNotFound|PropertyNotFound|ItemNotFound' ) if (-not $OptionalRegistryLogDirIsAbsent) { throw } Write-Verbose -Message "The optional alternate IIS log location is not configured: $($_.Exception.Message)" } try { $RegistryFormatSettings = Get-ItemProperty -LiteralPath 'HKLM:\System\CurrentControlSet\Services\W3SVC\Parameters' -Name 'LogFormat' -ErrorAction Stop $RegistryFormat = [string]$RegistryFormatSettings.LogFormat } catch { $OptionalRegistryFormatIsAbsent = ( $_.Exception -is [System.Management.Automation.ItemNotFoundException] -or ($_.Exception -is [System.Management.Automation.PSArgumentException] -and $_.Exception.Message -match '^Property .+ does not exist') -or $_.FullyQualifiedErrorId -match 'PathNotFound|PropertyNotFound|ItemNotFound' ) if (-not $OptionalRegistryFormatIsAbsent) { throw } } try { $RegistryRolloverSettings = Get-ItemProperty -LiteralPath 'HKLM:\System\CurrentControlSet\Services\W3SVC\Parameters' -Name 'LocalTimeRollover' -ErrorAction Stop if ($null -ne $RegistryRolloverSettings.LocalTimeRollover) { $RegistryLocalTimeRollover = [bool]$RegistryRolloverSettings.LocalTimeRollover } } catch { $OptionalRegistryRolloverIsAbsent = ( $_.Exception -is [System.Management.Automation.ItemNotFoundException] -or ($_.Exception -is [System.Management.Automation.PSArgumentException] -and $_.Exception.Message -match '^Property .+ does not exist') -or $_.FullyQualifiedErrorId -match 'PathNotFound|PropertyNotFound|ItemNotFound' ) if (-not $OptionalRegistryRolloverIsAbsent) { throw } } if ($null -ne $DefaultRootDefinition) { if (-not [string]::IsNullOrWhiteSpace($RegistryFormat)) { $DefaultRootDefinition.Format = $RegistryFormat $null = $DefaultDiscoveryErrorIds.Remove('IISLogFormatUnavailable') } if ($null -ne $RegistryLocalTimeRollover) { $DefaultRootDefinition.LocalTimeRollover = $RegistryLocalTimeRollover $null = $DefaultDiscoveryErrorIds.Remove('IISLocalTimeRolloverUnavailable') } } if (-not [string]::IsNullOrWhiteSpace($RegistryRoot)) { $RegistryDiscoveryErrorIds = [System.Collections.Generic.List[string]]::new() if ([string]::IsNullOrWhiteSpace($RegistryFormat)) { $null = $RegistryDiscoveryErrorIds.Add('IISLogFormatUnavailable') } if ($null -eq $RegistryLocalTimeRollover) { $null = $RegistryDiscoveryErrorIds.Add('IISLocalTimeRolloverUnavailable') } $Roots.Add([pscustomobject]@{ Path = $RegistryRoot DisplayName = 'Registry IIS log root' Source = 'Registry' Format = if ([string]::IsNullOrWhiteSpace($RegistryFormat)) { $null } else { $RegistryFormat } Service = 'W3SVC' LocalTimeRollover = $RegistryLocalTimeRollover DiscoveryErrorIds = $RegistryDiscoveryErrorIds }) } } catch { $OptionalRegistryValueIsAbsent = ( $_.Exception -is [System.Management.Automation.ItemNotFoundException] -or ($_.Exception -is [System.Management.Automation.PSArgumentException] -and $_.Exception.Message -match '^Property .+ does not exist') -or $_.FullyQualifiedErrorId -match 'PathNotFound|PropertyNotFound|ItemNotFound' ) if ($OptionalRegistryValueIsAbsent) { Write-Verbose -Message "The optional alternate IIS log location is not configured: $($_.Exception.Message)" } else { $DiscoveryErrorReported = $true $null = $DiscoveryErrorIds.Add('IISRegistryDiscoveryFailed') if ($null -ne $DefaultRootDefinition) { $null = $DefaultRootDefinition.DiscoveryErrorIds.Add('IISRegistryDiscoveryFailed') } $ErrorRecord = Get-TheCleanersErrorRecord -Exception $_.Exception -ErrorId 'IISRegistryDiscoveryFailed' -Category ReadError $PSCmdlet.WriteError($ErrorRecord) } } } foreach ($FtpDiscoveryErrorRecord in $FtpDiscoveryErrorRecords) { $PSCmdlet.WriteError($FtpDiscoveryErrorRecord) } $RootDefinitionsByPath = [System.Collections.Generic.Dictionary[string, object]]::new([System.StringComparer]::OrdinalIgnoreCase) $MergedRoots = [System.Collections.Generic.List[object]]::new() foreach ($RootDefinition in @($Roots.ToArray())) { try { $RootDefinitionPath = Convert-TheCleanersPathForComparison -Path ([string]$RootDefinition.Path) } catch { $MergedRoots.Add($RootDefinition) continue } if (-not $RootDefinitionsByPath.ContainsKey($RootDefinitionPath)) { $RootDefinitionsByPath.Add($RootDefinitionPath, $RootDefinition) $MergedRoots.Add($RootDefinition) continue } $ExistingRootDefinition = $RootDefinitionsByPath[$RootDefinitionPath] $ExistingFormat = [string]$ExistingRootDefinition.Format $CurrentFormat = [string]$RootDefinition.Format if ([string]::IsNullOrWhiteSpace($ExistingFormat) -and -not [string]::IsNullOrWhiteSpace($CurrentFormat)) { $ExistingRootDefinition.Format = $CurrentFormat } if ($null -eq $ExistingRootDefinition.LocalTimeRollover -and $null -ne $RootDefinition.LocalTimeRollover) { $ExistingRootDefinition.LocalTimeRollover = $RootDefinition.LocalTimeRollover } if (-not [string]::IsNullOrWhiteSpace([string]$ExistingRootDefinition.Format) -and $null -ne $ExistingRootDefinition.LocalTimeRollover) { $FilteredErrorIds = [System.Collections.Generic.List[string]]::new() foreach ($ExistingErrorId in @($ExistingRootDefinition.DiscoveryErrorIds)) { if ($ExistingErrorId -ne 'IISLogFormatUnavailable' -and $ExistingErrorId -ne 'IISLocalTimeRolloverUnavailable') { $null = $FilteredErrorIds.Add($ExistingErrorId) } } $ExistingRootDefinition.DiscoveryErrorIds = $FilteredErrorIds } $MergedRootFormat = [string]$ExistingRootDefinition.Format $MergedRootLocalTimeRollover = $ExistingRootDefinition.LocalTimeRollover foreach ($RootErrorId in @($RootDefinition.DiscoveryErrorIds)) { if ($RootErrorId -eq 'IISLogFormatUnavailable' -and -not [string]::IsNullOrWhiteSpace($MergedRootFormat)) { continue } if ($RootErrorId -eq 'IISLocalTimeRolloverUnavailable' -and $null -ne $MergedRootLocalTimeRollover) { continue } if (-not $ExistingRootDefinition.DiscoveryErrorIds.Contains($RootErrorId)) { $null = $ExistingRootDefinition.DiscoveryErrorIds.Add($RootErrorId) } } } $Roots = $MergedRoots $FoundExistingRoot = $false foreach ($RootDefinition in $Roots) { $RootDiscoveryErrorIds = [System.Collections.Generic.List[string]]::new() foreach ($RootErrorId in @($RootDefinition.DiscoveryErrorIds)) { $null = $RootDiscoveryErrorIds.Add($RootErrorId) } $RootDiscoveryErrorId = 'IISDiscoveryFailed' $RootExists = $false $ProtectedRoot = $false try { if (-not (Test-TheCleanersFullyQualifiedPath -Path $RootDefinition.Path)) { throw [System.IO.InvalidDataException]::new("The IIS log root is not a fully qualified filesystem path: '$($RootDefinition.Path)'.") } $ProtectedRoot = Test-TheCleanersIisProtectedPath -Path $RootDefinition.Path $RootExists = Test-Path -LiteralPath $RootDefinition.Path -PathType Container -ErrorAction Stop } catch { $DiscoveryErrorReported = $true $DiscoveryFailureResultReported = $true $null = $RootDiscoveryErrorIds.Add('IISDiscoveryFailed') $ErrorRecord = Get-TheCleanersErrorRecord -Exception $_.Exception -ErrorId 'IISDiscoveryFailed' -Category ReadError -TargetObject $RootDefinition.Path $PSCmdlet.WriteError($ErrorRecord) if ($PassThru) { $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $RootDefinition.Path -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource $RootDefinition.Source -DisplayName $RootDefinition.DisplayName -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = $RootDiscoveryErrorIds.Count $Result.ErrorIds = @($RootDiscoveryErrorIds | Sort-Object -Unique) $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } continue } if ($ProtectedRoot) { $FoundExistingRoot = $true $DiscoveryErrorReported = $true $null = $RootDiscoveryErrorIds.Add('IISProtectedRoot') $Exception = [System.UnauthorizedAccessException]::new("The IIS path is protected and cannot be used as a log root: '$($RootDefinition.Path)'.") $ErrorRecord = Get-TheCleanersErrorRecord -Exception $Exception -ErrorId 'IISProtectedRoot' -Category PermissionDenied -TargetObject $RootDefinition.Path $PSCmdlet.WriteError($ErrorRecord) if ($PassThru) { $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $RootDefinition.Path -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource $RootDefinition.Source -DisplayName $RootDefinition.DisplayName -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = $RootDiscoveryErrorIds.Count $Result.ErrorIds = @($RootDiscoveryErrorIds | Sort-Object -Unique) $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } continue } if (-not $RootExists) { if ($RootDiscoveryErrorIds -contains 'IISFtpDiscoveryFailed') { $DiscoveryFailureResultReported = $true if ($PassThru) { $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $RootDefinition.Path -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource $RootDefinition.Source -DisplayName $RootDefinition.DisplayName -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = $RootDiscoveryErrorIds.Count $Result.ErrorIds = @($RootDiscoveryErrorIds | Sort-Object -Unique) $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } } else { Write-Verbose -Message "IIS log root not present as a directory: $($RootDefinition.Path)" } continue } $FoundExistingRoot = $true $OldFiles = @() $NormalizedRoot = $null try { $LogRoot = Resolve-TheCleanersFileSystemPath -LiteralPath $RootDefinition.Path if ($LogRoot -isnot [System.IO.DirectoryInfo]) { throw [System.IO.InvalidDataException]::new("IIS log root is not a directory: '$($RootDefinition.Path)'.") } $TraversalRoot = Convert-TheCleanersPathForTraversal -Path $RootDefinition.Path $NormalizedRoot = Convert-TheCleanersPathForComparison -Path $LogRoot.FullName if (-not $SeenRoots.Add($NormalizedRoot)) { Write-Verbose -Message "Skipping duplicate IIS log root: $NormalizedRoot" continue } } catch { $DiscoveryErrorReported = $true $DiscoveryFailureResultReported = $true $null = $RootDiscoveryErrorIds.Add('IISDiscoveryFailed') $ErrorRecord = Get-TheCleanersErrorRecord -Exception $_.Exception -ErrorId 'IISDiscoveryFailed' -Category ReadError -TargetObject $RootDefinition.Path $PSCmdlet.WriteError($ErrorRecord) if ($PassThru) { $ResultRootPath = if ($null -ne $NormalizedRoot) { $NormalizedRoot } else { $RootDefinition.Path } $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $ResultRootPath -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource $RootDefinition.Source -DisplayName $RootDefinition.DisplayName -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = $RootDiscoveryErrorIds.Count $Result.ErrorIds = @($RootDiscoveryErrorIds | Sort-Object -Unique) $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } continue } if ($RootDiscoveryErrorIds.Count -gt 0) { $DiscoveryErrorReported = $true $DiscoveryFailureResultReported = $true foreach ($RootErrorId in @($RootDiscoveryErrorIds | Sort-Object -Unique)) { $MetadataErrorMessage = switch ($RootErrorId) { 'IISLogFormatUnavailable' { "The IIS logging format was unavailable for root '$($RootDefinition.Path)'."; break } 'IISLocalTimeRolloverUnavailable' { "The IIS local-time rollover setting was unavailable for root '$($RootDefinition.Path)'."; break } default { "IIS discovery metadata was unavailable for root '$($RootDefinition.Path)'." } } $MetadataException = [System.InvalidOperationException]::new($MetadataErrorMessage) $MetadataErrorRecord = Get-TheCleanersErrorRecord -Exception $MetadataException -ErrorId $RootErrorId -Category ReadError -TargetObject $RootDefinition.Path $PSCmdlet.WriteError($MetadataErrorRecord) } if ($PassThru) { $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $NormalizedRoot -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource $RootDefinition.Source -DisplayName $RootDefinition.DisplayName -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = $RootDiscoveryErrorIds.Count $Result.ErrorIds = @($RootDiscoveryErrorIds | Sort-Object -Unique) $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } continue } try { $Pending = [System.Collections.Generic.Stack[object]]::new() $InitialService = [string]$RootDefinition.Service $InitialDirectoryName = [System.IO.Path]::GetFileName($TraversalRoot.TrimEnd([char[]]@('\', '/'))) if ($InitialDirectoryName -match '^(FTPSVC|MSFTPSVC)\d+$') { $InitialService = $Matches[1] } elseif ($InitialDirectoryName -match '^W3SVC\d+$') { $InitialService = 'W3SVC' } $Pending.Push([pscustomobject]@{ Path = $TraversalRoot Service = $InitialService }) $Candidates = [System.Collections.Generic.List[object]]::new() while ($Pending.Count -gt 0) { $DirectoryState = $Pending.Pop() $DirectoryPath = [string]$DirectoryState.Path $DirectoryService = [string]$DirectoryState.Service if ($RootDefinition.Source -in @('DefaultPath', 'Registry') -and $DirectoryService -ne 'W3SVC') { $RootDiscoveryErrorId = 'IISServiceMetadataUnavailable' throw [System.InvalidOperationException]::new("IIS fallback metadata is only known for W3SVC; the service-specific log directory cannot be previewed safely: '$DirectoryPath'.") } if ($DirectoryPath -ne $TraversalRoot) { $Directory = Resolve-TheCleanersFileSystemPath -LiteralPath $DirectoryPath -RootPath $TraversalRoot if ($Directory -isnot [System.IO.DirectoryInfo]) { throw [System.IO.InvalidDataException]::new("IIS traversal path is not a directory: '$DirectoryPath'.") } } foreach ($Item in @(Get-ChildItem -LiteralPath $DirectoryPath -Force -ErrorAction Stop)) { $ItemPath = Convert-TheCleanersPathForTraversal -Path ($DirectoryPath.TrimEnd([char[]]@('\', '/')) + '\' + [string]$Item.Name) if ($Item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) { Write-Verbose -Message "Skipping reparse point: $ItemPath" continue } if ($Item.PSIsContainer) { $ChildService = $DirectoryService $ChildDirectoryName = [string]$Item.Name if ($ChildDirectoryName -match '^(FTPSVC|MSFTPSVC)\d+$') { $ChildService = $Matches[1] } elseif ($ChildDirectoryName -match '^W3SVC\d+$') { $ChildService = 'W3SVC' } $Pending.Push([pscustomobject]@{ Path = $ItemPath Service = $ChildService }) } elseif ((Test-TheCleanersIisLogFileName -Name $Item.Name -Format $RootDefinition.Format -Service $DirectoryService -LocalTimeRollover:$RootDefinition.LocalTimeRollover) -and $Item.LastWriteTimeUtc -le $CutoffUtc) { $Candidates.Add([pscustomobject]@{ FullName = $ItemPath Name = $Item.Name LastWriteTimeUtc = $Item.LastWriteTimeUtc }) } } } $OldFiles = @($Candidates.ToArray() | Sort-Object -Property FullName) } catch { $null = $RootDiscoveryErrorIds.Add($RootDiscoveryErrorId) $ErrorRecord = Get-TheCleanersErrorRecord -Exception $_.Exception -ErrorId $RootDiscoveryErrorId -Category ReadError -TargetObject $RootDefinition.Path $PSCmdlet.WriteError($ErrorRecord) if ($PassThru) { $ResultRootPath = if ($null -ne $NormalizedRoot) { $NormalizedRoot } else { $RootDefinition.Path } $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $ResultRootPath -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource $RootDefinition.Source -DisplayName $RootDefinition.DisplayName -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = $RootDiscoveryErrorIds.Count $Result.ErrorIds = @($RootDiscoveryErrorIds | Sort-Object -Unique) $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } continue } foreach ($File in $OldFiles) { $null = $PSCmdlet.ShouldProcess($File.FullName, 'Preview allowlisted candidate only; IIS removal is unavailable') } if ($PassThru) { $ResultDiscoveryStatus = 'Experimental' $ResultErrorIds = @($RootDiscoveryErrorIds | Sort-Object -Unique) if ($ResultErrorIds.Count -gt 0) { $ResultDiscoveryStatus = 'Failed' } $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $NormalizedRoot -CutoffUtc $CutoffUtc -DiscoveryStatus $ResultDiscoveryStatus -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource $RootDefinition.Source -DisplayName $RootDefinition.DisplayName -CandidatePaths @($OldFiles | ForEach-Object { $_.FullName }) -Status 'WhatIf' if ($ResultErrorIds.Count -gt 0) { $Result.DiscoveryErrorCount = $ResultErrorIds.Count $Result.ErrorIds = $ResultErrorIds } $Result.FileCandidateCount = $OldFiles.Count $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } } if (-not $FoundExistingRoot -and -not $DiscoveryFailureResultReported) { $FinalErrorIds = @($DiscoveryErrorIds | Sort-Object -Unique) if (-not $DiscoveryErrorReported) { $Exception = [System.InvalidOperationException]::new('IIS is not installed or no configured IIS log root could be discovered.') $ErrorRecord = Get-TheCleanersErrorRecord -Exception $Exception -ErrorId 'IISDiscoveryUnavailable' -Category ObjectNotFound $PSCmdlet.WriteError($ErrorRecord) $FinalErrorIds = @('IISDiscoveryUnavailable') } elseif ($FinalErrorIds.Count -eq 0) { $FinalErrorIds = @('IISDiscoveryFailed') } if ($PassThru) { $UnavailableRootPath = $null if ($Roots.Count -gt 0) { $UnavailableRootPath = [string]$Roots[0].Path } elseif (-not [string]::IsNullOrWhiteSpace($env:SystemDrive)) { $UnavailableRootPath = Join-Path -Path $env:SystemDrive -ChildPath 'inetpub/logs/LogFiles' } else { $UnavailableRootPath = 'IIS log roots' } $Result = Get-TheCleanersCleanupResult -Command 'Clear-OldIISLog' -RootPath $UnavailableRootPath -CutoffUtc $CutoffUtc -DiscoveryStatus 'Failed' -ProtectionStatus 'Validated' -ProtectionPathCount $IisProtectedPaths.Count -ProtectionPaths $IisProtectedPaths -DiscoverySource 'IIS default, registry, and WebAdministration roots' -DisplayName 'IIS discovery' -CandidatePaths @() -Status 'DiscoveryFailed' $Result.FileCandidateCount = $null $Result.DirectoryCandidateCount = $null $Result.DiscoveryErrorCount = $FinalErrorIds.Count $Result.ErrorIds = $FinalErrorIds $Result | Add-Member -MemberType NoteProperty -Name AllowedFilePatterns -Value @('IIS format allowlist') $Result } } } |