discover: broaden discovery to feed the new drivers

Per the standing directive (dynamic discovery per credential type as each
driver lands):
- env.go: tag DB connection URLs by scheme (postgres/postgresql, mysql/mariadb,
  redis/rediss) with a user:password authority, routing DATABASE_URL/REDIS_URL/
  MYSQL_URL straight to the in-place DB drivers, kept regardless of the
  secret-name heuristic.
- aws.go: emit the driver-ready single-line blob (was the bare secret) and
  resolve region from env / ~/.aws/config.
- ssh.go: content-based key detection (not just id_*) + ~/.ssh/config
  IdentityFile parsing.
- tea.go: new scanner mapping Gitea `tea` CLI config to a driver-ready PAT blob.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
leetcrypt
2026-06-18 14:48:30 -07:00
parent 59af53dcc0
commit 1c0896347b
5 changed files with 433 additions and 42 deletions
+120 -6
View File
@@ -2,6 +2,7 @@ package discover
import (
"context"
"net/url"
"os"
"path/filepath"
"strings"
@@ -79,9 +80,22 @@ func TestAWSScanner(t *testing.T) {
if !strings.Contains(c.Identity, "default") {
t.Errorf("identity %q missing profile", c.Identity)
}
if got := openSecret(t, v, c); got != "wJalrSECRETkeyMaterial0123" {
t.Errorf("secret round-trip = %q", got)
// Secret is now the driver-ready blob: aws://<keyID>:<secret>@aws/?region=...
blob := openSecret(t, v, c)
u, err := url.Parse(blob)
if err != nil {
t.Fatalf("secret is not a parseable blob: %v", err)
}
if u.Scheme != "aws" || u.User.Username() != "AKIAIOSFODNN7EXAMPLE" {
t.Errorf("blob keyID wrong: %q", blob)
}
if pw, _ := u.User.Password(); pw != "wJalrSECRETkeyMaterial0123" {
t.Errorf("blob secret round-trip = %q", pw)
}
if u.Query().Get("region") == "" {
t.Errorf("blob missing region: %q", blob)
}
// Identity/Meta must stay redacted even though the blob carries the full values.
assertNoLeak(t, creds, "wJalrSECRETkeyMaterial0123", "AKIAIOSFODNN7EXAMPLE")
}
@@ -89,7 +103,10 @@ func TestEnvScanner(t *testing.T) {
dir := t.TempDir()
t.Chdir(dir)
writeFixture(t, filepath.Join(dir, ".env"),
"PORT=8080\nDEBUG=true\nSTRIPE_API_KEY=sk_live_envSECRET0123456789\nexport DB_PASSWORD=\"pw-envSECRET-xyz\"\n")
"PORT=8080\nDEBUG=true\nSTRIPE_API_KEY=sk_live_envSECRET0123456789\nexport DB_PASSWORD=\"pw-envSECRET-xyz\"\n"+
"DATABASE_URL=postgres://labapp:pgSECRET@127.0.0.1:5432/labdb\n"+
"REDIS_URL=redis://:redisSECRET@127.0.0.1:6379\n"+
"MYSQL_URL=mysql://root:weak@127.0.0.1:3306/app\n")
v := vault.New()
defer v.Purge()
@@ -97,8 +114,8 @@ func TestEnvScanner(t *testing.T) {
if err != nil {
t.Fatal(err)
}
if len(creds) != 2 {
t.Fatalf("want 2 creds (secret-ish only), got %d: %+v", len(creds), creds)
if len(creds) != 5 {
t.Fatalf("want 5 creds (secret-ish + DB URLs), got %d: %+v", len(creds), creds)
}
idents := byIdentity(creds)
if _, ok := idents[".env / STRIPE_API_KEY"]; !ok {
@@ -109,7 +126,19 @@ func TestEnvScanner(t *testing.T) {
t.Errorf("non-secret %q should have been filtered out", id)
}
}
assertNoLeak(t, creds, "sk_live_envSECRET0123456789", "pw-envSECRET-xyz")
// DB connection strings must be re-tagged with their rotation-driver Source so
// the postgres/mysql/redis drivers can Detect them straight from a .env scan.
bySource := map[string]string{} // source -> identity
for _, c := range creds {
bySource[c.Source] = c.Identity
}
for _, want := range []string{"postgres", "mysql", "redis"} {
if _, ok := bySource[want]; !ok {
t.Errorf("DB URL not tagged Source=%q; got sources %v", want, bySource)
}
}
assertNoLeak(t, creds, "sk_live_envSECRET0123456789", "pw-envSECRET-xyz",
"pgSECRET", "redisSECRET", "weak")
}
func TestNetrcScanner(t *testing.T) {
@@ -257,6 +286,91 @@ func TestSSHScanner(t *testing.T) {
assertNoLeak(t, creds, "sshSECRETkeyccc", "sshSECRETencddd")
}
// TestSSHScannerDynamicDiscovery covers the broadened discovery: a key with a
// non-conventional name (no id_ prefix) inside ~/.ssh, and a key in a
// non-default location referenced by an IdentityFile directive in ~/.ssh/config.
func TestSSHScannerDynamicDiscovery(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
// Oddly-named key in ~/.ssh — found by content, not by id_ prefix.
writeFixture(t, filepath.Join(home, ".ssh", "work-deploy"),
"-----BEGIN OPENSSH PRIVATE KEY-----\nsshSECRETwork111\n-----END OPENSSH PRIVATE KEY-----\n")
// Non-key companions that must be ignored.
writeFixture(t, filepath.Join(home, ".ssh", "known_hosts"), "host ssh-ed25519 AAAA notakey")
writeFixture(t, filepath.Join(home, ".ssh", "work-deploy.pub"), "ssh-ed25519 AAAA pub")
// Key outside ~/.ssh, referenced via IdentityFile in config.
writeFixture(t, filepath.Join(home, "keys", "prod.key"),
"-----BEGIN OPENSSH PRIVATE KEY-----\nsshSECRETprod222\n-----END OPENSSH PRIVATE KEY-----\n")
writeFixture(t, filepath.Join(home, ".ssh", "config"),
"Host prod\n HostName prod.example.com\n IdentityFile ~/keys/prod.key\n")
v := vault.New()
defer v.Purge()
creds, err := (&sshScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
idents := byIdentity(creds)
if _, ok := idents["work-deploy"]; !ok {
t.Errorf("non-id_ key not discovered by content: %v", idents)
}
if c, ok := idents["prod.key"]; !ok {
t.Errorf("IdentityFile-referenced key not discovered: %v", idents)
} else if got := openSecret(t, v, c); !strings.Contains(got, "sshSECRETprod222") {
t.Errorf("prod.key secret = %q", got)
}
if _, ok := idents["known_hosts"]; ok {
t.Error("known_hosts wrongly treated as a key")
}
if _, ok := idents["work-deploy.pub"]; ok {
t.Error(".pub wrongly treated as a key")
}
if len(creds) != 2 {
t.Fatalf("want 2 keys (work-deploy + prod.key), got %d: %v", len(creds), idents)
}
assertNoLeak(t, creds, "sshSECRETwork111", "sshSECRETprod222")
}
func TestTeaScanner(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
t.Setenv("XDG_CONFIG_HOME", "") // force ~/.config fallback
writeFixture(t, filepath.Join(home, ".config", "tea", "config.yml"),
"logins:\n"+
"- name: work\n"+
" url: https://gitea.example.com\n"+
" user: alice\n"+
" token: teaSECRETtok111\n"+
" default: true\n"+
"- name: notoken\n"+
" url: https://other.example.com\n")
v := vault.New()
defer v.Purge()
creds, err := (&teaScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 1 {
t.Fatalf("want 1 gitea login (token-less skipped), got %d: %+v", len(creds), creds)
}
c := creds[0]
if c.Source != "gitea" {
t.Errorf("source = %q, want gitea (so the driver Detects it)", c.Source)
}
if c.Identity != "alice @ gitea.example.com" {
t.Errorf("identity = %q", c.Identity)
}
// Secret is the driver-ready blob carrying the token.
if got := openSecret(t, v, c); !strings.Contains(got, "teaSECRETtok111") ||
!strings.HasPrefix(got, "https://alice:") {
t.Errorf("secret blob = %q", got)
}
assertNoLeak(t, creds, "teaSECRETtok111")
}
func TestFileScanner(t *testing.T) {
root := t.TempDir()
writeFixture(t, filepath.Join(root, "github-token"), "fileSECRETtok")