Public/Add-NSPClientToken.ps1

function Add-NSPClientToken {
    <#
    .SYNOPSIS
        Adds client identifiers to this user's token list.
    .DESCRIPTION
        Tokens are what Find-NSPClientReference searches for: client names and abbreviations,
        domains, tenant names and IDs, object IDs, user and group names. Give them directly with
        -Token, or harvest them from local JSON exports (inventories, profiles) with -FromJson.

        -FromJson walks every object in the files and takes the string values (or lists of strings)
        of the properties named in -Property, plus every e-mail address anywhere. Properties named in
        -ExcludeProperty are skipped with everything under them (use it for Microsoft catalogs
        such as role templates, which are not client data).

        For a domain or e-mail address, its organization label is added too: acme-widgets.com adds
        acme-widgets, and contoso.onmicrosoft.com adds contoso. Tokens shorter than -MinimumLength, and
        tokens matching an -ExcludePattern, are skipped. The list is de-duplicated, ignoring case.
    .EXAMPLE
        Add-NSPClientToken -Token 'Acme', 'Acme Widgets' -Source 'client name'
    .EXAMPLE
        Add-NSPClientToken -FromJson (Get-ChildItem .\exports -Filter *.json -Recurse).FullName -ExcludeProperty RoleTemplates
    #>

    [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSReviewUnusedParameter', '', Justification = 'Used by the nested helpers, which the analyzer does not follow.')]
    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Token')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'Token', ValueFromPipeline, Position = 0)]
        [AllowEmptyString()][string[]]$Token,

        [Parameter(Mandatory, ParameterSetName = 'Json')]
        [string[]]$FromJson,

        [Parameter(ParameterSetName = 'Json')]
        [string[]]$Property = @('id', 'tenantId', 'displayName', 'userPrincipalName', 'mail', 'name', 'domain', 'tenantDomain', 'cidrAddress', 'onPremisesSamAccountName', 'companyName'),

        [Parameter(ParameterSetName = 'Json')]
        [string[]]$ExcludeProperty = @(),

        [string]$Source,
        [string[]]$ExcludePattern = @(),
        [ValidateRange(1, 100)][int]$MinimumLength = 4,
        [string]$Path = (Get-NSPClientTokenPath)
    )

    begin {
        $collected = [System.Collections.Generic.List[object]]::new()
        $mailProviders = @('gmail', 'outlook', 'hotmail', 'yahoo', 'icloud', 'live', 'microsoft', 'onmicrosoft', 'example', 'contoso', 'fabrikam')
        $emailPattern = '^[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,})$'
        $domainPattern = '^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$'

        function Add-One([string]$Value, [string]$From) {
            if ($null -eq $Value) { return }
            $v = $Value.Trim()
            if ($v.Length -lt $MinimumLength) { return }
            foreach ($pattern in $ExcludePattern) { if ($v -match $pattern) { return } }
            $collected.Add([pscustomobject]@{ Token = $v; Source = $From })
            # The organization label of a domain: the part before .onmicrosoft.com, or before the suffix.
            $domain = if ($v -match $emailPattern) { $Matches[1] } elseif ($v -match $domainPattern) { $v } else { $null }
            if ($domain) {
                $labels = $domain.ToLowerInvariant().Split('.')
                $label = if ($domain -match '\.onmicrosoft\.com$') { $labels[0] } else { $labels[$labels.Count - 2] }
                if ($label.Length -ge $MinimumLength -and $label -notin $mailProviders) {
                    $skip = $false
                    foreach ($pattern in $ExcludePattern) { if ($label -match $pattern) { $skip = $true } }
                    if (-not $skip) { $collected.Add([pscustomobject]@{ Token = $label; Source = $From }) }
                }
            }
        }

        function Get-JsonValue($Node, [string]$From) {
            if ($null -eq $Node) { return }
            if ($Node -is [string]) {
                if ($Node -match $emailPattern) { Add-One $Node $From }
                return
            }
            if ($Node -is [System.Collections.IEnumerable] -and $Node -isnot [System.Collections.IDictionary]) {
                foreach ($item in $Node) { Get-JsonValue $item $From }
                return
            }
            if ($Node -is [System.Management.Automation.PSCustomObject]) {
                foreach ($p in $Node.PSObject.Properties) {
                    if ($p.Name -in $ExcludeProperty) { continue }
                    if ($p.Name -in $Property) {
                        # A named property may hold one string or a list of them (a profile's Ids).
                        foreach ($v in @($p.Value)) { if ($v -is [string]) { Add-One $v $From } }
                    }
                    Get-JsonValue $p.Value $From
                }
            }
        }
    }

    process {
        if ($PSCmdlet.ParameterSetName -eq 'Token') {
            $from = if ($Source) { $Source } else { 'manual' }
            foreach ($t in $Token) { Add-One $t $from }
        }
    }

    end {
        if ($PSCmdlet.ParameterSetName -eq 'Json') {
            foreach ($file in $FromJson) {
                $from = if ($Source) { $Source } else { [IO.Path]::GetFileName($file) }
                $json = Get-Content -LiteralPath $file -Raw | ConvertFrom-Json
                Get-JsonValue $json $from
            }
        }

        $existing = @(Get-NSPClientToken -Path $Path)
        $known = @{}
        foreach ($row in $existing) { $known[$row.Token.ToLowerInvariant()] = $true }
        $new = [System.Collections.Generic.List[object]]::new()
        foreach ($row in $collected) {
            $key = $row.Token.ToLowerInvariant()
            if ($known.ContainsKey($key)) { continue }
            $known[$key] = $true
            $new.Add($row)
        }

        if ($new.Count -and $PSCmdlet.ShouldProcess($Path, "Add $($new.Count) client token(s)")) {
            $folder = Split-Path -Parent $Path
            if ($folder -and -not (Test-Path -LiteralPath $folder)) { New-Item -ItemType Directory -Path $folder -Force | Out-Null }
            @($existing) + @($new) | Sort-Object Source, Token | Export-Csv -LiteralPath $Path -NoTypeInformation -Encoding UTF8
        }
        [pscustomobject]@{ Path = $Path; Added = $new.Count; Total = $existing.Count + $new.Count }
    }
}