Skip to content

Commit 799ed08

Browse files
committed
Add a script for the one path the test suite cannot cover
Reading the Security channel needs an elevated process, so the automated suite can never exercise it: the test runner does not have the rights and cannot acquire them without an interactive UAC prompt. That left the headline feature verified only in its failure direction, which is the wrong half to be sure about. verify-elevated.ps1 covers it. It checks Get-WinEvent directly, then starts each application on a temporary port and database, confirms both status endpoints report elevated with no restricted channels, and collects from the Security channel through both APIs before cleaning up. It refuses to run from a window that is not elevated rather than reporting a misleading pass, and says why, since being an Administrator is not the same as running elevated and that distinction is what makes the underlying problem confusing. Two faults found while testing the script itself, both fixed: Start-Process reports a missing working directory without naming the path, so the application folders are now resolved and checked up front; and Stop-Process returns before the service releases its SQLite handle, so deleting the temporary database silently failed and left files in the temp folder. Cleanup now waits for the process to exit and retries. The mechanics were verified by running the same start, poll, query, and cleanup logic against the System channel, which needs no elevation: services start, respond, collect, and clean up with nothing left behind. The elevated result itself is the one thing still unverified, which is exactly what this script exists to let someone check.
1 parent b526ce2 commit 799ed08

2 files changed

Lines changed: 190 additions & 4 deletions

File tree

README.md

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -48,10 +48,11 @@ investigation timelines, and HTML/PDF reports.
4848

4949
```text
5050
SentinelOps/
51-
|-- v1.0/ Original stable application
52-
|-- v2.0/ Latest Phase 6 application
53-
|-- start-v1.ps1 v1.0 launcher
54-
`-- start-v2.ps1 v2.0 launcher
51+
|-- v1.0/ Original stable application
52+
|-- v2.0/ Latest Phase 6 application
53+
|-- start-v1.ps1 v1.0 launcher
54+
|-- start-v2.ps1 v2.0 launcher
55+
`-- verify-elevated.ps1 Checks Security log collection when elevated
5556
```
5657

5758
## Requirements
@@ -88,6 +89,18 @@ denied to it. Check which case you are in with:
8889

8990
`False` means the window is not elevated, regardless of your account type.
9091

92+
To confirm elevated collection actually works, run this from an elevated window:
93+
94+
```powershell
95+
.\verify-elevated.ps1
96+
```
97+
98+
It checks `Get-WinEvent` directly, then starts each application on a temporary
99+
port and database and collects from the Security channel through both APIs,
100+
cleaning up afterwards. It refuses to run unelevated rather than reporting a
101+
misleading pass. The automated test suite cannot cover this path, because it
102+
requires rights the test runner does not have.
103+
91104
Without elevation everything else still works. The `System`, `Application`,
92105
`PowerShell`, `Defender`, and `Sysmon` channels are readable normally, EVTX
93106
import is unaffected, and a collection from `Security` returns a clear

verify-elevated.ps1

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
# Verifies that live Security event log collection works when elevated.
2+
#
3+
# Windows restricts the Security channel to elevated processes, and being
4+
# signed in as an Administrator is not enough: under UAC an ordinary window
5+
# gets a filtered token. This script can only be run from an elevated window,
6+
# so it covers the one path the normal test suite cannot: that an elevated
7+
# collection actually returns events.
8+
#
9+
# Usage: open PowerShell with Run as Administrator, then
10+
# .\verify-elevated.ps1
11+
12+
$ErrorActionPreference = "Stop"
13+
14+
$identity = [Security.Principal.WindowsIdentity]::GetCurrent()
15+
$principal = New-Object Security.Principal.WindowsPrincipal($identity)
16+
$elevated = $principal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
17+
18+
Write-Host "User : $($identity.Name)"
19+
Write-Host "Elevated : $elevated"
20+
21+
if (-not $elevated) {
22+
Write-Host ""
23+
Write-Host "This window is not elevated, so the check cannot run." -ForegroundColor Yellow
24+
Write-Host "Being an Administrator is not enough; the window itself must be elevated."
25+
Write-Host "Right-click PowerShell, choose Run as Administrator, then run this again."
26+
exit 1
27+
}
28+
29+
$candidates = @(
30+
(Join-Path $env:LOCALAPPDATA "Programs\Python\Python313\python.exe"),
31+
(Join-Path $env:LOCALAPPDATA "Programs\Python\Python312\python.exe"),
32+
(Join-Path $env:LOCALAPPDATA "Programs\Python\Python311\python.exe"),
33+
(Join-Path $env:LOCALAPPDATA "Programs\Python\Python310\python.exe"),
34+
"py.exe",
35+
"python.exe"
36+
)
37+
$python = $null
38+
foreach ($candidate in $candidates) {
39+
try {
40+
if ([System.IO.Path]::IsPathRooted($candidate) -and -not (Test-Path -LiteralPath $candidate)) { continue }
41+
$version = & $candidate --version 2>&1
42+
if ($LASTEXITCODE -eq 0 -and "$version" -match "^Python 3\.(1[0-9]|[2-9][0-9])") { $python = $candidate; break }
43+
} catch { continue }
44+
}
45+
if (-not $python) { Write-Error "Python 3.10 or newer is required." }
46+
47+
# Resolve up front: Start-Process reports a missing WorkingDirectory with a
48+
# message that does not name the path, which is hard to act on.
49+
$v2Dir = Join-Path $PSScriptRoot "v2.0"
50+
$v1Dir = Join-Path $PSScriptRoot "v1.0"
51+
foreach ($dir in @($v2Dir, $v1Dir)) {
52+
if (-not (Test-Path -LiteralPath $dir -PathType Container)) {
53+
Write-Error "Application folder not found: $dir. Run this script from the repository root."
54+
}
55+
}
56+
57+
$results = [System.Collections.Generic.List[object]]::new()
58+
function Add-Result([string]$Name, [bool]$Ok, [string]$Detail) {
59+
$results.Add([pscustomobject]@{ Name = $Name; Ok = $Ok; Detail = $Detail })
60+
$status = if ($Ok) { "PASS" } else { "FAIL" }
61+
$colour = if ($Ok) { "Green" } else { "Red" }
62+
Write-Host (" {0} {1} {2}" -f $status, $Name.PadRight(46), $Detail) -ForegroundColor $colour
63+
}
64+
65+
function Stop-ServiceProcess($Process, [string]$DatabasePath) {
66+
# Wait for the process to actually exit before deleting its database.
67+
# Stop-Process returns before the SQLite handle is released, so removing
68+
# the file immediately fails and leaves it behind in the temp folder.
69+
if ($Process) {
70+
Stop-Process -Id $Process.Id -Force -ErrorAction SilentlyContinue
71+
try { $Process.WaitForExit(5000) | Out-Null } catch { }
72+
}
73+
if ($DatabasePath) {
74+
foreach ($attempt in 1..5) {
75+
Remove-Item -LiteralPath $DatabasePath -Force -ErrorAction SilentlyContinue
76+
if (-not (Test-Path -LiteralPath $DatabasePath)) { break }
77+
Start-Sleep -Milliseconds 300
78+
}
79+
}
80+
}
81+
82+
function Wait-ForService([string]$Url, [int]$TimeoutSeconds = 30) {
83+
$deadline = (Get-Date).AddSeconds($TimeoutSeconds)
84+
while ((Get-Date) -lt $deadline) {
85+
try {
86+
Invoke-RestMethod $Url -TimeoutSec 3 | Out-Null
87+
return $true
88+
} catch {
89+
Start-Sleep -Milliseconds 400
90+
}
91+
}
92+
return $false
93+
}
94+
95+
Write-Host ""
96+
Write-Host "1. Direct Get-WinEvent on the Security channel"
97+
try {
98+
$events = Get-WinEvent -LogName 'Security' -MaxEvents 3 -ErrorAction Stop
99+
Add-Result "Security log readable directly" ($events.Count -gt 0) "$($events.Count) events"
100+
} catch {
101+
Add-Result "Security log readable directly" $false $_.Exception.Message
102+
}
103+
104+
# --- v2.0 -------------------------------------------------------------------
105+
Write-Host ""
106+
Write-Host "2. SentinelOps v2.0 API"
107+
$v2Port = 8097
108+
$v2Db = Join-Path $env:TEMP "sentinelops-verify-v2.db"
109+
$v2Proc = $null
110+
try {
111+
$env:SENTINELOPS_V2_DB = $v2Db
112+
$env:SENTINELOPS_V2_PORT = "$v2Port"
113+
$v2Proc = Start-Process -FilePath $python -ArgumentList "-m", "backend.app" `
114+
-WorkingDirectory $v2Dir -PassThru -WindowStyle Hidden
115+
116+
if (-not (Wait-ForService "http://127.0.0.1:$v2Port/api/v2/status")) {
117+
Add-Result "v2.0 service started" $false "did not respond in time"
118+
} else {
119+
$status = Invoke-RestMethod "http://127.0.0.1:$v2Port/api/v2/status"
120+
Add-Result "v2.0 status reports elevated" ($status.elevated -eq $true) "elevated=$($status.elevated)"
121+
122+
$restricted = @($status.restrictedChannels).Count
123+
Add-Result "v2.0 lists no restricted channels" ($restricted -eq 0) "$restricted entries"
124+
125+
$collected = Invoke-RestMethod "http://127.0.0.1:$v2Port/api/v2/events/windows?channel=Security&max=5"
126+
Add-Result "v2.0 Security collection returns events" ($collected.newCount -gt 0) "$($collected.newCount) events"
127+
}
128+
} catch {
129+
Add-Result "v2.0 Security collection returns events" $false $_.Exception.Message
130+
} finally {
131+
Stop-ServiceProcess $v2Proc $v2Db
132+
Remove-Item Env:\SENTINELOPS_V2_DB -ErrorAction SilentlyContinue
133+
Remove-Item Env:\SENTINELOPS_V2_PORT -ErrorAction SilentlyContinue
134+
}
135+
136+
# --- v1.0 -------------------------------------------------------------------
137+
Write-Host ""
138+
Write-Host "3. SentinelOps v1.0 API"
139+
$v1Port = 8096
140+
$v1Db = Join-Path $env:TEMP "sentinelops-verify-v1.db"
141+
$v1Proc = $null
142+
try {
143+
$env:SENTINELOPS_PORT = "$v1Port"
144+
$env:SENTINELOPS_DB = $v1Db
145+
$v1Proc = Start-Process -FilePath $python -ArgumentList "server.py" `
146+
-WorkingDirectory $v1Dir -PassThru -WindowStyle Hidden
147+
148+
if (-not (Wait-ForService "http://127.0.0.1:$v1Port/api/status")) {
149+
Add-Result "v1.0 service started" $false "did not respond in time"
150+
} else {
151+
$status = Invoke-RestMethod "http://127.0.0.1:$v1Port/api/status"
152+
Add-Result "v1.0 status reports elevated" ($status.elevated -eq $true) "elevated=$($status.elevated)"
153+
154+
$collected = Invoke-RestMethod "http://127.0.0.1:$v1Port/api/windows-events?channel=Security&max=5"
155+
Add-Result "v1.0 Security collection returns events" ($collected.newCount -gt 0) "$($collected.newCount) events"
156+
}
157+
} catch {
158+
Add-Result "v1.0 Security collection returns events" $false $_.Exception.Message
159+
} finally {
160+
Stop-ServiceProcess $v1Proc $v1Db
161+
Remove-Item Env:\SENTINELOPS_PORT -ErrorAction SilentlyContinue
162+
Remove-Item Env:\SENTINELOPS_DB -ErrorAction SilentlyContinue
163+
}
164+
165+
Write-Host ""
166+
$failed = @($results | Where-Object { -not $_.Ok })
167+
Write-Host ("{0}/{1} checks passed" -f ($results.Count - $failed.Count), $results.Count)
168+
if ($failed.Count -gt 0) {
169+
Write-Host "Elevated collection is NOT working. See the failures above." -ForegroundColor Red
170+
exit 1
171+
}
172+
Write-Host "Elevated Security collection works end to end." -ForegroundColor Green
173+
exit 0

0 commit comments

Comments
 (0)