StainedGlass.psm1
|
function Backup-SchTasks { <# .SYNOPSIS Exports the local list of Scheduled Tasks into a single XML file. .DESCRIPTION This backs up the Scheduled Tasks set up on the system to a file which can be restored again later. This can be useful before migrating a server/workstation, or before making significant changes to some tasks that may need to be reverted. .NOTES Notice that the root Tasks element does not have an XML namespace, but the individual Task child element elements are in the http://schemas.microsoft.com/windows/2004/02/mit/task namespace. .FUNCTIONALITY Scheduled Tasks .LINK https://msdn.microsoft.com/library/windows/desktop/bb736357.aspx .EXAMPLE Backup-SchTasks Backs up Windows Scheduled Tasks to tasks.xml. .EXAMPLE Backup-SchTasks tasks-backup.xml -Stylesheet tasks.css Saves scheduled tasks to tasks-backup.xml using tasks.css as a display stylesheet. #> [CmdletBinding()][OutputType([void])] Param( # The XML file to backup the list of scheduled tasks to. [Parameter(Position=0)][string]$Path = 'tasks.xml', # A CSS or XSLT stylesheet to use when viewing the scheduled tasks XML. [string] $Stylesheet ) '<?xml version="1.0"?>' |Out-File $Path -Encoding utf8 if($Stylesheet) { "<?xml-stylesheet href=`"$Stylesheet`" type=`"text/$((Split-Path $Stylesheet -Extension).Trim('.'))`"?>" | Out-File $Path -Encoding utf8 -Append } schtasks /query /xml | Where-Object {$_ -notlike '<?xml *?>'} | Out-File $Path -Encoding utf8 -Width ([int]::MaxValue) -Append } function Backup-Workstation { <# .SYNOPSIS Adds various configuration files and exported settings to a ZIP file. .LINK Export-InstalledPackages.ps1 .LINK Export-EdgeKeywords.ps1 .LINK Export-SecretVault.ps1 .EXAMPLE Backup-Workstation.ps1 Saves various config data to COMPUTERNAME-20230304T125000.zip. #> #TODO: Add or replace dependencies. [CmdletBinding()] Param( [Parameter(Position=0)][string] $Path = (Join-Path ~ ('{0}-{1:yyyyMMdd\THHmmss}.zip' -f $env:COMPUTERNAME,(Get-Date))) ) filter Copy-ItemToBackup { Param( [Parameter(Position=0)][string] $Destination, [Parameter(ValueFromPipeline=$true)][string] $Item ) if(!(Test-Path $Item)) {Write-Verbose "'$Item' not found"; return} $path = "$(Resolve-Path $Item)" $relpath = $path.Replace((Join-Path $env:USERPROFILE ''),'') $subdir = Split-Path $relpath if(Test-Path $path -Type Leaf) { if($subdir) {mkdir (Join-Path $Destination $subdir) |Out-Null} Copy-Item -Path $path -Destination (Join-Path $Destination $relpath) } elseif(Test-Path $path -Type Container) { if($subdir) {mkdir (Join-Path $Destination $subdir) |Out-Null} Copy-Item -Path $path -Destination (Join-Path $Destination $relpath) -Container -Recurse } } function Copy-ContentToBackup { [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseProcessBlockForPipelineCommand','', Justification='This script uses $input within an End block.')] Param( [Parameter(Position=0)][string] $Destination, [Parameter(Position=1)][string] $FileName, [Parameter(ValueFromPipeline=$true)][psobject] $Item ) End {$input |ConvertTo-Json |Out-File (Join-Path $Destination $FileName) utf8} } function Backup-Workstation { $dir = Join-Path ([io.path]::GetTempPath()) "$(New-Guid)" New-Item $dir -ItemType Directory |Out-Null @( Join-Path ~ .ssh Join-Path ~ SeqCli.json Join-Path ~ .npmrc Join-Path $env:APPDATA 'GitHub CLI' config.yml Join-Path $env:APPDATA NuGet nuget.config Join-Path $env:LOCALAPPDATA Packages Microsoft.WindowsTerminal_* LocalState settings.json ) |Copy-ItemToBackup $dir @{ User = [Environment]::GetEnvironmentVariables('User') Machine = [Environment]::GetEnvironmentVariables('Machine') } |Copy-ContentToBackup $dir env-vars.json if(Join-Path $env:LocalAppData Microsoft Edge 'User Data' Default |Test-Path -Type Container) { Export-EdgeKeywords.ps1 |Copy-ContentToBackup $dir edge-keywords.json } Export-SecretVault.ps1 -Confirm:$false |Copy-ContentToBackup $dir secret-vault.json Export-InstalledPackages.ps1 |Copy-ContentToBackup $dir packages.json Get-ChildItem $dir |Compress-Archive -DestinationPath $Path Remove-Item $dir -Recurse -Force } Backup-Workstation } function Convert-ChocolateyToWinget { <# .SYNOPSIS Change from managing various packages with Chocolatey to WinGet. .EXAMPLE Convert-ChocolateyToWinget -SkipPackages autohotkey,git Moves package management from Chocolatey to WinGet for everything except autohotkey (maybey you are managing Adobe Digital Editions with Chocolatey), or git (maybe you are managing PoshGit with Chocolatey). .FUNCTIONALITY System and updates #> [CmdletBinding(ConfirmImpact='High',SupportsShouldProcess=$true)] Param( # A specific package to convert [Parameter(Position=0,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$true)] [string[]] $PackageName, # Chocolatey packages to skip. [string[]] $SkipPackages, # Fully uninstalls the Chocolatey package before installing the corresponding winget package, # instead of simply removing the package from the Chocolatey package list. [switch] $Force ) Begin { $cunistparams = if($Force) {@()} else {@('-n','--skipautouninstaller')} $packages = Data {ConvertFrom-StringData @' 7zip=7zip.7zip ag=JFLarvoire.Ag audacity=Audacity.Audacity authy-desktop=Twilio.Authy autohotkey=Lexikos.AutoHotkey azure-cli=Microsoft.AzureCLI azure-data-studio=Microsoft.AzureDataStudio azure-functions-core-tools=Microsoft.AzureFunctionsCoreTools cdburnerxp=Canneverbe.CDBurnerXP dellcommandupdate-uwp=Dell.CommandUpdate dotnet-sdk=Microsoft.dotnet dotnetfx=Microsoft.dotNetFramework dropbox=Dropbox.Dropbox emeditor=Emurasoft.EmEditor etcher=Balena.Etcher firefox=Mozilla.Firefox # winget is unable to pin packages, so specific hardware is not supported #geforce-experience=Nvidia.GeForceExperience gh=GitHub.cli git=Git.Git github=GitHub.GitHubDesktop github-desktop=GitHub.GitHubDesktop gitkraken=Axosoft.GitKraken google-chrome-x64=Google.Chrome googlechrome=Google.Chrome googledrive=Google.Drive googleearth=Google.EarthPro gotomeeting=LogMeIn.GoToMeeting gource=acaudwell.Gource graphviz=Graphviz.Graphviz handbrake=HandBrake.HandBrake hdhomerun-view=Silicondust.HDHomeRunTECH inkscape=Inkscape.Inkscape irfanview=IrfanSkiljan.IrfanView libreoffice=TheDocumentFoundation.LibreOffice microsoft-edge=Microsoft.Edge microsoft-teams=Microsoft.Teams # winget is unable to pin packages, so versions that use incompatible OpenSSL versions would be installed #microsoft-windows-terminal=Microsoft.WindowsTerminal mp3tag=Mp3tag.Mp3tag mremoteng=mRemoteNG.mRemoteNG # winget is unable to pin packages, so versions that use incompatible OpenSSL versions would be installed #nodejs=OpenJS.NodeJS NSwagStudio=RicoSuter.NSwagStudio oh-my-posh=JanDeDobbeleer.OhMyPosh onedrive=Microsoft.OneDrive # winget isn't as scriptable as choco for updating the shell basics #powershell-core=Microsoft.PowerShell powertoys=Microsoft.PowerToys python=Python.Python.3 python3=Python.Python.3 rpi-imager=RaspberryPiFoundation.RaspberryPiImager slack=SlackTechnologies.Slack steam=Valve.Steam steam-client=Valve.Steam sumatrapdf=SumatraPDF.SumatraPDF sumatrapdf.install=SumatraPDF.SumatraPDF ubisoft-connect=Ubisoft.Connect # vcredist-all is a convenient metapackage, which winget doesn't really support visualstudio2019buildtools=Microsoft.VisualStudio.2019.BuildTools visualstudio2022buildtools=Microsoft.VisualStudio.2022.BuildTools vlc=VideoLAN.VLC vlc.install=VideoLAN.VLC vscode=Microsoft.VisualStudioCode vscode.install=Microsoft.VisualStudioCode vscode-insiders=Microsoft.VisualStudioCode.Insiders vscode-insiders.install=Microsoft.VisualStudioCode.Insiders windirstat=WinDirStat.WinDirStat winmerge=WinMerge.WinMerge zoom=Zoom.Zoom '@} } Process { if(!$PackageName) {$PackageName = $packages.Keys |Sort-Object} foreach($p in $PackageName) { if($p -in $SkipPackages) {Write-Verbose "Skipping Chocolatey package '$p'"; continue} if(!$packages.ContainsKey($p)) {Write-Error "Chocolatey package '$p' has not been mapped to a WinGet package"; continue} if(!(choco list $p -erl --idonly |Select-Object -skip 1)) {Write-Verbose "Chocolatey package '$p' not found, skipping."; continue} if(!$PSCmdlet.ShouldProcess("Chocolatey package '$p' with WinGet package '$($packages.$p)'",'replace')) {continue} Write-Verbose "Uninstalling Chocolatey package '$p'" choco uninstall $p -y @cunistparams Write-Verbose "Installing WinGet package '$($packages.$p)'" winget install -e --id $packages.$p } } } function ConvertFrom-CimInstance { <# .SYNOPSIS Convert a CimInstance object to a PSObject. .INPUTS Microsoft.Management.Infrastructure.CimInstance to convert to a PSObject. .OUTPUTS PSObject converted from the CimInstance entered. .FUNCTIONALITY Scheduled Tasks .EXAMPLE $tasks = Get-ScheduledTask |ConvertFrom-CimInstance Gets the scheduled tasks as PSObjects that support tab completion and can be serialized and exported. #> [CmdletBinding()][OutputType([Management.Automation.PSCustomObject])] Param( # The CimInstance object to convert to a PSObject. [Parameter(Position=0,ValueFromPipeline=$true)] [Microsoft.Management.Infrastructure.CimInstance] $InputObject ) Process { $value = [ordered]@{'#CimClassName' = $InputObject.CimClass.CimClassName} foreach($name in $InputObject.CimClass.CimClassProperties) { if($null -eq $InputObject.$name) {continue} switch($InputObject.CimInstanceProperties[$name].CimType) { Instance {$value[$name] = $InputObject.$name |ConvertFrom-CimInstance} InstanceArray {$value[$name] = [psobject[]]@($InputObject.$name |ConvertFrom-CimInstance)} default {$value[$name] = $InputObject.$name} } } [pscustomobject]$value } } function ConvertTo-LogParserTimestamp { <# .SYNOPSIS Formats a datetime as a LogParser literal. .INPUTS System.DateTime to encode for use as a literal in a LogParser query. .OUTPUTS System.String to use as a timestamp literal in a LogParser query. .FUNCTIONALITY Date and time .LINK https://www.microsoft.com/en-us/download/details.aspx?id=24659 .EXAMPLE logparser "select * from ex17*.log where to_localtime(timestamp(date,time)) < $(Get-Date|ConvertTo-LogParserTimestamp)" #> [CmdletBinding()][OutputType([string])] Param( # The DateTime value to convert to a LogParser literal. [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)][datetime]$Value ) Process{"timestamp('$(Get-Date $Value -Format 'yyyy-MM-dd HH:mm:ss')','yyyy-MM-dd HH:mm:ss')"} } function Copy-SchTasks { <# .SYNOPSIS Copy scheduled jobs from another computer to this one, using a GUI list to choose jobs. .FUNCTIONALITY Scheduled Tasks .EXAMPLE Copy-SchTasks SourceComputer DestComputer Attempts to copy tasks from SourceComputer to DestComputer. #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter','', Justification='The parameter is used elsewhere.')] [CmdletBinding()][OutputType([void])] Param( # The name of the computer to copy jobs from. [Parameter(Mandatory=$true,Position=0)][Alias('CN','Source')][string]$ComputerName, # The name of the computer to copy jobs to (local computer by default). [Parameter(Position=1)][Alias('To','Destination')][string]$DestinationComputerName = $env:COMPUTERNAME ) $TempXml= [io.path]::GetTempFileName() $CredentialCache = @{} function Get-CachedCredentialFor([Parameter(Mandatory=$true,Position=0)][string]$UserName) { if(!$CredentialCache.ContainsKey($UserName)) { $CredentialCache.Add($UserName,(Get-Credential -Message "Enter credentials for $UserName tasks" -UserName $UserName)) } $CredentialCache[$UserName] } filter ConvertFrom-Credential ([Parameter(Mandatory=$true,Position=0,ValueFromPipeline=$true)][PSCredential][Management.Automation.Credential()]$Credential) { return $Credential.Password |ConvertFrom-SecureString -AsPlainText } schtasks /query /s $ComputerName /v /fo csv | ConvertFrom-Csv | Where-Object {$_.HostName -ne 'HostName'} | Out-GridView -PassThru -Title 'Select jobs to copy' | Select-Object TaskName,'Run As User' -Unique | ForEach-Object { schtasks /query /s $ComputerName /tn $_.TaskName /xml ONE | Out-File $TempXml unicode -Width ([int]::MaxValue) schtasks /create /s $DestinationComputerName /tn $_.TaskName /ru ($_.'Run As User') ` /rp (Get-CachedCredentialFor $_.'Run As User' |ConvertFrom-Credential) /xml $TempXml Remove-Item $TempXml } $CredentialCache.Clear() } function Find-ProjectPackages { <# .SYNOPSIS Find modules used in projects. .INPUTS System.String containing a package name (wildcards supported). .OUTPUTS System.Management.Automation.PSCustomObject each with properties for the Name, Version, and File of packages found. .FUNCTIONALITY Packages and libraries .LINK Select-Xml .LINK ConvertFrom-Json .EXAMPLE Find-ProjectPackages jQuery* Name Version File ---- ------- ---- jquery.datatables 1.10.9 C:\Repo\packages.config jQuery 1.7 C:\Repo\packages.config jQuery 1.8.3 C:\OtherRepo\packages.config #> [CmdletBinding()][OutputType([Management.Automation.PSCustomObject])] Param( <# The name of a package to search for. Wildcards (as supported by -like) are allowed. #> [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)][string] $PackageName, <# The path of a folder to search within. Uses the current working directory ($PWD) by default. #> [string] $Path = $PWD, # The minimum (inclusive) version of the package to return. [version] $MinVersion, # The maximum (inclusive) version of the package to return. [version] $MaxVersion ) Begin { function Compare-Version([string]$version) { if(!$MinVersion -and !$MaxVersion) {return $true} if(!($version -match '(?<Version>\b\d+\.\d+(?:\.\d+)?)')) {Write-Warning "Can't parse version $version"; return $true} $v = [version]$Matches.Version if(!$MaxVersion) {return $v -ge $MinVersion} elseif(!$MinVersion) {return $v -le $MaxVersion} else {return ($v -ge $MinVersion) -and ($v -le $MaxVersion)} } $action = 'Find Project Packages' Write-Progress $action 'Getting proj package lists' -PercentComplete 0 [string[]]$projFiles = Get-ChildItem $Path -Recurse -Filter *proj |Select-Object -ExpandProperty FullName if(!$projFiles) {[string[]]$projFiles = @()} Write-Verbose "Found $($projFiles.Length) *proj files." Write-Progress $action 'Getting NuGet package lists' -PercentComplete 15 [string[]]$nugetFiles = Get-ChildItem $Path -Recurse -Filter packages.config |Select-Object -ExpandProperty FullName if(!$nugetFiles) {[string[]]$nugetFiles = @()} Write-Verbose "Found $($nugetFiles.Length) packages.config files." Write-Progress $action 'Getting npm package lists' -PercentComplete 30 [string[]]$npmFiles = Get-ChildItem $Path -Recurse -Filter package.json |Select-Object -ExpandProperty FullName if(!$npmFiles) {[string[]]$npmFiles = @()} Write-Verbose "Found $($npmFiles.Length) package.json files." Write-Progress $action 'Getting paket.lock package lists' -PercentComplete 45 [string[]]$paketFiles = Get-ChildItem $Path -Recurse -Filter paket.lock |Select-Object -ExpandProperty FullName if(!$paketFiles) {[string[]]$paketFiles = @()} Write-Verbose "Found $($paketFiles.Length) paket.lock files." $max = $projFiles.Length + $nugetFiles.Length + $npmFiles.Length + $paketFiles.Length Write-Verbose "Found $max package files." if(!$max) {Write-Error "No package lists found!"; return} $packages,$i = (New-Object Collections.ArrayList),0 foreach($file in $projFiles) { Write-Verbose "Parsing $file" Write-Progress $action "Parsing *proj package files: found $($projFiles.Count)" -CurrentOperation $file -PercentComplete (10*$i++/$max+60) $p = (dotnet list $file package --format json |ConvertFrom-Json).projects | Select-Object -ExpandProperty frameworks | Where-Object {$_.PSObject.Properties.Match('topLevelPackages').Count} | Select-Object -ExpandProperty topLevelPackages | ForEach-Object {[pscustomobject]@{ name = $_.id version = $_.resolvedVersion file = $file }} if(!$p) {Write-Verbose "No packages found in $file"; continue} [void]$packages.AddRange([object[]]$p) } foreach($file in $nugetFiles) { Write-Verbose "Parsing $file" Write-Progress $action "Parsing NuGet package files: found $($nugetFiles.Count)" -CurrentOperation $file -PercentComplete (10*$i++/$max+70) $p = Select-Xml /packages/package $file |Select-Object -ExpandProperty Node if(!$p) {Write-Verbose "No packages found in $file"; continue} [void]$packages.AddRange([object[]]( $p |ForEach-Object {[pscustomobject]@{ name = $_.id version = $_.version file = $file }})) } foreach($file in $npmFiles) { Write-Verbose "Parsing $file" Write-Progress $action "Parsing npm package files: found $($npmFiles.Count)" -CurrentOperation $file -PercentComplete (10*$i++/$max+80) $j = ConvertFrom-Json (Get-Content $file -Raw) if(!(Get-Member -InputObject $j -Name dependencies)) {Write-Verbose "No dependencies found in $file"; continue} $p = Get-Member -InputObject $j.dependencies -MemberType NoteProperty |Select-Object -ExpandProperty Name if(!$p) {Write-Verbose "No packages found in $file"; continue} [void]$packages.AddRange([object[]]( $p |ForEach-Object {[pscustomobject]@{ name = $_ version = $j.dependencies.$_ file = $file }})) } foreach($file in $paketFiles) { Write-Verbose "Parsing $file" Write-Progress $action "Parsing paket.lock package files: found $($paketFiles.Count)" -CurrentOperation $file -PercentComplete (10*$i++/$max+90) $lockpattern = '\A\s{4}(?<Name>\w\S+)\s\((?:>= )?(?<Version>\d+(?:\.\d+)+)\b' $p = Get-Content $file |ForEach-Object {if($_ -match $lockpattern){[pscustomobject]@{Name=$Matches.Name;Version=$Matches.Version}}} if(!$p) {Write-Verbose "No packages found in $file"; continue} [void]$packages.AddRange([object[]]( $p |ForEach-Object {[pscustomobject]@{ name = $_.Name version = $_.Version file = $file }})) } $max = $packages.Count } Process { $packages | Where-Object {$_.Name -like $PackageName} | Where-Object {Compare-Version $_.Version} } } function Get-ADServiceAccountInfo { <# .SYNOPSIS Lists the Global Managed Service Accounts for the domain, including the computers they are bound to. .EXAMPLE Get-ADServiceAccountInfo |Format-Table -AutoSize Name HostComputers LastLogonDate Description Account ---- ------------- ------------- ----------- ------- service1 SERVERA 2023-08-27 11:14:19 First MSA {} service2 SERVERB 2023-08-27 10:27:03 Second MSA {} serivce3 SERVERC 2023-08-25 17:19:49 Third MSA {} #> [CmdletBinding()] Param( [Parameter(Position=0)][string] $Filter = '*' ) Get-ADServiceAccount -Filter $Filter -Properties Description,LastLogonDate,HostComputers | ForEach-Object {[pscustomobject]@{ Name = $_.Name HostComputers = $_.HostComputers | ForEach-Object {Get-ADComputer $_} | Select-Object -ExpandProperty Name LastLogonDate = $_.LastLogonDate Description = $_.Description Account = $_ }} } function Get-ADUserStatus { <# .SYNOPSIS Returns the current login properties of an ActiveDirectory user. .LINK https://learn.microsoft.com/powershell/module/activedirectory/get-aduser .LINK Get-ADUser .EXAMPLE Get-ADUserStatus alans PasswordExpires : 07/17/2023 12:01:01 AM BadLogonsRemaining : 5 AccountExpirationDate : AccountExpires : 9223372036854775807 AccountLockoutTime : BadLogonCount : 0 BadPwdCount : 0 DistinguishedName : CN=Alan Smithee,OU=Directors,DC=example,DC=local Enabled : True GivenName : Alan LastBadPasswordAttempt : 04/17/2023 2:16:20 PM LastLogonDate : 04/12/2023 5:07:55 PM LockedOut : False Name : Alan Smithee ObjectClass : user ObjectGUID : 0a2e6b9c-83d5-466b-b45b-69d6fe626b08 PasswordExpired : False PasswordLastSet : 04/18/2023 12:01:01 AM PwdLastSet : 133262748613029225 SamAccountName : alans SID : S-1-5-21-3351665499-1662801859-681883470-29151 Surname : Smithee UserPrincipalName : alans@example.local #> [CmdletBinding()][OutputType([Microsoft.ActiveDirectory.Management.ADUser])] Param( [Parameter(Position=0,Mandatory=$true)][Microsoft.ActiveDirectory.Management.ADUser] $Identity ) $policy = Get-ADDefaultDomainPasswordPolicy return Get-ADUser -Identity $Identity -Properties AccountExpirationDate, AccountExpires, AccountLockoutTime, BadLogonCount, BadPwdCount, LastBadPasswordAttempt, LastLogonDate, LockedOut, PasswordExpired, PasswordLastSet, PwdLastSet | Add-NoteProperty.ps1 PasswordExpires {$_.PasswordLastSet + $policy.MaxPasswordAge} -Force -PassThru | Add-NoteProperty.ps1 BadLogonsRemaining {$policy.LockoutThreshold - $_.BadLogonCount} -Force -PassThru #TODO: Add or replace dependencies. } function Get-AspNetEvents { <# .SYNOPSIS Parses ASP.NET errors from the event log on the given server. .OUTPUTS System.Management.Automation.PSObject containing the fields stored in the event. .LINK Get-WinEvent .EXAMPLE Get-AspNetEvents WebServer Returns any ASP.NET-related events from the WebServer Application event log that occurred today. #> [CmdletBinding()][OutputType([psobject])] Param( # The name of the server on which the error occurred. [Parameter(Position=0)][Alias('CN','Server')][string[]]$ComputerName = $env:COMPUTERNAME, <# Skip events older than this datetime. Defaults to 00:00 today. #> [Parameter(Position=1)][DateTime]$After = ([DateTime]::Today), <# Skip events newer than this datetime. Defaults to now. #> [Parameter(Position=2)][DateTime]$Before = ([DateTime]::Now), # Include all event fields as properties. [switch]$AllProperties ) $IdFields = @{ # 5, 9, 21 (1) Classic ASP errors, no fields # 1020 (0) IIS config failure, no fields # 1309 (30) late runtime issues 1309 = @('EventCode','EventMessage','EventTime','EventTimeUtc','EventId','EventSequence','EventOccurrence', 'EventDetailCode','AppDomain','TrustLevel','AppPath','AppLocalPath','MachineName','_','ProcessId','ProcessName', 'AccountName','ExceptionType','ExceptionMessage','RequestUrl','RequestPath','UserHostAddress','User','IsAuthenticated', 'AuthenticationType','ReqThreadAccountName','ThreadId','ThreadAccountName','IsImpersonating','StackTrace','CustomEventDetails') # 1310 (30) early configuration-type issues 1310 = @('EventCode','EventMessage','EventTime','EventTimeUtc','EventId','EventSequence','EventOccurrence', 'EventDetailCode','AppDomain','TrustLevel','AppPath','AppLocalPath','MachineName','_','ProcessId','ProcessName', 'AccountName','ExceptionType','ExceptionMessage','RequestUrl','RequestPath','UserHostAddress','User','IsAuthenticated', 'AuthenticationType','ReqThreadAccountName','ThreadId','ThreadAccountName','IsImpersonating','StackTrace','CustomEventDetails') # 1314 (24) access issues 1314 = @('EventCode','EventMessage','EventTime','EventTimeUtc','EventId','EventSequence','EventOccurrence', 'EventDetailCode','AppDomain','TrustLevel','AppPath','AppLocalPath','MachineName','_','ProcessId','ProcessName', 'AccountName','RequestUrl','RequestPath','UserHostAddress','User','IsAuthenticated','AuthenticationType', 'ThreadAccountName','CustomEventDetails') # 1315 (25) forms authentication failure 1315 = @('EventCode','EventMessage','EventTime','EventTimeUtc','EventId','EventSequence','EventOccurrence', 'EventDetailCode','AppDomain','TrustLevel','AppPath','AppLocalPath','MachineName','_','ProcessId','ProcessName', 'AccountName','RequestUrl','RequestPath','UserHostAddress','User','IsAuthenticated','AuthenticationType', 'ThreadAccountName','CustomEventDetails') # 1316 (31) session state failure 1316 = @('EventCode','EventMessage','EventTime','EventTimeUtc','EventId','EventSequence','EventOccurrence', 'EventDetailCode','AppDomain','TrustLevel','AppPath','AppLocalPath','MachineName','_','ProcessId','ProcessName', 'AccountName','ExceptionType','ExceptionMessage','RequestUrl','RequestPath','UserHostAddress','User','IsAuthenticated', 'AuthenticationType','ReqThreadAccountName','ThreadId','ThreadAccountName','IsImpersonating','StackTrace','CustomEventDetails') # 1325 (1) serious low-level stuff, no fields } $order = if($AllProperties) { @('MachineName','EventTime','EventTimeUtc','LogTime','EntryType','AppPath','ExceptionType', 'ExceptionMessage','AccountName','UserHostAddress','IsImpersonating','IsAuthenticated','AuthenticationType','User','TrustLevel', 'CustomEventDetails','AppLocalPath','RequestUrl','RequestPath','AppDomain','Source','EventCode','EventDetailCode','EventMessage', 'EventOccurrence','EventSequence','EventId','ProcessName','ProcessId','ThreadId','ThreadAccountName','ReqThreadAccountName', 'StackTrace') } else { @('MachineName','EventTime','EventTimeUtc','LogTime','EntryType','AppPath','ExceptionType','ExceptionMessage','AccountName', 'UserHostAddress','IsImpersonating','IsAuthenticated','CustomEventDetails') } $RemoveFields= '_','ThreadAccountName','ReqThreadAccountName' # blank or redundant fields $BoolFields= 'IsAuthenticated','IsImpersonating' $IntFields= 'EventOccurrence','EventSequence','EventCode','EventDetailCode','ProcessId','ThreadId' $query = [xml]@" <QueryList> <Query Id="0" Path="Application"> <Select Path="Application">*[System[Provider[@Name='Active Server Pages' or @Name='ASP.NET 2.0.50727.0' or @Name='ASP.NET 4.0.30319.0'] and TimeCreated[@SystemTime >= '$(Get-Date $After.ToUniversalTime() -Format yyyy-MM-ddTHH:mm:ss.000Z)' and @SystemTime <= '$(Get-Date $Before.ToUniversalTime() -Format yyyy-MM-ddTHH:mm:ss.999Z)']]]</Select> <Suppress Path="Application">*[System[EventID=1017 or EventID=1019 or EventID=1023 or EventID=1025 or EventID=1076 or EventID=1077]]</Suppress> </Query> </QueryList> "@ Write-Verbose $query.OuterXml $ComputerName | ForEach-Object {Get-WinEvent $query -CN $_} | ForEach-Object { $fields = @{EntryType=$_.LevelDisplayName;Source=$_.ProviderName} if($_.Properties.Count -lt 2 -or !$IdFields.Contains($_.Id)) { # not structured nicely Write-Verbose "Unstructured:`n$($_.Message)" if($_.Message -match '(?m)^Application ID: (?<AppId>.+)$'){$fields.AppId=$Matches.AppId.TrimEnd()} if($_.Message -match '(?m)^Process ID: (?<ProcessId>.+)$'){$fields.ProcessId=[int]$Matches.ProcessId.TrimEnd()} if($_.Message -match '(?m)^Exception: (\w+\.)*(?<ExceptionType>\w+)\s*$'){$fields.ExceptionType=$Matches.ExceptionType} if($_.Message -match '(?m)^Message: (?<ExceptionMessage>.+)$'){$fields.ExceptionMessage=$Matches.ExceptionMessage.TrimEnd()} if($_.Message -match '(?ms)^StackTrace: (?<StackTrace>.+)$'){$fields.StackTrace=$Matches.StackTrace.TrimEnd()} } else { $values = $_.Properties |Select-Object -ExpandProperty Value $names = $IdFields[$_.Id] if($values.Length -gt $names.Length) { Write-Warning ('Unexpected field values: {0} > {1}' -f $values.Length,$names.Length) } 0..($values.Length-1) |ForEach-Object {[void]$fields.Add($names[$_],$values[$_].TrimEnd())} $RemoveFields |ForEach-Object {$fields.Remove($_)} $BoolFields |ForEach-Object {$fields[$_]=[bool]$fields[$_]} $IntFields |ForEach-Object {$fields[$_]=[int]$fields[$_]} $fields.RequestUrl= [uri]$fields.RequestUrl $fields.EventTime= [datetime]::Parse($fields.EventTime,$null,[Globalization.DateTimeStyles]::AssumeLocal) if($AllProperties -or $fields.EventTime -ne $_.TimeCreated) {$fields.LogTime = $_.TimeCreated} $fields.EventTimeUtc= [datetime]::Parse($fields.EventTimeUtc,$null,[Globalization.DateTimeStyles]::AssumeUniversal) if(!$AllProperties -and $fields.EventTime -eq $fields.EventTimeUtc) {$fields.Remove('EventTimeUtc')} if($fields.ExceptionMessage -and $fields.StackTrace) { $fields.ExceptionMessage= $fields.ExceptionMessage.Replace($fields.StackTrace,'').TrimEnd() } # don't need stack trace twice } if($fields.Count -eq 2) {return} $ordered = [ordered]@{} $order |Where-Object {$fields.ContainsKey($_)} |ForEach-Object {[void]$ordered.Add($_,$fields.$_)} $value = New-Object PSObject -Property $ordered $value.PSObject.TypeNames.Insert(0,'AspNetApplicationEventLogEntry') $value } } function Get-DotNetFrameworkVersions { <# .SYNOPSIS Determine which .NET Frameworks are installed on the requested system. .OUTPUTS System.Collections.Hashtable of semantic version names to version numbers of .NET frameworks installed. .FUNCTIONALITY DotNet .COMPONENT Microsoft.Win32.RegistryKey .EXAMPLE Get-DotNetFrameworkVersions Name Value ---- ----- v4.6.2+win10ann 4.6.1586 v3.5 3.5.30729.4926 v2.0.50727 2.0.50727.4927 v3.0 3.0.30729.4926 #> [CmdletBinding()][OutputType([hashtable])] Param( # The computer to list the installed .NET Frameworks for. [Alias('CN','Server')][string]$ComputerName = $env:COMPUTERNAME ) function Add-Version([string]$Name,[string]$Version) {if($Version){@{$Name=[version]$Version}}else{@{}}} $hklm = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine',$ComputerName) $ndp = $hklm.OpenSubKey('SOFTWARE\Microsoft\NET Framework Setup\NDP') $versions = @{} # get v1-v3.x $ndp.GetSubKeyNames() | Where-Object {$_ -like 'v[123].*'} | ForEach-Object { $k = $ndp.OpenSubKey($_) $v = $k.GetValue('Version') Write-Verbose "Found '$_' subkey, version $v" $versions += Add-Version $_ $v [void]$k.Dispose(); $k = $null } # get v4.x $v4 = $ndp.OpenSubKey('v4\Full') try { [string]$release = $v4.GetValue('Release') Write-Verbose "v4 release $release" $name = [ordered]@{ # see https://msdn.microsoft.com/en-us/library/hh925568.aspx '528049' = 'v4.8' '528372' = 'v4.8+win10may2020' '528040' = 'v4.8+win10may2019' '461814' = 'v4.7.2' '461808' = 'v4.7.2+win10april2018' '461310' = 'v4.7.1' '461308' = 'v4.7.1+win10fcu' '460805' = 'v4.7' '460798' = 'v4.7+win10cu' '394806' = 'v4.6.2' '394802' = 'v4.6.2+win10ann' '394748' = 'v4.6.2-preview' '394747' = 'v4.6.2-preview' '394271' = 'v4.6.1' '394254' = 'v4.6.1+win10' '393297' = 'v4.6' '393295' = 'v4.6+win10' '379893' = 'v4.5.2' '378758' = 'v4.5.1' '378675' = 'v4.5.1+win8.1' '378389' = 'v4.5' } foreach($key in $name.Keys) { if($release -ge $key) { $versions += Add-Version $name.$key $v4.GetValue('Version') break } } } catch [ArgumentNullException] { Write-Warning "Unable to open the version 4 sub key." } finally { if($v4) {[void]$v4.Dispose(); $v4 = $null} [void]$hklm.Dispose(); $hklm = $null [void]$ndp.Dispose(); $ndp = $null } $versions } function Get-DotNetVersions { <# .SYNOPSIS Determine which .NET Core & Framework versions are installed. .FUNCTIONALITY DotNet .LINK Get-DotNetFrameworkVersions .EXAMPLE Get-DotNetVersions |Implementation Version |-------------- ------- |.NET Framework 4.8.4084 |.NET Core 3.1.19 |.NET 5.0.10 #> [CmdletBinding()] Param() foreach($v in (Get-DotNetFrameworkVersions).GetEnumerator()) { [pscustomobject]@{ Implementation = '.NET Framework' Version = $v.Value } } try { #TODO: Add or replace dependencies. Use-Command.ps1 dotnet $env:ProgramFiles\dotnet\dotnet.exe -Fail foreach($v in dotnet --list-runtimes) { $name,$version,$location = $v -split ' ',3 if($name -ne 'Microsoft.NETCore.App') {continue} [pscustomobject]@{ Implementation = if($version -like '5.*'){'.NET'}else{'.NET Core'} Version = $version } } } catch{Write-Warning "Unable to enumerate .NET versions: $_"} } function Get-IisLog { <# .SYNOPSIS Easily query IIS logs. .DESCRIPTION This allows searching through various versions of IIS logs for matches to date ranges, URI path, IP address, username, &c. and returns each entry as an object, with the Time converted to localtime. .OUTPUTS System.Management.Automation.PSObject[] with properties from the log file for each request found: * Time: DateTime of the request. * Server: Address of the server. * Filename: Which log file the request was found in. * Line: The line of the log file containing the request detail. * IpAddr: The client address. * Username: The username of an authenticated request. * UserAgent: The browser identification string send by the client. * Method: The HTTP verb used by the request. * UriPath: The location on the web server requested. * Query: The GET query parameters requested. * Referrer: The location that linked to this request, if provided. * StatusCode: The numeric HTTP status code. * Status: The HTTP success/error code of the response. * SubStatusCode: The numeric IIS status code that subdivides HTTP status values. * SubStatus: The HTTP status and IIS sub-status as a .-separated string. * WinStatusCode: The Windows status code, as a number. * WinStatus: The Windows status code, as a Win32Exception value. .COMPONENT LogParser .LINK ConvertFrom-Csv .LINK https://www.microsoft.com/download/details.aspx?id=24659 .LINK https://docs.microsoft.com/windows/win32/debug/system-error-codes .LINK https://support.microsoft.com/help/943891/the-http-status-code-in-iis-7-0-iis-7-5-and-iis-8-0 .LINK https://docs.microsoft.com/dotnet/api/system.net.http.httpresponsemessage.statuscode .LINK https://docs.microsoft.com/dotnet/api/system.componentmodel.win32exception .EXAMPLE Get-IisLog -LogDirectory \\Server\c$\inetpub\logs\LogFiles\W3SVC1 -After 2014-03-30 -UriPathLike '/WebApp/%' |select -First 1 Time : 2014-03-31 15:56:45 Server : 192.168.1.99:80 Filename : ex140331.log Line : 121555 IpAddr : 192.168.1.199 Username : UserAgent : Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.1; WOW64; Trident/6.0; SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0; .NET4.0C; .NET4.0E; InfoPath.3) Method : GET UriPath : /WebApp/ Query : Referrer : StatusCode : 401 Status : Unauthorized SubStatusCode : 2 SubStatus : Logon failed due to server configuration. WinStatusCode : 5 WinStatus : Access is denied #> [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingCmdletAliases','', Justification='This script sets up and uses logparser.')] [CmdletBinding()][OutputType([Management.Automation.PSCustomObject])] Param( # Attempts to use the LogFiles$ share of the computers listed as the log directory. [Parameter(ParameterSetName='Server')][Alias('Server','CN')][string[]] $ComputerName, # The directory(ies) containing the log files to query. [Parameter(ParameterSetName='Directory')][Alias('Dir')][IO.DirectoryInfo[]] $LogDirectory = $PWD.ProviderPath, # The minimum datetime to query. [Parameter(Position=1)][datetime] $After = [datetime]::MinValue, # The maximum datetime to query. [Parameter(Position=2)][datetime] $Before = <# the max logparser date value #> '8191-12-31', # The client IP address(es) to restrict the query to. [Alias("ClientIP")][string[]] $IpAddr, # The username to restrict the search to. [string[]] $Username, # The HTTP (major) status to restrict the search to. [int[]] $Status, # The HTTP method (GET or POST, &c) to restrict the search to. [Microsoft.PowerShell.Commands.WebRequestMethod[]] $Method, # A "like" pattern to match against the requested URI stem/path. [string] $UriPathLike, # A "like" pattern to match against the query string. [string] $QueryLike, # A "like" pattern to match against the HTTP referrer. [Alias("RefererLike")][string] $ReferrerLike, <# The format the logs are written in: * IIS: The old proprietary IIS log format. * IISW3C: The (transitional?) W3C extended log file format. * W3C: The newer W3C extended log file format. #> [ValidateSet('IIS','IISW3C','W3C')][string] $LogFormat = 'W3C' ) #TODO: Add or replace dependencies. Use-Command.ps1 logparser "${env:ProgramFiles(x86)}\Log Parser 2.2\LogParser.exe" ` -msi http://download.microsoft.com/download/f/f/1/ff1819f9-f702-48a5-bbc7-c9656bc74de8/LogParser.msi filter Format-DateTimeLiteral([Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true)][datetime]$DateTime) { return "to_timestamp('$(Get-Date $DateTime -Format 'yyyy-MM-dd HH:mm:ss')','yyyy-MM-dd HH:mm:ss')" } $logRowName = @{ IIS = 'LogRow' IISW3C = 'LogRow' W3C = 'RowNumber' } $iisSubStatus = @{ '400.1' = 'Invalid Destination Header.' '400.2' = 'Invalid Depth Header.' '400.3' = 'Invalid If Header.' '400.4' = 'Invalid Overwrite Header.' '400.5' = 'Invalid Translate Header.' '400.6' = 'Invalid Request Body.' '400.7' = 'Invalid Content Length.' '400.8' = 'Invalid Timeout.' '400.9' = 'Invalid Lock Token.' '401.1' = 'Logon failed.' '401.2' = 'Logon failed due to server configuration.' '401.3' = 'Unauthorized due to ACL on resource.' '401.4' = 'Authorization failed by filter.' '401.5' = 'Authorization failed by ISAPI/CGI application.' '401.501' = 'Access Denied: Too many requests from the same client IP; Dynamic IP Restriction Concurrent request rate limit reached.' '401.502' = 'Forbidden: Too many requests from the same client IP; Dynamic IP Restriction Maximum request rate limit reached.' '401.503' = 'Access Denied: the IP address is included in the Deny list of IP Restriction' '401.504' = 'Access Denied: the host name is included in the Deny list of IP Restriction' '403.1' = 'Execute access forbidden.' '403.2' = 'Read access forbidden.' '403.3' = 'Write access forbidden.' '403.4' = 'SSL required.' '403.5' = 'SSL 128 required.' '403.6' = 'IP address rejected.' '403.7' = 'Client certificate required.' '403.8' = 'Site access denied.' '403.9' = 'Forbidden: Too many clients are trying to connect to the web server.' '403.10' = 'Forbidden: web server is configured to deny Execute access.' '403.11' = 'Forbidden: Password has been changed.' '403.12' = 'Mapper denied access.' '403.13' = 'Client certificate revoked.' '403.14' = 'Directory listing denied.' '403.15' = 'Forbidden: Client access licenses have exceeded limits on the web server.' '403.16' = 'Client certificate is untrusted or invalid.' '403.17' = 'Client certificate has expired or is not yet valid.' '403.18' = 'Cannot execute requested URL in the current application pool.' '403.19' = 'Cannot execute CGI applications for the client in this application pool.' '403.20' = 'Forbidden: Passport logon failed.' '403.21' = 'Forbidden: Source access denied.' '403.22' = 'Forbidden: Infinite depth is denied.' '403.501' = 'Forbidden: Too many requests from the same client IP; Dynamic IP Restriction Concurrent request rate limit reached.' '403.502' = 'Forbidden: Too many requests from the same client IP; Dynamic IP Restriction Maximum request rate limit reached.' '403.503' = 'Forbidden: the IP address is included in the Deny list of IP Restriction' '403.504' = 'Forbidden: the host name is included in the Deny list of IP Restriction' '404.0' = 'Not found.' '404.1' = 'Site Not Found.' '404.2' = 'ISAPI or CGI restriction.' '404.3' = 'MIME type restriction.' '404.4' = 'No handler configured.' '404.5' = 'Denied by request filtering configuration.' '404.6' = 'Verb denied.' '404.7' = 'File extension denied.' '404.8' = 'Hidden namespace.' '404.9' = 'File attribute hidden.' '404.10' = 'Request header too long.' '404.11' = 'Request contains double escape sequence.' '404.12' = 'Request contains high-bit characters.' '404.13' = 'Content length too large.' '404.14' = 'Request URL too long.' '404.15' = 'Query string too long.' '404.16' = 'DAV request sent to the static file handler.' '404.17' = 'Dynamic content mapped to the static file handler via a wildcard MIME mapping.' '404.18' = 'Querystring sequence denied.' '404.19' = 'Denied by filtering rule.' '404.20' = 'Too Many URL Segments' '404.501' = 'Not Found: Too many requests from the same client IP; Dynamic IP Restriction Concurrent request rate limit reached.' '404.502' = 'Not Found: Too many requests from the same client IP; Dynamic IP Restriction Maximum request rate limit reached.' '404.503' = 'Not Found: the IP address is included in the Deny list of IP Restriction' '404.504' = 'Not Found: the host name is included in the Deny list of IP Restriction' '500.0' = 'Module or ISAPI error occurred.' '500.11' = 'Application is shutting down on the web server.' '500.12' = 'Application is busy restarting on the web server.' '500.13' = 'Web server is too busy.' '500.15' = 'Direct requests for Global.asax are not allowed.' '500.19' = 'Configuration data is invalid.' '500.21' = 'Module not recognized.' '500.22' = 'An ASP.NET httpModules configuration does not apply in Managed Pipeline mode.' '500.23' = 'An ASP.NET httpHandlers configuration does not apply in Managed Pipeline mode.' '500.24' = 'An ASP.NET impersonation configuration does not apply in Managed Pipeline mode.' '500.50' = 'A rewrite error occurred during RQ_BEGIN_REQUEST notification handling. A configuration or inbound rule execution error occurred.' '500.51' = 'A rewrite error occurred during GL_PRE_BEGIN_REQUEST notification handling. A global configuration or global rule execution error occurred.' '500.52' = 'A rewrite error occurred during RQ_SEND_RESPONSE notification handling. An outbound rule execution occurred.' '500.53' = 'A rewrite error occurred during RQ_RELEASE_REQUEST_STATE notification handling. An outbound rule execution error occurred. The rule is configured to be executed before the output user cache gets updated.' '500.100' = 'Internal ASP error.' '502.1' = 'CGI application timeout.' '502.2' = 'Bad gateway: Premature Exit.' '502.3' = 'Bad Gateway: Forwarder Connection Error (ARR).' '502.4' = 'Bad Gateway: No Server (ARR).' '503.0' = 'Application pool unavailable.' '503.2' = 'Concurrent request limit exceeded.' '503.3' = 'ASP.NET queue full' '503.4' = 'FastCGI queue full' } filter ConvertTo-SqlString { Param( [Parameter(Position=0,ValueFromPipeline=$true)][string] $Value ) return "'$($Value -replace "'","''")'" } function ConvertTo-SqlStringList { Param( [Parameter(Position=0)][string[]] $Values ) $Local:OFS = ';' return "($($Values |ConvertTo-SqlString))" } filter ConvertTo-Enum { Param( [Parameter(Position=0)][type] $EnumType, [Parameter(Position=1,ValueFromPipeline=$true)][string] $Name ) $value = 0 if([enum]::TryParse($EnumType,$Name,$true,[ref]$value)) {return $value} else {return $Name} } filter Convert-Result { Param( [Parameter(ValueFromPipelineByPropertyName=$true)][datetime] $Time, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Server, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Filename, [Parameter(ValueFromPipelineByPropertyName=$true)][long] $Line, [Parameter(ValueFromPipelineByPropertyName=$true)][Net.IpAddress] $IpAddress, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Username, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $UserAgent, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Method, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $UriPath, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Query, [Parameter(ValueFromPipelineByPropertyName=$true)][uri] $Referrer, [Parameter(ValueFromPipelineByPropertyName=$true)][short] $StatusCode, [Parameter(ValueFromPipelineByPropertyName=$true)][uint32] $SubStatusCode, [Parameter(ValueFromPipelineByPropertyName=$true)][uint32] $WinStatusCode ) $iisFullStatus,$subStatus = "$StatusCode.$SubStatusCode",'' if($iisSubStatus.ContainsKey($iisFullStatus)) {$subStatus = $iisSubStatus[$iisFullStatus]} return [pscustomobject]@{ Time = $Time Server = $Server Filename = $Filename Line = $Line IpAddress = $IpAddress Username = $Username UserAgent = $UserAgent Method = $Method |ConvertTo-Enum Microsoft.PowerShell.Commands.WebRequestMethod UriPath = $UriPath Query = $Query Referrer = $Referrer StatusCode = $StatusCode Status = $StatusCode |ConvertTo-Enum Net.HttpStatusCode SubStatusCode = $SubStatusCode SubStatus = $subStatus WinStatusCode = $WinStatusCode WinStatus = [ComponentModel.Win32Exception][int]$WinStatusCode } } # get the log files $LogDirectory = if ($ComputerName) { $ComputerName |ForEach-Object {"\\$_\LogFiles$"} } else { $LogDirectory |Resolve-Path |Select-Object -ExpandProperty Path } $from = ' from ' + ((Get-ChildItem $LogDirectory -Filter *.log | Where-Object LastWriteTime -GE $After.AddDays(-1) | Where-Object CreationTime -LE $Before.AddDays(1) | Select-Object -ExpandProperty FullName) -join ',') if($from -eq ' from ') { $from += $($LogDirectory|ForEach-Object {"$_\*.log"}) -join ',' } # build the where clause [string[]] $where = @(" where to_localtime(to_timestamp(date,time)) between $(Format-DateTimeLiteral $After)") $where += Format-DateTimeLiteral $Before if($IpAddr) { $where += "c-ip in $(ConvertTo-SqlStringList $IpAddr)" } if($Username) { $where += "cs-username in $(ConvertTo-SqlStringList $Username)" } if($Status) { $where += "sc-status in ($($Status -join ';'))" } if($Method -and $Method.Length) { $where += "cs-method in $(ConvertTo-SqlStringList ($Method.ToUpperInvariant()))" } if($UriPathLike) { $where += "cs-uri-stem like '$($UriPathLike -replace "'","''")'" } if($QueryLike) { $where += "cs-uri-query like '$($QueryLike -replace "'","''")'" } if($ReferrerLike) { $where += "cs(Referer) like '$($ReferrerLike -replace "'","''")'" } $where = $where -join "`n and " # assemble the SQL $sql = @" select to_localtime(to_timestamp(date,time)) as Time, case s-port when null then to_string(s-ip) else strcat(s-ip,strcat(':',to_string(s-port))) end as Server, extract_filename(LogFilename) as Filename, $($logRowName[$LogFormat]) as Line, coalesce(c-ip,'') as IpAddress, cs-username as Username, coalesce(replace_chr(cs(User-Agent),'+',' '),'') as UserAgent, cs-method as Method, coalesce(cs-uri-stem,'') as UriPath, coalesce(cs-uri-query,'') as Query, coalesce(cs(Referer),'') as Referrer, sc-status as StatusCode, sc-substatus as SubStatusCode, sc-win32-status as WinStatusCode into STDOUT $from $where "@ Write-Verbose $sql logparser $sql -i:$LogFormat -o:TSV -headers:off -stats:off -q | ConvertFrom-Csv -Delimiter "`t" -Header 'Time','Server','Filename','Line','IpAddress', 'Username','UserAgent','Method','UriPath','Query','Referrer', 'StatusCode','Status','SubStatusCode','SubStatus','WinStatusCode','WinStatus' | Convert-Result } function Get-SimpleSchTasks { <# .SYNOPSIS Returns simple scheduled task info. .FUNCTIONALITY Scheduled Tasks .EXAMPLE Get-SimpleSchTasks -TaskPath \ -NonInteractive Returns a simplified list of tasks for the local system that are not set to run interactively. #> [CmdletBinding()] Param( # Specifies an array of one or more names of a scheduled task. You can use "*" for a wildcard character query. [Parameter(Position=0)][string[]] $TaskName, # Specifies an array of one or more paths for scheduled tasks in Task Scheduler namespace. # You can use "*" for a wildcard character query. You can use \ for the root folder. # To specify a full TaskPath you need to include the leading and trailing \ *. # If you do not specify a path, the cmdlet uses the root folder. [Parameter(Position=1)][string[]] $TaskPath, # Exclude tasks that are set to run interactively, include only tasks with credentials set. [switch] $NonInteractive ) Begin { filter Get-ComHandler { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $ClassId, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Data ) return "CLSID:$ClassId $Data" } filter Get-ExecAction { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $WorkingDirectory, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Execute, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Arguments ) return '{0}> {1} {2}' -f (($WorkingDirectory ? $WorkingDirectory : "%SystemRoot%\system32"), ($Execute -replace '\A([^"].*\s.*)\z','"$1"'),$Arguments | ForEach-Object {[Environment]::ExpandEnvironmentVariables($_)}) } filter Get-Run { [CmdletBinding()] Param( [Parameter(ValueFromPipeline=$true)][Microsoft.Management.Infrastructure.CimInstance] $Action ) switch($Action.CimClass.CimClassName) { MSFT_TaskComHandlerAction {$Action |Get-ComHandler} MSFT_TaskExecAction {$Action |Get-ExecAction} default {"${_}: $($Action |Select-Object -ExcludeProperty Id,PSComputerName,Cim* |ConvertTo-Json -Compress -Depth 1)"} } } function Get-Weekdays { [CmdletBinding()] Param( [Parameter(Position=0)][ushort] $DaysOfWeek ) if($DaysOfWeek -band 0b1) {[dayofweek]::Sunday} if($DaysOfWeek -band 0b10) {[dayofweek]::Monday} if($DaysOfWeek -band 0b100) {[dayofweek]::Tuesday} if($DaysOfWeek -band 0b1000) {[dayofweek]::Wednesday} if($DaysOfWeek -band 0b10000) {[dayofweek]::Thursday} if($DaysOfWeek -band 0b100000) {[dayofweek]::Friday} if($DaysOfWeek -band 0b1000000) {[dayofweek]::Saturday} } function Get-NextWeekday { [CmdletBinding()] Param( [Parameter(Position=0,Mandatory=$true)][datetime] $FromDateTime, [Parameter(Position=1,Mandatory=$true)][dayofweek] $DayOfWeek ) $dow = $FromDateTime.DayOfWeek return $FromDateTime.AddDays(($dow - $DayOfWeek) + ($dow -gt $DayOfWeek ? 0 : 7)) } filter Get-Weekly { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $StartBoundary, [Parameter(ValueFromPipelineByPropertyName=$true)][ushort] $DaysOfWeek, [Parameter(ValueFromPipelineByPropertyName=$true)][ushort] $WeeksInterval ) Get-Weekdays $DaysOfWeek | ForEach-Object {'R/{0:yyyy-MM-ddTHH:mm:ss}/P{1}W' -f (Get-NextWeekday $StartBoundary $_),$WeeksInterval} } filter Get-Timing { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $StartBoundary, [Parameter(ValueFromPipelineByPropertyName=$true)][Microsoft.Management.Infrastructure.CimInstance] $Repetition ) if(!$Repetition.Interval) {$StartBoundary} else {'R/{0:yyyy-MM-ddTHH:mm:ss}/{1}' -f $StartBoundary,$Repetition.Interval} } filter Get-StateChange { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $UserId, [Parameter(ValueFromPipelineByPropertyName=$true)][uint] $StateChange ) $username = "$($UserId ? $UserId : 'any user')" switch($StateChange) { 1 {"On local connection to user session of $username"} 2 {"On local disconnect to user session of $username"} 3 {"On remote connection to user session of $username"} 4 {"On remote disconnect from user session of $username"} 7 {"On workstation lock of $username"} 8 {"On workstation unlock of $username"} default {"On state change #$_ of $username"} } } filter Format-EventQuery { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Path, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $InnerText ) return "On event - Log: $Path, Source: $InnerText" } filter Get-EventQuery { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][xml] $Subscription ) if(!$Subscription) {return} Select-Xml -Xml $Subscription -XPath "/QueryList/Query/Select" | Select-Object -ExpandProperty Node | Format-EventQuery } filter Get-Schedule { [CmdletBinding()] Param( [Parameter(ValueFromPipeline=$true)][Microsoft.Management.Infrastructure.CimInstance] $Trigger ) if(!$Trigger -or !$Trigger.Enabled) {return} switch($Trigger.CimClass.CimClassName) { MSFT_TaskWeeklyTrigger {$Trigger |Get-Weekly} MSFT_TaskTimeTrigger {$Trigger |Get-Timing} MSFT_TaskDailyTrigger {'R/{0:yyyy-MM-ddTHH:mm:ss}/P1D' -f $Trigger.StartBoundary} MSFT_TaskLogonTrigger {"At log on of $($Trigger.UserId ?? 'any user')"} MSFT_TaskBootTrigger {'At startup'} MSFT_TaskIdleTrigger {'On idle'} MSFT_TaskSessionStateChangeTrigger {$Trigger |Get-StateChange} MSFT_TaskRegistrationTrigger {'When the task is created or modified'} MSFT_TaskEventTrigger {$Trigger |Get-EventQuery} MSFT_TaskTrigger {'Monthly or task setting or custom (ambiguous)'} default {$Trigger |Get-Member -Type Properties |Write-Debug; $_} } } filter Get-SimpleTask { [CmdletBinding()] Param( [Parameter(ValueFromPipeline=$true)][Microsoft.Management.Infrastructure.CimInstance] $Task ) $info = Get-ScheduledTaskInfo -TaskName $Task.TaskName -TaskPath $task.TaskPath return [pscustomobject]@{ TaskName = $Task.TaskName Enabled = $Task.{Settings}?.Enabled State = $Task.State User = $Task.Principal.UserId LastRunTime = $info.LastRunTime LastTaskResult = $info.LastTaskResult Run = $Task.Actions |Get-Run Schedule = $Task.Triggers |Get-Schedule LastResult = [ComponentModel.Win32Exception][uint]$info.LastTaskResult ComputerName = $env:COMPUTERNAME } } } Process { $ScheduledTask = @{} if($TaskName) {$ScheduledTask['TaskName'] = $TaskName} if($TaskPath) {$ScheduledTask['TaskPath'] = $TaskPath} $tasks = Get-ScheduledTask @ScheduledTask if($NonInteractive) {$tasks = $tasks |Where-Object {$_.Principal.LogonType -eq 1}} $tasks |Get-SimpleTask } } function Get-SystemDetails { <# .SYNOPSIS Collects some useful system hardware and operating system details via CIM. .OUTPUTS System.Management.Automation.PSCustomObject with properties about the computer: * Name: The computer name. * Status: The reported computer status name. * Manufacturer: The reported computer manufacturer name. * Model: The reported computer model name. * PrimaryOwnerName: The reported name of the owner of the computer, if available. * Memory: The reported memory in the computer, and amount unused. * OperatingSystem: The name and type of operating system used by the computer. * Processors: CPU hardware details. * Video: Video controller hardware name. * Drives: Storage drives found on the computer. * Shares: The file shares configured, if any. * NetVersions: The versions of .NET on the system. .FUNCTIONALITY System and updates .LINK https://en.wikipedia.org/wiki/List_of_Microsoft_Windows_versions .LINK https://vdc-repo.vmware.com/vmwb-repository/dcr-public/3d076a12-29a2-4d17-9269-cb8150b5a37f/8b5969e2-1a66-4425-af17-feff6d6f705d/doc/class_CIM_OperatingSystem.html .LINK Get-CimInstance .EXAMPLE Get-SystemDetails Name : DEEPTHOUGHT Status : OK Manufacturer : Microsoft Corporation Model : Surface Pro 4 PrimaryOwnerName : Memory : 3.93 GiB (25.68 % free) OperatingSystem : Microsoft Windows 10 Pro OSVersion : 10.0.14393 OSUpdate : 1607 OSType : WINNT OSArchitecture : 64-bit Processors : Intel(R) Core(TM) i5-6300U CPU @ 2.40GHz Video : NVIDIA GeForce GTX 670 Drives : C: 118 GiB (31.47 % free) Shares : NetVersions : {v4.6.2+win10ann, v3.5, v2.0.50727, v3.0} #> [CmdletBinding()][OutputType([Management.Automation.PSCustomObject])] Param() $cs = Get-CimInstance CIM_ComputerSystem $os = Get-CimInstance CIM_OperatingSystem [pscustomobject]@{ Name = $cs.Name Status = $cs.Status Manufacturer = $cs.Manufacturer Model = $cs.Model PrimaryOwnerName = $cs.PrimaryOwnerName Memory = (Format-ByteUnits $cs.TotalPhysicalMemory -si -dot 2) + " ($('{0:p}' -f (1KB*$os.FreePhysicalMemory/$cs.TotalPhysicalMemory)) free)" OperatingSystem = $os.Caption + ' ' + $os.CSDVersion OSVersion = $os.Version OSUpdate = if($os.OSType -eq 18 -and $os.Version -like '10.*') { switch($os.BuildNumber) { 22621 {'22H2'} 22000 {'22H1'} 19044 {'21H2'} 19043 {'21H1'} 19042 {'20H2'} 19041 {'2004'} 18363 {'1909'} 18362 {'1903'} 17763 {'1809'} 17134 {'1803'} 16299 {'1709'} 15063 {'1703'} 14393 {'1607'} 10586 {'1511'} 10240 {'1507'} } } OSType = switch($os.OSType) { 0 {'Unknown'} 1 {$os.OtherTypeDescription} 2 {'MACOS'} 3 {'ATTUNIX'} 4 {'DGUX'} 5 {'DECNT'} 6 {'Tru64 UNIX'} 7 {'OpenVMS'} 8 {'HPUX'} 9 {'AIX'} 10 {'MVS'} 11 {'OS400'} 12 {'OS/2'} 13 {'JavaVM'} 14 {'MSDOS'} 15 {'WIN3x'} 16 {'WIN95'} 17 {'WIN98'} 18 {'WINNT'} 19 {'WINCE'} 20 {'NCR3000'} 21 {'NetWare'} 22 {'OSF'} 23 {'DC/OS'} 24 {'Reliant UNIX'} 25 {'SCO UnixWare'} 26 {'SCO OpenServer'} 27 {'Sequent'} 28 {'IRIX'} 29 {'Solaris'} 30 {'SunOS'} 31 {'U6000'} 32 {'ASERIES'} 33 {'HP NonStop OS'} 34 {'HP NonStop OSS'} 35 {'BS2000'} 36 {'LINUX'} 37 {'Lynx'} 38 {'XENIX'} 39 {'VM'} 40 {'Interactive UNIX'} 41 {'BSDUNIX'} 42 {'FreeBSD'} 43 {'NetBSD'} 44 {'GNU Hurd'} 45 {'OS9'} 46 {'MACH Kernel'} 47 {'Inferno'} 48 {'QNX'} 49 {'EPOC'} 50 {'IxWorks'} 51 {'VxWorks'} 52 {'MiNT'} 53 {'BeOS'} 54 {'HP MPE'} 55 {'NextStep'} 56 {'PalmPilot'} 57 {'Rhapsody'} 58 {'Windows 2000'} 59 {'Dedicated'} 60 {'OS/390'} 61 {'VSE'} 62 {'TPF'} 63 {'Windows (R) Me'} 64 {'Caldera Open UNIX'} 65 {'OpenBSD'} 66 {'Not Applicable'} 67 {'Windows XP'} 68 {'z/OS'} 69 {'Microsoft Windows Server 2003'} 70 {'Microsoft Windows Server 2003 64-Bit'} 71 {'Windows XP 64-Bit'} 72 {'Windows XP Embedded'} 73 {'Windows Vista'} 74 {'Windows Vista 64-Bit'} 75 {'Windows Embedded for Point of Service'} 76 {'Microsoft Windows Server 2008'} 77 {'Microsoft Windows Server 2008 64-Bit'} } OSArchitecture = $os.OSArchitecture Processors = (Get-CimInstance CIM_Processor | Select-Object -ExpandProperty Name | ForEach-Object {$_ -replace '\s{2,}',' '}) Video = Get-CimInstance CIM_VideoController |Select-Object -ExpandProperty Name Drives = (Get-CimInstance CIM_StorageVolume | Where-Object {$_.DriveType -eq 3 -and $_.DriveLetter -and $_.Capacity} | Sort-Object DriveLetter | ForEach-Object {"$($_.DriveLetter) $(Format-ByteUnits $_.Capacity -si -dot 2) ($('{0:p}' -f ($_.FreeSpace/$_.Capacity)) free)"}) Shares= (Get-CimInstance Win32_Share | Where-Object {$_.Type -eq 0} | ForEach-Object {"$($_.Name)=$($_.Path)"}) NetVersions = Get-DotNetFrameworkVersions } } function Measure-Caches { <# .SYNOPSIS Returns a list of matching cache directories, and their sizes, sorted. .NOTES A shocking number of apps don't seem to know how the Windows folder structure works, writing cache data into the user profile's Roaming folder, which gets recopied at every logon, which is extremely wrong. This script is primarily intended to show the scope of the problem, but can be used to measure other folder matches. .FUNCTIONALITY Files .EXAMPLE Measure-Caches |Format-Table -AutoSize Path Size DirectorySize DirectorySizeOnDisk ---- ---- ------------- ------------------- c:\users\usernam\appdata\roaming\code\cachedextensionvsixs 669.3MB 701856767 701915136 c:\users\usernam\appdata\roaming\slack\service worker\cachestorage 444MB 465538811 469655552 c:\users\usernam\appdata\roaming\slack\cache 340.2MB 356697780 357654528 c:\users\usernam\appdata\roaming\slack\cache\cache_data 340.2MB 356697780 357650432 c:\users\usernam\appdata\roaming\code\cache\cache_data 323.3MB 338983271 339546112 c:\users\usernam\appdata\roaming\code\cache 323.3MB 338983271 339550208 c:\users\usernam\appdata\roaming\slack\code cache 282MB 295745766 301752320 c:\users\usernam\appdata\roaming\code\cacheddata 172.6MB 181008350 191647744 #> [CmdletBinding()] Param( # The root directory to search from. [string] $Path = $env:APPDATA, # The directory name pattern to match using -Like. [string] $NamePattern = '*cache*' ) Begin { #TODO: Add or replace dependencies. Use-Command.ps1 du "$env:ChocolateyInstall\bin\du.exe" -cinst sysinternals } Process { return Get-ChildItem $Path -Directory -Recurse | Where-Object Name -like $NamePattern | Select-Object -ExpandProperty FullName | ForEach-Object {du -ct -nobanner $_ |Select-Object -skip 1} | ConvertFrom-Csv -Delimiter "`t" -Header Path,CurrentFileCount,CurrentFileSize,FileCount,DirectoryCount,DirectorySize,DirectorySizeOnDisk | ForEach-Object {[pscustomobject]@{ Path = $_.Path #TODO: Add or replace dependencies. Size = [long] $_.DirectorySize |Format-ByteUnits.ps1 -Precision 1 DirectorySize = [long] $_.DirectorySize DirectorySizeOnDisk = [long] $_.DirectorySizeOnDisk }} | Sort-Object DirectorySize -Descending } } function Remove-LockyFile { <# .SYNOPSIS Removes a file that may be prone to locking. .INPUTS System.String containing the path of a file to delete (or rename if deleting fails). .FUNCTIONALITY Files .EXAMPLE Remove-LockyFile InUse.dll (Tries to remove file, renames it if unable to, tries deleting at reboot as a last resort.) #> [CmdletBinding()][OutputType([void])] Param( <# Specifies a path to the items being removed. Wildcards are permitted. The parameter name ("-Path") is optional. #> [Parameter(Position=0,Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ValueFromRemainingArguments=$true)] [string]$Path ) Begin { try{[void][RebootFileAction]}catch{Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public class RebootFileAction { [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)] private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags); const int MOVEFILE_DELAY_UNTIL_REBOOT = 0x4; static public void Delete(string filename) { if(!MoveFileEx(filename,"",MOVEFILE_DELAY_UNTIL_REBOOT)) { throw new InvalidOperationException(String.Format("Unable to mark {0} for deletion at reboot.",filename)); } } } '@} } Process { try { Remove-Item $Path -Force -ErrorAction Stop } catch { $NewPath = "$Path~$([guid]::NewGuid().Guid)" try { Move-Item $Path $NewPath -Force -ErrorAction Stop $Path = Resolve-Path $NewPath try { Remove-Item $Path -Force -ErrorAction Stop } catch { Write-Warning "Renamed file, but unable to remove $Path"; throw } } catch { [RebootFileAction]::Delete((Resolve-Path $Path)) Write-Warning "File $Path has been marked for deletion at reboot." } } } } function Repair-AppxPackages { <# .SYNOPSIS Re-registers all installed Appx packages. .NOTES Must be run in Windows PowerShell, apparently. Do not run in Windows Terminal. .EXAMPLE Repair-AppxPackages Repairs what Windows Store apps it can when run from PowerShell 5.1 (not in Windows Terminal) as admin. #> [CmdletBinding()] Param() filter Repair-AppxPackage { [CmdletBinding()] Param( [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Name, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $Version, [Parameter(ValueFromPipelineByPropertyName=$true)][string] $InstallLocation ) try { Add-AppxPackage -DisableDevelopmentMode -Register "$InstallLocation\AppxManifest.xml" -ErrorAction Stop } catch { Write-Error "Error repairing '$Name' v$Version`nInstall location: $InstallLocation`n$_" } } Get-AppxPackage |Repair-AppxPackage } function Restore-SchTasks { <# .SYNOPSIS Imports from a single XML file into the local Scheduled Tasks. .FUNCTIONALITY Scheduled Tasks .LINK https://msdn.microsoft.com/library/windows/desktop/bb736357.aspx .LINK Select-Xml .LINK Get-Credential .EXAMPLE Restore-SchTasks (Imports scheduled tasks from tasks.xml, prompting for passwords as needed.) #> [CmdletBinding()][OutputType([void])] Param( # The file to import tasks from, as exported from Backup-SchTasks. [Parameter(Position=0)][string] $Path = 'tasks.xml', <# A wildcard pattern to match task "paths" (including folders) to skip. User tasks are usually just in the root, and generally machine-specific tasks Microsoft automatically sets up are in folders so this is *\* by default to exclude the weird magic tasks. #> [string] $Exclude = '*\*' ) $credentials = @{} $xmldecl = "<?xml version=`"1.0`" encoding=`"UTF-16`" ?>{0}{0}" -f [environment]::NewLine $Script:PSDefaultParameterValues = @{'Select-Xml:Namespace'=@{task='http://schemas.microsoft.com/windows/2004/02/mit/task'}} foreach($task in ((Get-Content $Path -Raw) -replace '(?<=\A|>)\s*</?Tasks>\s*(?=\S|\z)','') -split '(?<=</Task>)\s*?(?=<!--)') { $xml = [xml]$task $name = (Select-Xml '/comment()' -Xml $xml).Node.Value.Trim().TrimStart('\') if($Exclude -and ($name -like $Exclude)) { Write-Verbose "Skipping task $name"; continue } else { Write-Verbose "Importing task $name" } Out-File "$name.xml" -Encoding unicode -InputObject "$xmldecl$task" -Width ([int]::MaxValue) $logon = (Select-Xml '/task:Task/task:Principals/task:Principal[@id=''Author'']/task:LogonType' -Xml $xml).Node.InnerText if($logon -ne 'Password') { Write-Verbose "Importing logon type $logon" schtasks /Create /TN $name /XML "$name.xml" } else { $user = (Select-Xml '/task:Task/task:Principals/task:Principal[@id=''Author'']/task:UserId' -Xml $xml).Node.InnerText if(!$credentials.ContainsKey($user)) { $cred = Get-Credential $user -Message "Please enter credentials to run '$name' job as" if(!$credentials.ContainsKey($cred.UserName)) { $credentials[$cred.UserName] = $cred } } if($user -ne $cred.UserName) { Write-Verbose "Importing to run as $($cred.UserName)" } schtasks /Create /RU $cred.UserName /RP ($cred.Password |ConvertFrom-SecureString -AsPlainText) /TN $name /XML "$name.xml" } Remove-Item "$name.xml" } } function Restore-Workstation { <# .SYNOPSIS Restores various configuration files and exported settings from a ZIP file. .LINK Import-EdgeKeywords.ps1 .LINK Import-SecretVault.ps1 .EXAMPLE Restore-Workstation.ps1 COMPUTERNAME-20230304T125000.zip Restores various settings from a backup file. #> #TODO: Add or replace dependencies. [CmdletBinding()] Param( [ValidateScript({Test-Path $_ -Type Leaf})][Parameter(Position=0,Mandatory=$true)][string] $Path ) function Restore-Workstation { Expand-Archive -Path $Path -DestinationPath ~ -Force Join-Path ~ edge-keywords.json -OutVariable file |Get-Content |ConvertFrom-Json |Import-EdgeKeywords.ps1 Remove-Item $file Join-Path ~ secret-vault.json -OutVariable file |Get-Content |ConvertFrom-Json |Import-SecretVault.ps1 Remove-Item $file } Restore-Workstation } function Set-SchTaskMsa { <# .SYNOPSIS Sets a Scheduled Task's runtime user as the given gMSA/MSA. .FUNCTIONALITY Scheduled Tasks .INPUTS An object with a TaskName string property. .LINK https://learn.microsoft.com/windows-server/identity/ad-ds/manage/group-managed-service-accounts/group-managed-service-accounts/group-managed-service-accounts-overview .LINK Set-ScheduledTask .LINK New-ScheduledTaskPrincipal .EXAMPLE Set-SchTaskMsa 'Backup VSCode settings' automation Sets the tasks running user to the "automation" managed service account. #> [CmdletBinding()] Param( [Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)][string] $TaskName, [Parameter(Position=1,Mandatory=$true)][Alias('MSA','gMSA','UserId')][string] $ServiceAccount, [switch] $HighestRunLevel ) Process { Set-ScheduledTask -TaskName $TaskName -Principal (New-ScheduledTaskPrincipal -UserID $ServiceAccount ` -LogonType Password -RunLevel:($HighestRunLevel ? 'Highest' : 'Normal')) } } function Set-TerminalProfile { <# .SYNOPSIS Adds or updates a Windows Terminal command profile. .FUNCTIONALITY Windows Terminal .PARAMETER Type The CLI application to configure. .PARAMETER Name The name of the profile. .PARAMETER CommandLine The command line to use in the profile. .LINK https://aka.ms/terminal-documentation .LINK https://aka.ms/terminal-profiles-schema .LINK https://gist.github.com/shanselman/4d954449914664024ee20ba10c2aaa0d .LINK https://learn.microsoft.com/en-us/windows/terminal/json-fragment-extensions .LINK https://github.com/microsoft/terminal/issues/1918#issuecomment-2452815871 .EXAMPLE Set-TerminalProfile pwsh Adds a default profile for PowerShell Core. .EXAMPLE Set-TerminalProfile fsi Adds a default profile for F# Interactive. .EXAMPLE Set-TerminalProfile ssh servername 'ssh username@servername' Adds an ssh profile named "servername", using the specified command line. #> [CmdletBinding()] Param() DynamicParam { #TODO: Add or replace dependencies. $Script:data = Get-Content ([io.path]::ChangeExtension($PSCommandPath, 'json')) -Raw |ConvertFrom-Json -AsHashtable $data.Keys |Add-DynamicParam.ps1 -Name Type -Type string -Position 0 -Mandatory Add-DynamicParam.ps1 -Name Name -Type string -Position 1 Add-DynamicParam.ps1 -Name CommandLine -Type string -Position 2 $DynamicParams } Begin { function Initialize-Variables { $Script:settings = Join-Path $env:LOCALAPPDATA Packages Microsoft.WindowsTerminal_8wekyb3d8bbwe LocalState settings.json if(!(Test-Path $Script:settings -Type Leaf)) {throw "Could not find $Script:settings"} #TODO: Add or replace dependencies. $Script:profiles = Select-Json.ps1 -JsonPointer /profiles/list -Path $Script:settings } function Get-TerminalProfileByGuid { [CmdletBinding()] Param( [Parameter(Position=0,Mandatory=$true)][guid] $Guid ) return $Script:profiles |Where-Object {$_.ContainsKey('guid')} |Where-Object guid -eq $Guid.ToString('B') } function Get-TerminalProfileBySource { [CmdletBinding()] Param( [Parameter(Position=0,Mandatory=$true)][string] $Source ) return $Script:profiles |Where-Object {$_.ContainsKey('source')} |Where-Object source -eq $Source } function Get-TerminalProfileByName { [CmdletBinding()] Param( [Parameter(Position=0,ValueFromRemainingArguments=$true)][AllowNull()][string[]] $Name ) return $Script:profiles |Where-Object {$_.ContainsKey('name')} |Where-Object name -in $Name |Select-Object -First 1 } function Get-TerminalProfile { [CmdletBinding()] Param( [Parameter(Position=0,Mandatory=$true)][string] $Type, [Parameter(Position=1,Mandatory=$true)][AllowEmptyString()][string] $Name ) switch($Type) { pwsh { return (Get-TerminalProfileByGuid 574e775e-4f2a-5b96-ac1e-a2962a402336) ` ?? (Get-TerminalProfileBySource Windows.Terminal.PowershellCore) ` ?? (Get-TerminalProfileByName PowerShell 'PowerShell Core' 'PowerShell 7') ` ?? $Script:data[$_] } powershell { return (Get-TerminalProfileByGuid 61c54bbd-c2c6-5271-96e7-009a87ff44bf) ` ?? (Get-TerminalProfileByName 'Windows PowerShell') ` ?? $Script:data[$_] } wsl { return (Get-TerminalProfileByGuid 2c4de342-38b7-51cf-b940-2309a097f518) ` ?? (Get-TerminalProfileBySource Windows.Terminal.Wsl) ` ?? (Get-TerminalProfileByName Ubuntu) ` ?? $Script:data[$_] } azcs { return (Get-TerminalProfileByGuid b453ae62-4e3d-5e58-b989-0a998ec441b8) ` ?? (Get-TerminalProfileBySource Windows.Terminal.Azure) ` ?? (Get-TerminalProfileByName Azure Cloud Shell) ` ?? $Script:data[$_] } default { if(!$Name) {return $Script:data[$_]} else {return (Get-TerminalProfileByName $Name) ?? $Script:data[$_]} } } } function Initialize-TerminalProfile { [CmdletBinding()] Param( [Parameter(Position=0)][AllowEmptyString()][string] $Name, [Parameter(Position=1)][AllowEmptyString()][string] $CommandLine, [Parameter(ValueFromPipeline=$true,Mandatory=$true)][pscustomobject] $InputObject ) if(!$InputObject.ContainsKey('guid') -or !$InputObject['guid']) { $InputObject['guid'] = (New-Guid).ToString('B') } if($Name) { $InputObject['name'] = $Name } if($CommandLine) { $InputObject['commandline'] = $CommandLine } if($InputObject.ContainsKey('backgroundImage') -and $InputObject['backgroundImage'] -notlike '*:*') { $InputObject['backgroundImage'] = Join-Path $PSScriptRoot logos $InputObject['backgroundImage'] } if($InputObject.ContainsKey('icon') -and $InputObject['icon'] -notlike '*:*') { $InputObject['icon'] = Join-Path $PSScriptRoot logos $InputObject['icon'] } return $InputObject } function Update-TerminalProfile { [CmdletBinding()] Param( [Parameter(Position=0,Mandatory=$true)][string] $Type, [Parameter(Position=1)][string] $Name, [Parameter(Position=2)][string] $CommandLine ) Initialize-Variables $termprofile = Get-TerminalProfile $Type $Name |Initialize-TerminalProfile $Name $CommandLine Write-Information 'Found profile:' #-fg DarkGray $termprofile |Format-Table -AutoSize |Out-String |Write-Information #-fg Gray for($position = 0; $position -lt $Script:profiles.Count; $position++) { if($Script:profiles[$position]['guid'] -eq $termprofile['guid']) {break} } Write-Information "Setting position $position" Copy-Item $Script:settings ([io.path]::ChangeExtension($Script:settings, (Get-Date -Format yyyyMMdd\THHmmss))) #TODO: Add or replace dependencies. Set-Json.ps1 -JsonPointer "/profiles/list/$position" -PropertyValue $termprofile -Path $Script:settings } } Process { Update-TerminalProfile @PSBoundParameters } } function Test-LockedFile { <# .SYNOPSIS Returns true if the specified file is locked. .INPUTS Object with System.String property named Path containing the path to a file to test. .OUTPUTS System.Boolean indicating whether the file is locked. .FUNCTIONALITY Files #> [CmdletBinding()][OutputType([bool])] Param( # A path to a file to test. [Parameter(Position=0,Mandatory=$true,ValueFromPipelineByPropertyName=$true)] [Alias('FullName')][string] $Path ) Begin { try{[void][RebootFileAction]}catch{Add-Type -TypeDefinition @' using System; using System.Runtime.InteropServices; public class RebootFileAction { [DllImport("kernel32.dll", SetLastError=true, CharSet=CharSet.Auto)] private static extern bool MoveFileEx(string lpExistingFileName, string lpNewFileName, int dwFlags); const int MOVEFILE_DELAY_UNTIL_REBOOT = 0x4; static public void Delete(string filename) { if(!MoveFileEx(filename,"",MOVEFILE_DELAY_UNTIL_REBOOT)) { throw new InvalidOperationException(String.Format("Unable to mark {0} for deletion at reboot.",filename)); } } } '@} } Process { #TODO: ... } } function Test-WindowsTerminal { <# .SYNOPSIS Returns true if PowerShell is running within Windows Terminal. .FUNCTIONALITY Windows Terminal .LINK https://aka.ms/terminal-documentation .EXAMPLE Test-WindowsTerminal True #> [CmdletBinding()][OutputType([bool])] Param() if(!$IsWindows) {return $false} if($env:WT_SESSION) {return $true} for($process = Get-Process -Id $PID; $process; $process = $process.Parent) { if($process.ProcessName -eq 'WindowsTerminal') {return $true} } return $false } Export-ModuleMember -Function Backup-SchTasks,Backup-Workstation,Convert-ChocolateyToWinget,ConvertFrom-CimInstance,ConvertTo-LogParserTimestamp,Copy-SchTasks,Find-ProjectPackages,Get-ADServiceAccountInfo,Get-ADUserStatus,Get-AspNetEvents,Get-DotNetFrameworkVersions,Get-DotNetVersions,Get-IisLog,Get-SimpleSchTasks,Get-SystemDetails,Measure-Caches,Remove-LockyFile,Repair-AppxPackages,Restore-SchTasks,Restore-Workstation,Set-SchTaskMsa,Set-TerminalProfile,Test-LockedFile,Test-WindowsTerminal |