diff --git a/.cspell.json b/.cspell.json index 1883f69..a5f87f8 100644 --- a/.cspell.json +++ b/.cspell.json @@ -9,7 +9,7 @@ "ignoreWords": [ "msstore", "choco", - "Chocolately's", + "Chocolatey", "sarif", "psscriptanalyzer", "JDHIT", diff --git a/.editorconfig b/.editorconfig index 6c5c3f6..0a9e8d0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,7 +10,7 @@ indent_size = 4 trim_trailing_whitespace = true insert_final_newline = true -# Powershell files +# PowerShell files [*.{ps1,psd1,psm1}] indent_size = 4 diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fe598c4..2be9a5c 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -10,7 +10,7 @@ Please note there is a code of conduct, please follow it in all your interaction ## Contributing via Pull Requests -Please contribute pull requests to the `test` branch of this repository. If you're not sure how, feel free to reach out and ask! +Please contribute pull requests against the `main` branch of this repository. If you're not sure how, feel free to reach out and ask! ## Code of Conduct @@ -18,4 +18,4 @@ This project has a [Code of Conduct](CODE_OF_CONDUCT.md). ## Licensing -See the [LICENSE](LICENSE) file for our project's licensing. +See the [LICENSE](../LICENSE) file for our project's licensing. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0ce81da..6807d46 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -19,4 +19,4 @@ - [ ] 🕵️ I have reviewed my code for errors and tested it. - [ ] 🚩 My pull request does not contain multiple types of changes. -- [ ] 📄 By submitting this pull request, I confirm that my contribution is made under the terms of the projects associated license. +- [ ] 📄 By submitting this pull request, I confirm that my contribution is made under the terms of the project's associated license. diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 6a0828c..df39aaf 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -4,9 +4,9 @@ If you discover a vulnerability in **The Cleaners**, please follow the _following process_: -1. Open a generic bug issue advising you have discovered a vulnerability. - - Avoid sharing specifics or details of the vulnerability in an open GitHub issue. -2. A repo owner will reach out to you to establish a private form of communication. +1. Report the vulnerability privately through GitHub's private vulnerability reporting or security advisory workflow. + - Do not share specifics or details of the vulnerability in an open GitHub issue. +2. A repo owner will review the report and may follow up privately for more information. 3. We will evaluate the vulnerability and, if necessary, release a fix or mitigating steps to address it. We will contact you to let you know the outcome, and will credit you in the report. Please **do not disclose the vulnerability publicly** until a fix is released! diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..74999af --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,11 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + + - package-ecosystem: pip + directory: /docs + schedule: + interval: weekly diff --git a/.github/workflows/Deploy MkDocs.yml b/.github/workflows/Deploy MkDocs.yml index d0acedd..43196da 100644 --- a/.github/workflows/Deploy MkDocs.yml +++ b/.github/workflows/Deploy MkDocs.yml @@ -9,7 +9,7 @@ on: paths: - 'docs/**' - '.github/workflows/Deploy MkDocs.yml' - - '.github/mkdocs.yml' + - 'mkdocs.yml' # Allow manual triggering of the workflow. workflow_dispatch: @@ -19,7 +19,6 @@ jobs: build: permissions: contents: write - pull-requests: write runs-on: ubuntu-latest concurrency: @@ -38,7 +37,7 @@ jobs: - name: ➕ Install Dependencies run: | python -m pip install --upgrade pip - pip install mkdocs mkdocs-material + python -m pip install -r docs/requirements.txt - name: 👷‍♂️ Build & Deploy MkDocs run: | diff --git a/.github/workflows/Publish.yml b/.github/workflows/Publish.yml index 4e59b76..e9e36b9 100644 --- a/.github/workflows/Publish.yml +++ b/.github/workflows/Publish.yml @@ -1,6 +1,10 @@ name: Publish to PowerShell Gallery on: workflow_dispatch: + +permissions: + contents: read + jobs: publish: runs-on: ubuntu-latest @@ -9,5 +13,6 @@ jobs: uses: actions/checkout@v4 - name: Publish PowerShell Module shell: pwsh - run: | - ./publish.ps1 -PSGalleryApiKey ${{ secrets.POWERSHELLGALLERY_KEY }} + env: + PSGALLERY_API_KEY: ${{ secrets.POWERSHELLGALLERY_KEY }} + run: ./.github/workflows/publish.ps1 -PSGalleryApiKey $env:PSGALLERY_API_KEY diff --git a/.github/workflows/publish.ps1 b/.github/workflows/publish.ps1 index b236f67..3ee277e 100644 --- a/.github/workflows/publish.ps1 +++ b/.github/workflows/publish.ps1 @@ -1,8 +1,26 @@ -param ( - [string] $PSGalleryApiKey +<# +.SYNOPSIS + Publish TheCleaners to the PowerShell Gallery. + +.DESCRIPTION + Publishes the module from the source module path to the PowerShell Gallery + using an API key supplied by the workflow environment. + +.PARAMETER PSGalleryApiKey + The PowerShell Gallery API key used by Publish-Module. + +.EXAMPLE + ./.github/workflows/publish.ps1 -PSGalleryApiKey $env:PSGALLERY_API_KEY +#> +[CmdletBinding()] +param ( + [Parameter(Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $PSGalleryApiKey ) -$ErrorActionPreference = 'stop' +$ErrorActionPreference = 'Stop' $ModulePath = './src/TheCleaners' -Publish-Module -Path $ModulePath -NuGetApiKey $PSGalleryApiKey +Publish-Module -Path $ModulePath -NuGetApiKey $PSGalleryApiKey -ErrorAction Stop diff --git a/.gitignore b/.gitignore index 427dc0e..7dfaa9c 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ # Project Files # Archive Artifacts +src/Archive/ +src/Artifacts/ cov.xml coverage.xml Ignore diff --git a/.readthedocs.yaml b/.readthedocs.yaml index cbc2448..f1f80af 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -13,7 +13,7 @@ build: python: "3.12" mkdocs: - configuration: .github/mkdocs.yml + configuration: mkdocs.yml python: install: diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..fc250a8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2025 Sam Erde + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index fe11417..ff91abd 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![GitHub stars](https://img.shields.io/github/stars/samerde/TheCleaners?cacheSeconds=3600)](https://github.com/samerde/TheCleaners/stargazers/) ![PowerShell Gallery Version](https://img.shields.io/powershellgallery/v/TheCleaners?include_prereleases) ![PowerShell Gallery Downloads](https://img.shields.io/powershellgallery/dt/TheCleaners) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) [![GitHub contributors](https://img.shields.io/github/contributors/samerde/TheCleaners.svg)](https://github.com/samerde/TheCleaners/graphs/contributors/) ![GitHub top language](https://img.shields.io/github/languages/top/SamErde/TheCleaners) @@ -13,7 +13,7 @@ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/SamErde/TheCleaners/.github%2Fworkflows%2FDeploy%20MkDocs.yml?label=MkDocs) - +The Cleaners logo on a code hoodie ## Synopsis @@ -49,7 +49,7 @@ Install-Module -Name TheCleaners -AllowPrerelease ### Quick Start -#### Example1 +#### Example 1 ```powershell # See what jobs TheCleaners can do for you. diff --git a/actions_bootstrap.ps1 b/actions_bootstrap.ps1 index ef1f0a9..ee9d95e 100644 --- a/actions_bootstrap.ps1 +++ b/actions_bootstrap.ps1 @@ -3,9 +3,6 @@ # https://docs.microsoft.com/powershell/module/packagemanagement/get-packageprovider Get-PackageProvider -Name Nuget -ForceBootstrap | Out-Null -# https://docs.microsoft.com/powershell/module/powershellget/set-psrepository -Set-PSRepository -Name PSGallery -InstallationPolicy Trusted - # List of PowerShell Modules required for the build $modulesToInstall = New-Object System.Collections.Generic.List[object] # https://github.com/pester/Pester @@ -36,14 +33,16 @@ foreach ($module in $modulesToInstall) { Name = $module.ModuleName RequiredVersion = $module.ModuleVersion Repository = 'PSGallery' - SkipPublisherCheck = $true + Scope = 'CurrentUser' Force = $true ErrorAction = 'Stop' } try { - if ($module.ModuleName -eq 'Pester' -and ($IsWindows -or $PSVersionTable.PSVersion -ge [version]'5.1')) { - # special case for Pester certificate mismatch with older Pester versions - https://github.com/pester/Pester/issues/2389 - # this only affects windows builds + $isWindowsPowerShell = $PSVersionTable.PSEdition -eq 'Desktop' + $isWindowsHost = $isWindowsPowerShell -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows) + if ($module.ModuleName -eq 'Pester' -and $isWindowsHost) { + # Pester 5.6.1 has a known publisher certificate mismatch on Windows. + # Keep the bypass scoped to this one dependency until the pinned version changes. Install-Module @installSplat -SkipPublisherCheck } else { Install-Module @installSplat diff --git a/docs/Clear-OldIISLog.md b/docs/Clear-OldIISLog.md index 3f7ae3f..0fa3859 100644 --- a/docs/Clear-OldIISLog.md +++ b/docs/Clear-OldIISLog.md @@ -92,6 +92,6 @@ each web site. Otherwise, it checks the assumed default log folder location and the registry for the IIS log file location. -To Do: Add a summary of which blocks were run and possibly a count of log files removed. +Future enhancements may add a summary of which locations were processed and how many log files were removed. ## RELATED LINKS diff --git a/docs/index.md b/docs/index.md index fe11417..ff91abd 100644 --- a/docs/index.md +++ b/docs/index.md @@ -4,7 +4,7 @@ [![GitHub stars](https://img.shields.io/github/stars/samerde/TheCleaners?cacheSeconds=3600)](https://github.com/samerde/TheCleaners/stargazers/) ![PowerShell Gallery Version](https://img.shields.io/powershellgallery/v/TheCleaners?include_prereleases) ![PowerShell Gallery Downloads](https://img.shields.io/powershellgallery/dt/TheCleaners) -[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://makeapullrequest.com) [![GitHub contributors](https://img.shields.io/github/contributors/samerde/TheCleaners.svg)](https://github.com/samerde/TheCleaners/graphs/contributors/) ![GitHub top language](https://img.shields.io/github/languages/top/SamErde/TheCleaners) @@ -13,7 +13,7 @@ ![GitHub Actions Workflow Status](https://img.shields.io/github/actions/workflow/status/SamErde/TheCleaners/.github%2Fworkflows%2FDeploy%20MkDocs.yml?label=MkDocs) - +The Cleaners logo on a code hoodie ## Synopsis @@ -49,7 +49,7 @@ Install-Module -Name TheCleaners -AllowPrerelease ### Quick Start -#### Example1 +#### Example 1 ```powershell # See what jobs TheCleaners can do for you. diff --git a/mkdocs.yml b/mkdocs.yml index a350a3a..9f96775 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -93,9 +93,9 @@ nav: - Home: "index.md" - Functions: - "Start-Cleaning": "Start-Cleaning.md" - - "Clear-OldExchangeLogs": "Clear-OldExchangeLogs.md" - - "Clear-OldIISLogs": "Clear-OldIISLogs.md" - - "Clear-UserTemp": "Clear-UserTemp.md" + - "Clear-OldExchangeLog": "Clear-OldExchangeLog.md" + - "Clear-OldIISLog": "Clear-OldIISLog.md" + - "Clear-CurrentUserTemp": "Clear-CurrentUserTemp.md" - "Clear-WindowsTemp": "Clear-WindowsTemp.md" - "Get-StaleUserProfile": "Get-StaleUserProfile.md" diff --git a/src/Archive/TheCleaners_0.0.15_20250311.031804.zip b/src/Archive/TheCleaners_0.0.15_20250311.031804.zip deleted file mode 100644 index 782922e..0000000 Binary files a/src/Archive/TheCleaners_0.0.15_20250311.031804.zip and /dev/null differ diff --git a/src/Artifacts/Invoke-TheCleaners.ps1 b/src/Artifacts/Invoke-TheCleaners.ps1 deleted file mode 100644 index feedc72..0000000 --- a/src/Artifacts/Invoke-TheCleaners.ps1 +++ /dev/null @@ -1,20 +0,0 @@ -<#PSScriptInfo -.DESCRIPTION The Cleaners do the dirty work in your servers for you. We take care of temp files, IIS logs, Exchange Server logs, and more! -.VERSION 0.0.15 -.GUID bec6a004-45da-4062-ab78-b8eae99f29be -.AUTHOR Sam Erde -.COPYRIGHT (c) 2025 Sam Erde. All rights reserved. -.TAGS Update PowerShell Windows -.LICENSEURI https://github.com/SamErde/TheCleaners/blob/main/LICENSE -.PROJECTURI https://github.com/SamErde/TheCleaners/ -#> -function Invoke-TheCleaners { - [CmdletBinding()] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] - param() - Write-Host "Thank you for calling The Cleaners! 🧹`nRun " -ForegroundColor Green -NoNewline - Write-Host 'Start-Cleaning' -BackgroundColor Black -ForegroundColor White -NoNewline - Write-Host " to see the services we offer today.`n" -ForegroundColor Green -} - -Invoke-TheCleaners diff --git a/src/Artifacts/TheCleaners.psd1 b/src/Artifacts/TheCleaners.psd1 deleted file mode 100644 index 3e2553f..0000000 --- a/src/Artifacts/TheCleaners.psd1 +++ /dev/null @@ -1,124 +0,0 @@ -@{ - - # Script module or binary module file associated with this manifest. - RootModule = 'TheCleaners.psm1' - - # Version number of this module. - ModuleVersion = '0.0.15' - - # Supported PSEditions - CompatiblePSEditions = @('Core', 'Desktop') - - # ID used to uniquely identify this module - GUID = '96512386-bbd2-4e95-badd-5d175310bace' - - # Author of this module - Author = 'Sam Erde' - - # Company or vendor of this module - CompanyName = 'Sam Erde' - - # Copyright statement for this module - Copyright = '(c) 2025 Sam Erde. All rights reserved.' - - # Description of the functionality provided by this module - Description = 'The Cleaners do the dirty work in your servers for you. We take care of temp files, IIS logs, Exchange Server logs, and more!' - - # Minimum version of the PowerShell engine required by this module - PowerShellVersion = '5.1' - - # Modules that must be imported into the global environment prior to importing this module - # RequiredModules = @() - - # Assemblies that must be loaded prior to importing this module - # RequiredAssemblies = @() - - # Script files (.ps1) that are run in the caller's environment prior to importing this module. - ScriptsToProcess = @( - 'Invoke-TheCleaners.ps1' - ) - - # Type files (.ps1xml) to be loaded when importing this module - # TypesToProcess = @() - - # Format files (.ps1xml) to be loaded when importing this module - # FormatsToProcess = @() - - # Modules to import as nested modules of the module specified in RootModule/ModuleToProcess - # NestedModules = @() - - # Functions to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no functions to export. - FunctionsToExport = @( - 'Clear-OldExchangeLog', - 'Clear-OldIISLog', - 'Clear-CurrentUserTemp', - 'Clear-WindowsTemp', - 'Get-StaleUserProfile', - 'Start-Cleaning' - ) - - # Cmdlets to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no cmdlets to export. - CmdletsToExport = @() - - # Variables to export from this module - VariablesToExport = @() - - # Aliases to export from this module, for best performance, do not use wildcards and do not delete the entry, use an empty array if there are no aliases to export. - AliasesToExport = @( - 'Clean-CurrentUserTemp', - 'Clean-ExchangeLog', - 'Clean-IISLog', - 'Clean-WindowsTemp' - ) - - # DSC resources to export from this module - # DscResourcesToExport = @() - - # List of all modules packaged with this module - # ModuleList = @() - - # List of all files packaged with this module - # FileList = @() - - # Private data to pass to the module specified in RootModule/ModuleToProcess. This may also contain a PSData hashtable with additional module metadata used by PowerShell. - PrivateData = @{ - - PSData = @{ - - # Tags applied to this module. These help with module discovery in online galleries. - Tags = @('Windows', 'WindowsServer', 'Windows-Server', 'PowerShell', 'SysAdmin', 'Maintenance', 'Utility', 'Utilities', 'Exchange', 'ExchangeServer', 'IIS') - - # A URL to the license for this module. - LicenseUri = 'https://github.com/SamErde/TheCleaners/blob/main/LICENSE' - - # A URL to the main website for this project. - ProjectUri = 'https://github.com/SamErde/TheCleaners' - - # A URL to an icon representing this module. - IconUri = 'https://raw.githubusercontent.com/SamErde/TheCleaners/main/media/TheCleaners-Icon.png' - - # ReleaseNotes of this module - # ReleaseNotes = '' - - # Prerelease string of this module - Prerelease = 'beta' - - # Flag to indicate whether the module requires explicit user acceptance for install/update/save - # RequireLicenseAcceptance = $false - - # External dependent modules of this module - ExternalModuleDependencies = @( - 'WebAdministration' - ) - - } # End of PSData hashtable - - } # End of PrivateData hashtable - - # HelpInfo URI of this module - HelpInfoURI = 'https://raw.githubusercontent.com/SamErde/TheCleaners/main/src/Help/' - - # Default prefix for commands exported from this module. Override the default prefix using Import-Module -Prefix. - # DefaultCommandPrefix = '' - -} diff --git a/src/Artifacts/TheCleaners.psm1 b/src/Artifacts/TheCleaners.psm1 deleted file mode 100644 index e705170..0000000 --- a/src/Artifacts/TheCleaners.psm1 +++ /dev/null @@ -1,531 +0,0 @@ -# This is a locally sourced Imports file for local development. -# It can be imported by the psm1 in local development to add script level variables. -# It will merged in the build process. This is for local development only. - -# region script variables -# $script:resourcePath = "$PSScriptRoot\Resources" - - -<#PSScriptInfo -.DESCRIPTION The Cleaners do the dirty work in your servers for you. We take care of temp files, IIS logs, Exchange Server logs, and more! -.VERSION 0.0.15 -.GUID bec6a004-45da-4062-ab78-b8eae99f29be -.AUTHOR Sam Erde -.COPYRIGHT (c) 2025 Sam Erde. All rights reserved. -.TAGS Update PowerShell Windows -.LICENSEURI https://github.com/SamErde/TheCleaners/blob/main/LICENSE -.PROJECTURI https://github.com/SamErde/TheCleaners/ -#> -function Invoke-TheCleaners { - [CmdletBinding()] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] - param() - Write-Host "Thank you for calling The Cleaners! 🧹`nRun " -ForegroundColor Green -NoNewline - Write-Host 'Start-Cleaning' -BackgroundColor Black -ForegroundColor White -NoNewline - Write-Host " to see the services we offer today.`n" -ForegroundColor Green -} - -Invoke-TheCleaners - - -function Convert-SamAccountNameToSID { - <# - .SYNOPSIS - Translates a SamAccountName to a SID. - - .DESCRIPTION - Translates a SamAccountName to a SID. - - .PARAMETER domain - The domain to search for the SamAccountName. - - .PARAMETER SamAccountName - The SamAccountName to translate to a SID. - - .EXAMPLE - Convert-SamAccountNameToSID -Domain "contoso" -SamAccountName "jdoe" - - Translates the SamAccountName "jdoe" to a SID. - - .COMPONENT - TheCleaners - #> - [CmdletBinding()] - param ( - # The Domain - [Parameter(Mandatory)] - [string]$Domain, - - # The SamAccountName - [Parameter(Mandatory)] - [string]$SamAccountName - ) - - $User = New-Object System.Security.Principal.NTAccount($Domain,$SamAccountName) - $SID = $User.Translate([System.Security.Principal.SecurityIdentifier]) - $SID.Value -} # End function Convert-SamAccountNameToSID - - -function Convert-SIDtoSamAccountName { - <# - .SYNOPSIS - Translates a SID to a SamAccountName. - - .DESCRIPTION - Translates a SID to a SamAccountName. - - .PARAMETER SID - The SID to translate to a SamAccountName. - - .EXAMPLE - Convert-SIDtoSamAccountName -SID "S-1-5-21-3623811015-3361044348-30300820" - - Translates the SID to a SamAccountName. - - .COMPONENT - TheCleaners - #> - [CmdletBinding()] - param ( - # The SID as a string or a SID object. - [Parameter(Mandatory)] - $SID - ) - - $SID = New-Object System.Security.Principal.SecurityIdentifier($SID) - $User = $SID.Translate([System.Security.Principal.NTAccount]) - $User.Value -} # End function Convert-SIDtoSamAccountName - - -<# -.SYNOPSIS - Remove files in a path that are older than the specified number of days. - -.DESCRIPTION - Remove files in a path that are older than the specified number of days. This function is used by other functions within the module when removing old files. - -.EXAMPLE - Remove-OldFiles -Path "C:\Windows\Temp" -Days 60 -Recurse - - Removes all files older than 60 does in C:\Windows\Temp with recursion to clean subfolders. -#> -function Remove-OldFiles { - [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] - [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns')] - param ( - # The path containing files to remove - [string] - $Path, - - # How many days worth of logs to retain (how far back to filter) - [int16] - $Days = 60 - ) - - begin { - - } - - process { - Write-Verbose -Message "Finding and removing files older than $Days." - Get-ChildItem -Path $Path -Recurse | Where-Object { - $_.CreationTime -le ([datetime]::Now.AddDays( -$Days )) - } | Remove-Item - } - - end { - - } -} - - -function Show-TCLogo { - <# - .SYNOPSIS - Show an ASCII art logo for The Cleaners. - - .DESCRIPTION - Show a color or plain ASCII art logo for The Cleaners whenever you need it in another function. - - .PARAMETER Plain - Return a plan-text version of the logo instead of multi-colored Write-Host output. - - .EXAMPLE - Show-Logo - - .EXAMPLE - Show-Logo -Plain - - #> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost','')] - param ( - [Parameter()] - [switch] - $Plain - ) - - $Version = (Import-PowerShellDataFile -Path $PSScriptRoot\..\TheCleaners.psd1).ModuleVersion - - if ($Plain.IsPresent) { - $Logo = @" - - ╭━━━━┳╮╱╱╱╱╱╭━━━┳╮ - ┃╭╮╭╮┃┃╱╱╱╱╱┃╭━╮┃┃ v$Version - ╰╯┃┃╰┫╰━┳━━╮┃┃╱╰┫┃╭━━┳━━┳━╮ ╭━━┳━┳━━╮ - ╱╱┃┃╱┃╭╮┃┃━┫┃┃╱╭┫┃|┃━┫╭╮┃╭╮╮┃|━┫╭┫━━┫ - ╱╱┃┃╱┃┃┃┃┃━┫┃╰━╯┃╰┫┃━┫╭╮┃||┃┃|━┫|┣━━┃ - ╱╱╰╯╱╰╯╰┻━━╯╰━━━┻━┻━━┻╯╰┻╯╰┻┻━━┻╯╰━━╯ - -"@ - return $Logo - } else { - Write-Host '' - Write-Host ' ╭━━━━┳╮' -ForegroundColor DarkCyan -NoNewline - Write-Host '╱╱╱╱╱' -ForegroundColor Yellow -NoNewline - Write-Host '╭━━━┳╮' -ForegroundColor DarkCyan #NewLine - Write-Host ' ┃╭╮╭╮┃┃' -ForegroundColor DarkCyan -NoNewline - Write-Host '╱╱╱╱╱' -ForegroundColor Yellow -NoNewline - Write-Host '┃╭━╮┃┃' -ForegroundColor DarkCyan -NoNewline #NewLine - Write-Host " v$Version" -ForegroundColor Yellow - Write-Host ' ╰╯┃┃╰┫╰━┳━━╮┃┃' -ForegroundColor DarkCyan -NoNewline - Write-Host '/' -ForegroundColor Yellow -NoNewline - Write-Host '╰┫┃╭━━┳━━┳━╮' -ForegroundColor DarkCyan -NoNewline - Write-Host '*' -ForegroundColor Yellow -NoNewline - Write-Host '╭━━┳━┳━━╮' -ForegroundColor DarkCyan #NewLine - Write-Host ' ╱╱' -ForegroundColor Yellow -NoNewline - Write-Host '┃┃' -ForegroundColor DarkCyan -NoNewline - Write-Host '/' -ForegroundColor Yellow -NoNewline - Write-Host '┃╭╮┃┃━┫┃┃' -ForegroundColor DarkCyan -NoNewline - Write-Host '/' -ForegroundColor Yellow -NoNewline - Write-Host '╭┫┃|┃━┫╭╮┃╭╮╮┃|━┫╭┫━━┫' -ForegroundColor DarkCyan #NewLine - Write-Host ' ╱╱' -ForegroundColor Yellow -NoNewline - Write-Host '┃┃' -ForegroundColor DarkCyan -NoNewline - Write-Host '/' -ForegroundColor Yellow -NoNewline - Write-Host '┃┃┃┃┃━┫┃╰━╯┃╰┫┃━┫╭╮┃||┃┃|━┫|┣━━┃' -ForegroundColor DarkCyan #NewLine - Write-Host ' ╱╱' -ForegroundColor Yellow -NoNewline - Write-Host '┃┃' -ForegroundColor DarkCyan -NoNewline - Write-Host '/' -ForegroundColor Yellow -NoNewline - Write-Host '╰╯╰┻━━╯╰━━━┻━┻━━┻╯╰┻╯╰┻┻━━┻╯╰━━╯' -ForegroundColor DarkCyan #NewLine - Write-Host '' - } -} - - -function Clear-CurrentUserTemp { - <# -.EXTERNALHELP TheCleaners-help.xml -#> - [CmdletBinding(SupportsShouldProcess)] - [Alias('Clean-CurrentUserTemp')] - param ( - # Remove temp files that are $Days days old or older. - [Parameter()] - [ValidateRange(1, [int16]::MaxValue)] # Ensure it is a positive number. - [int16] - $Days = 30, - - # Time limit (seconds) for running the CleanEmptyDirectories loop. The default is 30 (seconds). - [Parameter()] - [ValidateRange(1, [int16]::MaxValue)] # Ensure it is a positive number. - [Int16] - $TimeOut = 30 - ) - - if ($IsLinux) { - $UserTempPath = '/tmp' - } else { - $UserTempPath = $env:TEMP - } - - if (-not (Test-Path -Path $UserTempPath)) { - Write-Warning -Message "Unable to find $UserTempPath." - return - } - - Write-Verbose "Getting files older than $($Days) days (inclusive) in `'$UserTempPath`'." - $OldFiles = Get-ChildItem -Path $UserTempPath -File -Recurse -ErrorAction SilentlyContinue | Where-Object { - $_.LastWriteTime -le ( (Get-Date).AddDays(-$Days) ) - } - - if ($OldFiles.Count -eq 0) { - Write-Output "No files found older than $Days days in `'$UserTempPath`'." - return - } - - Write-Output "Found $($OldFiles.Count) files and directories older than $Days days in $UserTempPath.`n" - - foreach ($file in $OldFiles) { - if ( $PSCmdlet.ShouldProcess("Removing $($file.FullName)", $file.FullName, 'Remove-Item') ) { - try { - Remove-Item $file -Confirm:$false -ErrorAction Stop - Write-Verbose -Message "Removed file: $($file.FullName)" - } catch { - Write-Output " $($Error[-1].Exception.Message)" - } - } - } - - #region CleanEmptyDirectories - <# - Find empty directories and then loop through them to remove sub-directories and then empty parent directories. - #> - # Set a timeout in case the do-until loop encounters a condition that prevents it from reaching zero (0). - $CleanEmptyDirectoriesStartTime = Get-Date - $TimeLimit = [timespan]::FromSeconds($TimeOut) - # Save the current ErrorActionPreference so we can restore it after using SilentlyContinue. - $RunningErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = 'SilentlyContinue' - do { - # Break from the do-until loop if the TimeLimit has been reached. - if ((Get-Date) - $CleanEmptyDirectoriesStartTime -ge $TimeLimit ) { - Write-Output "The CleanEmptyDirectories operation timed out after $TimeOut seconds. There are $($EmptyDirectories.Count) empty directories left." - break - } - # Get directories that have 0 files in them. - $EmptyDirectories = Get-ChildItem -Path $UserTempPath -Directory -Recurse | Where-Object { $_.GetFileSystemInfos().Count -eq 0 } | Out-Null - Write-Verbose "$($EmptyDirectories.Count) empty directories found." - $EmptyDirectories | Remove-Item - } until ( - $EmptyDirectories.Count -eq 0 - ) - $ErrorActionPreference = $RunningErrorActionPreference - #endregion CleanEmptyDirectories -} - - - -################################################################################### -# # -# WARNING: This script is still being developed and tested. Use at your own risk. # -# # -################################################################################### -function Clear-OldExchangeLog { - <# -.EXTERNALHELP TheCleaners-help.xml -#> - [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] - [Alias('Clean-ExchangeLog')] - #[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns')] - # Logs older than this number of days will be removed. - param ( - [Parameter()] - [int] - $Days = 60 - ) - - begin { - - try { - $ExchangeInstallPath = (Get-ItemProperty HKLM:\SOFTWARE\Microsoft\ExchangeServer\v15\Setup).MsiInstallPath - } catch { - Write-Warning -WarningAction Continue 'The Exchange Server installation path could not be found. Please ensure that Exchange Server is installed on this machine.' - return - } - - # Define the paths to the Exchange log files - $LogLocations = @{ - ExchangeLoggingPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'Logging\' -ErrorAction Ignore - ETLTracesPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'Bin\Search\Ceres\Diagnostics\ETLTraces\' -ErrorAction Ignore - DiagnosticLogsPath = Join-Path -Path $ExchangeInstallPath -ChildPath '\Bin\Search\Ceres\Diagnostics\Logs' -ErrorAction Ignore - MessageTrackingLogsPath = Join-Path -Path $ExchangeInstallPath -ChildPath '\TransportRoles\Logs\MessageTracking\' -ErrorAction Ignore - } - - $LastWriteDate = (Get-Date).AddDays(-$Days) - } # end begin - - process { - # Clean up the IIS log files - Clear-OldIisLogFiles -Days $Days - - foreach ($LogLocation in $LogLocations.GetEnumerator()) { - if (-not (Test-Path -Path $LogLocation.Value)) { - Write-Warning -WarningAction Continue "The folder $($LogLocation.Key) doesn't exist. Skipping this folder." - continue - } - - $OldFiles = Get-ChildItem -Path $($LogLocation.Value) -Recurse | - Where-Object { ($_.Name -like '*.log') -and ($_.lastWriteTime -le "$LastWriteDate") } | Select-Object FullName - - foreach ($file in $OldFiles) { - if ( $PSCmdlet.ShouldProcess($file.Name) ) { - $file.Delete() - } - } # end foreach $file - - } # end foreach LogLocation - } # end process - - end { - # Summarize the count and total size of files removed. - } -} - - - -function Clear-OldIISLog { - <# -.EXTERNALHELP TheCleaners-help.xml -#> - [CmdletBinding()] - [Alias('Clean-IISLog')] - #[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns')] - param ( - [Parameter()] - [ValidateRange(1, [int16]::MaxValue)] # Ensure it is a positive number. - [int16] - $Days = 60 - ) - - # Use the WebAdministration module if it is available - if (Get-Module -Name 'WebAdministration' -ListAvailable) { - # Get the logfile directory for each web site - $WebSites = Get-Website - foreach ($site in $WebSites) { - $SiteLogFileDirectory = ("$($Site.logFile.directory)\W3SVC$($Site.id)").Replace( '%SystemDrive%', $env:SystemDrive ) - Write-Information -MessageData "Removing old IIS log files from $($Site.name) at $SiteLogFileDirectory." -InformationAction Continue - try { - Remove-OldFiles -Path $SiteLogFileDirectory -Days $Days -WhatIf - } catch { - Write-Error -Message $_.Exception.Message -ErrorAction Continue - Write-Warning "Failed to remove old IIS log files from $($Site.name) at $SiteLogFileDirectory." -WarningAction Continue - } - } - } else { - # If the WebAdministration module is not available, check the default log file location - $DefaultIISLogLocation = "$env:SystemDrive\inetpub\logs\LogFiles" - Write-Information "The WebAdministration module is not installed. We will check the default IIS log file location at '$DefaultIISLogLocation'." -InformationAction Continue - if (Test-Path -Path $DefaultIISLogLocation -ErrorAction SilentlyContinue) { - try { - Remove-OldFiles -Path $DefaultIISLogLocation -Days $Days - } catch { - Write-Error -Message $_.Exception.Message -ErrorAction Continue - Write-Warning "Failed to remove old log files from the default IIS log file location at '$DefaultIISLogLocation'." -WarningAction Continue - } - } else { - Write-Information -MessageData "The default IIS log file location at '$DefaultIISLogLocation' does not exist." -InformationAction Continue - } - - # If the WebAdministration module is not available,try to check the IIS log file location from the registry (requires local admin rights to read this path) - $LogDir = Get-ItemProperty -Path 'HKLM:\\System\CurrentControlSet\Services\W3SVC\Parameters' -Name 'LogDir' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LogDir - if ($LogDir -and (Test-Path -Path $LogDir)) { - try { - Remove-OldFiles -Path $LogDir -Days $Days - } catch { - Write-Error -Message $_.Exception.Message -ErrorAction Continue - Write-Warning "Failed to remove old IIS log files from the location specified in the directory ($LogDir)." -WarningAction Continue - } - } else { - Write-Information -MessageData 'Unable to find an alternate IIS log file location from the registry.' -InformationAction Continue - } - } - -} - - - -function Clear-WindowsTemp { - <# -.EXTERNALHELP TheCleaners-help.xml -#> - [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Low')] - [Alias('Clean-WindowsTemp')] - param ( - # How many days worth of temp files to retain (how far back to filter). - [ValidateRange(1, [int16]::MaxValue)] # Ensure it is a positive number. - [int16] - $Days = 30 - ) - - $TempPath = Join-Path -Path $env:SystemRoot -ChildPath 'Temp' - if (-not (Test-Path -Path $TempPath)) { - Write-Warning -Message "Unable to find $TempPath." - return - } - $OldFiles = Get-ChildItem -Path $TempPath -Recurse | Where-Object { - $_.LastWriteTime -le ( (Get-Date).AddDays(-$Days) ) - } - - if ($OldFiles.Count -eq 0) { - Write-Output "No files found older than $Days days." - return - } - - Write-Output "Found $($OldFiles.Count) files and directories older than $Days days in the system temp folder.`n" - - foreach ($file in $OldFiles) { - if ( $PSCmdlet.ShouldProcess("Removing $($file.FullName)", $file.FullName, 'Remove-Item') ) { - try { - Remove-Item $file -Confirm:$false -ErrorAction Stop - Write-Verbose -Message "Removed file: $($file.FullName)" - } catch { - Write-Output " $($Error[-1].Exception.Message)" - } - } - } - -} - - - -function Get-StaleUserProfile { - <# -.EXTERNALHELP TheCleaners-help.xml -#> - [CmdletBinding()] - param ( - # Number of days to consider a profile stale. The default is 90. - [Parameter(Position = 0)] - [Int16] - $Days = 90, - - # Show a summary of the stale user profiles that were found. - [Parameter()] - [switch] - $ShowSummary - ) - - # Get all user profiles that have not been used in 60 days, are not currently loaded, and are not special accounts. - [array]$StaleUserProfiles = Get-CimInstance -Class Win32_UserProfile | Where-Object { ($_.LastUseTime -lt (Get-Date).AddDays(-$Days)) -and (!$_.Special) -and (!$_.Loaded) } - # Might need to check last modified date using NTFS: foreach ($profile in $StaleUserProfiles) { Get-Item -Path $($_.LocalPath).LastWriteTime } - - if ($StaleUserProfiles.Count -lt 1 -or !(StaleUserProfiles)) { - Write-Information 'No stale user profiles were found.' -InformationAction Continue - } else { - if ($ShowSummary) { - $StaleUserProfiles | Select-Object LocalPath, SID, @{ Name = 'Size'; Expression = { '{0} MB' -f [math]::Round(((Get-ChildItem $_.LocalPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum / 1MB)) } } | Out-Host - Write-Information -InformationAction Continue 'NOTE: If you do not have access to a profile folder, the size will show as 0 MB.' - } - $StaleUserProfiles - } -} - - - -function Start-Cleaning { - <# -.EXTERNALHELP TheCleaners-help.xml -#> - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseShouldProcessForStateChangingFunctions', '')] - [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost', '')] - param ( - # Show dedication - [Parameter()] - [switch] - $Dedication - ) - - if ($Dedication) { - Write-Host "`nThis is dedicated to the friends that I spent years working with`nand learning PowerShell with. Cheers to Alex, Lyle, Jon, & Rick! " -ForegroundColor Yellow -NoNewline - Write-Host "❤️`n" -ForegroundColor Red ; Write-Host '' - } - - Show-TCLogo - - Get-Command -Module TheCleaners | Select-Object @{Name = 'The Cleaners Offer These Services: 🧹'; Expression = { $_.Name } } -} - - - - diff --git a/src/Artifacts/en-US/TheCleaners-help.xml b/src/Artifacts/en-US/TheCleaners-help.xml deleted file mode 100644 index ff8c034..0000000 --- a/src/Artifacts/en-US/TheCleaners-help.xml +++ /dev/null @@ -1,561 +0,0 @@ - - - - - Clear-CurrentUserTemp - Clear - CurrentUserTemp - - Clean old temp files from user profiles. - - - - Remove temp files older than a given number of days from the user's local temp folder. - - - - Clear-CurrentUserTemp - - Days - - Remove temp files that are $Days days old or older. The default is 30. - - Int16 - - Int16 - - - 30 - - - TimeOut - - A time limit (seconds) for the looping operation that removes empty directories. The default is 30. - - Int16 - - Int16 - - - 30 - - - WhatIf - - Shows what would happen if the cmdlet runs. The cmdlet is not run. - - - SwitchParameter - - - False - - - Confirm - - Prompts you for confirmation before running the cmdlet. - - - SwitchParameter - - - False - - - - - - Days - - Remove temp files that are $Days days old or older. The default is 30. - - Int16 - - Int16 - - - 30 - - - TimeOut - - A time limit (seconds) for the looping operation that removes empty directories. The default is 30. - - Int16 - - Int16 - - - 30 - - - WhatIf - - Shows what would happen if the cmdlet runs. The cmdlet is not run. - - SwitchParameter - - SwitchParameter - - - False - - - Confirm - - Prompts you for confirmation before running the cmdlet. - - SwitchParameter - - SwitchParameter - - - False - - - - - - - - - - - - -------------------------- EXAMPLE 1 -------------------------- - Clear-CurrentUserTemp -Days 30 - - - - - - -------------------------- EXAMPLE 2 -------------------------- - Clean-CurrentUserTemp -Days 21 -TimeOut 30 - - - - - - - - - - Clear-OldExchangeLog - Clear - OldExchangeLog - - Clean out old Exchange Server logs. - - - - Remove any Exchange logs that are older than a specified date. - - - - Clear-OldExchangeLog - - Days - - The number of days to keep logs for. Any logs older than this will be removed. - - Int32 - - Int32 - - - 60 - - - WhatIf - - Shows what would happen if the cmdlet runs. The cmdlet is not run. - - - SwitchParameter - - - False - - - Confirm - - Prompts you for confirmation before running the cmdlet. - - - SwitchParameter - - - False - - - - - - Days - - The number of days to keep logs for. Any logs older than this will be removed. - - Int32 - - Int32 - - - 60 - - - WhatIf - - Shows what would happen if the cmdlet runs. The cmdlet is not run. - - SwitchParameter - - SwitchParameter - - - False - - - Confirm - - Prompts you for confirmation before running the cmdlet. - - SwitchParameter - - SwitchParameter - - - False - - - - - - - - - - - - -------------------------- EXAMPLE 1 -------------------------- - Clear-OldExchangeLog -Days 60 - - This will remove all Exchange logs older than 60 days. - - - - - - - - Clear-OldIISLog - Clear - OldIISLog - - A script to clean out old IIS log files. - - - - This script will clean out IIS log files older than x days. - - - - Clear-OldIISLog - - Days - - The number of days to keep log files. The default is 30 days. - - Int16 - - Int16 - - - 60 - - - - - - Days - - The number of days to keep log files. The default is 30 days. - - Int16 - - Int16 - - - 60 - - - - - - - If the WebAdministration module is available, it will use that to check the specific log file locations for each web site. Otherwise, it checks the assumed default log folder location and the registry for the IIS log file location. - To Do: Add a summary of which blocks were run and possibly a count of log files removed. - - - - - -------------------------- EXAMPLE 1 -------------------------- - Clear-OldIISLogFile -Days 60 - - Removes all IIS log files that are older than 60 days. - - - - - - - - Clear-WindowsTemp - Clear - WindowsTemp - - A script to clean out old Windows Temp files. - - - - This script will clean out Windows Temp files older than x days. - - - - Clear-WindowsTemp - - Days - - The number of days to keep temp files. The default is 30 days. - - Int16 - - Int16 - - - 30 - - - WhatIf - - Shows what would happen if the cmdlet runs. The cmdlet is not run. - - - SwitchParameter - - - False - - - Confirm - - Prompts you for confirmation before running the cmdlet. - - - SwitchParameter - - - False - - - - - - Days - - The number of days to keep temp files. The default is 30 days. - - Int16 - - Int16 - - - 30 - - - WhatIf - - Shows what would happen if the cmdlet runs. The cmdlet is not run. - - SwitchParameter - - SwitchParameter - - - False - - - Confirm - - Prompts you for confirmation before running the cmdlet. - - SwitchParameter - - SwitchParameter - - - False - - - - - - - - - - - - -------------------------- EXAMPLE 1 -------------------------- - Clear-WindowsTemp -Days 60 - - Removes all Windows Temp files that are older than 60 days. - - - - - - - - Get-StaleUserProfile - Get - StaleUserProfile - - A script to find old, unused user profiles in Windows. - - - - This script finds old, unused profiles in Windows and helps you remove them. It should exclude special accounts and system profiles. - - - - Get-StaleUserProfile - - Days - - Number of days to consider a profile stale. The default is 90. - - Int16 - - Int16 - - - 90 - - - ShowSummary - - Show a summary of the stale profiles found. - - - SwitchParameter - - - False - - - - - - Days - - Number of days to consider a profile stale. The default is 90. - - Int16 - - Int16 - - - 90 - - - ShowSummary - - Show a summary of the stale profiles found. - - SwitchParameter - - SwitchParameter - - - False - - - - - - - Partially inspired by http://woshub.com/delete-old-user-profiles-gpo-powershell/ - - - - - -------------------------- EXAMPLE 1 -------------------------- - $StaleUserProfile = Get-StaleUserProfile -ShowSummary - - Gets stale user profiles into the StaleUserProfiles variable while also showing a summary. - - - - - - - - Start-Cleaning - Start - Cleaning - - Show the commands you can give The Cleaners. - - - - Get started with a menu of services The Cleaners can offer. - - - - Start-Cleaning - - Dedication - - Show dedication - - - SwitchParameter - - - False - - - - - - Dedication - - Show dedication - - SwitchParameter - - SwitchParameter - - - False - - - - - - - - - - - - -------------------------- EXAMPLE 1 -------------------------- - Start-Cleaning - - View the menu of services that TheCleaners provide. - - - - - - \ No newline at end of file diff --git a/src/PSScriptAnalyzerSettings.psd1 b/src/PSScriptAnalyzerSettings.psd1 index 4038cf0..f9c891d 100644 --- a/src/PSScriptAnalyzerSettings.psd1 +++ b/src/PSScriptAnalyzerSettings.psd1 @@ -21,7 +21,7 @@ #________________________________________ #ExcludeRules - #Specify ExcludeRules when you want to exclude a certain rule from the the default set of rules. + #Specify ExcludeRules when you want to exclude a certain rule from the default set of rules. ExcludeRules = @( #'PSUseDeclaredVarsMoreThanAssignments', 'PSUseSingularNouns', diff --git a/src/Tests/Unit/CleanupBehavior.Tests.ps1 b/src/Tests/Unit/CleanupBehavior.Tests.ps1 index 594ad24..0d0edbc 100644 --- a/src/Tests/Unit/CleanupBehavior.Tests.ps1 +++ b/src/Tests/Unit/CleanupBehavior.Tests.ps1 @@ -2,8 +2,10 @@ BeforeAll { $ModuleRoot = (Resolve-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath '..\..\TheCleaners')).Path . (Join-Path -Path $ModuleRoot -ChildPath 'Private\Remove-OldFiles.ps1') . (Join-Path -Path $ModuleRoot -ChildPath 'Public\Clear-CurrentUserTemp.ps1') + . (Join-Path -Path $ModuleRoot -ChildPath 'Public\Clear-WindowsTemp.ps1') . (Join-Path -Path $ModuleRoot -ChildPath 'Public\Clear-OldIISLog.ps1') . (Join-Path -Path $ModuleRoot -ChildPath 'Public\Clear-OldExchangeLog.ps1') + . (Join-Path -Path $ModuleRoot -ChildPath 'Public\Get-StaleUserProfile.ps1') } Describe 'Remove-OldFiles' -Tag Unit { @@ -35,6 +37,59 @@ Describe 'Remove-OldFiles' -Tag Unit { } } +Describe 'Clear-WindowsTemp' -Tag Unit { + BeforeEach { + $PreviousSystemRoot = $env:SystemRoot + $TestRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([guid]::NewGuid().Guid) + $WindowsTempPath = Join-Path -Path $TestRoot -ChildPath 'Temp' + New-Item -Path $WindowsTempPath -ItemType Directory -Force | Out-Null + $OldFile = New-Item -Path (Join-Path -Path $WindowsTempPath -ChildPath 'old.tmp') -ItemType File + $NewFile = New-Item -Path (Join-Path -Path $WindowsTempPath -ChildPath 'new.tmp') -ItemType File + $OldDirectory = New-Item -Path (Join-Path -Path $WindowsTempPath -ChildPath 'old-dir') -ItemType Directory + $NewChildFile = New-Item -Path (Join-Path -Path $OldDirectory.FullName -ChildPath 'new-child.tmp') -ItemType File + $OldFile.LastWriteTime = (Get-Date).AddDays(-31) + $NewFile.LastWriteTime = Get-Date + $NewChildFile.LastWriteTime = Get-Date + $OldDirectory.LastWriteTime = (Get-Date).AddDays(-31) + $env:SystemRoot = $TestRoot + } + + AfterEach { + $env:SystemRoot = $PreviousSystemRoot + Remove-Item -LiteralPath $TestRoot -Recurse -Force -ErrorAction SilentlyContinue + } + + It 'removes only old system temp items' { + Clear-WindowsTemp -Days 30 -Confirm:$false | Out-Null + + $OldFile.FullName | Should -Not -Exist + $NewFile.FullName | Should -Exist + } + + It 'does not recursively delete old directories containing newer files' { + Clear-WindowsTemp -Days 30 -Confirm:$false | Out-Null + + $OldDirectory.FullName | Should -Exist + $NewChildFile.FullName | Should -Exist + } + + It 'does not remove matching system temp items when WhatIf is used' { + Clear-WindowsTemp -Days 30 -WhatIf + + $OldFile.FullName | Should -Exist + $NewFile.FullName | Should -Exist + } + + It 'writes a clear error when SystemRoot is missing' { + $env:SystemRoot = '' + + $ErrorRecord = Clear-WindowsTemp -Days 30 -ErrorAction SilentlyContinue -ErrorVariable WindowsTempError 2>$null + + $ErrorRecord | Should -BeNullOrEmpty + $WindowsTempError.Exception.Message | Should -Be 'Clear-WindowsTemp requires the SystemRoot environment variable to locate the system temp folder.' + } +} + Describe 'Clear-CurrentUserTemp' -Tag Unit { BeforeEach { $PreviousTemp = $env:TEMP @@ -58,6 +113,50 @@ Describe 'Clear-CurrentUserTemp' -Tag Unit { } } +Describe 'Get-StaleUserProfile' -Tag Unit { + BeforeEach { + $OldDate = (Get-Date).AddDays(-91) + $RecentDate = Get-Date + Mock Get-CimInstance { + @( + [pscustomobject]@{ + LocalPath = 'C:\Users\StaleUser' + SID = 'S-1-5-21-1000' + LastUseTime = $OldDate + Special = $false + Loaded = $false + } + [pscustomobject]@{ + LocalPath = 'C:\Users\RecentUser' + SID = 'S-1-5-21-1001' + LastUseTime = $RecentDate + Special = $false + Loaded = $false + } + [pscustomobject]@{ + LocalPath = 'C:\Users\LoadedUser' + SID = 'S-1-5-21-1002' + LastUseTime = $OldDate + Special = $false + Loaded = $true + } + ) + } + } + + It 'rejects non-positive retention days' { + { Get-StaleUserProfile -Days 0 } | Should -Throw + } + + It 'returns only old unloaded non-special profiles' -Skip:(-not ($PSVersionTable.PSEdition -eq 'Desktop' -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows))) { + $Result = Get-StaleUserProfile -Days 90 + + $Result | Should -HaveCount 1 + $Result.LocalPath | Should -Be 'C:\Users\StaleUser' + Should -Invoke Get-CimInstance -Exactly 1 + } +} + Describe 'Clear-OldExchangeLog' -Tag Unit { BeforeEach { $TestRoot = Join-Path -Path ([System.IO.Path]::GetTempPath()) -ChildPath ([guid]::NewGuid().Guid) diff --git a/src/TheCleaners.build.ps1 b/src/TheCleaners.build.ps1 index e5b3c6f..734fd6d 100644 --- a/src/TheCleaners.build.ps1 +++ b/src/TheCleaners.build.ps1 @@ -89,9 +89,8 @@ Enter-Build { $script:BuildModuleRootFile = Join-Path -Path $script:ArtifactsPath -ChildPath "$($script:ModuleName).psm1" - # Ensure our builds fail until if below a minimum defined code test coverage threshold - ##### Nerf this test by setting the threshold to 2 ##### - $script:coverageThreshold = 2 + # Ensure our builds fail if code coverage falls below the current tested baseline. + $script:coverageThreshold = 40 [version]$script:MinPesterVersion = '5.2.2' [version]$script:MaxPesterVersion = '5.99.99' @@ -124,7 +123,7 @@ Set-BuildFooter { Add-BuildTask ValidateRequirements { # this setting comes from the *.Settings.ps1 Write-Build White " Verifying at least PowerShell $script:requiredPSVersion..." - Assert-Build ($PSVersionTable.PSVersion -ge $script:requiredPSVersion) "At least Powershell $script:requiredPSVersion is required for this build to function properly" + Assert-Build ($PSVersionTable.PSVersion -ge $script:requiredPSVersion) "At least PowerShell $script:requiredPSVersion is required for this build to function properly" Write-Build Green ' ...Verification Complete!' } #ValidateRequirements @@ -151,9 +150,9 @@ Add-BuildTask ImportModuleManifest { Add-BuildTask Clean { Write-Build White ' Clean up our Artifacts/Archive directory...' - $null = Remove-Item $script:ArtifactsPath -Force -Recurse -ErrorAction 0 + $null = Remove-Item $script:ArtifactsPath -Force -Recurse -ErrorAction SilentlyContinue $null = New-Item $script:ArtifactsPath -ItemType:Directory - $null = Remove-Item $script:ArchivePath -Force -Recurse -ErrorAction 0 + $null = Remove-Item $script:ArchivePath -Force -Recurse -ErrorAction SilentlyContinue $null = New-Item $script:ArchivePath -ItemType:Directory Write-Build Green ' ...Clean Complete!' diff --git a/src/TheCleaners/Private/Remove-OldFiles.ps1 b/src/TheCleaners/Private/Remove-OldFiles.ps1 index 6567cfc..401abae 100644 --- a/src/TheCleaners/Private/Remove-OldFiles.ps1 +++ b/src/TheCleaners/Private/Remove-OldFiles.ps1 @@ -34,13 +34,14 @@ function Remove-OldFiles { process { Write-Verbose -Message "Finding and removing files older than $Days." + $CutoffDate = [datetime]::Now.AddDays(-$Days) $OldFiles = Get-ChildItem -LiteralPath $Path -File -Recurse -ErrorAction Stop | Where-Object { - $_.LastWriteTime -le ([datetime]::Now.AddDays(-$Days)) + $_.LastWriteTime -le $CutoffDate } foreach ($File in $OldFiles) { if ($PSCmdlet.ShouldProcess($File.FullName, 'Remove old file')) { - Remove-Item -LiteralPath $File.FullName -ErrorAction Stop + Remove-Item -LiteralPath $File.FullName -Confirm:$false -ErrorAction Stop } } } diff --git a/src/TheCleaners/Private/Show-TCLogo.ps1 b/src/TheCleaners/Private/Show-TCLogo.ps1 index dc30763..f79765f 100644 --- a/src/TheCleaners/Private/Show-TCLogo.ps1 +++ b/src/TheCleaners/Private/Show-TCLogo.ps1 @@ -7,13 +7,13 @@ Show a color or plain ASCII art logo for The Cleaners whenever you need it in another function. .PARAMETER Plain - Return a plan-text version of the logo instead of multi-colored Write-Host output. + Return a plain-text version of the logo instead of multi-colored Write-Host output. .EXAMPLE - Show-Logo + Show-TCLogo .EXAMPLE - Show-Logo -Plain + Show-TCLogo -Plain #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingWriteHost','')] diff --git a/src/TheCleaners/Public/Clear-CurrentUserTemp.ps1 b/src/TheCleaners/Public/Clear-CurrentUserTemp.ps1 index 1131621..75ec379 100644 --- a/src/TheCleaners/Public/Clear-CurrentUserTemp.ps1 +++ b/src/TheCleaners/Public/Clear-CurrentUserTemp.ps1 @@ -46,24 +46,30 @@ function Clear-CurrentUserTemp { } Write-Verbose "Getting files older than $($Days) days (inclusive) in `'$UserTempPath`'." - $OldFiles = Get-ChildItem -Path $UserTempPath -File -Recurse -ErrorAction SilentlyContinue | Where-Object { - $_.LastWriteTime -le ( (Get-Date).AddDays(-$Days) ) + try { + $CutoffDate = (Get-Date).AddDays(-$Days) + $OldFiles = @(Get-ChildItem -LiteralPath $UserTempPath -File -Recurse -Force -ErrorAction Stop | Where-Object { + $_.LastWriteTime -le $CutoffDate + }) + } catch { + Write-Warning -Message "Failed to enumerate '$UserTempPath': $($_.Exception.Message)" + return } if ($OldFiles.Count -eq 0) { - Write-Output "No files found older than $Days days in `'$UserTempPath`'." + Write-Information -MessageData "No files found older than $Days days in `'$UserTempPath`'." -InformationAction Continue return } - Write-Output "Found $($OldFiles.Count) files and directories older than $Days days in $UserTempPath.`n" + Write-Information -MessageData "Found $($OldFiles.Count) files older than $Days days in $UserTempPath." -InformationAction Continue - foreach ($file in $OldFiles) { - if ( $PSCmdlet.ShouldProcess("Removing $($file.FullName)", $file.FullName, 'Remove-Item') ) { + foreach ($File in $OldFiles) { + if ($PSCmdlet.ShouldProcess($File.FullName, 'Remove temp file')) { try { - Remove-Item $file -Confirm:$false -ErrorAction Stop - Write-Verbose -Message "Removed file: $($file.FullName)" + Remove-Item -LiteralPath $File.FullName -Confirm:$false -ErrorAction Stop + Write-Verbose -Message "Removed file: $($File.FullName)" } catch { - Write-Output " $($Error[-1].Exception.Message)" + Write-Warning -Message "Failed to remove file '$($File.FullName)': $($_.Exception.Message)" } } } @@ -77,34 +83,37 @@ function Clear-CurrentUserTemp { $TimeLimit = [timespan]::FromSeconds($TimeOut) # Save the current ErrorActionPreference so we can restore it after using SilentlyContinue. $RunningErrorActionPreference = $ErrorActionPreference - $ErrorActionPreference = 'SilentlyContinue' - do { - # Break from the do-until loop if the TimeLimit has been reached. - if ((Get-Date) - $CleanEmptyDirectoriesStartTime -ge $TimeLimit ) { - Write-Output "The CleanEmptyDirectories operation timed out after $TimeOut seconds. There are $($EmptyDirectories.Count) empty directories left." - break - } - # Get directories that have 0 files in them. - $EmptyDirectories = @(Get-ChildItem -Path $UserTempPath -Directory -Recurse | Where-Object { $_.GetFileSystemInfos().Count -eq 0 }) - Write-Verbose "$($EmptyDirectories.Count) empty directories found." - $RemovedDirectory = $false - foreach ($Directory in $EmptyDirectories) { - if ($PSCmdlet.ShouldProcess("Removing $($Directory.FullName)", $Directory.FullName, 'Remove-Item')) { - try { - Remove-Item -LiteralPath $Directory.FullName -ErrorAction Stop - $RemovedDirectory = $true - } catch { - Write-Warning -Message "Failed to remove directory '$($Directory.FullName)': $($_.Exception.Message)" + try { + $ErrorActionPreference = 'Stop' + do { + # Break from the do-until loop if the TimeLimit has been reached. + if ((Get-Date) - $CleanEmptyDirectoriesStartTime -ge $TimeLimit ) { + Write-Warning -Message "The CleanEmptyDirectories operation timed out after $TimeOut seconds. There are $($EmptyDirectories.Count) empty directories left." + break + } + # Get directories that have 0 files in them. + $EmptyDirectories = @(Get-ChildItem -LiteralPath $UserTempPath -Directory -Recurse -Force | Where-Object { $_.GetFileSystemInfos().Count -eq 0 }) + Write-Verbose "$($EmptyDirectories.Count) empty directories found." + $RemovedDirectory = $false + foreach ($Directory in $EmptyDirectories) { + if ($PSCmdlet.ShouldProcess($Directory.FullName, 'Remove empty temp directory')) { + try { + Remove-Item -LiteralPath $Directory.FullName -Confirm:$false -ErrorAction Stop + $RemovedDirectory = $true + } catch { + Write-Warning -Message "Failed to remove directory '$($Directory.FullName)': $($_.Exception.Message)" + } } } - } - if ($EmptyDirectories.Count -gt 0 -and -not $RemovedDirectory) { - break - } - } until ( - $EmptyDirectories.Count -eq 0 - ) - $ErrorActionPreference = $RunningErrorActionPreference + if ($EmptyDirectories.Count -gt 0 -and -not $RemovedDirectory) { + break + } + } until ( + $EmptyDirectories.Count -eq 0 + ) + } finally { + $ErrorActionPreference = $RunningErrorActionPreference + } #endregion CleanEmptyDirectories } diff --git a/src/TheCleaners/Public/Clear-OldExchangeLog.ps1 b/src/TheCleaners/Public/Clear-OldExchangeLog.ps1 index d374e36..630a390 100644 --- a/src/TheCleaners/Public/Clear-OldExchangeLog.ps1 +++ b/src/TheCleaners/Public/Clear-OldExchangeLog.ps1 @@ -44,10 +44,10 @@ function Clear-OldExchangeLog { # Define the paths to the Exchange log files $LogLocations = @{ - ExchangeLoggingPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'Logging\' -ErrorAction Ignore - ETLTracesPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'Bin\Search\Ceres\Diagnostics\ETLTraces\' -ErrorAction Ignore - DiagnosticLogsPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'Bin\Search\Ceres\Diagnostics\Logs' -ErrorAction Ignore - MessageTrackingLogsPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'TransportRoles\Logs\MessageTracking\' -ErrorAction Ignore + ExchangeLoggingPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'Logging\' + ETLTracesPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'Bin\Search\Ceres\Diagnostics\ETLTraces\' + DiagnosticLogsPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'Bin\Search\Ceres\Diagnostics\Logs' + MessageTrackingLogsPath = Join-Path -Path $ExchangeInstallPath -ChildPath 'TransportRoles\Logs\MessageTracking\' } $LastWriteDate = (Get-Date).AddDays(-$Days) @@ -63,14 +63,23 @@ function Clear-OldExchangeLog { continue } - $OldFiles = Get-ChildItem -Path $($LogLocation.Value) -File -Recurse | - Where-Object { ($_.Name -like '*.log') -and ($_.LastWriteTime -le $LastWriteDate) } + try { + $OldFiles = @(Get-ChildItem -LiteralPath $LogLocation.Value -File -Recurse -Force -ErrorAction Stop | + Where-Object { ($_.Name -like '*.log') -and ($_.LastWriteTime -le $LastWriteDate) }) + } catch { + Write-Warning -WarningAction Continue "Failed to enumerate $($LogLocation.Key): $($_.Exception.Message)" + continue + } foreach ($File in $OldFiles) { if ($PSCmdlet.ShouldProcess($File.FullName, 'Remove old Exchange log file')) { - # Confirmation is handled by the outer ShouldProcess check, so suppress - # nested Remove-Item confirmation to avoid duplicate prompts. - Remove-Item -LiteralPath $File.FullName -Confirm:$false -ErrorAction Stop + try { + # Confirmation is handled by the outer ShouldProcess check, so suppress + # nested Remove-Item confirmation to avoid duplicate prompts. + Remove-Item -LiteralPath $File.FullName -Confirm:$false -ErrorAction Stop + } catch { + Write-Warning -WarningAction Continue "Failed to remove Exchange log '$($File.FullName)': $($_.Exception.Message)" + } } } # end foreach $file diff --git a/src/TheCleaners/Public/Clear-OldIISLog.ps1 b/src/TheCleaners/Public/Clear-OldIISLog.ps1 index a5808a7..4aa034b 100644 --- a/src/TheCleaners/Public/Clear-OldIISLog.ps1 +++ b/src/TheCleaners/Public/Clear-OldIISLog.ps1 @@ -19,7 +19,7 @@ function Clear-OldIISLog { each web site. Otherwise, it checks the assumed default log folder location and the registry for the IIS log file location. - To Do: Add a summary of which blocks were run and possibly a count of log files removed. + Future enhancements may add a summary of which locations were processed and how many log files were removed. #> [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')] @@ -52,7 +52,7 @@ function Clear-OldIISLog { # If the WebAdministration module is not available, check the default log file location $DefaultIISLogLocation = "$env:SystemDrive\inetpub\logs\LogFiles" Write-Information "The WebAdministration module is not installed. We will check the default IIS log file location at '$DefaultIISLogLocation'." -InformationAction Continue - if (Test-Path -Path $DefaultIISLogLocation -ErrorAction SilentlyContinue) { + if (Test-Path -LiteralPath $DefaultIISLogLocation -PathType Container) { try { if ($PSCmdlet.ShouldProcess($DefaultIISLogLocation, "Remove IIS log files older than $Days days")) { Remove-OldFiles -Path $DefaultIISLogLocation -Days $Days -Confirm:$false @@ -65,9 +65,16 @@ function Clear-OldIISLog { Write-Information -MessageData "The default IIS log file location at '$DefaultIISLogLocation' does not exist." -InformationAction Continue } - # If the WebAdministration module is not available,try to check the IIS log file location from the registry (requires local admin rights to read this path) - $LogDir = Get-ItemProperty -Path 'HKLM:\\System\CurrentControlSet\Services\W3SVC\Parameters' -Name 'LogDir' -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LogDir - if ($LogDir -and (Test-Path -Path $LogDir)) { + # If the WebAdministration module is not available, try to check the IIS log file location from the registry (requires local admin rights to read this path) + try { + $LogDir = Get-ItemProperty -LiteralPath 'HKLM:\System\CurrentControlSet\Services\W3SVC\Parameters' -Name 'LogDir' -ErrorAction Stop | + Select-Object -ExpandProperty LogDir + } catch { + Write-Verbose -Message "Unable to read the alternate IIS log file location from the registry: $($_.Exception.Message)" + $LogDir = $null + } + + if ($LogDir -and (Test-Path -LiteralPath $LogDir -PathType Container)) { try { if ($PSCmdlet.ShouldProcess($LogDir, "Remove IIS log files older than $Days days")) { Remove-OldFiles -Path $LogDir -Days $Days -Confirm:$false diff --git a/src/TheCleaners/Public/Clear-WindowsTemp.ps1 b/src/TheCleaners/Public/Clear-WindowsTemp.ps1 index 86cbcc8..5cccf2a 100644 --- a/src/TheCleaners/Public/Clear-WindowsTemp.ps1 +++ b/src/TheCleaners/Public/Clear-WindowsTemp.ps1 @@ -19,34 +19,52 @@ function Clear-WindowsTemp { [Alias('Clean-WindowsTemp')] param ( # How many days worth of temp files to retain (how far back to filter). + [Parameter()] [ValidateRange(1, [int16]::MaxValue)] # Ensure it is a positive number. [int16] $Days = 30 ) + $IsWindowsHost = $PSVersionTable.PSEdition -eq 'Desktop' -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows) + if (-not $IsWindowsHost) { + Write-Error -Message 'Clear-WindowsTemp requires Windows because it cleans the system temp folder under SystemRoot.' + return + } + + if ([string]::IsNullOrWhiteSpace($env:SystemRoot)) { + Write-Error -Message 'Clear-WindowsTemp requires the SystemRoot environment variable to locate the system temp folder.' + return + } + $TempPath = Join-Path -Path $env:SystemRoot -ChildPath 'Temp' if (-not (Test-Path -Path $TempPath)) { Write-Warning -Message "Unable to find $TempPath." return } - $OldFiles = Get-ChildItem -Path $TempPath -Recurse | Where-Object { - $_.LastWriteTime -le ( (Get-Date).AddDays(-$Days) ) + try { + $CutoffDate = (Get-Date).AddDays(-$Days) + $OldFiles = @(Get-ChildItem -LiteralPath $TempPath -File -Recurse -Force -ErrorAction Stop | Where-Object { + $_.LastWriteTime -le $CutoffDate + }) + } catch { + Write-Warning -Message "Failed to enumerate '$TempPath': $($_.Exception.Message)" + return } if ($OldFiles.Count -eq 0) { - Write-Output "No files found older than $Days days." + Write-Information -MessageData "No files found older than $Days days." -InformationAction Continue return } - Write-Output "Found $($OldFiles.Count) files and directories older than $Days days in the system temp folder.`n" + Write-Information -MessageData "Found $($OldFiles.Count) files older than $Days days in the system temp folder." -InformationAction Continue - foreach ($file in $OldFiles) { - if ( $PSCmdlet.ShouldProcess("Removing $($file.FullName)", $file.FullName, 'Remove-Item') ) { + foreach ($File in $OldFiles) { + if ($PSCmdlet.ShouldProcess($File.FullName, 'Remove temp item')) { try { - Remove-Item $file -Confirm:$false -ErrorAction Stop - Write-Verbose -Message "Removed file: $($file.FullName)" + Remove-Item -LiteralPath $File.FullName -Confirm:$false -ErrorAction Stop + Write-Verbose -Message "Removed temp file: $($File.FullName)" } catch { - Write-Output " $($Error[-1].Exception.Message)" + Write-Warning -Message "Failed to remove '$($File.FullName)': $($_.Exception.Message)" } } } diff --git a/src/TheCleaners/Public/Get-StaleUserProfile.ps1 b/src/TheCleaners/Public/Get-StaleUserProfile.ps1 index b9ac789..b5c7319 100644 --- a/src/TheCleaners/Public/Get-StaleUserProfile.ps1 +++ b/src/TheCleaners/Public/Get-StaleUserProfile.ps1 @@ -24,6 +24,7 @@ function Get-StaleUserProfile { param ( # Number of days to consider a profile stale. The default is 90. [Parameter(Position = 0)] + [ValidateRange(1, [int16]::MaxValue)] [Int16] $Days = 90, @@ -33,16 +34,39 @@ function Get-StaleUserProfile { $ShowSummary ) - # Get all user profiles that have not been used in 60 days, are not currently loaded, and are not special accounts. - [array]$StaleUserProfiles = Get-CimInstance -Class Win32_UserProfile | Where-Object { ($_.LastUseTime -lt (Get-Date).AddDays(-$Days)) -and (!$_.Special) -and (!$_.Loaded) } + $IsWindowsHost = $PSVersionTable.PSEdition -eq 'Desktop' -or ($PSVersionTable.PSVersion.Major -ge 6 -and $IsWindows) + if (-not $IsWindowsHost) { + Write-Error -Message 'Get-StaleUserProfile requires Windows because it queries Win32_UserProfile.' + return + } + + try { + $CutoffDate = (Get-Date).AddDays(-$Days) + [array]$StaleUserProfiles = Get-CimInstance -Class Win32_UserProfile -ErrorAction Stop | Where-Object { + ($_.LastUseTime -lt $CutoffDate) -and (-not $_.Special) -and (-not $_.Loaded) + } + } catch { + Write-Error -Message "Failed to query Win32_UserProfile: $($_.Exception.Message)" + return + } # Might need to check last modified date using NTFS: foreach ($profile in $StaleUserProfiles) { Get-Item -Path $($_.LocalPath).LastWriteTime } if ($StaleUserProfiles.Count -lt 1 -or -not $StaleUserProfiles) { Write-Information 'No stale user profiles were found.' -InformationAction Continue } else { if ($ShowSummary) { - $StaleUserProfiles | Select-Object LocalPath, SID, @{ Name = 'Size'; Expression = { '{0} MB' -f [math]::Round(((Get-ChildItem $_.LocalPath -Recurse | Measure-Object -Property Length -Sum -ErrorAction Stop).Sum / 1MB)) } } | Out-Host - Write-Information -InformationAction Continue 'NOTE: If you do not have access to a profile folder, the size will show as 0 MB.' + $StaleUserProfiles | Select-Object LocalPath, SID, @{ + Name = 'Size' + Expression = { + try { + '{0} MB' -f [math]::Round(((Get-ChildItem -LiteralPath $_.LocalPath -Recurse -File -Force -ErrorAction Stop | Measure-Object -Property Length -Sum).Sum / 1MB)) + } catch { + Write-Warning -Message "Failed to measure profile '$($_.LocalPath)': $($_.Exception.Message)" + 'Unavailable' + } + } + } | Out-Host + Write-Information -InformationAction Continue 'NOTE: If you do not have access to a profile folder, the size will show as Unavailable.' } $StaleUserProfiles }