Skip to content

Commit f03d34e

Browse files
committed
v0.7.99 follow-up: add Pii-Guard.Tests.ps1 - block accidental PII in Tests/
The Tests/ folder ships in the public GitHub repo. This guard scans every file under Tests/ on each Pester run and fails the build if it finds: - Email addresses (anything matching local@domain.tld) - .onmicrosoft.com tenant UPN domains - GUIDs in identity contexts (tenantId / clientId / objectId / principalId / applicationId), excluding zero-GUID + explicit allow-list - Real public IPv4 addresses (excluding RFC1918 / loopback / link-local / CGNAT / RFC5737 doc / multicast / subnet-mask shapes / well-known public DNS like 8.8.8.8) Allow-list seeded with the existing synthetic fixtures the project owner has already confirmed are safe (sub ID fbaf508b..., RunId add1f87d..., action-plan ID 1084e062...). Add new tokens only after the same review. Also: rename the SbeVersion test fixture from "4.1.2.0" to "4.1.2.0-Marker" so it matches the existing "4.5.6.7-RegressionMarker" non-IP shape and the guard's word-boundary correctly excludes it. The BS8 schema test only checks property names, so the value change is inert. Pester: 830 passed / 0 failed / 1 skipped (baseline 824 + 6 new It blocks).
1 parent 9a09567 commit f03d34e

2 files changed

Lines changed: 173 additions & 1 deletion

File tree

AzLocal.UpdateManagement/Tests/AzLocal.UpdateManagement.Tests.ps1

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9497,7 +9497,7 @@ Describe 'Function: Get-AzLocalFleetHealthOverview - v0.7.70 (ARG-first fleet he
94979497
HealthStatus = 'Healthy'
94989498
UpdateStatus = 'AppliedSuccessfully'
94999499
CurrentVersion = '12.2503.0'
9500-
SbeVersion = '4.1.2.0'
9500+
SbeVersion = '4.1.2.0-Marker'
95019501
AzureConnection = 'Connected'
95029502
LastChecked = '2026-05-16T08:00:00Z'
95039503
HealthResultsAgeDays = 0
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#Requires -Module Pester
2+
<#
3+
.SYNOPSIS
4+
Repo-hygiene guard: fail the build if any file under Tests\ contains
5+
accidentally-added PII (real GUIDs in identity contexts, real public
6+
IPv4 addresses, real-looking email addresses, or AAD UPNs).
7+
8+
.DESCRIPTION
9+
The Tests\ folder ships in the public GitHub repo. This guard scans
10+
every file under Tests\ for patterns that would leak customer or
11+
corporate identifiers, and asserts every hit is on the known-safe
12+
allow-list. Add new fixture values to the allow-list ONLY after
13+
confirming they are synthetic.
14+
15+
What the guard checks (per file, all hits aggregated):
16+
- Email addresses (anything matching local@domain.tld where the
17+
local part is at least 3 chars).
18+
- Public IPv4 addresses (anything NOT in RFC1918, loopback,
19+
link-local, RFC5737 doc ranges, multicast, or the well-known
20+
public-DNS allow-list). Tokens that look like four-part
21+
version strings (e.g. SbeVersion 4.5.6.7-RegressionMarker)
22+
are excluded by the trailing word-boundary.
23+
- GUIDs appearing in identity contexts (tenantId, clientId,
24+
objectId, principalId, applicationId), excluding the zero-GUID
25+
and the explicit allow-list below.
26+
- AAD UPNs / domain names (anything ending in .onmicrosoft.com).
27+
28+
What the guard DOES NOT check:
29+
- Subscription IDs in resource paths (these are not credentials
30+
and the project owner has confirmed they are acceptable in
31+
public fixtures).
32+
- Run IDs / action-plan IDs in error-message strings (same
33+
rationale; already on the allow-list below).
34+
35+
To extend the allow-list, add the literal token to the matching
36+
$allowed* collection in BeforeAll. To extend the BLOCK list, add
37+
a new aggregator loop + Should assertion in the matching It below.
38+
39+
.NOTES
40+
Author: Neil Bird, Microsoft.
41+
Added: v0.7.99
42+
#>
43+
44+
Describe 'PII Guard: Tests folder' {
45+
46+
BeforeAll {
47+
$script:TestsRoot = $PSScriptRoot
48+
$script:GuardFileName = 'Pii-Guard.Tests.ps1'
49+
50+
$script:AllowedGuids = @(
51+
'00000000-0000-0000-0000-000000000000',
52+
'fbaf508b-cb61-4383-9cda-a42bfa0c7bc9',
53+
'add1f87d-4174-4997-ae39-d9d41088be27',
54+
'1084e062-5d0b-48c0-b4d6-c1693b575bc1'
55+
)
56+
57+
$script:AllowedEmailDomains = @(
58+
'example.com', 'example.org', 'example.net',
59+
'contoso.com', 'fabrikam.com',
60+
'noreply.github.com', 'users.noreply.github.com'
61+
)
62+
63+
$script:AllowedPublicIPs = @(
64+
'8.8.8.8', '8.8.4.4',
65+
'1.1.1.1', '1.0.0.1',
66+
'9.9.9.9', '149.112.112.112',
67+
'208.67.222.222', '208.67.220.220'
68+
)
69+
70+
$script:TargetFiles = Get-ChildItem -Path $script:TestsRoot -File -Recurse |
71+
Where-Object { $_.Name -ne $script:GuardFileName -and $_.Extension -in @('.ps1','.psm1','.psd1','.json','.md','.xml','.yml','.yaml','.csv') }
72+
}
73+
74+
It 'enumerates at least one target file' {
75+
($script:TargetFiles | Measure-Object).Count | Should -BeGreaterThan 0
76+
}
77+
78+
It 'allow-listed GUID list contains zero-GUID sentinel' {
79+
$script:AllowedGuids | Should -Contain '00000000-0000-0000-0000-000000000000'
80+
}
81+
82+
It 'finds no real-looking email addresses' {
83+
$emailRx = '(?<![A-Za-z0-9._%+-])[A-Za-z0-9._%+-]{3,}@[A-Za-z0-9.-]+\.[A-Za-z]{2,24}\b'
84+
$unsafe = New-Object 'System.Collections.Generic.List[string]'
85+
foreach ($f in $script:TargetFiles) {
86+
$text = [System.IO.File]::ReadAllText($f.FullName)
87+
foreach ($m in [regex]::Matches($text, $emailRx)) {
88+
$addr = $m.Value
89+
$dom = ($addr -split '@')[1].ToLowerInvariant()
90+
$ok = $false
91+
foreach ($d in $script:AllowedEmailDomains) {
92+
if ($dom -eq $d -or $dom.EndsWith(".$d")) { $ok = $true; break }
93+
}
94+
if (-not $ok) { $unsafe.Add("$($f.Name): $addr") }
95+
}
96+
}
97+
$unsafe | Should -BeNullOrEmpty -Because "found email-shaped tokens not on the allow-list: $($unsafe -join '; ')"
98+
}
99+
100+
It 'finds no real-looking AAD tenant UPN domains (.onmicrosoft.com)' {
101+
$unsafe = New-Object 'System.Collections.Generic.List[string]'
102+
foreach ($f in $script:TargetFiles) {
103+
$text = [System.IO.File]::ReadAllText($f.FullName)
104+
foreach ($m in [regex]::Matches($text, '\b[A-Za-z0-9-]+\.onmicrosoft\.com\b')) {
105+
$upnDomain = $m.Value
106+
if ($upnDomain -notmatch '^(contoso|fabrikam|example)\.onmicrosoft\.com$') {
107+
$unsafe.Add("$($f.Name): $upnDomain")
108+
}
109+
}
110+
}
111+
$unsafe | Should -BeNullOrEmpty -Because "found tenant UPN domains not on the allow-list: $($unsafe -join '; ')"
112+
}
113+
114+
It 'finds no GUIDs in identity contexts (tenantId/clientId/objectId/principalId/applicationId)' {
115+
$idCtxRx = '(?i)\b(?:tenant|client|object|principal|application)[-_ ]?id\b\s*[:=]\s*["'']?(?<g>[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})'
116+
$unsafe = New-Object 'System.Collections.Generic.List[string]'
117+
foreach ($f in $script:TargetFiles) {
118+
$text = [System.IO.File]::ReadAllText($f.FullName)
119+
foreach ($m in [regex]::Matches($text, $idCtxRx)) {
120+
$g = $m.Groups['g'].Value.ToLowerInvariant()
121+
if ($script:AllowedGuids -notcontains $g) {
122+
$unsafe.Add("$($f.Name): $g")
123+
}
124+
}
125+
}
126+
$unsafe | Should -BeNullOrEmpty -Because "found GUIDs in identity contexts not on the allow-list: $($unsafe -join '; ')"
127+
}
128+
129+
It 'finds no real-looking public IPv4 addresses' {
130+
# Dotted-quad with word-boundaries. The trailing lookahead excludes
131+
# tokens immediately followed by another digit, dot, hyphen, or
132+
# letter, which removes version-string false-positives like
133+
# '4.5.6.7-RegressionMarker' or SemVer 'X.Y.Z.W-suffix'.
134+
$ipRx = '(?<![\w.])(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})(?:\.(?:25[0-5]|2[0-4]\d|1\d\d|\d{1,2})){3}(?![\d.\-A-Za-z])'
135+
136+
$unsafe = New-Object 'System.Collections.Generic.List[string]'
137+
foreach ($f in $script:TargetFiles) {
138+
$text = [System.IO.File]::ReadAllText($f.FullName)
139+
$seen = New-Object 'System.Collections.Generic.HashSet[string]'
140+
foreach ($m in [regex]::Matches($text, $ipRx)) {
141+
$ip = $m.Value
142+
if (-not $seen.Add($ip)) { continue }
143+
if ($script:AllowedPublicIPs -contains $ip) { continue }
144+
145+
$octets = $ip -split '\.' | ForEach-Object { [int]$_ }
146+
$o1 = $octets[0]; $o2 = $octets[1]; $o3 = $octets[2]
147+
148+
if ($o1 -eq 0) { continue } # 0.0.0.0/8
149+
if ($o1 -eq 10) { continue } # RFC1918
150+
if ($o1 -eq 172 -and $o2 -ge 16 -and $o2 -le 31) { continue } # RFC1918
151+
if ($o1 -eq 192 -and $o2 -eq 168) { continue } # RFC1918
152+
if ($o1 -eq 127) { continue } # loopback
153+
if ($o1 -eq 169 -and $o2 -eq 254) { continue } # link-local
154+
if ($o1 -eq 100 -and $o2 -ge 64 -and $o2 -le 127){ continue } # CGNAT
155+
if ($o1 -ge 224) { continue } # multicast / reserved
156+
if ($o1 -eq 255) { continue } # broadcast / mask
157+
if ($o1 -eq 192 -and $o2 -eq 0 -and $o3 -eq 2) { continue } # RFC5737 TEST-NET-1
158+
if ($o1 -eq 198 -and $o2 -eq 51 -and $o3 -eq 100){ continue } # RFC5737 TEST-NET-2
159+
if ($o1 -eq 203 -and $o2 -eq 0 -and $o3 -eq 113){ continue } # RFC5737 TEST-NET-3
160+
if ($o1 -eq 198 -and ($o2 -eq 18 -or $o2 -eq 19)) { continue } # RFC2544 benchmarking
161+
162+
# Subnet-mask shapes (every octet is 0 or 255).
163+
$maskShape = $true
164+
foreach ($o in $octets) { if ($o -ne 0 -and $o -ne 255) { $maskShape = $false; break } }
165+
if ($maskShape) { continue }
166+
167+
$unsafe.Add("$($f.Name): $ip")
168+
}
169+
}
170+
$unsafe | Should -BeNullOrEmpty -Because "found public IPv4 addresses not on the allow-list: $($unsafe -join '; ')"
171+
}
172+
}

0 commit comments

Comments
 (0)