103 lines
4.0 KiB
PowerShell
103 lines
4.0 KiB
PowerShell
|
|
<#
|
||
|
|
.SYNOPSIS
|
||
|
|
Fails if the rule engine has acquired a dependency it must not have.
|
||
|
|
|
||
|
|
.DESCRIPTION
|
||
|
|
Constitution Principle IV: the pure rule engine must not depend on Microsoft
|
||
|
|
Graph, authentication, Azure Automation, or console rendering. Directory
|
||
|
|
structure alone does not enforce that — one convenient call is all it takes to
|
||
|
|
make the engine untestable offline, and the failure is silent until someone
|
||
|
|
tries to run the tests without a tenant.
|
||
|
|
|
||
|
|
This check is the enforcement. It runs in CI (pipelines/validate.yml) and is
|
||
|
|
cheap enough to run locally on every change.
|
||
|
|
|
||
|
|
.EXAMPLE
|
||
|
|
./tests/Test-EnginePurity.ps1
|
||
|
|
#>
|
||
|
|
[CmdletBinding()]
|
||
|
|
param(
|
||
|
|
[string] $EnginePath = (Join-Path (Split-Path $PSScriptRoot -Parent) 'src/RuleEngine'),
|
||
|
|
[switch] $PassThru
|
||
|
|
)
|
||
|
|
|
||
|
|
$ErrorActionPreference = 'Stop'
|
||
|
|
|
||
|
|
$forbidden = @(
|
||
|
|
@{ Name = 'Microsoft Graph call'; Regex = 'Invoke-MgGraphRequest|Connect-MgGraph|graph\.microsoft\.com|Invoke-PersonaGraphRequest' }
|
||
|
|
@{ Name = 'Authentication layer'; Regex = 'Connect-Persona\w+' }
|
||
|
|
@{ Name = 'Data provider layer'; Regex = 'Get-Persona(Users|GroupMembership|DirectoryRoles)' }
|
||
|
|
@{ Name = 'Persistence layer'; Regex = 'Set-UserPersonaAttribute|New-PersonaWriteBody|Compare-PersonaValue' }
|
||
|
|
@{ Name = 'Console rendering'; Regex = 'Write-Host|Write-UserPersonaResult|Write-PersonaSummary' }
|
||
|
|
@{ Name = 'Direct HTTP'; Regex = 'Invoke-RestMethod|Invoke-WebRequest|System\.Net\.Http' }
|
||
|
|
@{ Name = 'Filesystem access'; Regex = 'Get-Content|Set-Content|Out-File|Export-Csv' }
|
||
|
|
@{ Name = 'Non-deterministic input'; Regex = 'Get-Random|Get-Date|\[datetime\]::(Now|UtcNow|Today)|New-Guid' }
|
||
|
|
)
|
||
|
|
|
||
|
|
if (-not (Test-Path $EnginePath)) {
|
||
|
|
Write-Host "Engine path '$EnginePath' does not exist yet - nothing to check." -ForegroundColor Yellow
|
||
|
|
if ($PassThru) { return @() }
|
||
|
|
exit 0
|
||
|
|
}
|
||
|
|
|
||
|
|
$findings = [System.Collections.Generic.List[object]]::new()
|
||
|
|
|
||
|
|
foreach ($file in Get-ChildItem -Path $EnginePath -Filter '*.ps1' -File -Recurse) {
|
||
|
|
|
||
|
|
# Tokenize rather than scan raw text. Comments in this codebase legitimately
|
||
|
|
# name the forbidden functions when explaining why the engine does not call
|
||
|
|
# them, and a text scan cannot tell a trailing comment from code. The parser
|
||
|
|
# can, exactly.
|
||
|
|
$tokens = $null
|
||
|
|
$parseErrors = $null
|
||
|
|
$null = [System.Management.Automation.Language.Parser]::ParseFile(
|
||
|
|
$file.FullName, [ref]$tokens, [ref]$parseErrors)
|
||
|
|
|
||
|
|
if ($parseErrors.Count -gt 0) {
|
||
|
|
$findings.Add([pscustomobject]@{
|
||
|
|
File = $file.Name
|
||
|
|
Line = $parseErrors[0].Extent.StartLineNumber
|
||
|
|
Dependency = 'Parse error'
|
||
|
|
Text = $parseErrors[0].Message
|
||
|
|
})
|
||
|
|
continue
|
||
|
|
}
|
||
|
|
|
||
|
|
# Rebuild each line from its non-comment tokens.
|
||
|
|
$codeByLine = @{}
|
||
|
|
foreach ($token in $tokens) {
|
||
|
|
if ($token.Kind -eq 'Comment') { continue }
|
||
|
|
|
||
|
|
$line = $token.Extent.StartLineNumber
|
||
|
|
if (-not $codeByLine.ContainsKey($line)) { $codeByLine[$line] = [System.Text.StringBuilder]::new() }
|
||
|
|
$null = $codeByLine[$line].Append($token.Text).Append(' ')
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($line in ($codeByLine.Keys | Sort-Object)) {
|
||
|
|
$code = $codeByLine[$line].ToString()
|
||
|
|
|
||
|
|
foreach ($rule in $forbidden) {
|
||
|
|
if ($code -match $rule.Regex) {
|
||
|
|
$findings.Add([pscustomobject]@{
|
||
|
|
File = $file.Name
|
||
|
|
Line = $line
|
||
|
|
Dependency = $rule.Name
|
||
|
|
Text = $code.Trim()
|
||
|
|
})
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($findings.Count -gt 0) {
|
||
|
|
Write-Host "Engine purity check FAILED - $($findings.Count) violation(s) of Principle IV:" -ForegroundColor Red
|
||
|
|
$findings | Format-Table -AutoSize | Out-String | Write-Host
|
||
|
|
Write-Host 'The rule engine must remain testable offline with synthetic data.' -ForegroundColor Red
|
||
|
|
if ($PassThru) { return $findings }
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
Write-Host 'Engine purity check passed: the rule engine has no forbidden dependencies.' -ForegroundColor Green
|
||
|
|
if ($PassThru) { return @() }
|
||
|
|
exit 0
|