diff --git a/AGENTS.md b/AGENTS.md index f197eca1..ba6a4368 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -193,7 +193,7 @@ Layered prompt-injection / approval-fatigue defenses. Full reference: [docs/SECU - **git config code execution** (`internal/danger/classifier.go`) — `git -c alias.*=!`, `git -c core.pager=...`, `git -c core.fsmonitor=...`, `git -c credential.helper=...`, and the `git config` subcommand are classified as `code_execution` because they can inject arbitrary shell commands. - **find / rsync destructive flags** (`internal/danger/classifier.go`) — `find -delete` and `rsync --delete` / `--del` / `--remove-source-files` are classified as `destructive`; `find -fprint` / `-fprintf` are classified as `local_write`. - **Shell operand path classification** (`internal/danger/classifier.go`) — file operands and redirect targets of shell commands are checked with `ClassifyPath`, so writes to `~/.bashrc`, `~/.zshrc`, `~/.profile`, `~/.ssh`, `~/.odek`, etc. are escalated to `system_write` instead of auto-allowed `local_write`. -- **Secret redaction** (`internal/redact/redact.go`) — 20+ patterns: OpenAI, Anthropic, GitHub PAT, AWS, PEM, JWT, Vault, Google OAuth, SendGrid, Discord, DB URLs, etc. +- **Secret redaction** (`internal/redact/redact.go`) — 20+ patterns: OpenAI/Anthropic `sk-` (including underscore-bearing bodies), Groq `gsk_`, xAI `xai-`, HuggingFace `hf_`, GitHub PAT, AWS, PEM, JWT, Vault, Google OAuth, SendGrid, Discord, DB URLs, etc. ### Security findings (`sec_findings.md`) diff --git a/cmd/odek/security_report_validation_test.go b/cmd/odek/security_report_validation_test.go index 52b7ab18..70977db8 100644 --- a/cmd/odek/security_report_validation_test.go +++ b/cmd/odek/security_report_validation_test.go @@ -181,6 +181,10 @@ func TestReport_RedactMissesRealSecretFormats(t *testing.T) { {"db_url_mongo", "mongodb+srv://root:VeryLongMongoPassword1234@cluster.mongodb.net/db", "Mongo URL with embedded password"}, {"discord_bot", "N01234567890123456789012345.aBcDe.6789abcdef0123456789abcdef012345678", "Discord bot token (synthetic test value)"}, {"sendgrid", "SG.dQw4w9WgXcQ-AbCdEfGh.JkLmNoPqRsTuVwXyZ0123456789abcdefghij", "SendGrid API key"}, + {"groq", "gsk_abcdefghijklmnopqrstuvwxyz1234567890", "Groq API key (gsk_ prefix)"}, + {"xai", "xai-abcdefghijklmnopqrstuvwxyz1234567890_0123456789", "xAI API key (xai- prefix)"}, + {"huggingface", "hf_abcdefghijklmnopqrstuvwxyz1234567890", "HuggingFace token (hf_ prefix)"}, + {"anthropic_underscore", "sk-ant-api03-abcdefghijklmnopqrstuvwxyz_1234567890", "Anthropic key with underscore in body"}, } for _, tc := range cases { diff --git a/docs/SECURITY.md b/docs/SECURITY.md index a527e524..ec337d05 100644 --- a/docs/SECURITY.md +++ b/docs/SECURITY.md @@ -246,7 +246,7 @@ Files attached through the Web UI are sourced from the browser trust boundary. T ### 10. Secret redaction -`internal/redact` scans every tool output and session/memory write for known secret formats and replaces matches with `[REDACTED]` before they reach Telegram replies, persistent sessions, or memory. Patterns include OpenAI `sk-`, Anthropic `sk-ant-`, GitHub PATs (classic + fine-grained), AWS access keys, multi-line PEM private keys, JWT, generic `api_key=` / `password=` env lines, Slack `xoxb-`, Stripe `sk_live_`, Google API keys, Twilio `SK`, HashiCorp Vault `hvs.` / `hvb.`, Google OAuth `ya29.` / `1//0`, SendGrid `SG.`, Discord bot tokens (M/N/O-anchored), and DB URLs with embedded credentials (`postgresql://`, `mongodb://`, etc.). +`internal/redact` scans every tool output and session/memory write for known secret formats and replaces matches with `[REDACTED]` before they reach Telegram replies, persistent sessions, or memory. Patterns include OpenAI `sk-` (and underscore-bearing bodies such as Anthropic `sk-ant-...`), Groq `gsk_`, xAI `xai-`, HuggingFace `hf_`, GitHub PATs (classic + fine-grained), AWS access keys, multi-line PEM private keys, JWT, generic `api_key=` / `password=` env lines, Slack `xoxb-`, Stripe `sk_live_`, Google API keys, Twilio `SK`, HashiCorp Vault `hvs.` / `hvb.`, Google OAuth `ya29.` / `1//0`, SendGrid `SG.`, Discord bot tokens (M/N/O-anchored), and DB URLs with embedded credentials (`postgresql://`, `mongodb://`, etc.). If you find a format that leaks, add a regex to `internal/redact/redact.go:31-100` and a row to `TestReport_RedactMissesRealSecretFormats` in `cmd/odek/security_report_validation_test.go`. diff --git a/internal/redact/redact.go b/internal/redact/redact.go index 00ba236c..903e1bf6 100644 --- a/internal/redact/redact.go +++ b/internal/redact/redact.go @@ -34,9 +34,17 @@ import ( // The first matching pattern wins — subsequent patterns are skipped for // that portion of text. var patterns = []*regexp.Regexp{ - // OpenAI / AI provider keys: sk- - // sk-proj- variant for project-scoped keys - regexp.MustCompile(`sk-[a-zA-Z0-9-]{32,}`), + // Provider-specific AI API keys (ordered before the generic sk- pattern). + // Groq: gsk_ + regexp.MustCompile(`gsk_[a-zA-Z0-9]{32,}`), + // xAI: xai- + regexp.MustCompile(`xai-[a-zA-Z0-9_-]{32,}`), + // HuggingFace: hf_ + regexp.MustCompile(`hf_[a-zA-Z0-9]{32,}`), + // OpenAI / generic AI provider keys: sk- + // sk-proj- variant for project-scoped keys. The underscore is required + // because Anthropic keys (sk-ant-...) and some provider keys contain it. + regexp.MustCompile(`sk-[a-zA-Z0-9_-]{32,}`), // GitHub personal access tokens (classic + fine-grained) regexp.MustCompile(`ghp_[a-zA-Z0-9]{36,}`), diff --git a/internal/redact/redact_test.go b/internal/redact/redact_test.go index c2bb6dc9..3b757204 100644 --- a/internal/redact/redact_test.go +++ b/internal/redact/redact_test.go @@ -22,6 +22,8 @@ func TestRedactSecrets_OpenAIKey(t *testing.T) { tests := []string{ "sk-proj-abcdefghijklmnopqrstuvwxyz123456", "sk-1234567890abcdefghijklmnopqrstuv", + // Anthropic-style keys contain underscores in the body. + "sk-ant-api03-abcdefghijklmnopqrstuvwxyz_1234567890", } for _, input := range tests { result := RedactSecrets(input) @@ -31,6 +33,25 @@ func TestRedactSecrets_OpenAIKey(t *testing.T) { } } +func TestRedactSecrets_ProviderKeys(t *testing.T) { + cases := []struct { + name string + secret string + }{ + {"groq", "gsk_abcdefghijklmnopqrstuvwxyz1234567890"}, + {"xai", "xai-abcdefghijklmnopqrstuvwxyz1234567890_0123456789"}, + {"huggingface", "hf_abcdefghijklmnopqrstuvwxyz1234567890"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + result := RedactSecrets(tc.secret) + if result != "[REDACTED]" { + t.Errorf("%s key not redacted: %q → %q", tc.name, tc.secret, result) + } + }) + } +} + func TestRedactSecrets_GitHubToken(t *testing.T) { tests := []string{ "ghp_abcdefghijklmnopqrstuvwxyz1234567890",