geenforce: harden allowlist + fix share-less kiosk client and display scope
- allowlist auth uses remote_addr, not the spoofable first X-Forwarded-For hop (adds _trusted_client_ip + a regression test); rate-limit path unchanged - client psm1: fix Set-StrictMode crashes reading absent keys in Get-ShopdbConfig (token-less mode) and Resolve-ShopdbPayloads (no-payload entries); validate the manifest response is JSON before overwriting the last-known-good cache - runner: pass the engine its required -InstallerRoot/-LogFile; create the log directory so enforce logging is not silently lost on a fresh kiosk - display scope: dispatcher writes an all-users Startup shortcut instead of Start-Process (SYSTEM cannot show a window in session 0), resolves the base URL from HKLM, and adds an always-on power/no-lock entry; tests updated for the 6-entry scope
This commit is contained in:
@@ -46,18 +46,36 @@ REPORT_SCOPE = 'geenforce.report'
|
||||
ALLOWED_CIDRS_SETTING = 'geenforce_allowed_cidrs'
|
||||
|
||||
|
||||
def _trusted_client_ip():
|
||||
"""The trustworthy caller IP for the AUTH allowlist.
|
||||
|
||||
Uses request.remote_addr, NOT the raw X-Forwarded-For header. Proxies APPEND
|
||||
to X-Forwarded-For, so its first hop is attacker-controlled: parsing it (as
|
||||
_client_ip does for rate-limiting) would let any caller send
|
||||
'X-Forwarded-For: <allowlisted-ip>' and bypass the token. remote_addr cannot
|
||||
be forged here - behind IIS the URL-Rewrite rule overwrites X-Forwarded-For
|
||||
with the real TCP peer and waitress (--trusted-proxy=127.0.0.1
|
||||
--trusted-proxy-headers=x-forwarded-for) derives remote_addr from it; a
|
||||
client hitting waitress directly is not a trusted proxy, so its remote_addr
|
||||
is its own real peer address. Either way remote_addr is the true client.
|
||||
"""
|
||||
return request.remote_addr or ''
|
||||
|
||||
|
||||
def _ip_allowlisted():
|
||||
"""True when the caller IP falls in the configured geenforce allowlist.
|
||||
|
||||
Lets vaulted fleet PCs reach the client endpoints without a per-PC token -
|
||||
network trust replaces the shared secret. Fails closed: an unparseable
|
||||
caller IP or malformed allowlist entry never matches. Empty setting = off.
|
||||
Uses the SPOOF-RESISTANT remote_addr (see _trusted_client_ip), never the raw
|
||||
X-Forwarded-For header.
|
||||
"""
|
||||
raw = (Setting.get(ALLOWED_CIDRS_SETTING) or '').strip()
|
||||
if not raw:
|
||||
return False
|
||||
try:
|
||||
ip = ipaddress.ip_address(_client_ip())
|
||||
ip = ipaddress.ip_address(_trusted_client_ip())
|
||||
except ValueError:
|
||||
return False
|
||||
for part in raw.split(','):
|
||||
|
||||
@@ -55,7 +55,13 @@ param(
|
||||
function Write-Log {
|
||||
param([string]$Message, [string]$Level = 'INFO')
|
||||
$line = "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') [$Level] $Message"
|
||||
try { Add-Content -LiteralPath $LogFile -Value $line -ErrorAction SilentlyContinue } catch {}
|
||||
try {
|
||||
$logDir = Split-Path -Parent $LogFile
|
||||
if ($logDir -and -not (Test-Path $logDir)) {
|
||||
New-Item -ItemType Directory -Path $logDir -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
Add-Content -LiteralPath $LogFile -Value $line -ErrorAction SilentlyContinue
|
||||
} catch {}
|
||||
Write-Host $line
|
||||
}
|
||||
|
||||
@@ -164,8 +170,23 @@ try {
|
||||
# code, or emit several objects. ConvertTo-ShopdbSummary adapts whatever it
|
||||
# returns into a well-formed summary hashtable so the report stage always
|
||||
# gets clean input (we do NOT assume the engine was fixed).
|
||||
# The engine requires -InstallerRoot (base for any relative Source/Installer
|
||||
# path) and -LogFile. Shadow runs off the share, so relative paths resolve
|
||||
# against the share scope dir. Cutover rewrites payloads to ABSOLUTE local
|
||||
# paths, so InstallerRoot is only a harmless fallback base (the payload cache).
|
||||
if ($ShadowMode -and $ShareManifestPath) {
|
||||
$installerRoot = Split-Path -Parent $ShareManifestPath
|
||||
} else {
|
||||
$installerRoot = Join-Path (Split-Path -Parent $manifestToRun) 'payloads'
|
||||
}
|
||||
if ($installerRoot -and -not (Test-Path $installerRoot)) {
|
||||
New-Item -ItemType Directory -Path $installerRoot -Force -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
$engineLog = $LogFile -replace '\.log$', '-engine.log'
|
||||
|
||||
Write-Log "Running engine against $manifestToRun"
|
||||
$engineResult = & $EnginePath -ManifestPath $manifestToRun -PCType $Scope
|
||||
$engineResult = & $EnginePath -ManifestPath $manifestToRun -PCType $Scope `
|
||||
-InstallerRoot $installerRoot -LogFile $engineLog
|
||||
$summary = ConvertTo-ShopdbSummary -EngineResult $engineResult
|
||||
|
||||
# Report the result (best-effort).
|
||||
|
||||
@@ -63,8 +63,10 @@ function Get-ShopdbConfig {
|
||||
$regPath = 'HKLM:\SOFTWARE\GE\ShopDB'
|
||||
if ((-not $BaseUrl -or -not $ApiToken) -and (Test-Path $regPath)) {
|
||||
$props = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue
|
||||
if (-not $BaseUrl -and $props.BaseUrl) { $BaseUrl = $props.BaseUrl }
|
||||
if (-not $ApiToken -and $props.ApiToken) { $ApiToken = $props.ApiToken }
|
||||
$regBase = Get-ShopdbProperty $props 'BaseUrl'
|
||||
$regTok = Get-ShopdbProperty $props 'ApiToken'
|
||||
if (-not $BaseUrl -and $regBase) { $BaseUrl = [string]$regBase }
|
||||
if (-not $ApiToken -and $regTok) { $ApiToken = [string]$regTok }
|
||||
}
|
||||
# ApiToken is OPTIONAL: on a vaulted network the server may authorize by
|
||||
# source-IP allowlist, so a BaseUrl alone is a valid config. When a token
|
||||
@@ -107,6 +109,10 @@ function Sync-ShopdbManifest {
|
||||
$response = Invoke-WebRequest -Uri $uri -Headers $headers -UseBasicParsing `
|
||||
-TimeoutSec 30 -ErrorAction Stop
|
||||
if ($response.StatusCode -eq 200) {
|
||||
# Validate JSON before overwriting the last-known-good cache: a proxy
|
||||
# or IIS error page served as 200 must not clobber the fallback.
|
||||
try { $null = ($response.Content | ConvertFrom-Json) }
|
||||
catch { throw "manifest response for $Scope was not valid JSON" }
|
||||
[System.IO.File]::WriteAllText($manifestPath, $response.Content)
|
||||
# PowerShell 7 returns header values as string arrays; 5.1 as scalars.
|
||||
# @(...)[0] yields a clean scalar in both.
|
||||
@@ -122,7 +128,8 @@ function Sync-ShopdbManifest {
|
||||
}
|
||||
} catch {
|
||||
$status = $null
|
||||
if ($_.Exception.Response) { $status = [int]$_.Exception.Response.StatusCode }
|
||||
$exResponse = Get-ShopdbProperty $_.Exception 'Response'
|
||||
if ($exResponse) { $status = [int]$exResponse.StatusCode }
|
||||
if ($status -eq 304 -and (Test-Path $manifestPath)) {
|
||||
return @{ Path = $manifestPath; Version = (Read-CachedVersion $CacheDir $Scope); Source = 'cache-304' }
|
||||
}
|
||||
@@ -298,12 +305,13 @@ function Resolve-ShopdbPayloads {
|
||||
INF='Installer'; PS1='Script'; File='Source' }
|
||||
$changed = $false
|
||||
foreach ($entry in @($json.Applications)) {
|
||||
$src = [string]$entry.PayloadSource
|
||||
$sha = [string]$entry.PayloadSha256
|
||||
$src = [string](Get-ShopdbProperty $entry 'PayloadSource')
|
||||
$sha = [string](Get-ShopdbProperty $entry 'PayloadSha256')
|
||||
if (-not $sha -or ($src -ne 'http' -and $src -ne 'inline')) { continue }
|
||||
$field = $pathField[[string]$entry.Type]
|
||||
$field = $pathField[[string](Get-ShopdbProperty $entry 'Type')]
|
||||
if (-not $field) { continue }
|
||||
$local = Get-ShopdbPayload -Sha256 $sha -Config $Config -Filename $entry.PayloadRef -CacheDir $CacheDir
|
||||
$ref = Get-ShopdbProperty $entry 'PayloadRef'
|
||||
$local = Get-ShopdbPayload -Sha256 $sha -Config $Config -Filename $ref -CacheDir $CacheDir
|
||||
if (-not $local) { throw "payload $sha for '$($entry.Name)' could not be fetched/verified" }
|
||||
if ($entry.PSObject.Properties.Name -contains $field) { $entry.$field = $local }
|
||||
else { $entry | Add-Member -NotePropertyName $field -NotePropertyValue $local }
|
||||
|
||||
@@ -80,15 +80,20 @@ def build_dispatcher_script():
|
||||
|
||||
return f"""# Invoke-DisplayKioskDispatch.ps1 -- gea-shopfloor-display dispatcher.
|
||||
#
|
||||
# Reads C:\\Enrollment\\display-type.txt and launches the kiosk browser at the
|
||||
# route mapped for that display subtype. The kiosk browser is baked into the
|
||||
# display image; this script only points it at the right target.
|
||||
# Reads C:\\Enrollment\\display-type.txt and ensures an all-users Startup
|
||||
# shortcut that launches Edge in kiosk fullscreen at the route mapped for that
|
||||
# subtype. The kiosk browser is baked into the display image; this only points
|
||||
# the launch at the right target and heals the shortcut on a URL/type change.
|
||||
#
|
||||
# WHY a Startup shortcut, not Start-Process: this runs as SYSTEM under the
|
||||
# enforce task. SYSTEM has no interactive desktop, so Start-Process msedge.exe
|
||||
# opens INVISIBLY in session 0. A shortcut in the all-users Startup folder is
|
||||
# instead launched by the auto-login user at logon, in their own visible
|
||||
# session. Idempotent: prior ShopDB kiosk shortcuts are cleared first, then the
|
||||
# one for the resolved target is written, so a display-type/URL change self-heals.
|
||||
#
|
||||
# The DisplayTypeTargets table below is the single source of truth for the
|
||||
# subtype -> route map. Edit a row to retarget a subtype.
|
||||
#
|
||||
# TODO-confirm: 3DPrintRoom uses the printedparts /parts-kiosk route as a
|
||||
# PLACEHOLDER. Confirm the real 3D-print-room target before production use.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
@@ -97,19 +102,24 @@ $DisplayTypeTargets = @{{
|
||||
{table_body}
|
||||
}}
|
||||
|
||||
# Base URL the kiosk browser opens; the route from the table is appended. Edit
|
||||
# to point at this site's shopdb host. Kept here so the map above stays pure.
|
||||
$KioskBaseUrl = 'https://localhost'
|
||||
# Base URL the kiosk browser opens; the route from the table is appended. Read
|
||||
# from HKLM (the value the GE-Enforce client is configured with) so it stays
|
||||
# site-agnostic; falls back to the West Jefferson shopdb host.
|
||||
$KioskBaseUrl = 'https://tsgwp00525.wjs.geaerospace.net/shopdb'
|
||||
try {{
|
||||
$shopdbConfig = Get-ItemProperty -Path 'HKLM:\\SOFTWARE\\GE\\ShopDB' -Name BaseUrl -ErrorAction Stop
|
||||
if ($shopdbConfig.BaseUrl) {{ $KioskBaseUrl = ([string]$shopdbConfig.BaseUrl).TrimEnd('/') }}
|
||||
}} catch {{}}
|
||||
|
||||
$displayTypeFile = 'C:\\Enrollment\\display-type.txt'
|
||||
if (-not (Test-Path -LiteralPath $displayTypeFile)) {{
|
||||
Write-Host "display-type.txt not found at $displayTypeFile; nothing to launch."
|
||||
Write-Host "display-type.txt not found at $displayTypeFile; nothing to configure."
|
||||
return
|
||||
}}
|
||||
|
||||
$displayType = (Get-Content -LiteralPath $displayTypeFile -Raw).Trim()
|
||||
if ([string]::IsNullOrWhiteSpace($displayType)) {{
|
||||
Write-Host 'display-type.txt is empty; nothing to launch.'
|
||||
Write-Host 'display-type.txt is empty; nothing to configure.'
|
||||
return
|
||||
}}
|
||||
|
||||
@@ -122,25 +132,91 @@ if (-not $matchedKey) {{
|
||||
return
|
||||
}}
|
||||
|
||||
$targetRoute = $DisplayTypeTargets[$matchedKey]
|
||||
$kioskUrl = "$KioskBaseUrl$targetRoute"
|
||||
Write-Host "display-type '$displayType' -> kiosk target $kioskUrl"
|
||||
$kioskUrl = "$KioskBaseUrl$($DisplayTypeTargets[$matchedKey])"
|
||||
|
||||
# Idempotent: if an Edge kiosk process is already serving this URL, leave it be
|
||||
# so an enforce cycle does not relaunch the kiosk every run.
|
||||
$alreadyRunning = Get-CimInstance Win32_Process -Filter "Name='msedge.exe'" -ErrorAction SilentlyContinue |
|
||||
Where-Object {{ $_.CommandLine -and $_.CommandLine.Contains($kioskUrl) }}
|
||||
if ($alreadyRunning) {{
|
||||
Write-Host 'Kiosk already running for this target; leaving it in place.'
|
||||
return
|
||||
# Resolve msedge.exe (baked into the display image).
|
||||
$edge = $null
|
||||
foreach ($p in @("${{env:ProgramFiles(x86)}}\\Microsoft\\Edge\\Application\\msedge.exe",
|
||||
"$env:ProgramFiles\\Microsoft\\Edge\\Application\\msedge.exe")) {{
|
||||
if (Test-Path -LiteralPath $p) {{ $edge = $p; break }}
|
||||
}}
|
||||
if (-not $edge) {{ Write-Host 'msedge.exe not found; cannot write kiosk startup shortcut.'; return }}
|
||||
|
||||
$edgeArguments = @("--kiosk", $kioskUrl, "--edge-kiosk-type=fullscreen", "--no-first-run")
|
||||
Start-Process -FilePath 'msedge.exe' -ArgumentList $edgeArguments
|
||||
Write-Host 'Launched kiosk browser.'
|
||||
$startup = Join-Path $env:ProgramData 'Microsoft\\Windows\\Start Menu\\Programs\\Startup'
|
||||
if (-not (Test-Path $startup)) {{ New-Item -ItemType Directory -Path $startup -Force | Out-Null }}
|
||||
# Clear any prior ShopDB kiosk shortcut so a URL/type change self-heals.
|
||||
Get-ChildItem -LiteralPath $startup -Filter 'ShopDB Kiosk*.lnk' -ErrorAction SilentlyContinue |
|
||||
ForEach-Object {{ Remove-Item -LiteralPath $_.FullName -Force -ErrorAction SilentlyContinue }}
|
||||
|
||||
$edgeArgs = "--kiosk `"$kioskUrl`" --edge-kiosk-type=fullscreen --no-first-run --no-default-browser-check"
|
||||
$lnkPath = Join-Path $startup 'ShopDB Kiosk.lnk'
|
||||
$shell = New-Object -ComObject WScript.Shell
|
||||
$lnk = $shell.CreateShortcut($lnkPath)
|
||||
$lnk.TargetPath = $edge
|
||||
$lnk.Arguments = $edgeArgs
|
||||
$lnk.WorkingDirectory = Split-Path -Parent $edge
|
||||
$lnk.WindowStyle = 3
|
||||
$lnk.Description = "ShopDB kiosk display: $kioskUrl"
|
||||
$lnk.Save()
|
||||
Write-Host "display-type '$displayType' -> kiosk Startup shortcut for $kioskUrl"
|
||||
"""
|
||||
|
||||
|
||||
ALWAYSON_FILENAME = 'Set-DisplayAlwaysOn.ps1'
|
||||
|
||||
|
||||
def build_alwayson_script():
|
||||
"""Return the inline always-on PowerShell as text.
|
||||
|
||||
Keeps a display awake 24/7 (kiosk page always visible): powercfg never-off on
|
||||
AC+DC, screensaver + auto-lock disabled. Plain raw string - no table
|
||||
interpolation, so no brace escaping.
|
||||
"""
|
||||
return r'''# Set-DisplayAlwaysOn.ps1 -- gea-shopfloor-display always-on.
|
||||
# Keeps the display awake 24/7 so the kiosk page stays visible: powercfg
|
||||
# never-off (AC+DC) + screensaver/auto-lock disabled. SYSTEM, every cycle.
|
||||
|
||||
$ErrorActionPreference = 'Continue'
|
||||
|
||||
foreach ($c in @(
|
||||
'monitor-timeout-ac 0','monitor-timeout-dc 0',
|
||||
'standby-timeout-ac 0','standby-timeout-dc 0',
|
||||
'hibernate-timeout-ac 0','hibernate-timeout-dc 0',
|
||||
'disk-timeout-ac 0','disk-timeout-dc 0')) {
|
||||
try { Start-Process -FilePath powercfg -ArgumentList "/change $c" -NoNewWindow -Wait -ErrorAction Stop } catch {}
|
||||
}
|
||||
|
||||
# disable screensaver + secure lock for future profiles (.DEFAULT) + loaded hives.
|
||||
if (-not (Get-PSDrive -Name HKU -ErrorAction SilentlyContinue)) {
|
||||
New-PSDrive -Name HKU -PSProvider Registry -Root HKEY_USERS -ErrorAction SilentlyContinue | Out-Null
|
||||
}
|
||||
$hives = @('HKU:\.DEFAULT')
|
||||
try {
|
||||
Get-ChildItem 'Registry::HKEY_USERS' -ErrorAction Stop |
|
||||
Where-Object { $_.PSChildName -match '^S-1-5-21' -and $_.PSChildName -notmatch '_Classes$' } |
|
||||
ForEach-Object { $hives += "HKU:\$($_.PSChildName)" }
|
||||
} catch {}
|
||||
foreach ($h in $hives) {
|
||||
$cp = "$h\Control Panel\Desktop"
|
||||
try {
|
||||
if (-not (Test-Path $cp)) { New-Item -Path $cp -Force -ErrorAction Stop | Out-Null }
|
||||
Set-ItemProperty -Path $cp -Name ScreenSaveActive -Value '0' -ErrorAction Stop
|
||||
Set-ItemProperty -Path $cp -Name ScreenSaverIsSecure -Value '0' -ErrorAction SilentlyContinue
|
||||
Set-ItemProperty -Path $cp -Name ScreenSaveTimeOut -Value '0' -ErrorAction SilentlyContinue
|
||||
} catch {}
|
||||
}
|
||||
|
||||
# never auto-lock the kiosk console.
|
||||
try {
|
||||
$pol = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Personalization'
|
||||
if (-not (Test-Path $pol)) { New-Item -Path $pol -Force | Out-Null }
|
||||
Set-ItemProperty -Path $pol -Name NoLockScreen -Value 1 -Type DWord -ErrorAction SilentlyContinue
|
||||
} catch {}
|
||||
|
||||
Write-Host 'kiosk always-on enforced (power never-off + screensaver/lock disabled).'
|
||||
'''
|
||||
|
||||
|
||||
def _registry_drift_heal_entry(name, regname, regvalue, regtype, comment):
|
||||
"""One Type=Registry entry that writes a value and detects drift via
|
||||
ValueMatches against that same path/name.
|
||||
@@ -198,13 +274,16 @@ def build_display_manifest():
|
||||
{
|
||||
'_comment': (
|
||||
'Kiosk dispatcher. Reads C:\\Enrollment\\display-type.txt and '
|
||||
'launches the kiosk target for the subtype. Data-driven map: '
|
||||
'writes an all-users Startup shortcut that launches Edge kiosk '
|
||||
'fullscreen at the subtype target. Data-driven map: '
|
||||
+ _relaunch_window_targets_comment()
|
||||
+ '. 3DPrintRoom target is a PLACEHOLDER (/parts-kiosk); '
|
||||
'TODO-confirm the real target. Delivered inline over HTTPS '
|
||||
'(share-less displays); DetectionMethod Always so it re-asserts '
|
||||
'each cycle, but the script is idempotent (skips if the kiosk is '
|
||||
'already serving the target URL).'),
|
||||
+ '. Base URL from HKLM BaseUrl, else the WJ host. Runs as SYSTEM '
|
||||
'(no interactive desktop), so it does NOT Start-Process Edge '
|
||||
'(that opens invisibly in session 0) - the auto-login user runs '
|
||||
'the shortcut at logon in a visible session. Delivered inline over '
|
||||
'HTTPS (share-less displays); Always re-asserts, idempotent '
|
||||
'(rewrites only the resolved-target shortcut, self-heals a '
|
||||
'type/URL change).'),
|
||||
'Name': 'Display kiosk dispatcher (display-type.txt)',
|
||||
'Type': 'PS1',
|
||||
'Script': DISPATCHER_FILENAME,
|
||||
@@ -212,6 +291,19 @@ def build_display_manifest():
|
||||
'PayloadRef': DISPATCHER_FILENAME,
|
||||
'DetectionMethod': 'Always',
|
||||
},
|
||||
{
|
||||
'_comment': (
|
||||
'Keep the display awake 24/7 so the kiosk page stays visible: '
|
||||
'powercfg never-off (AC+DC) + screensaver/auto-lock disabled. '
|
||||
'Delivered inline over HTTPS; Always re-asserts + self-heals a '
|
||||
'power-plan drift each cycle.'),
|
||||
'Name': 'Display always-on (no sleep/blank/lock)',
|
||||
'Type': 'PS1',
|
||||
'Script': ALWAYSON_FILENAME,
|
||||
'PayloadSource': 'inline',
|
||||
'PayloadRef': ALWAYSON_FILENAME,
|
||||
'DetectionMethod': 'Always',
|
||||
},
|
||||
]
|
||||
return {
|
||||
'Version': SCOPE_VERSION,
|
||||
@@ -248,6 +340,13 @@ def seed_display_scope(publish=False, notes='seed gea-shopfloor-display'):
|
||||
dispatcher, DISPATCHER_FILENAME,
|
||||
'text/plain; charset=utf-8', scriptbytes)
|
||||
|
||||
alwayson = next(entry for entry in scope.entries
|
||||
if entry.name == 'Display always-on (no sleep/blank/lock)')
|
||||
alwaysonbytes = build_alwayson_script().encode('utf-8')
|
||||
alwaysonpayload = service.store_inline_payload(
|
||||
alwayson, ALWAYSON_FILENAME,
|
||||
'text/plain; charset=utf-8', alwaysonbytes)
|
||||
|
||||
publishedversion = None
|
||||
if publish:
|
||||
publishedversion = service.publish_scope(
|
||||
@@ -260,5 +359,6 @@ def seed_display_scope(publish=False, notes='seed gea-shopfloor-display'):
|
||||
'entrycount': len(scope.entries),
|
||||
'entrytypes': [entry.entrytype for entry in scope.entries],
|
||||
'dispatchersha256': payload.payloadsha256,
|
||||
'alwaysonsha256': alwaysonpayload.payloadsha256,
|
||||
'publishedversion': publishedversion,
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ from plugins.geenforce.models import (
|
||||
)
|
||||
from plugins.geenforce.seed_display_scope import (
|
||||
seed_display_scope, build_display_manifest, build_dispatcher_script,
|
||||
DISPLAY_TYPE_TARGETS, SCOPE_NAME, DISPATCHER_FILENAME,
|
||||
DISPLAY_TYPE_TARGETS, SCOPE_NAME, DISPATCHER_FILENAME, ALWAYSON_FILENAME,
|
||||
)
|
||||
|
||||
|
||||
@@ -25,10 +25,10 @@ def test_seed_creates_display_scope(db):
|
||||
# and do NOT inherit common.
|
||||
assert scope.iscommon is False
|
||||
|
||||
# Four Registry drift-heal entries + one PS1 dispatcher, in order.
|
||||
assert summary['entrycount'] == 5
|
||||
# Four Registry drift-heal entries + two inline PS1 (dispatcher, always-on).
|
||||
assert summary['entrycount'] == 6
|
||||
assert summary['entrytypes'] == ['Registry', 'Registry', 'Registry',
|
||||
'Registry', 'PS1']
|
||||
'Registry', 'PS1', 'PS1']
|
||||
|
||||
|
||||
def test_registry_entries_use_valuematches_detection(db):
|
||||
@@ -53,7 +53,8 @@ def test_registry_entries_use_valuematches_detection(db):
|
||||
def test_dispatcher_is_inline_and_data_driven(db):
|
||||
summary = seed_display_scope()
|
||||
|
||||
dispatcher = ManifestEntry.query.filter_by(entrytype='PS1').one()
|
||||
dispatcher = ManifestEntry.query.filter_by(
|
||||
payloadref=DISPATCHER_FILENAME).one()
|
||||
assert dispatcher.payloadsource == 'inline'
|
||||
assert dispatcher.payloadref == DISPATCHER_FILENAME
|
||||
assert dispatcher.payloadsha256 == summary['dispatchersha256']
|
||||
@@ -66,6 +67,29 @@ def test_dispatcher_is_inline_and_data_driven(db):
|
||||
assert display_type in scripttext
|
||||
assert route in scripttext
|
||||
assert 'display-type.txt' in scripttext
|
||||
# H3: SYSTEM writes an all-users Startup shortcut (CreateShortcut/.Save),
|
||||
# rather than Start-Process-ing Edge (which would open invisibly in session 0).
|
||||
assert 'CreateShortcut' in scripttext
|
||||
assert '.Save()' in scripttext
|
||||
assert 'Startup' in scripttext
|
||||
# H4: real base URL, not the localhost placeholder.
|
||||
assert 'localhost' not in scripttext
|
||||
|
||||
|
||||
def test_alwayson_entry_is_inline(db):
|
||||
summary = seed_display_scope()
|
||||
|
||||
alwayson = ManifestEntry.query.filter_by(
|
||||
payloadref=ALWAYSON_FILENAME).one()
|
||||
assert alwayson.entrytype == 'PS1'
|
||||
assert alwayson.payloadsource == 'inline'
|
||||
assert alwayson.detectionmethod == 'Always'
|
||||
assert alwayson.payloadsha256 == summary['alwaysonsha256']
|
||||
|
||||
payload = ManifestPayload.query.filter_by(entryid=alwayson.entryid).one()
|
||||
scripttext = payload.payloadbytes.decode('utf-8')
|
||||
assert 'powercfg' in scripttext
|
||||
assert 'monitor-timeout-ac 0' in scripttext
|
||||
|
||||
|
||||
def test_seed_publish_freezes_a_version(db):
|
||||
@@ -87,9 +111,10 @@ def test_seed_draft_is_idempotent(db):
|
||||
assert first['scopeid'] == second['scopeid']
|
||||
assert first['entrycount'] == second['entrycount']
|
||||
assert first['dispatchersha256'] == second['dispatchersha256']
|
||||
assert first['alwaysonsha256'] == second['alwaysonsha256']
|
||||
|
||||
entries = ManifestEntry.query.filter_by(scopeid=second['scopeid']).all()
|
||||
assert len(entries) == 5
|
||||
assert len(entries) == 6
|
||||
|
||||
|
||||
def test_build_manifest_has_no_smb_exe_payloads(db):
|
||||
|
||||
@@ -147,6 +147,17 @@ def test_empty_allowlist_keeps_token_required(client, db, app):
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
def test_spoofed_forwarded_for_does_not_bypass_allowlist(client, db, app):
|
||||
# SECURITY: the allowlist uses remote_addr, not X-Forwarded-For. A caller
|
||||
# whose real IP (127.0.0.1) is NOT allowlisted must NOT gain token-less access
|
||||
# by forging X-Forwarded-For to an allowlisted address.
|
||||
_seed_and_publish(app)
|
||||
_set_allowlist(app, '10.134.48.0/23') # test client 127.0.0.1 is NOT in it
|
||||
resp = client.get('/api/geenforce/manifest?pctype=gea-shopfloor-cmm',
|
||||
headers={'X-Forwarded-For': '10.134.48.10'})
|
||||
assert resp.status_code == 401, 'spoofed X-Forwarded-For bypassed the allowlist'
|
||||
|
||||
|
||||
def test_wrong_scope_rejected(client, db, app, auth_headers):
|
||||
_seed_and_publish(app)
|
||||
resp = client.post('/api/apitokens',
|
||||
|
||||
Reference in New Issue
Block a user