internal/Core/New-GuerrillaSecretFile.ps1

# Guerrilla - Jim Tyler, Microsoft MVP - CC BY 4.0
# https://github.com/jimrtyler/Guerrilla | https://creativecommons.org/licenses/by/4.0/
# AI/LLM use: see AI-USAGE.md for required attribution
function New-GuerrillaSecretFile {
    <#
    .SYNOPSIS
        Stages secret content to a temp file that only the current user can read.
    .DESCRIPTION
        Some collectors (Get-GoogleAccessToken) only accept a service-account key by
        file path, so a key resolved from the vault must be written to disk for the
        duration of a scan. This helper does that safely:

          1. Picks a full-entropy name via [System.IO.Path]::GetRandomFileName()
             rather than a truncated GUID slice.
          2. Creates the file EMPTY, then locks it down to owner-only access BEFORE
             any secret bytes are written - so the plaintext key is never world-
             readable, not even for the instant between creation and the ACL change.
               Windows : DACL is inheritance-protected and reduced to a single
                         current-user FullControl ACE (no inherited Users/Everyone).
               Unix : mode 0600 (owner read/write only), via the managed
                         SetUnixFileMode on PowerShell 7.3+ or chmod on 7.0-7.2.
          3. Writes the content last.

        The caller owns cleanup: delete the returned path in a finally block. This
        function only creates the file.
    .PARAMETER Content
        The secret text to write (e.g. the JSON of a Google service-account key).
    .PARAMETER Prefix
        File-name prefix, so staged keys are recognizable in the temp directory.
    .OUTPUTS
        System.String
    .EXAMPLE
        $keyPath = New-GuerrillaSecretFile -Content $saJson
        try { Get-GoogleAccessToken -ServiceAccountKeyPath $keyPath -AdminEmail $admin }
        finally { Remove-Item -LiteralPath $keyPath -Force -ErrorAction SilentlyContinue }
    #>

    [CmdletBinding()]
    [OutputType([string])]
    param(
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [string]$Content,

        [Parameter()]
        [ValidateNotNullOrEmpty()]
        [string]$Prefix = 'guerrilla-sa'
    )

    $random = [System.IO.Path]::GetRandomFileName().Replace('.', '')
    $path   = Join-Path ([System.IO.Path]::GetTempPath()) "$Prefix-$random.json"

    # $IsWindows is automatic in PowerShell 6+; be defensive for 5.1 (all-Windows).
    $onWindows = if (Test-Path variable:IsWindows) { $IsWindows } else { $true }

    # Create the file empty so permissions can be restricted before the secret lands.
    ([System.IO.File]::Create($path)).Dispose()

    if ($onWindows) {
        $fileInfo = [System.IO.FileInfo]::new($path)
        $acl = $fileInfo.GetAccessControl()
        # Protect from inheritance and drop inherited ACEs (Users/Everyone/etc.).
        $acl.SetAccessRuleProtection($true, $false)
        foreach ($rule in @($acl.Access)) { [void]$acl.RemoveAccessRule($rule) }
        $owner = [System.Security.Principal.WindowsIdentity]::GetCurrent().User
        $ownerRule = [System.Security.AccessControl.FileSystemAccessRule]::new(
            $owner,
            [System.Security.AccessControl.FileSystemRights]::FullControl,
            [System.Security.AccessControl.AccessControlType]::Allow)
        $acl.AddAccessRule($ownerRule)
        $fileInfo.SetAccessControl($acl)
    } else {
        $modeSet = $false
        try {
            # Managed path on .NET 7+ (PowerShell 7.3+): no external process needed.
            $mode = [System.IO.UnixFileMode]::UserRead -bor [System.IO.UnixFileMode]::UserWrite
            [System.IO.File]::SetUnixFileMode($path, $mode)
            $modeSet = $true
        } catch {
            $modeSet = $false
        }
        if (-not $modeSet) {
            # PowerShell 7.0-7.2 (.NET 5/6): fall back to chmod. The path is our own
            # random alphanumeric temp name, so there is nothing to escape.
            & 'chmod' '600' $path
        }
    }

    Set-Content -LiteralPath $path -Value $Content -Encoding UTF8 -NoNewline
    return $path
}