Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 27 additions & 9 deletions .github/workflows/copilot_credential_health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ jobs:
env:
PAT: ${{ secrets.PAT }}
run: |
set +x
set -euo pipefail
[[ -n "${PAT:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $PAT" -H 'Accept: application/vnd.github+json' https://api.github.com/user >/dev/null
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $PAT" -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/${GITHUB_REPOSITORY}" >/dev/null
[[ "$PAT" != *$'\r'* && "$PAT" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$PAT" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- --header 'Accept: application/vnd.github+json' https://api.github.com/user >/dev/null
printf 'Authorization: Bearer %s\n' "$PAT" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- --header 'Accept: application/vnd.github+json' "https://api.github.com/repos/${GITHUB_REPOSITORY}" >/dev/null

verify-openai:
if: ${{ inputs.check_openai }}
Expand All @@ -75,9 +77,11 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${OPENAI_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENAI_API_KEY" https://api.openai.com/v1/models >/dev/null
[[ "$OPENAI_API_KEY" != *$'\r'* && "$OPENAI_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$OPENAI_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://api.openai.com/v1/models >/dev/null

verify-anthropic:
if: ${{ inputs.check_anthropic }}
Expand All @@ -90,9 +94,11 @@ jobs:
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${ANTHROPIC_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' https://api.anthropic.com/v1/models >/dev/null
[[ "$ANTHROPIC_API_KEY" != *$'\r'* && "$ANTHROPIC_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'x-api-key: %s\n' "$ANTHROPIC_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- --header 'anthropic-version: 2023-06-01' https://api.anthropic.com/v1/models >/dev/null

verify-google:
if: ${{ inputs.check_google }}
Expand All @@ -105,9 +111,11 @@ jobs:
env:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${GOOGLE_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 "https://generativelanguage.googleapis.com/v1beta/models?key=$GOOGLE_API_KEY" >/dev/null
[[ "$GOOGLE_API_KEY" != *$'\r'* && "$GOOGLE_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'x-goog-api-key: %s\n' "$GOOGLE_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://generativelanguage.googleapis.com/v1beta/models >/dev/null

verify-openrouter:
if: ${{ inputs.check_openrouter }}
Expand All @@ -120,9 +128,11 @@ jobs:
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${OPENROUTER_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENROUTER_API_KEY" https://openrouter.ai/api/v1/models >/dev/null
[[ "$OPENROUTER_API_KEY" != *$'\r'* && "$OPENROUTER_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$OPENROUTER_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://openrouter.ai/api/v1/models >/dev/null

verify-cursor:
if: ${{ inputs.check_cursor }}
Expand All @@ -135,9 +145,13 @@ jobs:
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${CURSOR_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -u "$CURSOR_API_KEY:" 'https://api.cursor.com/analytics/ai-code/changes?startDate=30d&page=1&pageSize=1' >/dev/null
[[ "$CURSOR_API_KEY" != *$'\r'* && "$CURSOR_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
basic_auth="$(printf '%s:' "$CURSOR_API_KEY" | base64 | tr -d '\r\n')"
printf 'Authorization: Basic %s\n' "$basic_auth" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- 'https://api.cursor.com/analytics/ai-code/changes?startDate=30d&page=1&pageSize=1' >/dev/null
unset basic_auth

verify-opencode:
if: ${{ inputs.check_opencode }}
Expand All @@ -150,9 +164,11 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${OPENCODE_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENCODE_API_KEY" https://opencode.ai/zen/v1/models >/dev/null
[[ "$OPENCODE_API_KEY" != *$'\r'* && "$OPENCODE_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$OPENCODE_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://opencode.ai/zen/v1/models >/dev/null

verify-codex-api-key:
if: ${{ inputs.check_codex_api_key }}
Expand All @@ -165,6 +181,8 @@ jobs:
env:
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${CODEX_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $CODEX_API_KEY" https://api.openai.com/v1/models >/dev/null
[[ "$CODEX_API_KEY" != *$'\r'* && "$CODEX_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$CODEX_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://api.openai.com/v1/models >/dev/null
2 changes: 1 addition & 1 deletion build/cli/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -100913,7 +100913,7 @@ const MAX_WORKFLOW_PAGES = 5;
const MAX_WORKFLOW_FILE_CHECKS = 64;
// Hash of the packaged credential-health workflow: every job is guarded by a
// false-by-default input. A different remote file is never dispatched directly.
const TRUSTED_HEALTH_WORKFLOW_SHA256 = '7fa36bf72d144df6fe2ccc45b805ad442187aa6979dfda71446a54f607b18d61';
const TRUSTED_HEALTH_WORKFLOW_SHA256 = '5f093b2c03bef389ccea68ef43bec3577cb1bfddc038da1884a146088f39a4ec';
/** Dispatches a disabled-job branch override of a workflow already registered on default. */
async function probeActions(context) {
const preparationHttp = context.operatorHttp ?? context.http;
Expand Down
14 changes: 14 additions & 0 deletions docs/security-operations/security/credentials.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,17 @@ Temporary permission probes are separate disposable resources with collision
checks, private names and cleanup receipts; their final review warns that GitHub
Secret writes are upserts without an atomic create-only guarantee. Pending
cleanup stops installation. Revoke the temporary operator PAT yourself when done.

The opt-in credential health workflow sends authentication headers to curl over
stdin. Raw keys and Basic authentication values stay out of process arguments
and request URLs. Only the selected check receives its named Secret. Empty or
multiline values fail before a request, and HTTPS requests reject redirects so
custom provider headers cannot follow them to another origin. Response bodies
are discarded; existing job names, flags and 20-second request limits remain.

If a check reports `Credential contains invalid line breaks.`, replace that
named Secret with its single-line value and explicitly rerun the check. If its
canonical endpoint starts redirecting, review the provider endpoint before
updating the workflow. Installations receive this correction by updating their
managed `copilot_credential_health.yml` through the reviewed setup upgrade; a
merge in Copilot alone does not replace existing consumer workflows.
36 changes: 27 additions & 9 deletions setup/workflows/copilot_credential_health.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,12 @@ jobs:
env:
PAT: ${{ secrets.PAT }}
run: |
set +x
set -euo pipefail
[[ -n "${PAT:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $PAT" -H 'Accept: application/vnd.github+json' https://api.github.com/user >/dev/null
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $PAT" -H 'Accept: application/vnd.github+json' "https://api.github.com/repos/${GITHUB_REPOSITORY}" >/dev/null
[[ "$PAT" != *$'\r'* && "$PAT" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$PAT" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- --header 'Accept: application/vnd.github+json' https://api.github.com/user >/dev/null
printf 'Authorization: Bearer %s\n' "$PAT" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- --header 'Accept: application/vnd.github+json' "https://api.github.com/repos/${GITHUB_REPOSITORY}" >/dev/null

verify-openai:
if: ${{ inputs.check_openai }}
Expand All @@ -75,9 +77,11 @@ jobs:
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${OPENAI_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENAI_API_KEY" https://api.openai.com/v1/models >/dev/null
[[ "$OPENAI_API_KEY" != *$'\r'* && "$OPENAI_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$OPENAI_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://api.openai.com/v1/models >/dev/null

verify-anthropic:
if: ${{ inputs.check_anthropic }}
Expand All @@ -90,9 +94,11 @@ jobs:
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${ANTHROPIC_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "x-api-key: $ANTHROPIC_API_KEY" -H 'anthropic-version: 2023-06-01' https://api.anthropic.com/v1/models >/dev/null
[[ "$ANTHROPIC_API_KEY" != *$'\r'* && "$ANTHROPIC_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'x-api-key: %s\n' "$ANTHROPIC_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- --header 'anthropic-version: 2023-06-01' https://api.anthropic.com/v1/models >/dev/null

verify-google:
if: ${{ inputs.check_google }}
Expand All @@ -105,9 +111,11 @@ jobs:
env:
GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${GOOGLE_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 "https://generativelanguage.googleapis.com/v1beta/models?key=$GOOGLE_API_KEY" >/dev/null
[[ "$GOOGLE_API_KEY" != *$'\r'* && "$GOOGLE_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'x-goog-api-key: %s\n' "$GOOGLE_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://generativelanguage.googleapis.com/v1beta/models >/dev/null

verify-openrouter:
if: ${{ inputs.check_openrouter }}
Expand All @@ -120,9 +128,11 @@ jobs:
env:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${OPENROUTER_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENROUTER_API_KEY" https://openrouter.ai/api/v1/models >/dev/null
[[ "$OPENROUTER_API_KEY" != *$'\r'* && "$OPENROUTER_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$OPENROUTER_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://openrouter.ai/api/v1/models >/dev/null

verify-cursor:
if: ${{ inputs.check_cursor }}
Expand All @@ -135,9 +145,13 @@ jobs:
env:
CURSOR_API_KEY: ${{ secrets.CURSOR_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${CURSOR_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -u "$CURSOR_API_KEY:" 'https://api.cursor.com/analytics/ai-code/changes?startDate=30d&page=1&pageSize=1' >/dev/null
[[ "$CURSOR_API_KEY" != *$'\r'* && "$CURSOR_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
basic_auth="$(printf '%s:' "$CURSOR_API_KEY" | base64 | tr -d '\r\n')"
printf 'Authorization: Basic %s\n' "$basic_auth" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- 'https://api.cursor.com/analytics/ai-code/changes?startDate=30d&page=1&pageSize=1' >/dev/null
unset basic_auth

verify-opencode:
if: ${{ inputs.check_opencode }}
Expand All @@ -150,9 +164,11 @@ jobs:
env:
OPENCODE_API_KEY: ${{ secrets.OPENCODE_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${OPENCODE_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $OPENCODE_API_KEY" https://opencode.ai/zen/v1/models >/dev/null
[[ "$OPENCODE_API_KEY" != *$'\r'* && "$OPENCODE_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$OPENCODE_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://opencode.ai/zen/v1/models >/dev/null

verify-codex-api-key:
if: ${{ inputs.check_codex_api_key }}
Expand All @@ -165,6 +181,8 @@ jobs:
env:
CODEX_API_KEY: ${{ secrets.CODEX_API_KEY }}
run: |
set +x
set -euo pipefail
[[ -n "${CODEX_API_KEY:-}" ]] || { echo 'Required credential is missing.' >&2; exit 1; }
curl --fail --silent --show-error --location --max-time 20 -H "Authorization: Bearer $CODEX_API_KEY" https://api.openai.com/v1/models >/dev/null
[[ "$CODEX_API_KEY" != *$'\r'* && "$CODEX_API_KEY" != *$'\n'* ]] || { echo 'Credential contains invalid line breaks.' >&2; exit 1; }
printf 'Authorization: Bearer %s\n' "$CODEX_API_KEY" | curl -q --fail --silent --show-error --location --max-redirs 0 --proto '=https' --proto-redir '=https' --max-time 20 --header @- https://api.openai.com/v1/models >/dev/null
Loading
Loading