Merge pull request #16 from Yamato-Security/15-output-horizontally

feat: Output horizontally
This commit is contained in:
Zach Mathis (田中ザック)
2025-03-24 11:31:24 +09:00
committed by GitHub
4 changed files with 267 additions and 157 deletions

View File

@@ -2,7 +2,7 @@ name: Check audit setting
on: on:
push: push:
branches: [ "main" ] branches: [ "*" ]
workflow_dispatch: workflow_dispatch:
jobs: jobs:

View File

@@ -19,15 +19,3 @@ jobs:
- name: Run - name: Run
run: cd wela-extractor && cargo run --release -- ../hayabusa-rules ../config/eid_subcategory_mapping.csv ../config/security_rules.json run: cd wela-extractor && cargo run --release -- ../hayabusa-rules ../config/eid_subcategory_mapping.csv ../config/security_rules.json
- name: Push changes
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
git config --global user.name 'github-actions[bot]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
git add *.json
git commit -m "Automated update"
if [ "$(git log -1 --pretty=%B)" = "Automated update" ]; then
git push origin main
fi

183
WELA.ps1
View File

@@ -1,127 +1,4 @@
function CheckRegistryValue { Import-Module -Name ./WELAFunctions.psm1
param (
[string]$registryPath,
[string]$valueName,
[int]$expectedValue
)
try {
$value = Get-ItemProperty -Path $registryPath -Name $valueName -ErrorAction Stop
if ($value.$valueName -eq $expectedValue) {
return $true
} else {
return $false
}
} catch {
return $false
}
}
function Set-Applicable {
param (
[string]$autidpolTxt,
[string]$jsonRulePath
)
$extractedGuids = [System.Collections.Generic.HashSet[string]]::new()
Get-Content -Path $autidpolTxt | Select-String -NotMatch "No Auditing" | ForEach-Object {
if ($_ -match '{(.*?)}') {
[void]$extractedGuids.Add($matches[1])
}
}
$pwshModuleLogging = CheckRegistryValue -registryPath "HKLM:\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ModuleLogging" -valueName "EnableModuleLogging" -expectedValue 1
$pwshScriptLogging = CheckRegistryValue -registryPath "HKLM:\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -valueName "EnableScriptBlockLogging" -expectedValue 1
$jsonContent = Get-Content -Path $jsonRulePath -Raw | ConvertFrom-Json
foreach ($rule in $jsonContent) {
$rule | Add-Member -MemberType NoteProperty -Name "applicable" -Value $false
if ($rule.channel -eq "pwsh") {
if ($rule.event_ids -contains "400") {
$rule.applicable = $true
} elseif ($rule.event_ids -contains "4103") {
$rule.applicable = $pwshModuleLogging
} elseif ($rule.event_ids -contains "4104") {
$rule.applicable = $pwshScriptLogging
}
continue
}
foreach ($guid in $rule.subcategory_guids) {
if ($extractedGuids.Contains($guid)) {
$rule.applicable = $true
break
}
}
}
return $jsonContent
}
function Get-RuleCounts {
param ($rules)
$rules | Group-Object -Property level | ForEach-Object {
[PSCustomObject]@{
Level = $_.Name
Count = $_.Count
}
}
}
function CalculateUsableRate {
param ($counts, $totalCounts)
$result = @()
$totalCounts | ForEach-Object {
$level = $_.Level
$total = $_.Count
$usableCount = ($counts | Where-Object Level -eq $level | Select-Object -ExpandProperty Count -First 1)
if ($null -eq $usableCount) { $usableCount = 0 }
$percentage = if ($total -ne 0) { "{0:N2}" -f ($usableCount / $total * 100) } else { "0.00" }
$result += [PSCustomObject]@{
Level = $level
UsableCount = $usableCount
TotalCount = $total
Percentage = $percentage
}
}
return $result
}
function ShowRulesCountsByLevel {
param ($usableRate, $msg)
Write-Output $msg
$levelColorMap = [ordered]@{
"critical" = "Red"
"high" = "DarkYellow"
"medium" = "Yellow"
"low" = "Green"
"informational" = "White" # Assuming a default color for informational
}
$usableRate | Sort-Object { $levelColorMap.Keys.IndexOf($_.Level) } | ForEach-Object {
$color = $levelColorMap[$_.Level]
Write-Host "$($_.Level) rules: $($_.UsableCount) / $($_.TotalCount) ($($_.Percentage)%)" -ForegroundColor $color
}
Write-Output ""
Write-Output ""
}
function Test-IsAdministrator {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$adminRole = [Security.Principal.WindowsBuiltInRole]::Administrator
return (New-Object Security.Principal.WindowsPrincipal($currentUser)).IsInRole($adminRole)
}
if (-not (Test-IsAdministrator)) {
Write-Output "This script must be run as an Administrator."
exit
}
# Set the console encoding to UTF-8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# Step 1: Run the auditpol command using cmd.exe and redirect its output to a file
$autidpolTxt = "auditpol_output.txt"
Start-Process -FilePath "cmd.exe" -ArgumentList "/c chcp 437 & auditpol /get /category:* /r" -NoNewWindow -Wait -RedirectStandardOutput $autidpolTxt
$logo = @" $logo = @"
@@ -132,51 +9,68 @@ $logo = @"
by Yamato Security by Yamato Security
"@ "@
# Set the console encoding to UTF-8
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
# Step 1: Run the auditpol command using cmd.exe and redirect its output to a file
$autidpolTxt = "auditpol_output.txt"
Start-Process -FilePath "cmd.exe" -ArgumentList "/c chcp 437 & auditpol /get /category:* /r" -NoNewWindow -Wait -RedirectStandardOutput $autidpolTxt
Write-Host $logo -ForegroundColor Green Write-Host $logo -ForegroundColor Green
# Step 3: Set the applicable flag for each rule # Step 3: Set the applicable flag for each rule
$rules = Set-Applicable -autidpolTxt $autidpolTxt -jsonRulePath "./config/security_rules.json" $rules = Set-Applicable -autidpolTxt $autidpolTxt -jsonRulePath "./config/security_rules.json"
$allSecRules = $rules | Where-Object { $_.channel -eq "sec" }
$allSecRules = $rules | Where-Object { $_.channel -eq "sec" } $allPwsRules = $rules | Where-Object { $_.channel -eq "pwsh" }
$allPwsClaRules = $rules | Where-Object { $_.channel -eq "pwsh" -and $_.event_ids -contains "400" } $allPwsClaRules = $rules | Where-Object { $_.channel -eq "pwsh" -and ($_.event_ids -contains "400" -or $_.event_ids -contains "600" -or $_.event_ids.Count -eq 0) }
$allPwsModRules = $rules | Where-Object { $_.channel -eq "pwsh" -and $_.event_ids -contains "4103" } $allPwsModRules = $rules | Where-Object { $_.channel -eq "pwsh" -and $_.event_ids -contains "4103" }
$allPwsScrRules = $rules | Where-Object { $_.channel -eq "pwsh" -and $_.event_ids -contains "4104" } $allPwsScrRules = $rules | Where-Object { $_.channel -eq "pwsh" -and $_.event_ids -contains "4104" }
$usableSecRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "sec" } $usableSecRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "sec" }
$usablePwsRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "pwsh" } $usablePwsRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "pwsh" }
$usablePwsClaRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "pwsh" -and $_.event_ids -contains "400" } $usablePwsClaRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "pwsh" -and ($_.event_ids -contains "400" -or $_.event_ids -contains "600" -or $_.event_ids.Count -eq 0) }
$usablePwsModRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "pwsh" -and $_.event_ids -contains "4103" } $usablePwsModRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "pwsh" -and $_.event_ids -contains "4103" }
$usablePwsScrRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "pwsh" -and $_.event_ids -contains "4104" } $usablePwsScrRules = $rules | Where-Object { $_.applicable -eq $true -and $_.channel -eq "pwsh" -and $_.event_ids -contains "4104" }
$unusableRules = $rules | Where-Object { $_.applicable -eq $false }
# Step 4: Count the number of usable and unusable rules for each level # Step 4: Count the number of usable and unusable rules for each level
$totalCounts = Get-RuleCounts -rules $rules $totalCounts = Get-RuleCounts -rules $rules
$totalSecCounts = Get-RuleCounts -rules $allSecRules $totalSecCounts = Get-RuleCounts -rules $allSecRules
$totalPwsCounts = Get-RuleCounts -rules $allPwsClaRules $totalPwsCounts = Get-RuleCounts -rules $allPwsRules
$totalPwsClaCounts = Get-RuleCounts -rules $allPwsClaRules $totalPwsClaCounts = Get-RuleCounts -rules $allPwsClaRules
$totalPwsModCounts = Get-RuleCounts -rules $allPwsModRules $totalPwsModCounts = Get-RuleCounts -rules $allPwsModRules
$totalPwsScrCounts = Get-RuleCounts -rules $allPwsScrRules $totalPwsScrCounts = Get-RuleCounts -rules $allPwsScrRules
$usableSecCounts = Get-RuleCounts -rules $usableSecRules $usableSecCounts = Get-RuleCounts -rules $usableSecRules
$usablePwsCounts = Get-RuleCounts -rules $usablePwsRules $usablePwsCounts = Get-RuleCounts -rules $usablePwsRules
$usablePwsClaCounts = Get-RuleCounts -rules $usablePwsClaRules $usablePwsClaCounts = Get-RuleCounts -rules $usablePwsClaRules
$usablePwsModCounts = Get-RuleCounts -rules $usablePwsModRules $usablePwsModCounts = Get-RuleCounts -rules $usablePwsModRules
$usablePwsScrCounts = Get-RuleCounts -rules $usablePwsScrRules $usablePwsScrCounts = Get-RuleCounts -rules $usablePwsScrRules
# Step 5: Calculate the usable rate for each level # Step 5: Calculate the usable rate for each level
$usableSecRate = CalculateUsableRate -counts $usableSecCounts -totalCounts $totalSecCounts $usableSecRate = CalculateUsableRate -counts $usableSecCounts -totalCounts $totalSecCounts
$usablePwsRate = CalculateUsableRate -counts $usablePwsCounts -totalCounts $totalPwsCounts $usablePwsRate = CalculateUsableRate -counts $usablePwsCounts -totalCounts $totalPwsCounts
$usablePwsClaRate = CalculateUsableRate -counts $usablePwsClaCounts -totalCounts $totalPwsClaCounts $usablePwsClaRate = CalculateUsableRate -counts $usablePwsClaCounts -totalCounts $totalPwsClaCounts
$usablePwsModRate = CalculateUsableRate -counts $usablePwsModCounts -totalCounts $totalPwsModCounts $usablePwsModRate = CalculateUsableRate -counts $usablePwsModCounts -totalCounts $totalPwsModCounts
$usablePwsScrRate = CalculateUsableRate -counts $usablePwsScrCounts -totalCounts $totalPwsScrCounts $usablePwsScrRate = CalculateUsableRate -counts $usablePwsScrCounts -totalCounts $totalPwsScrCounts
# Step 6: Show the number of usable and unusable rules for each level # Step 6: Show the number of usable and unusable rules for each level
ShowRulesCountsByLevel -usableRate $usableSecRate -msg "Security event log detection rules:" $pwsModEnabled = CheckRegistryValue -registryPath "HKLM:\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ModuleLogging" -valueName "EnableModuleLogging" -expectedValue 1
ShowRulesCountsByLevel -usableRate $usablePwsClaRate -msg "PowerShell classic logging detection rules:" $pwsScrEnabled = CheckRegistryValue -registryPath "HKLM:\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -valueName "EnableScriptBlockLogging" -expectedValue 1
ShowRulesCountsByLevel -usableRate $usablePwsModRate -msg "PowerShell module logging detection rules:" $pwsModStatus = if ($pwsModEnabled) { "Enabled" } else { "Disabled" }
ShowRulesCountsByLevel -usableRate $usablePwsScrRate -msg "PowerShell script block logging detection rules:" $pwsSrcStatus = if ($pwsScrEnabled) { "Enabled" } else { "Disabled" }
# Step 7: Calculate the total usable rate
$totalUsableSecRate = CalculateTotalUsableRate -usableRate $usableSecRate
$totalUsablePwsClaRate = CalculateTotalUsableRate -usableRate $usablePwsClaRate
$totalUsablePwsModRate = CalculateTotalUsableRate -usableRate $usablePwsModRate
$totalUsablePwsScrRate = CalculateTotalUsableRate -usableRate $usablePwsScrRate
ShowRulesCountsByLevel -usableRate $usableSecRate -msg "Security event log detection rules: " -colorMsg "$totalUsableSecRate (Partially Enabled)"
ShowRulesCountsByLevel -usableRate $usablePwsClaRate -msg "PowerShell classic logging detection rules: " -colorMsg "$totalUsablePwsClaRate (Enabled)"
ShowRulesCountsByLevel -usableRate $usablePwsModRate -msg "PowerShell module logging detection rules: " -colorMsg "$totalUsablePwsModRate ($pwsModStatus)"
ShowRulesCountsByLevel -usableRate $usablePwsScrRate -msg "PowerShell script block logging detection rules: " -colorMsg "$totalUsablePwsScrRate ($pwsSrcStatus)"
Write-Output "Usable detection rules list saved to: UsableRules.csv" Write-Output "Usable detection rules list saved to: UsableRules.csv"
Write-Output "Unusable detection rules list saved to: UnusableRules.csv" Write-Output "Unusable detection rules list saved to: UnusableRules.csv"
@@ -186,6 +80,7 @@ $totalRulesCount = ($totalCounts | Measure-Object -Property Count -Sum).Sum
$utilizationPercentage = "{0:N2}" -f (($totalUsable / $totalRulesCount) * 100) $utilizationPercentage = "{0:N2}" -f (($totalUsable / $totalRulesCount) * 100)
Write-Output "You can utilize $utilizationPercentage% of your detection rules." Write-Output "You can utilize $utilizationPercentage% of your detection rules."
# Step 7: Save the lists of usable and unusable rules to CSV files # Step 8: Save the lists of usable and unusable rules to CSV files
$unusableRules = $rules | Where-Object { $_.applicable -eq $false }
$usableSecRules | Select-Object title, level, id | Export-Csv -Path "UsableRules.csv" -NoTypeInformation $usableSecRules | Select-Object title, level, id | Export-Csv -Path "UsableRules.csv" -NoTypeInformation
$unusableRules | Select-Object title, level, id | Export-Csv -Path "UnusableRules.csv" -NoTypeInformation $unusableRules | Select-Object title, level, id | Export-Csv -Path "UnusableRules.csv" -NoTypeInformation

227
WELAFunctions.psm1 Normal file
View File

@@ -0,0 +1,227 @@
<#
.SYNOPSIS
Checks if a registry value matches the expected value.
.DESCRIPTION
This function retrieves a registry value and compares it to the expected value.
.PARAMETER registryPath
The path to the registry key.
.PARAMETER valueName
The name of the registry value.
.PARAMETER expectedValue
The expected value to compare against.
.RETURNS
[bool] $true if the registry value matches the expected value, otherwise $false.
#>
function CheckRegistryValue {
param (
[string]$registryPath,
[string]$valueName,
[int]$expectedValue
)
try {
$value = Get-ItemProperty -Path $registryPath -Name $valueName -ErrorAction Stop
if ($value.$valueName -eq $expectedValue) {
return $true
} else {
return $false
}
} catch {
return $false
}
}
<#
.SYNOPSIS
Sets the applicable rules based on the provided audit policy text and JSON rule path.
.DESCRIPTION
This function reads the audit policy text file and extracts GUIDs. It then checks the registry values for PowerShell logging settings and updates the applicability of rules in the JSON file based on these settings and the extracted GUIDs.
.PARAMETER autidpolTxt
The path to the audit policy text file.
.PARAMETER jsonRulePath
The path to the JSON rule file.
.RETURNS
The updated JSON content with the applicability of rules set.
.EXAMPLE
Set-Applicable -autidpolTxt "C:\path\to\auditpol.txt" -jsonRulePath "C:\path\to\rules.json"
#>
function Set-Applicable {
param (
[string]$autidpolTxt,
[string]$jsonRulePath
)
$extractedGuids = [System.Collections.Generic.HashSet[string]]::new()
Get-Content -Path $autidpolTxt | Select-String -NotMatch "No Auditing" | ForEach-Object {
if ($_ -match '{(.*?)}') {
[void]$extractedGuids.Add($matches[1])
}
}
$pwshModuleLogging = CheckRegistryValue -registryPath "HKLM:\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ModuleLogging" -valueName "EnableModuleLogging" -expectedValue 1
$pwshScriptLogging = CheckRegistryValue -registryPath "HKLM:\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell\ScriptBlockLogging" -valueName "EnableScriptBlockLogging" -expectedValue 1
$jsonContent = Get-Content -Path $jsonRulePath -Raw | ConvertFrom-Json
foreach ($rule in $jsonContent) {
$rule | Add-Member -MemberType NoteProperty -Name "applicable" -Value $false
if ($rule.channel -eq "pwsh") {
if ($rule.event_ids -contains "400" -or $rule.event_ids -contains "600" -or $rule.event_ids.Count -eq 0) {
$rule.applicable = $true
} elseif ($rule.event_ids -contains "4103") {
$rule.applicable = $pwshModuleLogging
} elseif ($rule.event_ids -contains "4104") {
$rule.applicable = $pwshScriptLogging
}
continue
}
foreach ($guid in $rule.subcategory_guids) {
if ($extractedGuids.Contains($guid)) {
$rule.applicable = $true
break
}
}
}
return $jsonContent
}
<#
.SYNOPSIS
Groups the rules by their level and counts the number of rules in each level.
.PARAMETER rules
The collection of rules to be grouped and counted.
.RETURNS
A hashtable with the count of rules for each level.
#>
function Get-RuleCounts {
param ($rules)
$levels = @("critical", "high", "medium", "low", "informational")
$counts = @{}
$rules | Group-Object -Property level | ForEach-Object {
$counts[$_.Name] = $_.Count
}
foreach ($level in $levels) {
if (-not $counts.ContainsKey($level)) {
$counts[$level] = 0
}
}
return $counts.GetEnumerator() | ForEach-Object {
[PSCustomObject]@{
Level = $_.Key
Count = $_.Value
}
}
}
<#
.SYNOPSIS
Calculates the usable rate of rules based on their counts and total counts.
.PARAMETER counts
The counts of usable rules for each level.
.PARAMETER totalCounts
The total counts of rules for each level.
.RETURNS
A collection of objects representing the usable rate for each level.
#>
function CalculateUsableRate {
param ($counts, $totalCounts)
$result = @()
$totalCounts | ForEach-Object {
$level = $_.Level
$total = $_.Count
$usableCount = ($counts | Where-Object Level -eq $level | Select-Object -ExpandProperty Count -First 1)
if ($null -eq $usableCount) { $usableCount = 0 }
$percentage = if ($total -ne 0) { "{0:N2}" -f ($usableCount / $total * 100) } else { "0.00" }
$result += [PSCustomObject]@{
Level = $level
UsableCount = $usableCount
TotalCount = $total
Percentage = $percentage
}
}
return $result
}
<#
.SYNOPSIS
Calculates the total usable rate of rules.
.PARAMETER usableRate
The collection of objects representing the usable rate for each level.
.RETURNS
A string representing the total usable rate as a percentage.
#>
function CalculateTotalUsableRate {
param ($usableRate)
$totalUsable = ($usableRate | Measure-Object -Property UsableCount -Sum).Sum
$totalRulesCount = ($usableRate | Measure-Object -Property TotalCount -Sum).Sum
return "{0:N2}%" -f ($totalUsable / $totalRulesCount * 100)
}
<#
.SYNOPSIS
Displays the counts of rules by their level with color-coded output.
.PARAMETER usableRate
The collection of objects representing the usable rate for each level.
.PARAMETER msg
The message to display before the counts.
.PARAMETER colorMsg
The message to display with color coding.
#>
function ShowRulesCountsByLevel {
param ($usableRate, $msg, $colorMsg)
Write-Host -NoNewline $msg
$color = if ($colorMsg -match "Disabled") { "Red" } elseif ($colorMsg -match "Partially") { "Yellow" } else { "Green" }
Write-Host "$colorMsg" -ForegroundColor $color
$levelColorMap = [ordered]@{
"critical" = "Red"
"high" = "DarkYellow"
"medium" = "Yellow"
"low" = "Green"
"informational" = "White" # Assuming a default color for informational
}
$i = 0
Write-Host -NoNewline " - "
$usableRate | Sort-Object { $levelColorMap.Keys.IndexOf($_.Level) } | ForEach-Object {
$color = $levelColorMap[$_.Level]
$level = if ($_.Level -match "informational") { "info" } else { $_.Level }
Write-Host -NoNewline "$($level): $($_.UsableCount)/$($_.TotalCount) ($($_.Percentage)%)" -ForegroundColor $color
if ($i -lt $usableRate.Count - 1)
{
Write-Host -NoNewline ", "
}
$i++
}
Write-Output ""
Write-Output ""
}
<#
.SYNOPSIS
Checks if the current user is an administrator.
.DESCRIPTION
This function determines if the current user has administrative privileges.
.RETURNS
[bool] $true if the current user is an administrator, otherwise $false.
#>
function Test-IsAdministrator {
$currentUser = [Security.Principal.WindowsIdentity]::GetCurrent()
$adminRole = [Security.Principal.WindowsBuiltInRole]::Administrator
return (New-Object Security.Principal.WindowsPrincipal($currentUser)).IsInRole($adminRole)
}
if (-not (Test-IsAdministrator)) {
Write-Output "This script must be run as an Administrator."
exit
}