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:
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user