Files
incredigo/internal/discover/scanners_test.go
T
leetcrypt 425c299359 Initial commit: Incredigo — local-first RAM-only credential custody
v1: discover → migrate → expiry tracking → sealed export/import.

- vault: memguard mlock/DONTDUMP arena, handle indirection, zeroize-on-drop
- discover: registry + 8 read-only scanners (aws, env, netrc, ssh, docker,
  kube, git) and a file/dir harvester (--path)
- sink: gopass streaming insert; length-prefixed bundle framing; Sealer
  interface with three impls — age (default, authenticated), hmac
  (authenticated, openssl-only encrypt-then-MAC), openssl (CBC fallback,
  unauthenticated; OpenSSL 3.x enc refuses AEAD)
- policy: local expiry engine, 60d/8w threshold parser
- audit: redacted append-only JSONL, injectable clock
- export/import: passphrase from no-echo prompt or $INCREDIGO_PASSPHRASE into
  locked memory; secrets stream gopass<->Sealer as bytes, never Go strings
- tests: scanner leak-checks, vault zeroize, bundle round-trip via fake gopass;
  go test ./... green, -race clean

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-15 07:57:34 -07:00

288 lines
8.2 KiB
Go

package discover
import (
"context"
"os"
"path/filepath"
"strings"
"testing"
"incredigo/internal/vault"
)
func writeFixture(t *testing.T, path, content string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, []byte(content), 0o600); err != nil {
t.Fatal(err)
}
}
// assertNoLeak fails if any secret substring appears in a non-secret field
// (Identity, Location, Source, Kind, or any Meta key/value).
func assertNoLeak(t *testing.T, creds []Credential, secrets ...string) {
t.Helper()
for _, c := range creds {
fields := []string{c.Identity, c.Source, c.Location, string(c.Kind)}
for k, val := range c.Meta {
fields = append(fields, k, val)
}
for _, f := range fields {
for _, s := range secrets {
if s != "" && strings.Contains(f, s) {
t.Errorf("secret %q leaked into non-secret field %q", s, f)
}
}
}
}
}
func openSecret(t *testing.T, v *vault.Vault, c Credential) string {
t.Helper()
buf, err := v.Open(c.Secret)
if err != nil {
t.Fatalf("open vaulted secret: %v", err)
}
return string(buf.Bytes())
}
// byIdentity indexes creds by Identity for assertions.
func byIdentity(creds []Credential) map[string]Credential {
m := make(map[string]Credential, len(creds))
for _, c := range creds {
m[c.Identity] = c
}
return m
}
func TestAWSScanner(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "credentials")
writeFixture(t, p, "[default]\naws_access_key_id = AKIAIOSFODNN7EXAMPLE\naws_secret_access_key = wJalrSECRETkeyMaterial0123\n")
t.Setenv("AWS_SHARED_CREDENTIALS_FILE", p)
v := vault.New()
defer v.Purge()
creds, err := (&awsScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 1 {
t.Fatalf("want 1 cred, got %d", len(creds))
}
c := creds[0]
if c.Kind != KindAWSKey {
t.Errorf("kind = %s, want aws_key", c.Kind)
}
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)
}
assertNoLeak(t, creds, "wJalrSECRETkeyMaterial0123", "AKIAIOSFODNN7EXAMPLE")
}
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")
v := vault.New()
defer v.Purge()
creds, err := (&envScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 2 {
t.Fatalf("want 2 creds (secret-ish only), got %d: %+v", len(creds), creds)
}
idents := byIdentity(creds)
if _, ok := idents[".env / STRIPE_API_KEY"]; !ok {
t.Errorf("missing STRIPE_API_KEY; got %v", idents)
}
for id := range idents {
if strings.Contains(id, "PORT") || strings.Contains(id, "DEBUG") {
t.Errorf("non-secret %q should have been filtered out", id)
}
}
assertNoLeak(t, creds, "sk_live_envSECRET0123456789", "pw-envSECRET-xyz")
}
func TestNetrcScanner(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
writeFixture(t, filepath.Join(home, ".netrc"),
"machine api.example.com login alice password netrcSECRETaaa\ndefault login bob password netrcDEFAULTbbb\n")
v := vault.New()
defer v.Purge()
creds, err := (&netrcScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 2 {
t.Fatalf("want 2 creds, got %d", len(creds))
}
idents := byIdentity(creds)
if c, ok := idents["alice @ api.example.com"]; !ok {
t.Errorf("missing machine entry; got %v", idents)
} else if got := openSecret(t, v, c); got != "netrcSECRETaaa" {
t.Errorf("secret = %q", got)
}
if _, ok := idents["bob @ default"]; !ok {
t.Errorf("missing default entry; got %v", idents)
}
assertNoLeak(t, creds, "netrcSECRETaaa", "netrcDEFAULTbbb")
}
func TestDockerScanner(t *testing.T) {
dir := t.TempDir()
t.Setenv("DOCKER_CONFIG", dir)
writeFixture(t, filepath.Join(dir, "config.json"),
`{"auths":{"registry.example.com":{"auth":"ZG9ja2VyU0VDUkVUZWVl"},"ghcr.io":{"identitytoken":"dockerTOKENfff"}}}`)
v := vault.New()
defer v.Purge()
creds, err := (&dockerScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 2 {
t.Fatalf("want 2 creds, got %d", len(creds))
}
idents := byIdentity(creds)
if c, ok := idents["registry.example.com"]; !ok || c.Kind != KindPassword {
t.Errorf("registry entry wrong: %+v", c)
}
if c, ok := idents["ghcr.io"]; !ok || c.Kind != KindToken {
t.Errorf("ghcr entry wrong: %+v", c)
}
assertNoLeak(t, creds, "ZG9ja2VyU0VDUkVUZWVl", "dockerTOKENfff")
}
func TestKubeScanner(t *testing.T) {
dir := t.TempDir()
p := filepath.Join(dir, "config")
t.Setenv("KUBECONFIG", p)
writeFixture(t, p, `apiVersion: v1
users:
- name: admin
user:
token: kubeSECRETggg
- name: certuser
user:
client-key-data: a3ViZUtFWWhoaA==
- name: execuser
user:
exec:
command: aws
`)
v := vault.New()
defer v.Purge()
creds, err := (&kubeScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 2 {
t.Fatalf("want 2 creds (exec user skipped), got %d", len(creds))
}
idents := byIdentity(creds)
if c, ok := idents["admin"]; !ok || c.Kind != KindToken {
t.Errorf("admin entry wrong: %+v", c)
}
if c, ok := idents["certuser"]; !ok || c.Kind != KindPrivateKey {
t.Errorf("certuser entry wrong: %+v", c)
}
if _, ok := idents["execuser"]; ok {
t.Error("execuser (dynamic) should be skipped")
}
assertNoLeak(t, creds, "kubeSECRETggg", "a3ViZUtFWWhoaA==")
}
func TestGitScanner(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
writeFixture(t, filepath.Join(home, ".git-credentials"),
"https://alice:gitSECRETiii@github.com\nhttps://gitlab.com\n")
v := vault.New()
defer v.Purge()
creds, err := (&gitScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 1 {
t.Fatalf("want 1 cred (no-cred line skipped), got %d", len(creds))
}
c := creds[0]
if c.Identity != "alice @ github.com" {
t.Errorf("identity = %q", c.Identity)
}
if got := openSecret(t, v, c); got != "gitSECRETiii" {
t.Errorf("secret = %q", got)
}
assertNoLeak(t, creds, "gitSECRETiii")
}
func TestSSHScanner(t *testing.T) {
home := t.TempDir()
t.Setenv("HOME", home)
writeFixture(t, filepath.Join(home, ".ssh", "id_ed25519"),
"-----BEGIN OPENSSH PRIVATE KEY-----\nsshSECRETkeyccc\n-----END OPENSSH PRIVATE KEY-----\n")
writeFixture(t, filepath.Join(home, ".ssh", "id_ed25519.pub"), "ssh-ed25519 AAAA pub")
writeFixture(t, filepath.Join(home, ".ssh", "id_rsa_enc"),
"-----BEGIN RSA PRIVATE KEY-----\nProc-Type: 4,ENCRYPTED\nsshSECRETencddd\n-----END RSA PRIVATE KEY-----\n")
v := vault.New()
defer v.Purge()
creds, err := (&sshScanner{}).Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 2 {
t.Fatalf("want 2 private keys (.pub ignored), got %d: %+v", len(creds), creds)
}
idents := byIdentity(creds)
if c, ok := idents["id_rsa_enc"]; !ok || c.Meta["encrypted"] != "true" {
t.Errorf("encrypted key not flagged: %+v", c)
}
if c, ok := idents["id_ed25519"]; !ok || c.Meta["encrypted"] != "false" {
t.Errorf("plaintext key wrongly flagged: %+v", c)
}
assertNoLeak(t, creds, "sshSECRETkeyccc", "sshSECRETencddd")
}
func TestFileScanner(t *testing.T) {
root := t.TempDir()
writeFixture(t, filepath.Join(root, "github-token"), "fileSECRETtok")
writeFixture(t, filepath.Join(root, "keys", "deploy.pem"),
"-----BEGIN OPENSSH PRIVATE KEY-----\nfileKEYmaterial\n-----END OPENSSH PRIVATE KEY-----\n")
writeFixture(t, filepath.Join(root, "empty"), "")
v := vault.New()
defer v.Purge()
fs := &fileScanner{targets: []string{root}}
creds, err := fs.Scan(context.Background(), v)
if err != nil {
t.Fatal(err)
}
if len(creds) != 2 {
t.Fatalf("want 2 creds (empty skipped), got %d: %+v", len(creds), creds)
}
idents := byIdentity(creds)
if c, ok := idents["github-token"]; !ok {
t.Errorf("missing github-token; got %v", idents)
} else if got := openSecret(t, v, c); got != "fileSECRETtok" {
t.Errorf("secret = %q", got)
}
if c, ok := idents[filepath.Join("keys", "deploy.pem")]; !ok || c.Kind != KindPrivateKey {
t.Errorf("pem not classified as private_key: %+v", c)
}
assertNoLeak(t, creds, "fileSECRETtok", "fileKEYmaterial")
}