diff --git a/scripts/setup-github-datasource.sh b/scripts/setup-github-datasource.sh index 6d7a2cefd7..17daee5ce5 100755 --- a/scripts/setup-github-datasource.sh +++ b/scripts/setup-github-datasource.sh @@ -20,10 +20,22 @@ set -euo pipefail GRAFANA_URL="${GRAFANA_URL:-http://localhost:3000}" -[ -f .env ] && { set -a; . ./.env; set +a; } +[ -f .env ] && . ./.env : "${GITHUB_TOKEN:?Set GITHUB_TOKEN (a read-only fine-grained PAT) in the environment or .env}" : "${GRAFANA_ADMIN_PASSWORD:?Set GRAFANA_ADMIN_PASSWORD in the environment or .env}" -AUTH="admin:${GRAFANA_ADMIN_PASSWORD}" + +TMP_DIR="$(mktemp -d)" +NETRC_FILE="$TMP_DIR/netrc" +trap 'rm -rf "$TMP_DIR"' EXIT +GRAFANA_HOSTPORT="${GRAFANA_URL#*://}" +GRAFANA_HOSTPORT="${GRAFANA_HOSTPORT%%/*}" +GRAFANA_HOST="${GRAFANA_HOSTPORT%%:*}" +printf 'machine %s login %s password %s\n' "$GRAFANA_HOST" admin "$GRAFANA_ADMIN_PASSWORD" >"$NETRC_FILE" +chmod 600 "$NETRC_FILE" + +grafana_curl() { + env -u GRAFANA_ADMIN_PASSWORD -u GITHUB_TOKEN curl -sf --netrc-file "$NETRC_FILE" "$@" +} payload() { cat </dev/null 2>&1; then +if grafana_curl "$GRAFANA_URL/api/datasources/uid/github" >/dev/null 2>&1; then echo "Updating existing GitHub data source…" - curl -sf -u "$AUTH" -H 'content-type: application/json' -X PUT \ - "$GRAFANA_URL/api/datasources/uid/github" -d "$(payload)" >/dev/null + payload | grafana_curl -H 'content-type: application/json' -X PUT \ + "$GRAFANA_URL/api/datasources/uid/github" --data-binary @- >/dev/null else echo "Creating GitHub data source…" - curl -sf -u "$AUTH" -H 'content-type: application/json' -X POST \ - "$GRAFANA_URL/api/datasources" -d "$(payload)" >/dev/null + payload | grafana_curl -H 'content-type: application/json' -X POST \ + "$GRAFANA_URL/api/datasources" --data-binary @- >/dev/null fi echo "Done. Verifying health…" -curl -sf -u "$AUTH" -X POST "$GRAFANA_URL/api/datasources/uid/github/health" 2>/dev/null \ +grafana_curl -X POST "$GRAFANA_URL/api/datasources/uid/github/health" 2>/dev/null \ | grep -q '"status":"OK"' && echo "✓ GitHub data source healthy" || echo "⚠ Added, but health check did not return OK — verify the token scopes." diff --git a/scripts/setup-sentry-datasource.sh b/scripts/setup-sentry-datasource.sh index 7408311be1..327a5d556b 100755 --- a/scripts/setup-sentry-datasource.sh +++ b/scripts/setup-sentry-datasource.sh @@ -27,14 +27,26 @@ set -euo pipefail GRAFANA_URL="${GRAFANA_URL:-http://localhost:3000}" -[ -f .env ] && { set -a; . ./.env; set +a; } +[ -f .env ] && . ./.env : "${SENTRY_API_TOKEN:?Set SENTRY_API_TOKEN (a Sentry Internal Integration token, NOT your SENTRY_DSN) in the environment or .env}" : "${SENTRY_ORG_SLUG:?Set SENTRY_ORG_SLUG (your Sentry organization slug) in the environment or .env}" : "${GRAFANA_ADMIN_PASSWORD:?Set GRAFANA_ADMIN_PASSWORD in the environment or .env}" -AUTH="admin:${GRAFANA_ADMIN_PASSWORD}" # https://sentry.io for Sentry SaaS; override for a self-hosted Sentry instance. SENTRY_API_URL="${SENTRY_API_URL:-https://sentry.io}" +TMP_DIR="$(mktemp -d)" +NETRC_FILE="$TMP_DIR/netrc" +trap 'rm -rf "$TMP_DIR"' EXIT +GRAFANA_HOSTPORT="${GRAFANA_URL#*://}" +GRAFANA_HOSTPORT="${GRAFANA_HOSTPORT%%/*}" +GRAFANA_HOST="${GRAFANA_HOSTPORT%%:*}" +printf 'machine %s login %s password %s\n' "$GRAFANA_HOST" admin "$GRAFANA_ADMIN_PASSWORD" >"$NETRC_FILE" +chmod 600 "$NETRC_FILE" + +grafana_curl() { + env -u GRAFANA_ADMIN_PASSWORD -u SENTRY_API_TOKEN curl -sf --netrc-file "$NETRC_FILE" "$@" +} + payload() { cat </dev/null 2>&1; then +if grafana_curl "$GRAFANA_URL/api/datasources/uid/sentry" >/dev/null 2>&1; then echo "Updating existing Sentry data source…" - curl -sf -u "$AUTH" -H 'content-type: application/json' -X PUT \ - "$GRAFANA_URL/api/datasources/uid/sentry" -d "$(payload)" >/dev/null + payload | grafana_curl -H 'content-type: application/json' -X PUT \ + "$GRAFANA_URL/api/datasources/uid/sentry" --data-binary @- >/dev/null else echo "Creating Sentry data source…" - curl -sf -u "$AUTH" -H 'content-type: application/json' -X POST \ - "$GRAFANA_URL/api/datasources" -d "$(payload)" >/dev/null + payload | grafana_curl -H 'content-type: application/json' -X POST \ + "$GRAFANA_URL/api/datasources" --data-binary @- >/dev/null fi echo "Done. Verifying health…" -curl -sf -u "$AUTH" -X POST "$GRAFANA_URL/api/datasources/uid/sentry/health" 2>/dev/null \ +grafana_curl -X POST "$GRAFANA_URL/api/datasources/uid/sentry/health" 2>/dev/null \ | grep -q '"status":"OK"' && echo "✓ Sentry data source healthy" || echo "⚠ Added, but health check did not return OK — verify SENTRY_API_TOKEN's scopes and SENTRY_ORG_SLUG." diff --git a/test/unit/selfhost-grafana-github-datasource.test.ts b/test/unit/selfhost-grafana-github-datasource.test.ts new file mode 100644 index 0000000000..5c448bc8d0 --- /dev/null +++ b/test/unit/selfhost-grafana-github-datasource.test.ts @@ -0,0 +1,43 @@ +import { readFileSync, statSync } from "node:fs"; +import { join } from "node:path"; +import { describe, expect, it } from "vitest"; + +// scripts/setup-github-datasource.sh had no dedicated coverage before this (only referenced in passing by +// selfhost-grafana-sentry-datasource.test.ts's own comparison assertions). Mirrors that file's credential-safety +// test: this script shares the exact same GRAFANA_ADMIN_PASSWORD-via-curl-argv/env leak this PR fixes. +describe("scripts/setup-github-datasource.sh", () => { + function readScript(): string { + return readFileSync(join(process.cwd(), "scripts/setup-github-datasource.sh"), "utf8"); + } + + it("is idempotent (update-vs-create) and ships a health check, matching setup-sentry-datasource.sh's own shape", () => { + const script = readScript(); + + expect(script).toContain("GITHUB_TOKEN"); + expect(script).toContain("grafana-github-datasource"); + expect(script).toContain("api/datasources/uid/github"); + expect(script).toMatch(/-X PUT/); + expect(script).toMatch(/-X POST/); + expect(script).toContain("secureJsonData"); + expect(script).toContain("accessToken"); + expect(script).toContain("/health"); + }); + + it("keeps GitHub and Grafana credentials out of curl argv and child environments", () => { + const script = readScript(); + + expect(script).not.toContain("set -a"); + expect(script).not.toContain('AUTH="admin:${GRAFANA_ADMIN_PASSWORD}"'); + expect(script).not.toContain('-u "$AUTH"'); + expect(script).not.toContain('-d "$(payload)"'); + expect(script).toContain('--netrc-file "$NETRC_FILE"'); + expect(script).toContain('--data-binary @-'); + expect(script).toMatch(/env -u GRAFANA_ADMIN_PASSWORD -u GITHUB_TOKEN curl/); + }); + + it("is executable", () => { + const mode = statSync(join(process.cwd(), "scripts/setup-github-datasource.sh")).mode; + // Owner-execute bit (0o100). + expect(mode & 0o100).not.toBe(0); + }); +}); diff --git a/test/unit/selfhost-grafana-sentry-datasource.test.ts b/test/unit/selfhost-grafana-sentry-datasource.test.ts index eb7a18ed1f..17ba7db64a 100644 --- a/test/unit/selfhost-grafana-sentry-datasource.test.ts +++ b/test/unit/selfhost-grafana-sentry-datasource.test.ts @@ -75,6 +75,18 @@ describe("Grafana Sentry data source (#5369)", () => { expect(script).toContain("/health"); }); + it("keeps Sentry and Grafana credentials out of curl argv and child environments", () => { + const script = readFileSync(join(process.cwd(), "scripts/setup-sentry-datasource.sh"), "utf8"); + + expect(script).not.toContain("set -a"); + expect(script).not.toContain('AUTH="admin:${GRAFANA_ADMIN_PASSWORD}"'); + expect(script).not.toContain('-u "$AUTH"'); + expect(script).not.toContain('-d "$(payload)"'); + expect(script).toContain('--netrc-file "$NETRC_FILE"'); + expect(script).toContain('--data-binary @-'); + expect(script).toMatch(/env -u GRAFANA_ADMIN_PASSWORD -u SENTRY_API_TOKEN curl/); + }); + it("setup-sentry-datasource.sh is executable, matching setup-github-datasource.sh's own mode", () => { const mode = statSync(join(process.cwd(), "scripts/setup-sentry-datasource.sh")).mode; // Owner-execute bit (0o100).