Private/Get-TripwireStatePath.ps1

<#
.SYNOPSIS
    Where local state lives: the decoy inventory, the baseline, the licence, captures.

.DESCRIPTION
    XDG on Linux, %APPDATA% on Windows, ~/Library on macOS, overridable with
    $env:ENTRATRIPWIRE_CONFIG_DIR so tests and CI never touch a real profile.

    The decoy inventory is deliberately local rather than derived from the tenant. Two
    reasons, and both are the point of the tool: an inventory kept in the directory would
    be readable by exactly the attacker the decoys are meant to catch, and a decoy whose
    "is this a decoy?" answer lives in the directory can be filtered out of an enumeration
    by anyone who reads the same attribute.
#>

function Get-TripwireStateRoot {
    [CmdletBinding()]
    [OutputType([string])]
    param()

    if ($env:ENTRATRIPWIRE_CONFIG_DIR) { $base = $env:ENTRATRIPWIRE_CONFIG_DIR }
    elseif ($IsWindows) { $base = $env:APPDATA }
    elseif ($IsMacOS) { $base = Join-Path $HOME 'Library/Application Support' }
    else { $base = if ($env:XDG_CONFIG_HOME) { $env:XDG_CONFIG_HOME } else { Join-Path $HOME '.config' } }

    return Join-Path $base 'EntraTripwire'
}

<#
.SYNOPSIS
    Path to a named state file, creating the directory on demand.
#>

function Get-TripwireStatePath {
    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('decoys.json', 'baseline.json', 'license.json')]
        [string] $Name
    )

    $root = Get-TripwireStateRoot
    if (-not (Test-Path -LiteralPath $root)) {
        New-Item -ItemType Directory -Path $root -Force | Out-Null
    }
    return Join-Path $root $Name
}

<#
.SYNOPSIS
    Path to a file that ships inside the module, used by DEMO mode.
#>

function Get-TripwireDemoPath {
    [CmdletBinding()]
    [OutputType([string])]
    param([Parameter(Mandatory)] [string] $Name)

    return Join-Path (Join-Path (Split-Path -Parent $PSScriptRoot) 'Demo') $Name
}

<#
.SYNOPSIS
    Read a JSON state file, returning $null when it does not exist.
.DESCRIPTION
    A malformed state file throws rather than returning an empty object. An empty decoy
    inventory and an unreadable decoy inventory produce identical downstream behaviour --
    zero trips -- and only one of them means "nothing happened".
#>

function Read-TripwireJsonFile {
    [CmdletBinding()]
    param([Parameter(Mandatory)] [string] $Path)

    if (-not (Test-Path -LiteralPath $Path)) { return $null }
    $raw = Get-Content -LiteralPath $Path -Raw
    if ([string]::IsNullOrWhiteSpace($raw)) { return $null }
    try {
        return ($raw | ConvertFrom-Json)
    }
    catch {
        throw "'$Path' exists but is not valid JSON: $($_.Exception.Message). Refusing to continue with an unreadable state file."
    }
}