425c299359
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>
102 lines
3.0 KiB
Go
102 lines
3.0 KiB
Go
// Package discover holds the read-only credential scanners and their registry.
|
|
//
|
|
// Each scanner understands exactly one location/format, never modifies the files
|
|
// it reads, and emits Credential records whose secret value is a handle into the
|
|
// RAM vault — never a plaintext copy.
|
|
package discover
|
|
|
|
import (
|
|
"context"
|
|
"sort"
|
|
"sync"
|
|
"time"
|
|
|
|
"incredigo/internal/vault"
|
|
)
|
|
|
|
// Kind classifies a credential for policy and pathing purposes.
|
|
type Kind string
|
|
|
|
const (
|
|
KindAWSKey Kind = "aws_key"
|
|
KindToken Kind = "token"
|
|
KindPassword Kind = "password"
|
|
KindPrivateKey Kind = "private_key"
|
|
)
|
|
|
|
// Credential is a normalized, non-secret-bearing record. The actual secret is
|
|
// referenced by Secret (a vault handle); nothing here should ever be a plaintext
|
|
// secret. Identity must be pre-redacted (e.g. "default / AKIA…REDACTED").
|
|
type Credential struct {
|
|
Source string // scanner name, e.g. "aws"
|
|
Kind Kind // classification
|
|
Identity string // non-secret label
|
|
Location string // file the cred came from
|
|
Modified time.Time // source mtime — input to expiry policy
|
|
Secret *vault.Handle // opaque handle into the RAM vault
|
|
Meta map[string]string // non-secret extras
|
|
}
|
|
|
|
// Scanner is the single interface a source must implement. Add a source by
|
|
// dropping one new file in this package that Registers a Scanner in init().
|
|
type Scanner interface {
|
|
Name() string
|
|
// Available reports whether this source exists on the current machine, so we
|
|
// can skip cleanly rather than erroring on absent files.
|
|
Available() bool
|
|
// Scan reads the source read-only and stores any secrets in v, returning
|
|
// metadata records that reference them.
|
|
Scan(ctx context.Context, v *vault.Vault) ([]Credential, error)
|
|
}
|
|
|
|
var (
|
|
regMu sync.Mutex
|
|
registry = map[string]Scanner{}
|
|
)
|
|
|
|
// Register adds a scanner to the global registry. Call from a scanner's init().
|
|
func Register(s Scanner) {
|
|
regMu.Lock()
|
|
defer regMu.Unlock()
|
|
registry[s.Name()] = s
|
|
}
|
|
|
|
// Scanners returns all registered scanners sorted by name. If names is non-empty,
|
|
// only those are returned (unknown names are silently ignored by the caller's
|
|
// filtering; use Lookup if you need to detect unknown names).
|
|
func Scanners(names ...string) []Scanner {
|
|
regMu.Lock()
|
|
defer regMu.Unlock()
|
|
|
|
var out []Scanner
|
|
if len(names) == 0 {
|
|
for _, s := range registry {
|
|
out = append(out, s)
|
|
}
|
|
} else {
|
|
for _, n := range names {
|
|
if s, ok := registry[n]; ok {
|
|
out = append(out, s)
|
|
}
|
|
}
|
|
}
|
|
sort.Slice(out, func(i, j int) bool { return out[i].Name() < out[j].Name() })
|
|
return out
|
|
}
|
|
|
|
// ScanAll runs the selected (or all) available scanners and aggregates results.
|
|
func ScanAll(ctx context.Context, v *vault.Vault, names ...string) ([]Credential, error) {
|
|
var all []Credential
|
|
for _, s := range Scanners(names...) {
|
|
if !s.Available() {
|
|
continue
|
|
}
|
|
creds, err := s.Scan(ctx, v)
|
|
if err != nil {
|
|
return all, err
|
|
}
|
|
all = append(all, creds...)
|
|
}
|
|
return all, nil
|
|
}
|