Files
WELA/WELA.ps1
T
Shirofune-SecurityandClaude Opus 4.8 db9a966a8b Add 'configure-sacl': targeted File System/Registry audit SACLs for detection
'configure' already enables Object Access subcategories such as File Share, SAM
and Certification Services, but File System (4663), Registry (4657) and Handle
Manipulation (4656) auditing produce no events without SACLs on the audited
objects - and enabling them globally floods the log. This adds targeted SACLs on
only the autostart/persistence registry keys (ASEPs) and sensitive files that the
Hayabusa/Sigma Security-channel rules actually watch, so those rules can fire
without global object auditing.

- config/audit_sacl_targets.json: curated, commented list of 30 registry keys
  (Run/RunOnce, Winlogon, IFEO, AppInit, Explorer shell extensions, Active Setup,
  Command Processor AutoRun, Session Manager, LSA packages, Winsock LSP, protocol
  handlers, logon scripts, Defender exclusions, service create/delete, ...) and 7
  files (NTDS dir, SAM/SECURITY/SYSTEM hives, lsass.exe, ntdsutil, vssadmin),
  each tagged with the ATT&CK technique / rule class it serves.
- WELA.ps1: new 'configure-sacl' command. Enables the File System / Registry /
  Handle Manipulation subcategories (by GUID) and applies the SACLs from the
  config (principal Everyone, Success+Failure, ContainerInherit on registry keys),
  idempotently, honoring -Auto / -WhatIf / -Confirm. Enables SeSecurityPrivilege
  first; skips objects absent on the host.

Per-user objects (HKCU / profile AppData) and live LSASS memory/handle access are
intentionally out of scope (need a per-user mechanism / Sysmon EID 10) and are
documented as such.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MVUmXZBkr5FnZ2hwFkDhx7
2026-09-17 21:50:25 +09:00

1516 lines
66 KiB
PowerShell

param (
[string]$Cmd,
[string]$OutType = "std",
[switch]$Debug,
[string]$Baseline,
[switch]$Auto,
[switch]$Help
)
$WELAVersion = "2.2.0"
$WELAReleaseName = "Dev Release"
# 実行時のカレントディレクトリに依存しないよう、すべてスクリプトの場所を基準にする
$ScriptRoot = if ($PSScriptRoot) { $PSScriptRoot } else { (Get-Location).Path }
$BaselineConfigPath = Join-Path $ScriptRoot "config/baselines.json"
$SecurityRulesPath = Join-Path $ScriptRoot "config/security_rules.json"
$EidMappingPath = Join-Path $ScriptRoot "config/eid_subcategory_mapping.csv"
$AuditpolTxtPath = Join-Path $ScriptRoot "auditpol.txt"
$SaclTargetsPath = Join-Path $ScriptRoot "config/audit_sacl_targets.json"
# 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。32bit 用に両方を扱う。
$PowerShellPolicyRoots = @(
"HKLM:\SOFTWARE\Policies\Microsoft\Windows\PowerShell",
"HKLM:\SOFTWARE\Wow6432Node\Policies\Microsoft\Windows\PowerShell"
)
class WELA {
static [array] $Levels = @('critical', 'high', 'medium', 'low', 'informational')
[string] $Category
[string] $SubCategory
[string] $CurrentSetting = ""
[array] $Rules
[hashtable] $RulesCount
[string] $DefaultSetting = ""
[string] $RecommendedSetting = ""
[string] $Volume = ""
[string] $Note = ""
WELA([string] $Category, [string] $SubCategory, [String] $CurrentSetting, [array] $Rules) {
$this.Category = $Category
$this.SubCategory = $SubCategory
$this.CurrentSetting = $CurrentSetting
$this.Rules = $Rules
$this.RulesCount = @{'critical' = 0; 'high' = 0; 'medium' = 0; 'low' = 0; 'informational' = 0}
}
WELA([string] $Category, [string] $SubCategory, [string] $CurrentSetting, [array] $Rules, [string] $DefaultSetting, [string] $RecommendedSetting, [string] $Volume, [string] $Note) {
$this.Category = $Category
$this.SubCategory = $SubCategory
$this.CurrentSetting = $CurrentSetting
$this.Rules = $Rules
$this.DefaultSetting = $DefaultSetting
$this.RecommendedSetting = $RecommendedSetting
$this.Volume = $Volume
$this.Note = $Note
$this.RulesCount = @{'critical' = 0; 'high' = 0; 'medium' = 0; 'low' = 0; 'informational' = 0}
}
[void] CountByLevel() {
$this.RulesCount = @{}
foreach ($level in [WELA]::Levels) {
$this.RulesCount[$level] = @($this.Rules | Where-Object { $_.level -eq $level }).Count
}
}
[void] Output([string] $Format) {
switch ($Format.ToLower()) {
"std" {
# -contains は文字列に対しては完全一致なので、部分一致には -like を使う
$color = if ($this.CurrentSetting -eq "Enabled" -or $this.CurrentSetting -like "*Success*" -or $this.CurrentSetting -like "*Failure*") { "Green" }
elseif ($this.CurrentSetting -eq "Unknown") { "DarkYellow" }
else { "Red" }
$ruleCounts = ""
$logEnabled = $this.CurrentSetting
$nonZeroLevels = ($this.RulesCount.Values | Where-Object { $_ -ne 0 }).Count
if ($nonZeroLevels -eq 0) {
$ruleCounts = "(no rules)"
$color = "DarkYellow"
} else {
$ruleCounts = "$($logEnabled) ("
foreach ($level in [WELA]::Levels) {
$count = $this.RulesCount[$level]
if (-not $count) {
$count = 0 # 明示的に0を設定しないと空文字列に変換されるため
}
if ($level -eq "informational") {
$ruleCounts += "info: $([string]$count)"
} else {
$ruleCounts += "$($level): $($count), "
}
}
$ruleCounts += ")"
}
if ($this.SubCategory) {
Write-Host " - $($this.SubCategory): $ruleCounts" -ForegroundColor $color
} else {
Write-Host " - $($ruleCounts)" -ForegroundColor $color
}
if ($this.DefaultSetting) {
Write-Host " - Default Setting: $($this.DefaultSetting)"
}
if ($this.CurrentSetting) {
Write-Host " - Current Setting: $($this.CurrentSetting)"
}
if ($this.RecommendedSetting) {
Write-Host " - Recommended Setting: $($this.RecommendedSetting)"
}
if ($this.Volume) {
Write-Host " - Volume: $($this.Volume)"
}
if ($this.Note) {
Write-Host " - Note: $($this.Note)"
}
}
default {
Write-Error "Invalid output format specified."
}
}
}
}
function ApplyRules {
# 指定されたサブカテゴリGUIDを持つルールを抜き出すだけの関数。
# applicable の更新は BuildAuditResult 側でカテゴリ横断のORとして行う。
param (
[array] $rules,
[string] $guid
)
return ,@($rules | Where-Object { $_.subcategory_guids -contains $guid }) # 暗黙の型変換でPSCustomObjectに変換されてしまうため、型を明示
}
function RuleFilter {
# 指定された条件をすべて満たすルールだけを通す(AND)。
# 空の条件は「指定なし」として無視するが、条件が1つも無い場合は何も通さない。
[OutputType([bool])]
param (
[pscustomobject] $rule,
[array] $category_eids,
[array] $category_channels,
[string] $category_guid
)
$hasCriteria = $false
if ($category_channels.Count -gt 0) {
$hasCriteria = $true
if (-not ($rule.channel | Where-Object { $category_channels -contains $_ })) {
return $false
}
}
if ($category_eids.Count -gt 0) {
$hasCriteria = $true
# event_ids を持たないルールは、EIDで絞られたカテゴリには属さない
if (-not ($rule.event_ids | Where-Object { $category_eids -contains $_ })) {
return $false
}
}
if ($category_guid) {
$hasCriteria = $true
if (-not ($rule.subcategory_guids | Where-Object { $category_guid -eq $_ })) {
return $false
}
}
return $hasCriteria
}
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
}
}
function GetAuditpol {
# auditpol /r の出力は CRCRLF や chcp の行が混ざるため、行数の決め打ちはせず
# 「GUIDらしき列を持つ行」だけを拾う。権限不足時のエラー行なども自然に無視される。
$mapping = @{}
if (-not (Test-Path -Path $script:AuditpolTxtPath)) {
Write-Host "[ERROR] Audit policy output not found: $script:AuditpolTxtPath" -ForegroundColor Red
return $mapping
}
Get-Content -Path $script:AuditpolTxtPath | ForEach-Object {
if ([string]::IsNullOrWhiteSpace($_)) {
return
}
$columns = $_ -split ','
if ($columns.Count -lt 5) {
return # ヘッダ行や "Active code page: 437"、エラーメッセージなど
}
$guid = $columns[3].Trim() -replace '^\{|\}$', '' # 波括弧を削除
if ($guid -notmatch '^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$') {
return
}
$inclusionSetting = $columns[4].Trim()
if ($inclusionSetting) {
$mapping[$guid] = $inclusionSetting
}
}
return $mapping
}
function TestWindows {
# Windows PowerShell 5.1 には $IsWindows が無いが、その場合は必ず Windows
return ($null -eq $IsWindows) -or $IsWindows
}
function TestAdministrator {
if (-not (TestWindows)) {
return $true # 非Windows(検証用)ではチェックしない
}
return ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
[Security.Principal.WindowsBuiltInRole]::Administrator)
}
function CollectAuditpol {
# auditpol の出力を取得する。取得できたかどうかを返す。
param ([switch] $UseCached)
if ($UseCached) {
return (Test-Path -Path $script:AuditpolTxtPath)
}
try {
Start-Process -FilePath "cmd.exe" `
-ArgumentList "/c chcp 437 & auditpol /get /category:* /r" `
-NoNewWindow -Wait -RedirectStandardOutput $script:AuditpolTxtPath -ErrorAction Stop
} catch {
Write-Host "[ERROR] Failed to run auditpol: $_" -ForegroundColor Red
return $false
}
if (-not (Test-Path -Path $script:AuditpolTxtPath)) {
return $false
}
# 権限不足などで1件も取得できていないケースを検出する
if ((GetAuditpol).Count -eq 0) {
Write-Host "[ERROR] auditpol returned no subcategories. Administrator privileges are required." -ForegroundColor Red
return $false
}
return $true
}
function AsArray {
# ConvertFrom-Json returns $null for an absent property and a bare scalar for
# a single-element array, so normalise before handing anything to RuleFilter.
param ($value)
if ($null -eq $value) {
return @()
}
return , @($value)
}
function GetBaselineConfig {
if (-not (Test-Path -Path $script:BaselineConfigPath)) {
throw "Baseline config not found: $script:BaselineConfigPath"
}
return Get-Content -Path $script:BaselineConfigPath -Raw | ConvertFrom-Json
}
function GetBaselineNames {
return @((GetBaselineConfig).baselines.PSObject.Properties.Name)
}
function BuildAuditResult {
param (
[object[]] $all_rules,
[string] $Baseline,
[array] $enabledguid
)
$config = GetBaselineConfig
# ベースライン名は大文字小文字を無視して解決する
$baselineName = $config.baselines.PSObject.Properties.Name |
Where-Object { $_ -eq $Baseline } |
Select-Object -First 1
if (-not $baselineName) {
throw "Unknown baseline '$Baseline'. Available: $($config.baselines.PSObject.Properties.Name -join ', ')"
}
$settings = $config.baselines.$baselineName
$auditpol = GetAuditpol
$auditResult = @()
foreach ($item in $config.catalog) {
$setting = $settings.($item.id)
if (-not $setting) {
throw "Baseline '$baselineName' has no entry for catalog id '$($item.id)'."
}
# 現在の設定と、そのサブカテゴリ/チャネルが有効かどうかを決める
switch ($item.currentSetting.type) {
"static" {
$enabled = $true
$current = $item.currentSetting.value
}
"auditpol" {
$enabled = $enabledguid -contains $item.select.guid
$current = $auditpol[$item.select.guid]
}
"channel" {
# レジストリの Enabled 値はマニフェストの既定値のままだと存在しないことがあり、
# 「値が無い」を無効と解釈すると既定で有効なチャネルを誤判定する。
# また役割未導入でチャネル自体が無い場合と無効化されている場合も区別できないため、
# 実際のチャネル状態を Get-WinEvent から取得する。
$logInfo = $null
try {
# Windows 以外や役割未導入の環境では取得できないので、その場合は判定不能とする
$logInfo = Get-WinEvent -ListLog $item.currentSetting.channel -ErrorAction Stop
} catch {
$logInfo = $null
}
if ($null -eq $logInfo) {
$enabled = $false
$current = "Unknown"
} else {
$enabled = [bool]$logInfo.IsEnabled
$current = if ($enabled) { "Enabled" } else { "Disabled" }
}
}
"registry" {
# 64bit/32bit でレジストリビューが分かれる設定があるため、いずれかで有効なら有効とみなす
$enabled = $false
foreach ($path in (AsArray $item.currentSetting.paths)) {
if (CheckRegistryValue -registryPath $path `
-valueName $item.currentSetting.name `
-expectedValue $item.currentSetting.value) {
$enabled = $true
break
}
}
$current = if ($enabled) { "Enabled" } else { "Disabled" }
}
default {
throw "Unknown currentSetting type '$($item.currentSetting.type)' for catalog id '$($item.id)'."
}
}
# 該当するルールを抽出する
if ($item.select.type -eq "guid") {
$rules = ApplyRules -rules $all_rules -guid $item.select.guid
} else {
$eids = AsArray $item.select.eventIds
$channels = AsArray $item.select.channels
$guid = $item.select.guid
$rules = $all_rules | Where-Object { RuleFilter $_ $eids $channels $guid }
}
# 1つのルールは複数カテゴリに属しうるので、有効なカテゴリが1つでもあれば
# 利用可能とする(OR)。カテゴリごとに上書きすると最後のカテゴリで結果が決まってしまう。
if ($enabled) {
$rules | ForEach-Object { $_.applicable = $true }
}
if ($setting.ideal) {
$rules | ForEach-Object { $_.ideal = $true }
}
$auditResult += [WELA]::New(
$item.category,
$item.subCategory,
$current,
[array]$rules,
$setting.defaultSetting,
$setting.recommendedSetting,
$setting.volume,
$setting.note
)
}
# どのカテゴリにも該当しなかったルールを取りこぼさない。
# 集計対象から黙って消えると、利用率の分母がルール総数と合わなくなる。
$covered = [System.Collections.Generic.HashSet[string]]::new()
foreach ($entry in $auditResult) {
foreach ($rule in $entry.Rules) {
[void]$covered.Add($rule.id)
}
}
$uncovered = @($all_rules | Where-Object { -not $covered.Contains($_.id) })
if ($uncovered.Count -gt 0) {
$auditResult += [WELA]::New(
"Uncategorized",
"",
"Unknown",
$uncovered,
"",
"",
"",
"Rules whose channel or subcategory is not covered by this baseline. WELA cannot tell whether these logs are enabled."
)
}
return $auditResult
}
function AuditLogSetting {
param (
[string] $outType,
[string] $Baseline,
[switch] $debug
)
if (-not $debug -and -not (TestAdministrator)) {
Write-Host "[ERROR] 'audit-settings' needs Administrator privileges to read the audit policy." -ForegroundColor Red
return
}
if (-not (CollectAuditpol -UseCached:$debug)) {
return
}
$enabledguid = [System.Collections.Generic.HashSet[string]]::new()
foreach ($guid in (GetAuditpol).GetEnumerator()) {
if ($guid.Value -ne "No Auditing") {
[void]$enabledguid.Add($guid.Key)
}
}
if (-not (Test-Path -Path $script:SecurityRulesPath)) {
Write-Host "[ERROR] Detection rules not found: $script:SecurityRulesPath" -ForegroundColor Red
return
}
$all_rules = Get-Content -Path $script:SecurityRulesPath -Raw | ConvertFrom-Json
$all_rules | ForEach-Object {
$_ | Add-Member -MemberType NoteProperty -Name "applicable" -Value $false
$_ | Add-Member -MemberType NoteProperty -Name "ideal" -Value $false
}
$auditResult = BuildAuditResult -all_rules $all_rules -Baseline $Baseline -enabledguid $enabledguid
# ベースラインが扱っていないサブカテゴリでも、そのサブカテゴリが有効ならルールは動く。
# ルール自身が持つ subcategory_guids を見て救済する。
$all_rules | ForEach-Object {
if (-not $_.applicable) {
foreach ($guid in $_.subcategory_guids) {
if ($enabledguid -contains $guid) {
$_.applicable = $true
break
}
}
}
}
$auditResult | ForEach-Object { $_.CountByLevel() }
$auditResult | ForEach-Object {
$_ | Add-Member -MemberType NoteProperty -Name RuleCount -Value 0
$_.RuleCount = ($_.Rules | Measure-Object).Count
$_ | Add-Member -MemberType NoteProperty -Name RuleCountByLevel -Value ""
$ruleCounts = ""
foreach ($level in [WELA]::Levels) {
$count = $_.RulesCount[$level]
if (-not $count) {
$count = 0
}
if ($level -eq "informational") {
$ruleCounts += "info:$([string]$count)"
} else {
$ruleCounts += "$($level):$($count), "
}
}
$_.RuleCountByLevel = $ruleCounts
}
if ($outType -eq "std") {
$auditResult | Group-Object -Property Category | ForEach-Object {
$notEnabled = @("No Auditing", "Disabled", "Unknown")
$enabledCount = ($_.Group | Where-Object { $notEnabled -notcontains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum
$disabledCount = ($_.Group | Where-Object { $notEnabled -contains $_.CurrentSetting } | ForEach-Object { $_.Rules.Count } | Measure-Object -Sum).Sum
$out = ""
$color = ""
if (@($_.Group | Where-Object { $_.CurrentSetting -ne "Unknown" }).Count -eq 0) {
# 設定を確認できないカテゴリ。無効と断定はできない
$out = "Unknown"
$color = "DarkYellow"
}
elseif ($disabledCount -eq 0 -and $enabledCount -ne 0){
$out = "Enabled"
$color = "Green"
}
elseif ($disabledCount -ne 0 -and $enabledCount -eq 0)
{
$out = "Disabled"
$color = "Red"
}
else
{
$out = "Partially Enabled"
$color = "DarkYellow"
}
$enabledPercentage = "0.00%"
if ($enabledCount + $disabledCount -ne 0) {
$enabledPercentage = "({0:N2}%)" -f (($enabledCount / ($enabledCount + $disabledCount)) * 100)
}
if ($_.Name -notmatch "Powershell" -and $_.Name -notmatch "Security Advanced") {
$enabledPercentage = ""
}
Write-Host "$( $_.Name ): $out$($enabledPercentage)" -ForegroundColor $color
$_.Group | ForEach-Object {
$_.Output($outType)
}
Write-Host ""
}
} elseif ($outType -eq "table") {
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, DefaultSetting, CurrentSetting, RecommendedSetting, Volume | Format-Table
}
# 1つのルールが複数カテゴリに属するため、集計とCSVはルールID単位で重複排除する
$uniqueRules = $auditResult | Select-Object -ExpandProperty Rules | Sort-Object -Property id -Unique
$usableRules = @($uniqueRules | Where-Object { $_.applicable -eq $true })
$unUsableRules = @($uniqueRules | Where-Object { $_.applicable -eq $false })
$auditCsv = Join-Path $script:ScriptRoot "WELA-Audit-Result.csv"
$usableCsv = Join-Path $script:ScriptRoot "UsableRules.csv"
$unusableCsv = Join-Path $script:ScriptRoot "UnusableRules.csv"
$currentJson = Join-Path $script:ScriptRoot "mitre-ttp-navigator-current.json"
$idealJson = Join-Path $script:ScriptRoot "mitre-ttp-navigator-ideal.json"
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, RuleCountByLevel, DefaultSetting, CurrentSetting, RecommendedSetting, Volume, Note | Export-Csv -Path $auditCsv -NoTypeInformation
$usableRules | Select-Object title, level, service, category, description, id | Export-Csv -Path $usableCsv -NoTypeInformation
$unUsableRules | Select-Object title, level, service, category, description, id | Export-Csv -Path $unusableCsv -NoTypeInformation
if ($outType -eq "gui") {
$usableRules | Select-Object title, level, service, category, description, id | Out-GridView -Title "Usable Detection Rules"
$unUsableRules | Select-Object title, level, service, category, description, id | Out-GridView -Title "Unusable Detection Rules"
$auditResult | Select-Object -Property Category, SubCategory, RuleCount, RuleCountByLevel, DefaultSetting, CurrentSetting, RecommendedSetting, Volume, Note | Out-GridView -Title "WELA Audit Result"
}
Write-Output "Audit check result saved to: $auditCsv"
Write-Output "Usable detection rules list saved to: $usableCsv"
Write-Output "Unusable detection rules list saved to: $unusableCsv"
Export-MitreHeatmap -sigmaRules $uniqueRules -OutputPath $currentJson
Write-Output "MITRE ATT&CK Navigator data(based on current settings) saved to: $currentJson"
Export-MitreHeatmap -sigmaRules $uniqueRules -OutputPath $idealJson -UseIdealCount $true
Write-Output "MITRE ATT&CK Navigator data(based on ideal settings) saved to: $idealJson"
$totalRulesCount = @($uniqueRules).Count
$usableRulesCount = $usableRules.Count
Write-Host ""
if ($totalRulesCount -eq 0) {
Write-Host "No detection rules were loaded, so utilization cannot be calculated." -ForegroundColor Red
} else {
# 数値のまま閾値判定する。書式化した文字列で比較すると辞書順比較になる
$utilization = ($usableRulesCount / $totalRulesCount) * 100
$color = if ($utilization -ge 70) { "Green" } elseif ($utilization -ge 10) { "DarkYellow" } else { "Red" }
Write-Host ("You can utilize {0:N2}% of your detection rules." -f $utilization) -ForegroundColor $color
}
Write-Host ""
}
function Export-MitreHeatmap {
param (
[Parameter(Mandatory = $true)]
[array]$sigmaRules,
[Parameter(Mandatory = $false)]
[string]$OutputPath = "mitre-ttp-heatmap.json",
[Parameter(Mandatory=$false)]
[bool]$UseIdealCount = $false
)
$tagMapping = @{}
$sigmaRules | ForEach-Object {
$rule = $_
if ($rule.tags) {
$rule.tags | ForEach-Object {
$tag = $_
# ATT&CK Navigator のレイヤに載るのはテクニックIDのみ。
# tactic(TA....)や cve./car./attack.g.... といったタグは対象外。
if ($tag -notmatch '^T\d{4}(\.\d{3})?$') {
return
}
if (-not $tagMapping.ContainsKey($tag)) {
$tagMapping[$tag] = @{
titles = @()
idealCount = 0
applicableCount = 0
}
}
$tagMapping[$tag].titles += $rule.title
if ($rule.applicable -eq $true) {
$tagMapping[$tag].applicableCount++
}
if ($rule.ideal -eq $true) {
$tagMapping[$tag].idealCount++
}
}
}
}
$techniques = @()
$tagMapping.Keys | ForEach-Object {
$techniqueId = $_
$info = $tagMapping[$techniqueId]
$titlesCount = $info.titles.Count
$matched = if ($UseIdealCount) { $info.idealCount } else { $info.applicableCount }
$score = if ($titlesCount -gt 0) {
[int][math]::Round(($matched / $titlesCount) * 100, 2)
} else {
0
}
$techniques += @{
techniqueID = $techniqueId
score = $score
comment = ($info.titles -join ", ")
showSubtechniques = $true
}
}
$colors = @(
"#c62828", # Red
"#fff176", # Yellow
"#ffa726", # Orange
"#c8e6c9", # Light Green
"#2e7d32" # Dark Green
)
$heatmap = @{
"name" = "WELA detection heatmap"
"versions" = @{
"attack" = "18"
"navigator" = "5.2.0"
"layer" = "4.5"
}
"domain" = "enterprise-attack"
"description" = "WELA detection heatmap"
"techniques" = $techniques
"gradient" = @{
"colors" = $colors
"minValue" = 0
"maxValue" = 100
}
"legendItems" = @()
"metadata" = @()
"links" = @()
"showTacticRowBackground" = $false
"tacticRowBackground" = "#dddddd"
"selectTechniquesAcrossTactics" = $true
"selectSubtechniquesWithParent" = $false
"selectVisibleTechniques" = $false
}
# PowerShell 5.1 の Out-File 既定は UTF-16LE で、ATT&CK Navigator が読めないため UTF-8 で書く
$heatmap | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Encoding utf8
}
function AuditFileSize {
# 推奨サイズはベースラインによらず共通のため、パラメータは取らない
if (-not (TestWindows)) {
Write-Host "[ERROR] 'audit-filesize' reads Windows event logs and can only run on Windows." -ForegroundColor Red
return
}
# 対象のイベントログ名をハッシュテーブル化
$logNames = @{
"Application" = @("20 MB", "128 MB+")
"Microsoft-Windows-AppLocker/EXE and DLL" = @("1 MB", "256 MB+")
"Microsoft-Windows-AppLocker/MSI and Script" = @("1 MB", "256 MB+")
"Microsoft-Windows-AppLocker/Packaged app-Deployment" = @("1 MB", "256 MB+")
"Microsoft-Windows-AppLocker/Packaged app-Execution" = @("1 MB", "256 MB+")
"Microsoft-Windows-Bits-Client/Analytic" = @("1 MB", "128 MB+")
"Microsoft-Windows-Bits-Client/Operational" = @("1 MB", "128 MB+")
"Microsoft-Windows-CodeIntegrity/Operational" = @("1 MB", "128 MB+")
"Microsoft-Windows-Crypto-DPAPI/Debug" = @("1 MB", "128 MB+")
"Microsoft-Windows-DFSN-Server/Admin" = @("1 MB", "128 MB+")
"Microsoft-Windows-DriverFrameworks-UserMode/Operational" = @("1 MB", "128 MB+")
"Microsoft-Windows-NTLM/Operational" = @("1 MB", "128 MB+")
"Microsoft-Windows-PowerShell/Operational" = @("15 MB", "256 MB+")
"Microsoft-Windows-PrintService/Admin" = @("1 MB", "128 MB+")
"Microsoft-Windows-PrintService/Operational" = @("1 MB", "128 MB+")
"Microsoft-Windows-Security-Mitigations/KernelMode" = @("1 MB", "128 MB+")
"Microsoft-Windows-Security-Mitigations/UserMode" = @("1 MB", "128 MB+")
"Microsoft-Windows-SmbClient/Security" = @("8 MB", "128 MB+")
"Microsoft-Windows-TaskScheduler/Operational" = @("1 MB", "128 MB+")
"Microsoft-Windows-TerminalServices-LocalSessionManager/Operational" = @("1 MB", "128 MB+")
"Microsoft-Windows-Windows Defender/Operational" = @("16MB", "128 MB+")
"Microsoft-Windows-Windows Firewall With Advanced Security/Firewall" = @("1 MB", "256 MB+")
"Microsoft-Windows-WMI-Activity/Operational" = @("1 MB", "128 MB+")
"Security" = @("20 MB", "256 MB+")
"System" = @("20 MB", "128 MB+")
"Windows PowerShell" = @("15 MB", "256 MB+")
}
$results = @()
$missingLogs = @()
foreach ($logName in $logNames.Keys | Sort-Object) {
# 存在しないログ(役割やOSエディションによる)で全体を止めない
$logInfo = Get-WinEvent -ListLog $logName -ErrorAction SilentlyContinue
if (-not $logInfo) {
$missingLogs += $logName
continue
}
$maxLogSize = [math]::Floor($logInfo.MaximumSizeInBytes / 1MB)
$recommendedSize = [int]($logNames[$logName][1] -replace " MB\+?", "")
# ローテーション直前までファイルは上限まで育つので、95%以上を「ほぼ満杯」とみなす
$logIsFull = $logInfo.MaximumSizeInBytes -gt 0 -and
$logInfo.FileSize -ge ($logInfo.MaximumSizeInBytes * 0.95)
$logMode = if ($logInfo.LogMode -eq "Retain") { "NoOverwrite" } else { $logInfo.LogMode }
$correctSetting = if ($maxLogSize -ge $recommendedSize -and $logMode -ne "NoOverwrite") { "Y" } else { "N" }
$results += [PSCustomObject]@{
LogFile = Split-Path $logInfo.LogFilePath -Leaf
CurrentLogSize = "{0:N2} MB" -f ($logInfo.FileSize / 1MB)
MaxLogSize = "$maxLogSize MB"
Default = $logNames[$logName][0]
Recommended = $logNames[$logName][1]
IsLogFull = $logIsFull
LogMode = $logMode
CorrectSetting = $correctSetting
}
}
# Format-Tableには色つき出力の機能はないので、Write-Hostで色をつける
$tableLayout = "{0,-75} {1,-15} {2,-10} {3,-10} {4,-15} {5,-10} {6,-15} {7,-10}"
Write-Host ($tableLayout -f `
"Log File", `
"Current Size", `
"Max Size", `
"Default", `
"Recommended", `
"Is Full", `
"Log Mode", `
"Correct Setting" `
)
Write-Host ($tableLayout -f `
"--------", `
"------------", `
"--------", `
"------", `
"-----------", `
"-------", `
"--------", `
"--------------" `
)
foreach ($result in $results) {
$color = if ($result.CorrectSetting -eq "Y") { "Green" } else { "Red" }
Write-Host ($tableLayout -f `
$result.LogFile, `
$result.CurrentLogSize, `
$result.MaxLogSize, `
$result.Default, `
$result.Recommended, `
$result.IsLogFull, `
$result.LogMode, `
$result.CorrectSetting `
) -ForegroundColor $color
}
if ($missingLogs.Count -gt 0) {
Write-Host ""
Write-Host "Skipped $($missingLogs.Count) log(s) that do not exist on this machine:" -ForegroundColor DarkYellow
$missingLogs | ForEach-Object { Write-Host " - $_" -ForegroundColor DarkYellow }
}
$fileSizeCsv = Join-Path $script:ScriptRoot "WELA-FileSize-Result.csv"
$results | Export-Csv -Path $fileSizeCsv -NoTypeInformation
Write-Host ""
Write-Host "Audit file size result saved to: $fileSizeCsv"
}
function UpdateRules {
$baseUrl = "https://raw.githubusercontent.com/Yamato-Security/WELA/main/config"
$downloads = @(
@{ Url = "$baseUrl/eid_subcategory_mapping.csv"; Path = $script:EidMappingPath },
@{ Url = "$baseUrl/security_rules.json"; Path = $script:SecurityRulesPath }
)
$failed = 0
foreach ($item in $downloads) {
Write-Host "Downloading $($item.Url)"
# 途中で失敗しても既存の設定ファイルを壊さないよう、一時ファイルに落としてから差し替える
$tempPath = "$($item.Path).download"
try {
Invoke-WebRequest -Uri $item.Url -OutFile $tempPath -UseBasicParsing -ErrorAction Stop
Move-Item -Path $tempPath -Destination $item.Path -Force
Write-Host "Saved to $($item.Path)" -ForegroundColor Green
}
catch {
$failed++
Write-Host "[ERROR] Failed to download $($item.Url): $_" -ForegroundColor Red
Write-Host " $($item.Path) was left unchanged." -ForegroundColor Red
}
finally {
if (Test-Path -Path $tempPath) {
Remove-Item -Path $tempPath -Force -ErrorAction SilentlyContinue
}
}
Write-Host ""
}
if ($failed -gt 0) {
Write-Host "$failed of $($downloads.Count) file(s) could not be updated." -ForegroundColor Red
}
}
function Set-RegistryConfig {
# レジストリを変更するため -WhatIf / -Confirm に対応する
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')]
param (
[Parameter(Mandatory = $true)]
[array]$RegPaths,
[Parameter(Mandatory = $false)]
[switch]$Auto
)
foreach ($reg in $RegPaths) {
try {
$currentValue = "Not Set"
$pathExists = Test-Path $reg.Path
if ($pathExists) {
$prop = Get-ItemProperty -Path $reg.Path -Name $reg.Name -ErrorAction SilentlyContinue
if ($prop) {
$currentValue = $prop.$($reg.Name)
}
}
Write-Host "Registry: $($reg.Path) Value: $($reg.Name)"
if ($currentValue -eq $reg.Value) {
Write-Host "[SKIPPED] $($reg.Name) : Already set to $($reg.Value)." -ForegroundColor Yellow
Write-Host ""
continue
}
if ($Auto) {
$response = "Y"
} else {
$response = Read-Host "Your current setting is $currentValue. Do you want to change it to $( $reg.Value )? (Y/n)"
}
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
if ($PSCmdlet.ShouldProcess("$($reg.Path)\$($reg.Name)", "Set to $($reg.Value)")) {
if (-not $pathExists) {
New-Item -Path $reg.Path -Force | Out-Null
}
Set-ItemProperty -Path $reg.Path -Name $reg.Name -Value $reg.Value -Type DWord
Write-Host "[OK] Set $($reg.Name)" -ForegroundColor Green
}
} else {
Write-Host "[SKIPPED] $($reg.Name)" -ForegroundColor Yellow
}
}
catch {
Write-Host "[ERROR] Failed to set registry: $_" -ForegroundColor Red
}
Write-Host ""
}
}
function ConfigureAuditSettings {
param (
[switch] $Auto,
[switch] $Debug
)
if (-not (TestWindows)) {
Write-Host "[ERROR] 'configure' changes Windows settings and can only run on Windows." -ForegroundColor Red
return
}
# 管理者権限の確認
if (-not (TestAdministrator)) {
Write-Error "This script requires Administrator privileges"
exit 1
}
if (-not (CollectAuditpol -UseCached:$Debug)) {
return
}
# ログサイズ定数
$oneGB = 1073741824
$oneTwentyEightMB = 134217728
# セキュリティおよびPowerShellログを1GBに設定
Write-Host "Configuring Event Logs..."
Write-Host ""
$largeLogs = @(
"Security",
"Microsoft-Windows-PowerShell/Operational",
"Windows PowerShell"
)
foreach ($log in $largeLogs) {
try {
$logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop
$currentSize = [math]::Floor($logInfo.MaximumSizeInBytes / 1MB)
$newSize = 1024
Write-Host "Log: $log"
if ($currentSize -ge $newSize) {
Write-Host "[SKIPPED] $log : Current size ($currentSize MB) is already greater than or equal to $newSize MB." -ForegroundColor Yellow
Write-Host ""
continue
}
if ($Auto) {
$response = "Y"
} else {
$response = Read-Host "Your current setting is $currentSize MB. Do you want to change it to 1024 MB? (Y/n)"
}
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
wevtutil sl $log /ms:$oneGB 2>&1 | Out-Null
Write-Host "[OK] $log : 1024 MB" -ForegroundColor Green
} else {
Write-Host "[SKIPPED] $log" -ForegroundColor Yellow
}
}
catch {
Write-Host "[ERROR] $log : $_" -ForegroundColor Red
}
Write-Host ""
}
# その他の重要なログを128MBに設定
$mediumLogs = @(
"System",
"Application",
"Microsoft-Windows-Windows Defender/Operational",
"Microsoft-Windows-Bits-Client/Operational",
"Microsoft-Windows-Windows Firewall With Advanced Security/Firewall",
"Microsoft-Windows-NTLM/Operational",
"Microsoft-Windows-Security-Mitigations/KernelMode",
"Microsoft-Windows-Security-Mitigations/UserMode",
"Microsoft-Windows-PrintService/Admin",
"Microsoft-Windows-PrintService/Operational",
"Microsoft-Windows-SmbClient/Security",
"Microsoft-Windows-AppLocker/MSI and Script",
"Microsoft-Windows-AppLocker/EXE and DLL",
"Microsoft-Windows-AppLocker/Packaged app-Deployment",
"Microsoft-Windows-AppLocker/Packaged app-Execution",
"Microsoft-Windows-CodeIntegrity/Operational",
"Microsoft-Windows-Crypto-DPAPI/Debug",
"Microsoft-Windows-Diagnosis-Scripted/Operational",
"Microsoft-Windows-DriverFrameworks-UserMode/Operational",
"Microsoft-Windows-WMI-Activity/Operational",
"Microsoft-Windows-TerminalServices-LocalSessionManager/Operational",
"Microsoft-Windows-TaskScheduler/Operational"
)
foreach ($log in $mediumLogs) {
try {
$logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop
$currentSize = [math]::Floor($logInfo.MaximumSizeInBytes / 1MB)
$newSize = 128
Write-Host "Log: $log"
if ($currentSize -ge $newSize) {
Write-Host "[SKIPPED] $log : Current size ($currentSize MB) is already greater than or equal to $newSize MB." -ForegroundColor Yellow
Write-Host ""
continue
}
if ($Auto) {
$response = "Y"
} else {
$response = Read-Host "Your current setting is $currentSize MB. Do you want to change it to 128 MB? (Y/n)"
}
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
wevtutil sl $log /ms:$oneTwentyEightMB 2>&1 | Out-Null
Write-Host "[OK] $log : 128 MB" -ForegroundColor Green
} else {
Write-Host "[SKIPPED] $log" -ForegroundColor Yellow
}
}
catch {
Write-Host "[ERROR] $log : $_" -ForegroundColor Red
}
Write-Host ""
}
# 特定のログの有効化
Write-Host "Enabling Event Logs..."
Write-Host ""
foreach ($log in @("Microsoft-Windows-TaskScheduler/Operational", "Microsoft-Windows-DriverFrameworks-UserMode/Operational", "Microsoft-Windows-Crypto-DPAPI/Debug")) {
try {
$logInfo = Get-WinEvent -ListLog $log -ErrorAction Stop
$currentState = if ($logInfo.IsEnabled) { "Enabled" } else { "Disabled" }
$newState = "Enabled"
Write-Host "Log: $log"
if ($currentState -eq $newState) {
Write-Host "[SKIPPED] $log : Already Enabled." -ForegroundColor Yellow
Write-Host ""
continue
}
if ($Auto) {
$response = "Y"
} else {
$response = Read-Host "Your current setting is $currentState. Do you want to change it to Enabled? (Y/n)"
}
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
wevtutil sl $log /e:true 2>&1 | Out-Null
Write-Host "[OK] Enabled: $log" -ForegroundColor Green
} else {
Write-Host "[SKIPPED] $log" -ForegroundColor Yellow
}
}
catch {
Write-Host "[ERROR] Failed to enable $log : $_" -ForegroundColor Red
}
Write-Host ""
}
# PowerShell ロギングの設定
Write-Host "Configuring PowerShell Logging..."
Write-Host ""
# 64bit の PowerShell と GPO が読むのは Wow6432Node の無いパス。
# 32bit の PowerShell 用に Wow6432Node 側も併せて設定する。
$regPaths = @()
foreach ($root in $script:PowerShellPolicyRoots) {
$regPaths += @{Path = "$root\ModuleLogging"; Name = "EnableModuleLogging"; Value = 1}
$regPaths += @{Path = "$root\ScriptBlockLogging"; Name = "EnableScriptBlockLogging"; Value = 1}
}
Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto
# モジュール名レジストリの設定
foreach ($root in $script:PowerShellPolicyRoots) {
try {
$moduleLoggingPath = "$root\ModuleLogging\ModuleNames"
$currentValue = "Not Set"
$pathExists = Test-Path $moduleLoggingPath
if ($pathExists) {
$prop = Get-ItemProperty -Path $moduleLoggingPath -Name "*" -ErrorAction SilentlyContinue
if ($prop) {
$currentValue = $prop."*"
}
}
Write-Host "Registry: $moduleLoggingPath"
if ($currentValue -eq "*") {
Write-Host "[SKIPPED] Module logging : Already set to * (all modules)." -ForegroundColor Yellow
Write-Host ""
} else
{
if ($Auto)
{
$response = "Y"
}
else
{
$response = Read-Host "Your current setting is $currentValue. Do you want to change it to * (all modules)? (Y/n)"
}
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y")
{
if (-not $pathExists)
{
New-Item -Path $moduleLoggingPath -Force | Out-Null
}
Set-ItemProperty -Path $moduleLoggingPath -Name "*" -Value "*" -Type String
Write-Host "[OK] Module logging enabled for all modules" -ForegroundColor Green
}
else
{
Write-Host "[SKIPPED] Module logging" -ForegroundColor Yellow
}
}
}
catch {
Write-Host "[ERROR] Failed to configure module names: $_" -ForegroundColor Red
}
Write-Host ""
}
# コマンドライン監査の有効化
Write-Host "Enabling Command Line Auditing..."
Write-Host ""
$regPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System\Audit"
$valueName = "ProcessCreationIncludeCmdLine_Enabled"
try {
$currentValue = "Not Set"
if (Test-Path $regPath) {
$prop = Get-ItemProperty -Path $regPath -Name $valueName -ErrorAction SilentlyContinue
$currentValue = $prop.$valueName
}
Write-Host "Registry: $regPath"
if ($currentValue -eq 1) {
Write-Host "[SKIPPED] Command Line Auditing : Already Enabled." -ForegroundColor Yellow
Write-Host ""
} else
{
if ($Auto)
{
$response = "Y"
}
else
{
$response = Read-Host "Your current setting is $currentValue. Do you want to change it to 1 (Enabled)? (Y/n)"
}
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y")
{
$regPath = $regPath -replace "HKLM:", "HKLM"
$arguments = "add $regPath /v $valueName /f /t REG_DWORD /d 1"
$process = Start-Process -FilePath "reg.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow -RedirectStandardOutput "NUL"
if ($process.ExitCode -eq 0)
{
Write-Host "[OK] Command line auditing enabled" -ForegroundColor Green
}
else
{
Write-Host "[ERROR] Command line auditing failed (ExitCode: $( $process.ExitCode ))" -ForegroundColor Red
}
}
else
{
Write-Host "[SKIPPED] Command line auditing" -ForegroundColor Yellow
}
}
}
catch {
Write-Host "[ERROR] Failed to check command line auditing: $_" -ForegroundColor Red
}
Write-Host ""
# NTLM認証の監査設定
Write-Host "Configuring NTLM Audit Settings..."
Write-Host ""
$regPaths = @(
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0"; Name = "RestrictSendingNTLMTraffic"; Value = 2},
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0"; Name = "AuditReceivingNTLMTraffic"; Value = 2},
@{Path = "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters"; Name = "AuditNTLMInDomain"; Value = 2}
)
Set-RegistryConfig -RegPaths $regPaths -Auto:$Auto
# 監査ポリシーの設定
Write-Host "Configuring Audit Policies..."
Write-Host ""
$auditPolicies = @(
@{Category = "Account Logon"; Name = "Credential Validation"; GUID = "0CCE923F-69AE-11D9-BED3-505054503030"},
@{Category = "Account Logon"; Name = "Kerberos Authentication Service"; GUID = "0CCE9242-69AE-11D9-BED3-505054503030"},
@{Category = "Account Logon"; Name = "Kerberos Service Ticket Operations"; GUID = "0CCE9240-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "Computer Account Management"; GUID = "0CCE9236-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "Distribution Group Management"; GUID = "0CCE9238-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "Other Account Management Events"; GUID = "0CCE923A-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "Security Group Management"; GUID = "0CCE9237-69AE-11D9-BED3-505054503030"},
@{Category = "Account Management"; Name = "User Account Management"; GUID = "0CCE9235-69AE-11D9-BED3-505054503030"},
@{Category = "Detailed Tracking"; Name = "Plug and Play"; GUID = "0cce9248-69ae-11d9-bed3-505054503030"},
@{Category = "Detailed Tracking"; Name = "Process Creation"; GUID = "0CCE922B-69AE-11D9-BED3-505054503030"},
@{Category = "Detailed Tracking"; Name = "RPC Events"; GUID = "0CCE922E-69AE-11D9-BED3-505054503030"},
@{Category = "DS Access"; Name = "Directory Service Access"; GUID = "0CCE923B-69AE-11D9-BED3-505054503030"},
@{Category = "DS Access"; Name = "Directory Service Changes"; GUID = "0CCE923C-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Account Lockout"; GUID = "0CCE9217-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Logoff"; GUID = "0CCE9216-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Logon"; GUID = "0CCE9215-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Other Logon/Logoff Events"; GUID = "0CCE921C-69AE-11D9-BED3-505054503030"},
@{Category = "Logon/Logoff"; Name = "Special Logon"; GUID = "0CCE921B-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Certification Services"; GUID = "0CCE9221-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "File Share"; GUID = "0CCE9224-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Filtering Platform Connection"; GUID = "0CCE9226-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Other Object Access Events"; GUID = "0CCE9227-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "Removable Storage"; GUID = "0CCE9245-69AE-11D9-BED3-505054503030"},
@{Category = "Object Access"; Name = "SAM"; GUID = "0CCE9220-69AE-11D9-BED3-505054503030"},
@{Category = "Policy Change"; Name = "Audit Policy Change"; GUID = "0CCE922F-69AE-11D9-BED3-505054503030"},
@{Category = "Policy Change"; Name = "Authentication Policy Change"; GUID = "0CCE9230-69AE-11D9-BED3-505054503030"},
@{Category = "Policy Change"; Name = "Other Policy Change Events"; GUID = "0CCE9234-69AE-11D9-BED3-505054503030"},
@{Category = "Privilege Use"; Name = "Sensitive Privilege Use"; GUID = "0CCE9228-69AE-11D9-BED3-505054503030"},
@{Category = "System"; Name = "Security State Change"; GUID = "0CCE9210-69AE-11D9-BED3-505054503030"},
@{Category = "System"; Name = "Security System Extension"; GUID = "0CCE9211-69AE-11D9-BED3-505054503030"},
@{Category = "System"; Name = "System Integrity"; GUID = "0CCE9212-69AE-11D9-BED3-505054503030"},
@{Category = "System"; Name = "Other System Events"; GUID = "0CCE9214-69AE-11D9-BED3-505054503030"}
)
$currentAuditPol = GetAuditpol
foreach ($policy in $auditPolicies)
{
$newSetting = "Success and Failure"
$currentSetting = if ($currentAuditPol.ContainsKey($policy.GUID))
{
$currentAuditPol[$policy.GUID]
}
else
{
"Unknown"
}
Write-Host "Audit Policy: $( $policy.Category ) - $( $policy.Name )"
if ($currentSetting -eq $newSetting)
{
Write-Host "[SKIPPED] $( $policy.Category ) - $( $policy.Name ) : Already set to $newSetting." -ForegroundColor Yellow
Write-Host ""
continue
}
if ($Auto) {
$response = "Y"
} else {
$response = Read-Host "Your current setting is $currentSetting. Do you want to change it to $newSetting? (Y/n)"
}
if ($response -eq "" -or $response -eq "Y" -or $response -eq "y") {
$arguments = "/set /subcategory:{$($policy.GUID)} /success:enable /failure:enable"
$process = Start-Process -FilePath "auditpol.exe" -ArgumentList $arguments -Wait -PassThru -NoNewWindow -RedirectStandardOutput "NUL"
if ($process.ExitCode -eq 0) {
Write-Host "[OK] $($policy.Category) - $($policy.Name)" -ForegroundColor Green
}
else {
Write-Host "[ERROR] $($policy.Category) - $($policy.Name) (ExitCode: $($process.ExitCode))" -ForegroundColor Red
}
} else {
Write-Host "[SKIPPED] $($policy.Category) - $($policy.Name)" -ForegroundColor Yellow
}
Write-Host ""
}
# AD CS AuditFilter の設定
Write-Host "Configuring AD CS Audit Settings..."
try {
$installed = (Get-WindowsFeature -Name AD-Certificate).InstallState -eq "Installed"
} catch {
$installed = $false
}
if ($installed) {
try {
$csRootKey = "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\"
$caName = (Get-ItemProperty $csRootKey -ErrorAction Stop).Active
$regPath = "HKLM:\SYSTEM\CurrentControlSet\Services\CertSvc\Configuration\$caName"
$prop = Get-ItemProperty -Path $regPath -Name "AuditFilter" -ErrorAction SilentlyContinue
$currentValue = if ($null -ne $prop) { [int]$prop.AuditFilter } else { "Not Set" }
if ($currentValue -eq 127) {
Write-Host "[OK] AuditFilter is already 127" -ForegroundColor Green
}
else {
$proceed = $false
if ($Auto) {
$proceed = $true
}
else {
$response = Read-Host "Do you want to set AuditFilter to 127 and restart Certificate Services? (Y/n)"
$proceed = ($response -eq "" -or $response -match "^[Yy]$")
}
if ($proceed) {
try {
# AuditFilter の設定
& certutil.exe -setreg "CA\AuditFilter" 127 >$null 2>&1
# 証明書サービスの再起動
Restart-Service -Name "CertSvc" -Force -ErrorAction Stop
# 反映確認
$propAfter = Get-ItemProperty -Path $regPath -Name "AuditFilter" -ErrorAction SilentlyContinue
$newValue = if ($null -ne $propAfter) { [int]$propAfter.AuditFilter } else { $null }
if ($newValue -eq 127) {
Write-Host "[OK] AuditFilter set to 127 and CertSvc restarted" -ForegroundColor Green
}
else {
Write-Host "[ERROR] AuditFilter did not apply as expected (current: $newValue)" -ForegroundColor Red
}
}
catch {
Write-Host "[ERROR] Failed to set AuditFilter or restart CertSvc: $_" -ForegroundColor Red
}
}
else {
Write-Host "[SKIP] No changes applied to AuditFilter"
}
}
}
catch {
Write-Host "[ERROR] Failed to process AD CS audit settings: $_" -ForegroundColor Red
}
}
else {
Write-Host "[INFO] AD Certificate Services is not installed. Skipping." -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Configuration completed successfully" -ForegroundColor Green
}
$logo = @"
┏┓┏┓┏┳━━━┳┓ ┏━━━┓
┃┃┃┃┃┃┏━━┫┃ ┃┏━┓┃
┃┃┃┃┃┃┗━━┫┃ ┃┃ ┃┃
┃┗┛┗┛┃┏━━┫┃ ┏┫┗━┛┃
┗┓┏┓┏┫┗━━┫┗━┛┃┏━┓┃
┗┛┗┛┗━━━┻━━━┻┛ ┗┛
by Yamato Security
"@
function Enable-WelaPrivilege {
# Enable SeSecurityPrivilege (required to read/write SACLs) + backup/restore, in the current token.
param([string[]] $Privileges = @("SeSecurityPrivilege","SeBackupPrivilege","SeRestorePrivilege"))
if (-not ("WELA.PrivHelper" -as [type])) {
Add-Type -Namespace WELA -Name PrivHelper -MemberDefinition @"
[DllImport("advapi32.dll", SetLastError=true)] public static extern bool OpenProcessToken(IntPtr h, uint acc, out IntPtr tok);
[DllImport("advapi32.dll", SetLastError=true)] public static extern bool LookupPrivilegeValue(string host, string name, out long luid);
[DllImport("advapi32.dll", SetLastError=true)] public static extern bool AdjustTokenPrivileges(IntPtr tok, bool dis, ref TOKEN_PRIVILEGES np, uint len, IntPtr prev, IntPtr rl);
[DllImport("kernel32.dll")] public static extern IntPtr GetCurrentProcess();
[System.Runtime.InteropServices.StructLayout(System.Runtime.InteropServices.LayoutKind.Sequential)]
public struct TOKEN_PRIVILEGES { public uint Count; public long Luid; public uint Attr; }
public static bool Enable(string priv) {
IntPtr tok; if(!OpenProcessToken(GetCurrentProcess(), 0x20, out tok)) return false;
long luid; if(!LookupPrivilegeValue(null, priv, out luid)) return false;
TOKEN_PRIVILEGES tp = new TOKEN_PRIVILEGES(); tp.Count=1; tp.Luid=luid; tp.Attr=0x2;
return AdjustTokenPrivileges(tok, false, ref tp, 0, IntPtr.Zero, IntPtr.Zero);
}
"@
}
foreach ($p in $Privileges) { [void][WELA.PrivHelper]::Enable($p) }
}
function Set-AuditSacl {
# Apply TARGETED audit SACLs (from config/audit_sacl_targets.json) so that File System (4663),
# Registry (4657) and Handle Manipulation (4656) auditing fires only on the specific ASEP keys
# and sensitive files the detection rules watch - never globally.
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')]
param([switch] $Auto)
if (-not (TestWindows)) {
Write-Host "[ERROR] 'configure-sacl' changes Windows settings and can only run on Windows." -ForegroundColor Red; return
}
if (-not (TestAdministrator)) { Write-Error "This command requires Administrator privileges"; return }
if (-not (Test-Path $script:SaclTargetsPath)) {
Write-Host "[ERROR] Missing config: $script:SaclTargetsPath (run 'update-rules' or reinstall WELA)." -ForegroundColor Red; return
}
Enable-WelaPrivilege
$targets = Get-Content -Path $script:SaclTargetsPath -Raw | ConvertFrom-Json
# 1) Enable ONLY the object-access subcategories these SACLs need (by GUID, locale-independent).
# With no SACLs beyond the targeted ones below, these subcategories stay effectively silent.
Write-Host "Enabling Object Access subcategories (File System, Registry, Handle Manipulation)..."
$subs = @(
@{Name="File System"; GUID="0CCE921D-69AE-11D9-BED3-505054503030"},
@{Name="Registry"; GUID="0CCE921E-69AE-11D9-BED3-505054503030"},
@{Name="Handle Manipulation"; GUID="0CCE9223-69AE-11D9-BED3-505054503030"}
)
foreach ($s in $subs) {
if ($Auto -or $PSCmdlet.ShouldProcess($s.Name, "auditpol enable Success+Failure")) {
$p = Start-Process -FilePath "auditpol.exe" -ArgumentList "/set /subcategory:{$($s.GUID)} /success:enable /failure:enable" -Wait -PassThru -NoNewWindow -RedirectStandardOutput "NUL"
if ($p.ExitCode -eq 0) { Write-Host "[OK] subcategory: $($s.Name)" -ForegroundColor Green }
else { Write-Host "[ERROR] subcategory: $($s.Name) (ExitCode $($p.ExitCode))" -ForegroundColor Red }
}
}
Write-Host ""
$everyone = New-Object System.Security.Principal.SecurityIdentifier("S-1-1-0")
$auditFlags = [System.Security.AccessControl.AuditFlags]"Success,Failure"
# 2) Registry SACLs
Write-Host "Applying targeted REGISTRY audit SACLs..."
foreach ($t in $targets.registry) {
try {
if (-not (Test-Path -LiteralPath $t.path)) {
Write-Host "[SKIPPED] $($t.path) : key not present on this host" -ForegroundColor DarkYellow; continue
}
$rights = [System.Security.AccessControl.RegistryRights]($t.rights -join ",")
$inh = if ($t.inherit) { [System.Security.AccessControl.InheritanceFlags]"ContainerInherit" } else { [System.Security.AccessControl.InheritanceFlags]"None" }
$acl = Get-Acl -LiteralPath $t.path -Audit
$already = $acl.Audit | Where-Object { $_.IdentityReference.Value -eq $everyone.Value -and (($_.RegistryRights -band $rights) -eq $rights) -and ($_.AuditFlags -eq $auditFlags) }
if ($already) { Write-Host "[SKIPPED] $($t.path) : SACL already present ($($t.note))" -ForegroundColor Yellow; continue }
if ($Auto -or $PSCmdlet.ShouldProcess($t.path, "add audit SACL [$($t.rights -join ',')] Everyone Success+Failure")) {
$rule = New-Object System.Security.AccessControl.RegistryAuditRule($everyone, $rights, $inh, "None", $auditFlags)
$acl.AddAuditRule($rule); Set-Acl -LiteralPath $t.path -AclObject $acl
Write-Host "[OK] $($t.path) ($($t.note))" -ForegroundColor Green
}
} catch { Write-Host "[ERROR] $($t.path) : $_" -ForegroundColor Red }
}
Write-Host ""
# 3) File / directory SACLs
Write-Host "Applying targeted FILE audit SACLs..."
foreach ($t in $targets.files) {
try {
if (-not (Test-Path -LiteralPath $t.path)) {
Write-Host "[SKIPPED] $($t.path) : path not present on this host" -ForegroundColor DarkYellow; continue
}
$isDir = (Get-Item -LiteralPath $t.path -Force).PSIsContainer
$rights = [System.Security.AccessControl.FileSystemRights]($t.rights -join ",")
$inh = if ($isDir -and $t.inherit) { [System.Security.AccessControl.InheritanceFlags]"ContainerInherit,ObjectInherit" } else { [System.Security.AccessControl.InheritanceFlags]"None" }
$acl = Get-Acl -LiteralPath $t.path -Audit
$already = $acl.Audit | Where-Object { $_.IdentityReference.Value -eq $everyone.Value -and (($_.FileSystemRights -band $rights) -eq $rights) -and ($_.AuditFlags -eq $auditFlags) }
if ($already) { Write-Host "[SKIPPED] $($t.path) : SACL already present ($($t.note))" -ForegroundColor Yellow; continue }
if ($Auto -or $PSCmdlet.ShouldProcess($t.path, "add audit SACL [$($t.rights -join ',')] Everyone Success+Failure")) {
$rule = New-Object System.Security.AccessControl.FileSystemAuditRule($everyone, $rights, $inh, "None", $auditFlags)
$acl.AddAuditRule($rule); Set-Acl -LiteralPath $t.path -AclObject $acl
Write-Host "[OK] $($t.path) ($($t.note))" -ForegroundColor Green
}
} catch { Write-Host "[ERROR] $($t.path) : $_" -ForegroundColor Red }
}
Write-Host ""
Write-Host "Done. Targeted object-access auditing is enabled without global file/registry auditing." -ForegroundColor Cyan
Write-Host "Note: per-user (HKCU / profile AppData) objects are out of scope for a machine-wide SACL policy." -ForegroundColor DarkCyan
}
$usage = @"
Usage:
./WELA.ps1 audit-settings -Baseline YamatoSecurity # Audit current setting and show in stdout, save to csv
./WELA.ps1 audit-settings -Baseline ASD -OutType gui # Audit current setting and show in gui, save to csv
./WELA.ps1 audit-filesize -Baseline YamatoSecurity # Audit current file size and show in stdout, save to csv
./WELA.ps1 configure -Baseline YamatoSecurity # Configure audit settings based on the specified baseline
./WELA.ps1 configure -Baseline YamatoSecurity -Auto # Configure audit settings automatically without prompts
./WELA.ps1 configure-sacl # Add targeted File System/Registry audit SACLs (ASEP keys + sensitive files) needed by the rules, without global auditing
./WELA.ps1 configure-sacl -Auto # ...automatically without prompts
./WELA.ps1 update-rules # Update rule config files from https://github.com/Yamato-Security/WELA
./WELA.ps1 version # Show the WELA version
./WELA.ps1 help # Show this help
"@
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Host $logo -ForegroundColor Green
Write-Host ""
Write-Host "WELA v$WELAVersion - $WELAReleaseName"
Write-Host ""
switch ($Cmd.ToLower()) {
"audit-settings" {
if ($Help -or [string]::IsNullOrEmpty($Baseline)){
Write-Host "Audit current Windows Event Log settings and compare with baseline"
Write-Host ""
Write-Host "Usage: ./WELA.ps1 audit-settings -Baseline <YamatoSecurity|ASD|Microsoft_Client|Microsoft_Server> [-OutType <std|gui|table>]"
Write-Host ""
Write-Host "Options:"
Write-Host " -Baseline Specify the baseline (YamatoSecurity, ASD, Microsoft_Client, Microsoft_Server)"
Write-Host " -OutType Output type: std (default) or gui or table"
Write-Host ""
return
}
$validGuides = GetBaselineNames
if (-not ($validGuides -contains $Baseline)) {
Write-Host "Invalid Guide specified. Valid options are: $($validGuides -join ', ')."
break
}
AuditLogSetting -outType $OutType -Baseline $Baseline -debug:$Debug
}
"audit-filesize" {
if ($Help){
Write-Host "Audit current Windows Event Log file sizes"
Write-Host ""
Write-Host "Usage: ./WELA.ps1 audit-filesize"
Write-Host ""
Write-Host "Note: the recommended sizes are the same for every baseline, so -Baseline is not required."
Write-Host ""
return
}
if (-not [string]::IsNullOrEmpty($Baseline) -and $Baseline -ne "YamatoSecurity") {
Write-Host "Note: audit-filesize uses the same recommended sizes for every baseline; '-Baseline $Baseline' is ignored." -ForegroundColor DarkYellow
Write-Host ""
}
AuditFileSize
}
"configure" {
if ($Help){
Write-Host "Configure Windows Event Log audit settings based on the YamatoSecurity baseline"
Write-Host ""
Write-Host "Usage: ./WELA.ps1 configure [-Auto]"
Write-Host ""
Write-Host "Options:"
Write-Host " -Auto Automatically configure without prompts"
Write-Host ""
Write-Host "Note: only the YamatoSecurity baseline is currently supported for 'configure'."
Write-Host ""
return
}
if (-not [string]::IsNullOrEmpty($Baseline) -and $Baseline -ne "YamatoSecurity") {
Write-Host "'configure' currently supports only the YamatoSecurity baseline, but '-Baseline $Baseline' was given." -ForegroundColor Red
Write-Host "Re-run with '-Baseline YamatoSecurity' (or omit -Baseline) if that is what you want."
break
}
ConfigureAuditSettings -Auto:$Auto -Debug:$Debug
}
"configure-sacl" {
if ($Help){
Write-Host "Add TARGETED object-access audit SACLs so File System (4663) / Registry (4657) /"
Write-Host "Handle Manipulation (4656) auditing fires only on the specific autostart/persistence"
Write-Host "registry keys and sensitive files the Hayabusa/Sigma rules watch - never globally."
Write-Host ""
Write-Host "Usage: ./WELA.ps1 configure-sacl [-Auto]"
Write-Host ""
Write-Host "Options:"
Write-Host " -Auto Apply without per-object prompts"
Write-Host ""
Write-Host "Targets are defined in config/audit_sacl_targets.json (edit to customize)."
Write-Host "Objects absent on the host are skipped; per-user HKCU/AppData objects are out of scope."
Write-Host ""
return
}
Set-AuditSacl -Auto:$Auto
}
"update-rules" {
if ($Help) {
Write-Host "Update detection rule configuration files from GitHub repository"
Write-Host ""
Write-Host "Usage: ./WELA.ps1 update-rules"
Write-Host ""
Write-Host "Download and update rule configuration files from GitHub repository"
Write-Host ""
return
}
UpdateRules
}
"version" {
# バージョンはバナーで表示済みなので、ここでは何もしない
}
"help" {
Write-Host $usage
}
default {
Write-Host "Invalid command. Use 'help' to see available commands."
Write-Host $usage
}
}