mirror of
https://github.com/garrytan/gstack.git
synced 2026-11-13 20:07:08 +01:00
fix: keep the gbrain database URL off argv; redact more credential formats (GST-1747) (#4114)
* fix: keep the gbrain database URL off argv; redact more credential formats (GST-1747) /setup-gbrain runs gstack-gbrain-source-wireup without --database-url; the helper reads ~/.gbrain/config.json once at startup and pins the URL. A new guard test keeps setup-gbrain docs from passing the URL on argv. lib/redact-patterns.ts adds HIGH patterns for OpenRouter keys, GitHub App ghu_/ghr_ tokens, Stripe live restricted keys, Supabase sb_secret_ and sbp_ tokens, and matches temporary ASIA AWS key IDs in aws.access_key. Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * chore: name the changelog fragment after the branch (GST-1747) Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: pre-landing review fixes Clear GBRAIN_DATABASE_URL and DATABASE_URL for the setup-gbrain source wireup call so the helper pins the ~/.gbrain/config.json URL, as the --database-url flag did before. Guard test added; changelog fragment scoped to the wireup step. Review-Finding: p1/specialist-batched#1 reader=security Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> * fix: match full-length GitHub refresh tokens in the redaction gate github.refresh now accepts a body of 36 or more characters, so the 76-character refresh tokens GitHub issues match and are masked whole. Test samples use a 76-character body; the 35-character near-miss stays negative. Review-Finding: p3/adversarial-native#1 reader=adversarial-native Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com> --------- Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
This commit is contained in:
1 parent
0dbc60708e
commit
e75a69c9e1
7 files changed
+150
-30
No files matched your search
@@ -19,6 +19,8 @@ const SAMPLES: Record<string, string> = {
|
||||
"github.pat": j("ghp_", "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8"),
|
||||
"github.oauth": j("gho_", "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8"),
|
||||
"github.server": j("ghs_", "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8"),
|
||||
"github.user_to_server": j("ghu_", "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8"),
|
||||
"github.refresh": j("ghr_", "a1B2c3D4e5F6g7H8i9J0".repeat(3), "k1L2m3N4o5P6q7R8"),
|
||||
"github.fine_grained": j("github_pat_", "a1B2c3D4e5F6g7H8i9J0".repeat(4), "kL"),
|
||||
"gitlab.token": j("glpat-", "a1B2c3D4e5F6g7H8i9J0"),
|
||||
"groq.key": j("gsk_", "a1B2c3D4e5F6g7H8i9J0k1L2"),
|
||||
@@ -31,7 +33,11 @@ const SAMPLES: Record<string, string> = {
|
||||
"anthropic.key": j("sk-ant-", "api03-a1B2c3D4e5F6g7H8i9J0k1L2"),
|
||||
"openai.key": j("sk-proj-", "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V2w3X4"),
|
||||
"sendgrid.key": j("SG.", "a1B2c3D4e5F6g7H8i9J0k1", ".", "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V"),
|
||||
"openrouter.key": j("sk-or-v1-", hex32, hex32),
|
||||
"stripe.secret": j("sk_live_", "a1B2c3D4e5F6g7H8i9J0k1L2"),
|
||||
"stripe.restricted": j("rk_live_", "a1B2c3D4e5F6g7H8i9J0k1L2"),
|
||||
"supabase.secret": j("sb_secret_", "tR4i6cyMIrYTE3s6OyHGHw_ppx2p6WK"),
|
||||
"supabase.pat": j("sbp_", hex32, "9f8e7d6c"),
|
||||
"slack.token": j("xoxb-", "12345", "67890-a1B2c3D4e5F6"),
|
||||
"slack.webhook": j("https://hooks.slack.com/services/", "T0ABCDEFG/B0ABCDEFG/", "a1B2c3D4e5F6g7H8i9J0k1L2"),
|
||||
"discord.webhook": j("https://discord.com/api/webhooks/", "123456789012345678/", "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0u1V2w3X4y5Z6a7B8c9D0e1F2"),
|
||||
|
||||
@@ -62,6 +62,13 @@ describe("HIGH credential patterns", () => {
|
||||
],
|
||||
["google.oauth_client_secret", 'client_secret: "GOCSPX-' + "Ab3xQ9zLmNp2RtVw7YkD1sHf" + '"'],
|
||||
["telegram.bot_token", "TELEGRAM_TOKEN=8326208591:AA" + "HdqRy9Lm2ZpXvKb4NcQw8TuEr6YoP1sVg"],
|
||||
["aws.access_key", "temporary key ASIA" + "ZQ3XN7P2LM4RT8WV"],
|
||||
["github.user_to_server", "ghu_" + "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8"],
|
||||
["github.refresh", "ghr_" + "a1B2c3D4e5F6g7H8i9J0".repeat(3) + "k1L2m3N4o5P6q7R8"],
|
||||
["openrouter.key", "OPENROUTER sk-or-v1-" + "9f8e7d6c5b4a39281706f5e4d3c2b1a0".repeat(2)],
|
||||
["stripe.restricted", "rk_live_" + "a1B2c3D4e5F6g7H8i9J0k1L2"],
|
||||
["supabase.secret", "sb_secret_" + "tR4i6cyMIrYTE3s6OyHGHw_ppx2p6WK"],
|
||||
["supabase.pat", "sbp_" + "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c"],
|
||||
];
|
||||
for (const [id, text] of cases) {
|
||||
test(`flags ${id}`, () => {
|
||||
@@ -463,6 +470,20 @@ describe("#1946 pattern negatives (placeholders never fire)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("OpenRouter, GitHub ghu_/ghr_, Stripe restricted, Supabase and AWS temporary key negatives", () => {
|
||||
test("near-miss shapes never fire", () => {
|
||||
expect(ids("sk-or-v1-" + "9f8e7d6c5b4a39281706f5e4d3c2b1a0")).not.toContain("openrouter.key");
|
||||
expect(ids("ghu_" + "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R")).not.toContain("github.user_to_server");
|
||||
expect(ids("ghr_" + "a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R")).not.toContain("github.refresh");
|
||||
expect(ids("rk_test_" + "a1B2c3D4e5F6g7H8i9J0k1L2")).not.toContain("stripe.restricted");
|
||||
expect(ids("sb_publishable_" + "tR4i6cyMIrYTE3s6OyHGHw_ppx2p6WK")).not.toContain("supabase.secret");
|
||||
expect(ids("sb_secret_key")).not.toContain("supabase.secret");
|
||||
expect(ids("sbp_" + "9f8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6")).not.toContain("supabase.pat");
|
||||
expect(ids("sbp_" + "Zf8e7d6c5b4a39281706f5e4d3c2b1a09f8e7d6c")).not.toContain("supabase.pat");
|
||||
expect(ids("ASIA" + "ZQ3XN7P2LM4RT8W")).not.toContain("aws.access_key");
|
||||
});
|
||||
});
|
||||
|
||||
describe("google.oauth_client_secret / telegram.bot_token negatives", () => {
|
||||
test("undersized and placeholder shapes never fire", () => {
|
||||
// Length floor keeps short repo fixtures quiet (e.g. the 19-char body in
|
||||
@@ -749,6 +770,12 @@ describe("redactFindingSpans — machine-egress masking (#1947)", () => {
|
||||
expect(out).toBe("auth <REDACTED-github.pat> rejected");
|
||||
});
|
||||
|
||||
test("a 76-character GitHub refresh token is masked whole", () => {
|
||||
const token = "ghr_" + "a1B2c3D4e5F6g7H8i9J0".repeat(3) + "k1L2m3N4o5P6q7R8";
|
||||
const out = redactFindingSpans(`auth ${token} rejected`, { repoVisibility: "private" });
|
||||
expect(out).toBe("auth <REDACTED-github.refresh> rejected");
|
||||
});
|
||||
|
||||
test("multiple findings are all replaced (right-to-left splice keeps offsets valid)", () => {
|
||||
const pat = "ghp_" + "1234567890abcdefghijklmnopqrstuvwxyz";
|
||||
const aws = "AKIA1234567890ABCDEF";
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// setup-gbrain keeps the gbrain database URL off the process command line.
|
||||
//
|
||||
// The URL carries the Postgres password. A value passed as an argument is
|
||||
// readable by every local user through `ps` for as long as the process runs,
|
||||
// so gstack passes credentials through env or a config file, never argv (the
|
||||
// same rule lib/gbrain-supabase-provision.ts states for DB_PASS). The wireup
|
||||
// helper already reads ~/.gbrain/config.json once at startup and pins it for
|
||||
// every child gbrain call (covered in test/gstack-gbrain-source-wireup.test.ts),
|
||||
// so no skill doc needs to hand it the URL.
|
||||
|
||||
import { describe, test, expect } from 'bun:test';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
const ROOT = path.resolve(import.meta.dir, '..');
|
||||
const SKILL_DIR = path.join(ROOT, 'setup-gbrain');
|
||||
const SECTIONS_DIR = path.join(SKILL_DIR, 'sections');
|
||||
|
||||
const docs: Array<[string, string]> = [
|
||||
'SKILL.md.tmpl',
|
||||
'SKILL.md',
|
||||
...fs
|
||||
.readdirSync(SECTIONS_DIR)
|
||||
.filter((f) => f.endsWith('.md.tmpl') || f.endsWith('.md'))
|
||||
.sort()
|
||||
.map((f) => path.join('sections', f)),
|
||||
].map((rel) => [rel, fs.readFileSync(path.join(SKILL_DIR, rel), 'utf-8')]);
|
||||
|
||||
// One shell command: the helper name plus any backslash-continued lines.
|
||||
const wireupCommand = /gstack-gbrain-source-wireup\b(?:[^\n]*\\\r?\n)*[^\n]*/g;
|
||||
|
||||
describe('setup-gbrain passes no database URL on argv', () => {
|
||||
test('the wireup command is still documented (guard is not vacuous)', () => {
|
||||
const invocations = docs.flatMap(([, text]) => text.match(wireupCommand) ?? []);
|
||||
expect(invocations.some((cmd) => cmd.includes('--strict'))).toBe(true);
|
||||
});
|
||||
|
||||
test('no gstack-gbrain-source-wireup invocation carries --database-url', () => {
|
||||
const offenders = docs.flatMap(([rel, text]) =>
|
||||
(text.match(wireupCommand) ?? []).filter((cmd) => cmd.includes('--database-url')).map((cmd) => `${rel}: ${cmd}`),
|
||||
);
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
|
||||
test('the wireup command clears ambient database URLs so config.json wins', () => {
|
||||
const lines = docs.flatMap(([rel, text]) =>
|
||||
text.split(/\r?\n/).filter((l) => /bin\/gstack-gbrain-source-wireup --strict/.test(l)).map((l) => `${rel}: ${l.trim()}`),
|
||||
);
|
||||
expect(lines.length).toBeGreaterThan(0);
|
||||
const offenders = lines.filter((l) => !/: GBRAIN_DATABASE_URL= DATABASE_URL= ~\/\.claude\//.test(l));
|
||||
expect(offenders).toEqual([]);
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user