Private/Get-MsecCachePath.ps1
|
function Get-MsecCachePath { <# .SYNOPSIS Path of a named on-disk cache used to make tab completion instant. .DESCRIPTION The module's only on-disk state. It exists for one reason: some parameters can only be completed from a list that lives in Azure, and a completer that calls Azure blocks the prompt on every Tab - worse, when ARM is unhealthy it does not fail fast, it hangs. So completers read these files and nothing else. Each cache is refreshed as a side effect of a call that already fetched the data, so keeping them warm costs no extra API calls. WHAT DOES AND DOES NOT BELONG HERE. Cache a list only when there is no correct local source for it: * subscriptions - Get-AzContext -ListAvailable looks like a free local answer and is not: it reflects what you have signed into, which drifts from what you can actually reach, in BOTH directions. Get-AzSubscription is authoritative, and it is a network call, so it gets cached. * graph-<type>-<name> - the result of a bundled Resource Graph query, e.g. graph-loganalytics-all. Written by every real call, read back by -UseCache and by completers. * directory-roles - the tenant's Entra role definitions, written by every Get-MsecEntraRoleHolder run so its -Role completer can offer real role names. There is no local source at all: the names are per-tenant, a tenant may rename a role, and custom roles exist nowhere but the directory. The curated built-in names in Get-MsecPrivilegedRoleTemplate cover only the privileged subset, and the completer falls back to them when this cache is cold. Nothing else currently qualifies. The -ResourceType and -Name completers read .kql files from the module folder: already local, already instant, and caching them would add staleness for no gain. Nothing sensitive is stored - names, ids, resource groups, locations. No keys, no tokens, no data. MSEC_CACHE_DIR overrides the directory. That exists so the test suite can redirect the whole cache to a temp folder: completers resolve their path through this function and there is otherwise no seam, so tests would have to write the real per-user cache and put it back. Backup-and-restore around files that several tests overwrite is the kind of thing that works until it doesn't - and when it doesn't it silently costs a developer their warm cache. Also useful for CI. Tests that mock Az and forget to redirect are contained rather than dangerous: their fake context yields a fake tenant, so they write to a folder of their own and cannot touch a real tenant's cache. Worth knowing before adding machinery to police it - an earlier guard test tried to, could not see transitive writes through the call graph, and was deleted once partitioning made the damage cosmetic. .PARAMETER Name Cache name, used as the file's base name. 'subscriptions', 'graph-loganalytics-all'. .OUTPUTS String. The full path. The file itself may not exist yet. #> [CmdletBinding()] param( [Parameter(Mandatory)] [ValidatePattern('^[a-z0-9-]+$')] [string] $Name ) # Tenant-scoped, one subfolder per tenant. Flipping context with Select-MsecAzureContext is # routine, and a single shared file would mean each flip discards the other tenant's cache # and overwrites it - so ping-ponging between two tenants leaves the cache permanently cold # and completion dead after every switch. Separate folders let both stay warm, and make # cross-tenant bleed impossible by construction rather than by a check on read. $tenantId = (Get-AzContext -ErrorAction SilentlyContinue).Tenant.Id if (-not $tenantId) { # Callers all treat a failure here as "no cache": Read-MsecCache returns empty, # Save-MsecCache logs to verbose, completers offer nothing. Without a context there is # nothing to query anyway. throw 'No Azure context, so there is no tenant to scope the cache to.' } $dir = $env:MSEC_CACHE_DIR if (-not $dir) { $base = [Environment]::GetFolderPath('LocalApplicationData') if (-not $base) { # GetFolderPath returns empty rather than throwing on minimal Linux containers where # neither XDG_DATA_HOME nor HOME is set the way .NET expects. $base = Join-Path ($HOME ?? [System.IO.Path]::GetTempPath()) '.local/share' } $dir = Join-Path $base 'msec' } Join-Path (Join-Path $dir $tenantId) "$Name.json" } |