param([Parameter(Mandatory = $true)][string]$WebRoot) Set-StrictMode -Version 3.0 $ErrorActionPreference = 'Stop' $root = [IO.Path]::GetFullPath($WebRoot) $indexPath = Join-Path $root 'index.html' if (-not (Test-Path -LiteralPath $indexPath -PathType Leaf)) { throw "Web index not found: $indexPath" } $rootPrefix = $root.TrimEnd('\') + '\' $html = Get-Content -LiteralPath $indexPath -Raw $references = [regex]::Matches($html, '(?i)(?:src|href)\s*=\s*["''](?[^"'']+)["'']') $checked = 0 foreach ($match in $references) { $assetReference = $match.Groups['path'].Value.Trim() if (-not $assetReference -or $assetReference.StartsWith('//') -or $assetReference -match '^[a-z][a-z0-9+.-]*:') { continue } $assetPath = ($assetReference -split '[?#]', 2)[0] if ([IO.Path]::GetExtension($assetPath).ToLowerInvariant() -notin @('.js', '.css')) { continue } $relative = [Uri]::UnescapeDataString($assetPath).TrimStart('/').Replace('/', '\') if (-not $relative) { throw "Web index contains an empty local asset path: $assetReference" } $resolved = [IO.Path]::GetFullPath((Join-Path $root $relative)) if (-not $resolved.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) { throw "Web index asset escapes the web root: $assetReference" } if (-not (Test-Path -LiteralPath $resolved -PathType Leaf)) { throw "Web index references missing local asset: $assetReference" } $checked++ } if ($checked -eq 0) { throw 'Web index does not reference any local JavaScript or CSS assets.' } Write-Host "Sense web asset audit passed: $checked local references."