Skip to content

Instantly share code, notes, and snippets.

@zbalkan
Last active January 22, 2024 10:59
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zbalkan/17fbe38864a900a2f1eeac2088c5d49e to your computer and use it in GitHub Desktop.
Save zbalkan/17fbe38864a900a2f1eeac2088c5d49e to your computer and use it in GitHub Desktop.
If you use Sysmon and enabled FileDelete events started with Sysmon 11, you probably came up with the issue of instantly growing hidden archive. For those who have not solved the problem yet, I came up with a PowerShell cmdlet (run as SYSTEM) based on the article https://blog.nviso.eu/2022/06/30/enforcing-a-sysmon-archive-quota/
#Requires -RunAsAdministrator
<#
.Synopsis
Generates Sysmon Archive file quota for `File Delete` events to help managing the size.
.DESCRIPTION
Based on: https://blog.nviso.eu/2022/06/30/enforcing-a-sysmon-archive-quota/
.INPUTS
None. Cmdlet does not accept pipe values.
.OUTPUTS
None if succeeds, exception if fails.
.EXAMPLE
New-SysmonArchiveQuota -Force
.EXAMPLE
New-SysmonArchiveQuota -ArchiveFolder Sysmon -Size 2 -Unit GB -Force
.LINK
https://gist.github.com/zbalkan/17fbe38864a900a2f1eeac2088c5d49e
#>
function New-SysmonArchiveQuota
{
[CmdletBinding(SupportsShouldProcess=$true,
PositionalBinding=$false,
HelpUri = 'https://gist.github.com/zbalkan/17fbe38864a900a2f1eeac2088c5d49e',
ConfirmImpact='Medium')]
Param
(
# Folder name where Sysmon stores archive files
[Parameter(Mandatory=$false,
HelpMessage="Folder name where Sysmon stores archive files")]
[ValidateLength(0,120)]
[String]
$ArchiveFolder = 'Sysmon',
# Size as a `float`
[Parameter(Mandatory=$false)]
[ValidateScript({($_ -gt 0)})]
[float]
$Size = 1.0,
# "TB", "GB", "MB" or "KB"
[Parameter(Mandatory=$false)]
[ValidateSet("TB", "GB", "MB", "KB")]
$Unit = 'GB',
# Overwrite current quota configuration
[Parameter(Mandatory=$false)]
[switch]
$Force
)
Begin
{
# Check if SYSTEM user
$sid = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value
if ($sid -ne "S-1-5-18")
{
Write-Error "The script must be run as SYSTEM. Use 'psexec -is powershell' then run the script."
exit 1 # Return ERROR exit code to set $LASTEXITCODE
}
$DelaySeconds = 10
$SizeWithUnit = "$($Size)$($Unit)"
}
Process
{
foreach ($Drive in (Get-PSDrive -PSProvider FileSystem | Select-Object -Property Root))
{
$Archive = Join-Path -Path $Drive.Root -ChildPath $ArchiveFolder
Write-Verbose "Sysmon Archive path: $Archive"
if ($pscmdlet.ShouldProcess($Archive, "Create archive size quota"))
{
$FilterName = "SysmonArchiveWatcher_$($Drive.Root.Remove(1))"
Write-Verbose "New WMI filter: $FilterName"
$ConsumerName = "SysmonArchiveCleaner_$($Drive.Root.Remove(1))"
Write-Verbose "New WMI event: $ConsumerName"
$PreviousBindings = @(Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding | Where-Object {$_.Consumer.Name -Like $ConsumerName} )
if ($PreviousBindings.Count -ne 0){
if($Force)
{
Write-Verbose "Removing previous filter-event binding."
$PreviousBindings | ForEach-Object { Remove-CimInstance $_ }
}
else
{
Write-Warning "The WMI filter-event binding already exists. Skipping."
continue;
}
}
$PreviousFilters = @(Get-CimInstance -Namespace root/subscription -ClassName __EventFilter | Where-Object -Property Name -EQ $FilterName)
if ($PreviousFilters.Count -ne 0){
if($Force)
{
Write-Verbose "Removing previous WMI filter."
$PreviousFilters | ForEach-Object { Remove-CimInstance $_ }
}
else
{
Write-Warning "The WMI filter $FilterName already exists. Skipping."
continue;
}
}
$PreviousConsumers = @(Get-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer | Where-Object -Property Name -EQ $ConsumerName)
if ($PreviousConsumers.Count -ne 0){
if($Force)
{
Write-Verbose "Removing previous WMI event."
$PreviousConsumers | ForEach-Object { Remove-CimInstance $_ }
}
else
{
Write-Warning "The consumer $ConsumerName already exists. Skipping."
continue;
}
}
# Create a WMI filter for files being created within the Sysmon archive.
Write-Verbose "Creating WMI Filter $FilterName"
$Query = "SELECT * FROM __InstanceCreationEvent WITHIN $DelaySeconds WHERE TargetInstance ISA 'CIM_DataFile' AND TargetInstance.Drive='$($Drive.Root.Remove(2))' AND TargetInstance.Path='\\$($ArchiveFolder)' GROUP WITHIN $DelaySeconds"
Write-Verbose "Query: $Query"
$Filter = New-CimInstance -Namespace root/subscription -ClassName __EventFilter -Property @{
Name = $FilterName;
EventNameSpace = 'root\cimv2';
QueryLanguage = "WQL";
Query = $Query
}
Write-Verbose "Creating WMI consumer event $ConsumerName"
# Create a command line consumer which will clean up the Sysmon archive folder until the quota is reached.
$Consumer = New-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer -Property @{
Name = $ConsumerName;
ExecutablePath = (Get-Command PowerShell).Source;
CommandLineTemplate = "-NoLogo -NoProfile -NonInteractive -WindowStyle Hidden -Command `"`$Archived = Get-ChildItem -Path '$Archive' -File | Where-Object {`$_.LinkType -ne 'HardLink'} | Sort-Object -Property LastAccessTimeUtc; `$SizeWithUnit = (`$Archived | Measure-Object -Sum -Property Length).Sum; for(`$Index = 0; (`$Index -lt `$Archived.Count) -and (`$SizeWithUnit -gt $Limit); `$Index++){ try {`$Archived[`$Index] | Remove-Item -Force -ErrorAction Stop; `$SizeWithUnit -= `$Archived[`$Index].Length} catch {}}`""
}
# Create a WMI binding from the filter to the consumer.
Write-Verbose "Creating WMI binding between filter $FilterName and consumer $ConsumerName"
$Binding = New-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding -Property @{
Filter = [Ref]$Filter;
Consumer = [Ref]$Consumer;
}
}
}
}
End
{
}
}
@zbalkan
Copy link
Author

zbalkan commented Jun 4, 2023

Use the commands below for cleanup. It is too simple for a new cmdlet.

Get-CimInstance -Namespace root/subscription -ClassName __EventFilter | Where-Object -Property Name -like "*Sysmon*" | ForEach-Object { Remove-CimInstance $_ }
Get-CimInstance -Namespace root/subscription -ClassName CommandLineEventConsumer | Where-Object -Property Name -like "*Sysmon*" | ForEach-Object { Remove-CimInstance $_ }
Get-CimInstance -Namespace root/subscription -ClassName __FilterToConsumerBinding | Where-Object -Property Consumer -like "*Sysmon*" | ForEach-Object { Remove-CimInstance $_ }

@waydaws
Copy link

waydaws commented Dec 28, 2023

Hi, Nice idea, Zafer I'll note, howerver, there's a comment in the linked nvisio article that one has to make sure that event has to do the deletion as system user. That makes sense since the sysmon folder is the default archive folder, and is owned by SYSTEM. Your script requires run as admin, but that's not the same thing as system. I guess one could take ownership, to avoid that problem, but that wouldn't be ideal for security reasons.

@zbalkan
Copy link
Author

zbalkan commented Jan 19, 2024

Hi @waydaws, you are right. I should have noted somewhere. In Powershell #requires statements, that's the highest possible. Maybe I can check if the calling user is SYSTEM or SID is S-1-5-18 in the beginning and note that the script should be called by running psexec -i - s powershell.exe first.

@waydaws
Copy link

waydaws commented Jan 21, 2024

That’s a good idea.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment