Skip to content

Harden the export sanitizer after adversarial review - #6

Merged
aappuupatell merged 1 commit into
mainfrom
feat/repo-structure
Oct 18, 2026
Merged

aappuupatell merged 1 commit into
mainfrom
feat/repo-structure

Conversation

@aappuupatell

Copy link
Copy Markdown
Owner

What this changes

PR #1 merged the first version of ConvertTo-SanitizedIntuneExport.ps1. Three independent reviewers then ran that version against realistic Graph and admin-center export shapes and leak-path fixtures, and confirmed several defects by execution. This PR carries the single commit that fixes all of them, with a regression test for each.

Serializer

  • Escape by code point. A switch on a [char] compares culture-aware, so soft hyphens, zero-width characters and control characters were silently written as \b.
  • Fractional numbers and negative zero are normalized; Local DateTime values are written as UTC so output does not depend on the host time zone.

Plumbing

  • Top-level arrays no longer unroll (a one-element array became an object, [] became null).
  • Relative -OutputPath and -NameMapPath resolve against PowerShell's location instead of the process directory.
  • Name-map values are validated so a display name cannot break the <GROUP:...> placeholder or smuggle an address into it.

Coverage

  • Secrets: *SecretSettingValue values, encrypted OMA-URI values and secretReferenceValueId, and password / preSharedKey / sharedSecret / *Token keys.
  • Composite assignment ids and sourceId; <nav>@odata.context annotations.
  • Tenant ids learned from tenantId keys (dashed and dash-less forms).
  • GUIDs under any *Id, group, user, device, app or filter key, and inside /groups/, /users/, /directoryObjects/ URLs; ADMX definition ids are kept.
  • Guest (#EXT#), %40-encoded and non-ASCII UPNs; .mail.onmicrosoft.com; SharePoint hosts and /personal/ paths; MAC addresses, UNC hosts, .local / .corp / .internal / .lan hostnames, RFC 1918 addresses.
  • Device names, user display names, product keys, thumbprints, base64 payloads and certificates.
  • Object keys are sanitized too. A ReviewHints list in the per-file summary points at lines that still carry a GUID or IPv4 for a human to read.

Guard (.github/scripts/Test-RepoSanitized.ps1)

  • Stops flagging JSON keys such as definition@odata.bind as e-mail addresses.
  • Catches secrets in JSON form and under Wi-Fi / VPN key names.
  • Matches guest, encoded and non-ASCII UPNs; no longer trusts admin@ on a real domain; checks the real label of .mail routing domains.

Type of change

  • Documentation (docs/, runbooks/, examples/)
  • Policy artifact (artifacts/)
  • Script (scripts/)
  • Remediation pair (remediations/)
  • Repo tooling / CI
  • Other:

Platform and licensing

Runs anywhere PowerShell 5.1 or 7 runs. No Intune licensing dependency; the script is offline and never connects to Graph.

Testing

Synthetic exports only, on PowerShell 7.6 (Linux): 160 Pester tests pass (31 for the sanitizer, 25 for the guard, the rest structure checks). Still not run against a real tenant export; the .NOTES block says so. Windows PowerShell 5.1 compatibility is by construction (no 7-only syntax, two-argument Join-Path, parameter probing for ConvertFrom-Json), not by execution.

Checklist

  • Sanitized. Fixtures build every identifier at run time; nothing in the diff looks like a real tenant, group, or user.
  • Artifact + doc. Not applicable: tooling change, documented in the script's comment-based help and CONTRIBUTING.md.
  • Rollback documented. Revert the commit; the script has no persistent state.
  • "Does not touch" section. The .NOTES block lists the known gaps the script cannot recognise.
  • Exit codes. Not applicable: the script throws on bad input and never calls exit.
  • Lint clean. Invoke-ScriptAnalyzer -Path . -Recurse -Settings ./PSScriptAnalyzerSettings.psd1 reports nothing.
  • Tested in a non-production tenant. Untested against a real export.

Generated by Claude Code

Three independent reviewers ran the script against realistic Graph and
portal export shapes and leak-path fixtures. Every confirmed finding is
fixed and covered by a test:

Serializer
- escape by code point: soft hyphens, zero-width and control characters
  were written as \b because a switch on [char] compares culture-aware
- fractional numbers and negative zero normalized; Local DateTime values
  written as UTC so output does not depend on the host time zone

Plumbing
- top-level arrays no longer unroll (one-element arrays became objects,
  [] became null)
- relative -OutputPath and -NameMapPath resolve against PowerShell's
  location instead of the process directory
- name map values are validated so a display name cannot break the
  <GROUP:...> placeholder or smuggle an address into it

Coverage
- secrets: *SecretSettingValue values, encrypted OMA-URI values and
  secretReferenceValueId, password/preSharedKey/sharedSecret/*Token keys
- composite assignment ids and sourceId, <nav>@odata.context annotations
- tenant ids learned from tenantId keys (dashed and dash-less forms)
- GUIDs under any *Id, group, user, device, app or filter key, and inside
  /groups/, /users/, /directoryObjects/ URLs; ADMX definition ids kept
- guest (#EXT#), %40-encoded and non-ASCII UPNs; .mail.onmicrosoft.com;
  SharePoint hosts and /personal/ paths; MAC addresses, UNC hosts,
  .local/.corp/.internal/.lan hostnames, RFC 1918 addresses
- device names, user display names, product keys, thumbprints, base64
  payloads and certificates
- object keys are sanitized too; a ReviewHints list in the summary points
  at lines that still carry a GUID or IPv4 for a human to read

Guard
- stops flagging JSON keys such as definition@odata.bind as addresses
- catches secrets in JSON form and under Wi-Fi/VPN key names
- matches guest, encoded and non-ASCII UPNs; no longer trusts admin@ on
  a real domain; checks the real label of .mail routing domains

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019NLBetJRtHG4ZgUfj37xGn
@aappuupatell
aappuupatell merged commit 9b3270a into main Oct 18, 2026
6 checks passed
@aappuupatell
aappuupatell deleted the feat/repo-structure branch October 18, 2026 19:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants