diff --git a/plugins/antigravity/scripts/env-file.ps1 b/plugins/antigravity/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/antigravity/scripts/env-file.ps1 +++ b/plugins/antigravity/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?ROGUE_API_KEY=["'']?[^"''\s]') { return $true } + } + return $false +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/antigravity/scripts/env-file.sh b/plugins/antigravity/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/antigravity/scripts/env-file.sh +++ b/plugins/antigravity/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/antigravity/scripts/setup.ps1 b/plugins/antigravity/scripts/setup.ps1 index 10e8c8a..60b9620 100644 --- a/plugins/antigravity/scripts/setup.ps1 +++ b/plugins/antigravity/scripts/setup.ps1 @@ -5,7 +5,7 @@ # # Usage: powershell -NoProfile -File setup.ps1 param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '' ) @@ -13,9 +13,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -RequireProtection -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/antigravity/scripts/setup.sh b/plugins/antigravity/scripts/setup.sh index f5d24c9..a1a3867 100644 --- a/plugins/antigravity/scripts/setup.sh +++ b/plugins/antigravity/scripts/setup.sh @@ -13,13 +13,24 @@ set -euo pipefail # 2) ${PLUGIN_ROOT}/env (bundled, for compiled customer plugins) # 3) ~/.rogue-env (per-user, written by this script) +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh }" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/antigravity/skills/setup/SKILL.md b/plugins/antigravity/skills/setup/SKILL.md index e13b91e..ff50787 100644 --- a/plugins/antigravity/skills/setup/SKILL.md +++ b/plugins/antigravity/skills/setup/SKILL.md @@ -11,6 +11,15 @@ Help the user set up their Rogue Security AIDR integration for Google Antigravit ## Step 1: Check existing configuration +Check the machine env file first: + +- macOS / Linux: `grep -q ROGUE_API_KEY /etc/rogue/env 2>/dev/null && echo machine || echo none` +- Windows: `if (Test-Path "$env:ProgramData\rogue\env") { Select-String -Path "$env:ProgramData\rogue\env" -Pattern ROGUE_API_KEY -Quiet } else { $false }` + +A machine env file that holds `ROGUE_API_KEY` and is owned by root (SYSTEM/Administrators on Windows) is the file the hooks read, alone, so credentials are already configured: say so and stop, without writing the user env file. + +Otherwise check the user env file: + - macOS / Linux: `test -f ~/.rogue-env && echo "exists" || echo "not found"` - Windows: `if (Test-Path "$env:USERPROFILE\.rogue-env") { 'exists' } else { 'not found' }` diff --git a/plugins/codex/commands/setup.md b/plugins/codex/commands/setup.md index d4db999..1445134 100644 --- a/plugins/codex/commands/setup.md +++ b/plugins/codex/commands/setup.md @@ -14,7 +14,9 @@ each step is shown after the bash block where it differs. ## Step 1: Check existing configuration -Check if `~/.rogue-env` exists with `test -f ~/.rogue-env && echo "exists" || echo "not found"`. +Check the machine env file first with `grep -q ROGUE_API_KEY /etc/rogue/env 2>/dev/null && echo machine || echo none` (Windows: `if (Test-Path "$env:ProgramData\rogue\env") { Select-String -Path "$env:ProgramData\rogue\env" -Pattern ROGUE_API_KEY -Quiet } else { $false }`). A machine env file that holds `ROGUE_API_KEY` and is owned by root (SYSTEM/Administrators on Windows) is the file the hooks read, alone, so credentials are already configured: say so and stop, without writing the user env file. + +Otherwise check if `~/.rogue-env` exists with `test -f ~/.rogue-env && echo "exists" || echo "not found"`. If already configured, tell the user and ask if they want to reconfigure. If not, continue. diff --git a/plugins/codex/scripts/env-file.ps1 b/plugins/codex/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/codex/scripts/env-file.ps1 +++ b/plugins/codex/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?ROGUE_API_KEY=["'']?[^"''\s]') { return $true } + } + return $false +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/codex/scripts/env-file.sh b/plugins/codex/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/codex/scripts/env-file.sh +++ b/plugins/codex/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/codex/scripts/setup.ps1 b/plugins/codex/scripts/setup.ps1 index 5d9b736..ae4d7be 100644 --- a/plugins/codex/scripts/setup.ps1 +++ b/plugins/codex/scripts/setup.ps1 @@ -5,7 +5,7 @@ # Usage: powershell -NoProfile -File setup.ps1 [surface] # surface: codex_app | codex_cli (default codex_cli) param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '', [string]$Surface = 'codex_cli' @@ -14,9 +14,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -RequireProtection -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/codex/scripts/setup.sh b/plugins/codex/scripts/setup.sh index c18202b..2513f10 100755 --- a/plugins/codex/scripts/setup.sh +++ b/plugins/codex/scripts/setup.sh @@ -14,14 +14,25 @@ set -euo pipefail # 2) ${PLUGIN_ROOT}/env (bundled, for compiled customer plugins) # 3) ~/.rogue-env (per-user, written by this script) +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh [surface]}" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" SURFACE="${4:-codex_cli}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/copilot/commands/setup.md b/plugins/copilot/commands/setup.md index fe9a373..99a51e1 100644 --- a/plugins/copilot/commands/setup.md +++ b/plugins/copilot/commands/setup.md @@ -14,7 +14,9 @@ each step is shown after the bash block where it differs. ## Step 1: Check existing configuration -Check if `~/.rogue-env` exists with `test -f ~/.rogue-env && echo "exists" || echo "not found"`. +Check the machine env file first with `grep -q ROGUE_API_KEY /etc/rogue/env 2>/dev/null && echo machine || echo none` (Windows: `if (Test-Path "$env:ProgramData\rogue\env") { Select-String -Path "$env:ProgramData\rogue\env" -Pattern ROGUE_API_KEY -Quiet } else { $false }`). A machine env file that holds `ROGUE_API_KEY` and is owned by root (SYSTEM/Administrators on Windows) is the file the hooks read, alone, so credentials are already configured: say so and stop, without writing the user env file. + +Otherwise check if `~/.rogue-env` exists with `test -f ~/.rogue-env && echo "exists" || echo "not found"`. If already configured, tell the user and ask if they want to reconfigure. If not, continue. diff --git a/plugins/copilot/scripts/env-file.ps1 b/plugins/copilot/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/copilot/scripts/env-file.ps1 +++ b/plugins/copilot/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?ROGUE_API_KEY=["'']?[^"''\s]') { return $true } + } + return $false +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/copilot/scripts/env-file.sh b/plugins/copilot/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/copilot/scripts/env-file.sh +++ b/plugins/copilot/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/copilot/scripts/setup.ps1 b/plugins/copilot/scripts/setup.ps1 index 1e99259..2e501c4 100644 --- a/plugins/copilot/scripts/setup.ps1 +++ b/plugins/copilot/scripts/setup.ps1 @@ -5,7 +5,7 @@ # # Usage: powershell -NoProfile -File setup.ps1 param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '' ) @@ -13,9 +13,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -RequireProtection -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/copilot/scripts/setup.sh b/plugins/copilot/scripts/setup.sh index 2d41aa2..744d5ce 100755 --- a/plugins/copilot/scripts/setup.sh +++ b/plugins/copilot/scripts/setup.sh @@ -13,13 +13,24 @@ set -euo pipefail # 2) ${PLUGIN_ROOT}/env (bundled, for compiled customer plugins) # 3) ~/.rogue-env (per-user, written by this script) +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh }" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/cursor/commands/setup.md b/plugins/cursor/commands/setup.md index c2eb5cf..e5c3668 100644 --- a/plugins/cursor/commands/setup.md +++ b/plugins/cursor/commands/setup.md @@ -11,6 +11,15 @@ Help the user set up their Rogue Security AIDR integration for Cursor. Follow th ## Step 1: Check existing configuration +Check the machine env file first: + +- macOS / Linux: `grep -q ROGUE_API_KEY /etc/rogue/env 2>/dev/null && echo machine || echo none` +- Windows: `if (Test-Path "$env:ProgramData\rogue\env") { Select-String -Path "$env:ProgramData\rogue\env" -Pattern ROGUE_API_KEY -Quiet } else { $false }` + +A machine env file that holds `ROGUE_API_KEY` and is owned by root (SYSTEM/Administrators on Windows) is the file the hooks read, alone, so credentials are already configured: say so and stop, without writing the user env file. + +Otherwise check the user env file: + - macOS / Linux: `test -f ~/.rogue-env && echo exists || echo missing` - Windows: `if (Test-Path "$env:USERPROFILE\.rogue-env") { 'exists' } else { 'missing' }` diff --git a/plugins/cursor/scripts/env-file.ps1 b/plugins/cursor/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/cursor/scripts/env-file.ps1 +++ b/plugins/cursor/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?ROGUE_API_KEY=["'']?[^"''\s]') { return $true } + } + return $false +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/cursor/scripts/env-file.sh b/plugins/cursor/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/cursor/scripts/env-file.sh +++ b/plugins/cursor/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/cursor/scripts/setup.ps1 b/plugins/cursor/scripts/setup.ps1 index 5d13e0c..f278980 100644 --- a/plugins/cursor/scripts/setup.ps1 +++ b/plugins/cursor/scripts/setup.ps1 @@ -4,7 +4,7 @@ # # Usage: powershell -NoProfile -File setup.ps1 param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '' ) @@ -12,9 +12,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/cursor/scripts/setup.sh b/plugins/cursor/scripts/setup.sh index 2e652e2..2e0d0a1 100755 --- a/plugins/cursor/scripts/setup.sh +++ b/plugins/cursor/scripts/setup.sh @@ -5,13 +5,24 @@ # Usage: setup.sh set -euo pipefail +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh }" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/gemini/commands/setup.toml b/plugins/gemini/commands/setup.toml index 1943165..2d2bc0f 100644 --- a/plugins/gemini/commands/setup.toml +++ b/plugins/gemini/commands/setup.toml @@ -6,6 +6,13 @@ You are helping the user set up the Rogue Security AIDR integration for Gemini C The extension is installed at `~/.gemini/extensions/rogue` (`%USERPROFILE%\\.gemini\\extensions\\rogue` on Windows). Its credential helper is `scripts/setup.mjs` there. ## Step 1: Check existing configuration +Check the machine env file first: +- macOS / Linux: `grep -q ROGUE_API_KEY /etc/rogue/env 2>/dev/null && echo machine || echo none` +- Windows: `if (Test-Path "$env:ProgramData\\rogue\\env") { Select-String -Path "$env:ProgramData\\rogue\\env" -Pattern ROGUE_API_KEY -Quiet } else { $false }` + +A machine env file that holds `ROGUE_API_KEY` and is owned by root (SYSTEM/Administrators on Windows) is the file the hooks read, alone, so credentials are already configured: say so and stop, without writing the user env file. + +Otherwise check the user env file: - macOS / Linux: `test -f ~/.rogue-env && echo exists || echo "not found"` - Windows: `if (Test-Path "$env:USERPROFILE\\.rogue-env") { 'exists' } else { 'not found' }` diff --git a/plugins/gemini/scripts/setup.mjs b/plugins/gemini/scripts/setup.mjs index 51a52d3..16d4a4d 100644 --- a/plugins/gemini/scripts/setup.mjs +++ b/plugins/gemini/scripts/setup.mjs @@ -9,10 +9,26 @@ // // Hooks read the first of these that holds ROGUE_API_KEY, alone: /etc/rogue/env // (C:\ProgramData\rogue\env on Windows) → /env → ~/.rogue-env (written here). +// A trusted machine file holding a key therefore makes this script a no-op. import fs from "node:fs"; import os from "node:os"; import path from "node:path"; +import { MACHINE_ENV_FILE, envFileHasKey, isTrustedEnvFile } from "./shared.mjs"; + +const HOME = os.homedir() || process.env.HOME || process.env.USERPROFILE || "."; +const ENV_FILE = path.join(HOME, ".rogue-env"); + +// A trusted machine env file holding a key is read ALONE by the hooks, so +// ENV_FILE written here would never be consulted. Nothing to do. +if (envFileHasKey(MACHINE_ENV_FILE) && isTrustedEnvFile(MACHINE_ENV_FILE)) { + process.stdout.write("OK\n"); + process.stdout.write(`ENV_FILE=${MACHINE_ENV_FILE}\n`); + process.stdout.write( + `Credentials come from the machine env file ${MACHINE_ENV_FILE} - ${ENV_FILE} not written\n`, + ); + process.exit(0); +} const [apiKey, actorEmail = "", actorName = ""] = process.argv.slice(2); if (!apiKey) { @@ -20,9 +36,6 @@ if (!apiKey) { process.exit(1); } -const HOME = os.homedir() || process.env.HOME || process.env.USERPROFILE || "."; -const ENV_FILE = path.join(HOME, ".rogue-env"); - const q = (s) => `'${String(s).replace(/'/g, "'\\''")}'`; const MANAGED = { diff --git a/plugins/gemini/scripts/shared.mjs b/plugins/gemini/scripts/shared.mjs index bdc4966..94bb547 100644 --- a/plugins/gemini/scripts/shared.mjs +++ b/plugins/gemini/scripts/shared.mjs @@ -34,6 +34,10 @@ export function shellUnquote(raw) { return v; } +// The MDM-managed machine file, named once so setup.mjs and the readers cannot +// disagree about which path is policy. +export const MACHINE_ENV_FILE = IS_WIN ? "C:\\ProgramData\\rogue\\env" : "/etc/rogue/env"; + // ── Credential resolution ──────────────────────────────────────────────────── // Only root or the current user may supply configuration, and nobody else may // write it; the machine file must be root's (env-file.sh's rule). Windows has no @@ -53,6 +57,17 @@ export function isTrustedEnvFile(file) { return (st.mode & 0o022) === 0; } +// A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty value. +export function envFileHasKey(file) { + try { + return /^[ \t]*(?:export[ \t]+)?ROGUE_API_KEY=["']?[^"'\s]/m.test( + fs.readFileSync(file, "utf8"), + ); + } catch { + return false; + } +} + // Same env-file rule as the other monorepo plugins: the first trusted file holding // ROGUE_API_KEY is used alone, and its values override the process env: // /etc/rogue/env (machine, MDM) → /env (bundled) → ~/.rogue-env (per-user) @@ -62,7 +77,7 @@ export function loadEnvFiles() { if (k.startsWith("ROGUE_") && process.env[k]) merged[k] = process.env[k]; } const files = [ - IS_WIN ? "C:\\ProgramData\\rogue\\env" : "/etc/rogue/env", + MACHINE_ENV_FILE, path.join(EXT_ROOT, "env"), path.join(HOME, ".rogue-env"), ]; diff --git a/plugins/kiro/scripts/env-file.ps1 b/plugins/kiro/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/kiro/scripts/env-file.ps1 +++ b/plugins/kiro/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?ROGUE_API_KEY=["'']?[^"''\s]') { return $true } + } + return $false +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/kiro/scripts/env-file.sh b/plugins/kiro/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/kiro/scripts/env-file.sh +++ b/plugins/kiro/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/rogue/scripts/env-file.ps1 b/plugins/rogue/scripts/env-file.ps1 index c24662e..9327279 100644 --- a/plugins/rogue/scripts/env-file.ps1 +++ b/plugins/rogue/scripts/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?ROGUE_API_KEY=["'']?[^"''\s]') { return $true } + } + return $false +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/plugins/rogue/scripts/env-file.sh b/plugins/rogue/scripts/env-file.sh index eb1b839..3c9bd1c 100644 --- a/plugins/rogue/scripts/env-file.sh +++ b/plugins/rogue/scripts/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/plugins/rogue/scripts/setup.ps1 b/plugins/rogue/scripts/setup.ps1 index e6e3a60..61f2fa3 100644 --- a/plugins/rogue/scripts/setup.ps1 +++ b/plugins/rogue/scripts/setup.ps1 @@ -4,7 +4,7 @@ # # Usage: powershell -NoProfile -File setup.ps1 param( - [Parameter(Mandatory = $true)][string]$ApiKey, + [string]$ApiKey = '', [string]$Email = '', [string]$Name = '' ) @@ -12,9 +12,24 @@ param( $ErrorActionPreference = 'Stop' $EnvFile = Join-Path $env:USERPROFILE '.rogue-env' +$MachineEnvFile = 'C:\ProgramData\rogue\env' . ([scriptblock]::Create((Get-Content -Raw -LiteralPath (Join-Path $PSScriptRoot 'env-file.ps1')))) +# A trusted machine env file holding a key is read ALONE by every dispatcher, +# so $EnvFile written here would never be consulted. Nothing to do. +if ((Test-RogueEnvFileHasKey $MachineEnvFile) -and (Test-RogueEnvFile $MachineEnvFile -System)) { + Write-Output "OK" + Write-Output "ENV_FILE=$MachineEnvFile" + Write-Output "Credentials come from the machine env file $MachineEnvFile - $EnvFile not written" + exit 0 +} + +if (-not $ApiKey) { + Write-Error 'Usage: setup.ps1 ' + exit 1 +} + $restricted = Write-RogueEnvFile -Path $EnvFile -Values ([ordered]@{ ROGUE_API_KEY = $ApiKey ROGUE_ACTOR_EMAIL = $Email diff --git a/plugins/rogue/scripts/setup.sh b/plugins/rogue/scripts/setup.sh index 2f27738..bf5a22b 100755 --- a/plugins/rogue/scripts/setup.sh +++ b/plugins/rogue/scripts/setup.sh @@ -12,13 +12,24 @@ set -euo pipefail # 2) ${CLAUDE_PLUGIN_ROOT}/env (bundled, for compiled customer plugins) # 3) ~/.rogue-env (per-user, written by this script) +ENV_FILE="$HOME/.rogue-env" +MACHINE_ENV_FILE="/etc/rogue/env" + +. "$(dirname "$0")/env-file.sh" + +# A trusted machine env file holding a key is read ALONE by every hook, so +# $ENV_FILE written here would never be consulted. Nothing to do. +if rogue_env_has_key "$MACHINE_ENV_FILE" && rogue_env_is_trusted "$MACHINE_ENV_FILE" 1; then + echo "OK" + echo "ENV_FILE=$MACHINE_ENV_FILE" + echo "Credentials come from the machine env file $MACHINE_ENV_FILE - $ENV_FILE not written" + exit 0 +fi + API_KEY="${1:?Usage: setup.sh }" ACTOR_EMAIL="${2:-}" ACTOR_NAME="${3:-}" -ENV_FILE="$HOME/.rogue-env" - -. "$(dirname "$0")/env-file.sh" rogue_write_env_file "$ENV_FILE" \ ROGUE_API_KEY "$API_KEY" \ ROGUE_ACTOR_EMAIL "$ACTOR_EMAIL" \ diff --git a/plugins/rogue/skills/setup/SKILL.md b/plugins/rogue/skills/setup/SKILL.md index 12fbd63..8fa7dce 100644 --- a/plugins/rogue/skills/setup/SKILL.md +++ b/plugins/rogue/skills/setup/SKILL.md @@ -11,6 +11,15 @@ Help the user set up their Rogue Security AIDR integration for Claude Code. Foll ## Step 1: Check existing configuration +Check the machine env file first: + +- macOS / Linux: `grep -q ROGUE_API_KEY /etc/rogue/env 2>/dev/null && echo machine || echo none` +- Windows: `if (Test-Path "$env:ProgramData\rogue\env") { Select-String -Path "$env:ProgramData\rogue\env" -Pattern ROGUE_API_KEY -Quiet } else { $false }` + +A machine env file that holds `ROGUE_API_KEY` and is owned by root (SYSTEM/Administrators on Windows) is the file the hooks read, alone, so credentials are already configured: say so and stop, without writing the user env file. + +Otherwise check the user env file: + - macOS / Linux: `test -f ~/.rogue-env && echo "exists" || echo "not found"` - Windows: `if (Test-Path "$env:USERPROFILE\.rogue-env") { 'exists' } else { 'not found' }` diff --git a/scripts/shared/env-file.ps1 b/scripts/shared/env-file.ps1 index c24662e..9327279 100644 --- a/scripts/shared/env-file.ps1 +++ b/scripts/shared/env-file.ps1 @@ -34,6 +34,19 @@ function Test-RogueEnvFile { } catch { return $false } } +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +function Test-RogueEnvFileHasKey { + param([string]$Path) + # Guard the read: -Encoding is a FileSystem-provider dynamic parameter, and a + # Windows machine path evaluated off Windows resolves to no provider at all. + if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) { return $false } + foreach ($line in (Get-Content -LiteralPath $Path -Encoding UTF8 -ErrorAction SilentlyContinue)) { + if ($line -match '^\s*(?:export\s+)?ROGUE_API_KEY=["'']?[^"''\s]') { return $true } + } + return $false +} + function Read-RogueEnvFile { param([string]$Path) if (Test-RogueEnvFile $Path -System:($Path -eq 'C:\ProgramData\rogue\env')) { diff --git a/scripts/shared/env-file.sh b/scripts/shared/env-file.sh index eb1b839..3c9bd1c 100644 --- a/scripts/shared/env-file.sh +++ b/scripts/shared/env-file.sh @@ -12,6 +12,12 @@ rogue_env_is_trusted() ( [ "$((0$mode & 022))" = 0 ] ) +# A candidate file "holds a key" when ROGUE_API_KEY is assigned a non-empty +# value - the same test every reader makes before selecting it. +rogue_env_has_key() { # rogue_env_has_key + [ -r "$1" ] && grep -Eq "^[[:space:]]*(export[[:space:]]+)?ROGUE_API_KEY=[\"']?[^\"'[:space:]]" "$1" +} + rogue_source_env() { if rogue_env_is_trusted "$1" "${2:-0}"; then . "$1" diff --git a/tests/test_setup_env.ps1 b/tests/test_setup_env.ps1 index a37b1f4..389e658 100644 --- a/tests/test_setup_env.ps1 +++ b/tests/test_setup_env.ps1 @@ -360,6 +360,149 @@ Check 'library: merge read fails loudly' $true ` ((Get-Content -Raw -LiteralPath (Join-Path $repo 'scripts/shared/env-file.ps1')) -match ` 'Get-Content -LiteralPath \$Path -Encoding UTF8 -ErrorAction Stop') +# -- The machine env file (FIRE-2135) ----------------------------------------- +# A trusted machine env file holding ROGUE_API_KEY is read ALONE by every +# dispatcher, so setup writes no user env file, names that file and exits 0. +# Each plugin's setup.ps1 runs from a COPY whose machine path literal points into +# the sandbox, beside the real env-file.ps1 it loads. Off Windows a `stat` shim on +# PATH reports the file as root-owned; on Windows its owner is set to +# Administrators - the only way to stage the machine candidate without root. +$machineRoot = Join-Path $sandbox 'machine' +New-Item -ItemType Directory -Path $machineRoot -Force | Out-Null +$MachineEnvFile = Join-Path $machineRoot 'machine-env' +$unix = $PSVersionTable.PSVersion.Major -ge 6 -and -not $IsWindows +$prevPath = $env:PATH +if ($unix) { + $mbin = Join-Path $machineRoot 'bin' + New-Item -ItemType Directory -Path $mbin -Force | Out-Null + $realStat = (Get-Command stat -CommandType Application | Select-Object -First 1).Source + [System.IO.File]::WriteAllText((Join-Path $mbin 'stat'), @" +#!/usr/bin/env bash +for a in "`$@"; do + if [ "`$a" = "$MachineEnvFile" ]; then + if "$realStat" --version >/dev/null 2>&1; then mode="`$("$realStat" -c %a "`$a")"; else mode="`$("$realStat" -f %Lp "`$a")"; fi + printf '%s %s\n' "`${ROGUE_TEST_MACHINE_OWNER:-0}" "`$mode"; exit 0 + fi +done +exec "$realStat" "`$@" +"@) + & chmod +x (Join-Path $mbin 'stat') + $env:PATH = "$mbin$([System.IO.Path]::PathSeparator)$env:PATH" +} + +# Trusted = owned by root/Administrators and writable by no one else; untrusted = +# the same file a standard user could rewrite, which would let them replace the +# key the MDM pushed. Recreated rather than overwritten: the previous call leaves +# an ACL this process may not be able to write through. +function Set-SetupMachineFile { + param([string]$Content, [switch]$Untrusted) + Remove-Item -LiteralPath $MachineEnvFile -Force -ErrorAction SilentlyContinue + [System.IO.File]::WriteAllText($MachineEnvFile, $Content) + if ($unix) { + if ($Untrusted) { & chmod 666 $MachineEnvFile } else { & chmod 644 $MachineEnvFile } + return + } + $admins = New-Object System.Security.Principal.SecurityIdentifier('S-1-5-32-544') + $me = [System.Security.Principal.WindowsIdentity]::GetCurrent().User + $rights = 'Read' + if ($Untrusted) { $rights = 'Read, Write' } + $acl = Get-Acl -LiteralPath $MachineEnvFile + $acl.SetAccessRuleProtection($true, $false) + $acl.SetOwner($admins) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($admins, 'FullControl', 'Allow'))) + $acl.AddAccessRule((New-Object System.Security.AccessControl.FileSystemAccessRule($me, $rights, 'Allow'))) + Set-Acl -LiteralPath $MachineEnvFile -AclObject $acl +} + +function New-RedirectedSetup { + param([string]$Plugin) + $dir = Join-Path $machineRoot (Join-Path $Plugin 'scripts') + New-Item -ItemType Directory -Path $dir -Force | Out-Null + Copy-Item -LiteralPath (Join-Path $repo "plugins/$Plugin/scripts/env-file.ps1") -Destination $dir + $src = [System.IO.File]::ReadAllText((Join-Path $repo "plugins/$Plugin/scripts/setup.ps1")) + $out = $src.Replace("`$MachineEnvFile = 'C:\ProgramData\rogue\env'", "`$MachineEnvFile = '$MachineEnvFile'") + Check "${Plugin}: machine path redirected for the test" $true ($out -ne $src) + $path = Join-Path $dir 'setup.ps1' + [System.IO.File]::WriteAllText($path, $out) + return $path +} + +# Invoke-Setup