Merge remote-tracking branch 'origin/main' into garrytan/gbrain-code-smell-audit

# Conflicts:
#	CHANGELOG.md
#	browse/test/dual-listener.test.ts
#	browse/test/fixtures/security-bench-haiku-responses.json
#	browse/test/sidebar-tabs.test.ts
#	browse/test/sidebar-ux.test.ts
#	browse/test/terminal-agent.test.ts
#	claude/SKILL.md.tmpl
#	scripts/gen-skill-docs.ts
#	scripts/proactive-suggestions.json
#	spec/SKILL.md
#	test/gen-skill-docs.test.ts
#	test/host-config.test.ts
This commit is contained in:
Garry Tan
2026-08-15 07:31:02 -07:00
246 changed files with 8802 additions and 1257 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ export const INJECTION_PATTERNS: readonly RegExp[] = [
/you\s+are\s+now\s+/i,
/always\s+output\s+no\s+findings/i,
/skip\s+(all\s+)?(security|review|checks)/i,
/override[:\s]/i,
/\boverride\s+(all\s+)?(previous|prior|above|the\s+(rules|instructions|system\s+prompt))/i,
/\bsystem\s*:/i,
/\bassistant\s*:/i,
/\buser\s*:/i,
+104 -2
View File
@@ -138,6 +138,36 @@ function looksLikeWallet(span: string): boolean {
return span.length >= 26 && span.length <= 62;
}
// Compact log/backup stamps (`20260727202423` = YYYYMMDDHHMMSS) are bare digit
// runs that the phone regex happily eats. Only a SEPARATOR-FREE 14-digit span
// qualifies: E.164 tops out at 15 digits and real numbers carry a + or spacing,
// so rejecting this shape costs no phone coverage.
function looksLikeCompactTimestamp(span: string): boolean {
if (!/^\d{14}$/.test(span)) {
return false;
}
const n = (from: number, to: number) => Number(span.slice(from, to));
const [year, month, day, hour, minute, second] = [
n(0, 4),
n(4, 6),
n(6, 8),
n(8, 10),
n(10, 12),
n(12, 14),
];
return (
year >= 1900 &&
year <= 2999 &&
month >= 1 &&
month <= 12 &&
day >= 1 &&
day <= 31 &&
hour <= 23 &&
minute <= 59 &&
second <= 59
);
}
// ── Placeholder suppression (per-matched-span, NOT per-line) ─────────────────
/**
@@ -174,6 +204,53 @@ export function isPlaceholderSpan(span: string): boolean {
return false;
}
/** Canonical 8-4-4-4-12 hex UUID. Global: a line may hold several. */
const UUID_RE = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}/g;
/** How far either side of a span to look for an enclosing UUID. A UUID is 36
* chars, so 40 covers one that starts immediately before the span. Bounded so
* this stays cheap on a multi-megabyte buffer. */
const UUID_CONTEXT_CHARS = 40;
/**
* True when the matched span sits ENTIRELY inside a UUID.
*
* Digit-only UUIDs — `00000000-0000-0000-0000-000000000000`,
* `11111111-1111-…` — are the standard fixture shape in test suites, and their
* digit runs collide with both the credit-card and phone patterns: a 16-digit
* slice of one is Luhn-valid often enough to matter, and the hyphen groups read
* as national phone formatting. Observed live: 14 of 21 MEDIUM findings on one
* ordinary branch were this, all from test files. That volume is what stops
* people reading MEDIUM output at all, so it costs real detection elsewhere.
*
* Containment must be TOTAL, deliberately. A span merely adjacent to or
* overlapping a UUID still reports — suppression is the exception, so it may
* only fire when the whole match is demonstrably UUID interior.
*
* Takes the match (not just the span) because the decision needs surrounding
* context; span offset is derived exactly as redact-engine.ts derives it, so
* the two cannot disagree about where the span begins.
*/
export function insideUuid(match: RegExpExecArray): boolean {
const input = match.input ?? "";
// Mirror the engine: capture group 1 when present, else the whole match.
const spanStartInMatch = match[1] !== undefined ? match[0].indexOf(match[1]) : 0;
const spanStart = match.index + Math.max(0, spanStartInMatch);
const spanEnd = spanStart + (match[1] ?? match[0]).length;
const from = Math.max(0, spanStart - UUID_CONTEXT_CHARS);
const window = input.slice(from, spanEnd + UUID_CONTEXT_CHARS);
UUID_RE.lastIndex = 0;
let u: RegExpExecArray | null;
while ((u = UUID_RE.exec(window)) !== null) {
const uuidStart = from + u.index;
const uuidEnd = uuidStart + u[0].length;
if (spanStart >= uuidStart && spanEnd <= uuidEnd) return true;
}
return false;
}
// ── The taxonomy ─────────────────────────────────────────────────────────────
export const PATTERNS: RedactPattern[] = [
@@ -327,6 +404,25 @@ export const PATTERNS: RedactPattern[] = [
nearRegex: /\bAC[a-f0-9]{32}\b/,
nearWindow: 200,
},
{
id: "google.oauth_client_secret",
tier: "HIGH",
category: "secret",
// Distinct from google.api_key (MEDIUM): an AIza key is often a public
// client key, but a GOCSPX- client secret is never publishable — leaking
// it lets anyone impersonate the OAuth app's token exchange.
description: "Google OAuth client secret (GOCSPX-…)",
regex: /\b(GOCSPX-[A-Za-z0-9_-]{20,40})(?![A-Za-z0-9_-])/,
validate: (span) => !isPlaceholderSpan(span),
},
{
id: "telegram.bot_token",
tier: "HIGH",
category: "secret",
description: "Telegram bot token (<bot-id>:AA…)",
regex: /\b([0-9]{6,16}:A[A-Za-z0-9_-]{34})(?![A-Za-z0-9_-])/,
validate: (span) => !isPlaceholderSpan(span),
},
{
id: "pem.private_key",
tier: "HIGH",
@@ -431,7 +527,11 @@ export const PATTERNS: RedactPattern[] = [
regex: /(?<![\w.])(\+?[1-9]\d{0,2}[ \-.]?\(?\d{2,4}\)?[ \-.]?\d{3,4}[ \-.]?\d{3,4})(?![\w.])/,
autoRedactable: true,
redactToken: "<REDACTED-PHONE>",
validate: (span) => span.replace(/\D/g, "").length >= 10,
// A digit-only UUID's hyphen groups read as national phone formatting.
validate: (span, match) =>
!insideUuid(match) &&
span.replace(/\D/g, "").length >= 10 &&
!looksLikeCompactTimestamp(span),
},
{
id: "pii.ssn",
@@ -455,7 +555,9 @@ export const PATTERNS: RedactPattern[] = [
regex: /\b((?:\d[ \-]?){13,19})\b/,
autoRedactable: true,
redactToken: "<REDACTED-CC>",
validate: (span) => luhnValid(span),
// A 13-19 digit slice of a digit-only UUID passes Luhn often enough to
// matter; the enclosing-UUID check runs first so it never reaches Luhn.
validate: (span, match) => !insideUuid(match) && luhnValid(span),
},
{
id: "pii.ip_public",