appsettings.json and appsettings.<Environment>.json tracked by git are listed under Repos, keys named as .NET names them (ConnectionStrings:Default). - The secret patterns pick the keys, the rest hidden, with dotnet.secretPatterns for .NET files' own. - An App Service Key Vault reference, @Microsoft.KeyVault(…), is marked as a reference; a BOM at the start is fine. - A .NET icon, themeable with dotnetColor.
171 lines
5.4 KiB
Go
171 lines
5.4 KiB
Go
package repo
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
|
|
"git.kebcloud.no/keb/lazysecrets/internal/jasypt"
|
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
|
)
|
|
|
|
// Config files (Spring's, compose's) are read directly, like env files,
|
|
// but most of their keys aren't secret: those the secret patterns
|
|
// (patterns.go) don't match are hidden, and a file with none of them is.
|
|
|
|
// configType is a type of config file: what it's called (in settings, as
|
|
// spring:, and on the file's row) and how its keys are read.
|
|
type configType struct {
|
|
name string
|
|
is func(file string) bool
|
|
format func(file string) string // yaml, properties
|
|
parse func(file string, b []byte) ([]configKey, error)
|
|
// jasyptSetting is the key that holds the Jasypt password for the
|
|
// key named name; nil when only $JASYPT_ENCRYPTOR_PASSWORD can.
|
|
jasyptSetting func(name string) string
|
|
// placeholder reads a value that only refers to another,
|
|
// ${DB_PASSWORD}: what it names, and its default if it has one.
|
|
placeholder func(v string) (name, def string, hasDef, ok bool)
|
|
}
|
|
|
|
var configTypes = []configType{springType, composeType, dotnetType}
|
|
|
|
func isConfig(f string) bool {
|
|
_, ok := configTypeOf(f)
|
|
return ok
|
|
}
|
|
|
|
// configTypeOf is the type of config file f is, if it's one.
|
|
func configTypeOf(f string) (configType, bool) {
|
|
for _, t := range configTypes {
|
|
if t.is(f) {
|
|
return t, true
|
|
}
|
|
}
|
|
return configType{}, false
|
|
}
|
|
|
|
// detail is the file's row: "spring · yaml".
|
|
func (t configType) detail(file string) string { return t.name + " · " + t.format(file) }
|
|
|
|
// configKey is a key of a config file, with its value.
|
|
type configKey struct {
|
|
key string // as the app sees it: spring.datasource.password, DB_PASSWORD
|
|
name string // one key in the file: spring.datasource.password (prod), api.DB_PASSWORD
|
|
value string
|
|
ref bool // the value only refers to the secret, set elsewhere: ${DB_PASSWORD}
|
|
line int // 1-based
|
|
}
|
|
|
|
func (k configKey) jasypt() bool { return jasypt.Is(k.value) }
|
|
|
|
// plain reports whether the file holds the value itself, readable.
|
|
func (k configKey) plain() bool { return k.value != "" && !k.ref && !k.jasypt() }
|
|
|
|
// reference reports whether v is only a placeholder without a default:
|
|
// one with a default has the default in the file.
|
|
func reference(placeholder func(string) (string, string, bool, bool), v string) bool {
|
|
_, _, hasDef, ok := placeholder(v)
|
|
return ok && !hasDef
|
|
}
|
|
|
|
func (t configType) read(file string) ([]configKey, error) {
|
|
b, err := os.ReadFile(file)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return t.parse(file, b)
|
|
}
|
|
|
|
func (t configType) keyNamed(file, name string) (configKey, error) {
|
|
keys, err := t.read(file)
|
|
if err != nil {
|
|
return configKey{}, err
|
|
}
|
|
for _, k := range keys {
|
|
if k.name == name {
|
|
return k, nil
|
|
}
|
|
}
|
|
return configKey{}, fmt.Errorf("%s isn't in %s", name, filepath.Base(file))
|
|
}
|
|
|
|
func (t configType) value(file, name string) (provider.SecretValue, error) {
|
|
k, err := t.keyNamed(file, name)
|
|
if err != nil {
|
|
return provider.SecretValue{}, err
|
|
}
|
|
return provider.NewSecretValue([]byte(k.value)), nil
|
|
}
|
|
|
|
// configHidden reports whether a config file has no secret keys. One that
|
|
// can't be read isn't hidden: its error is worth seeing.
|
|
func (p *Provider) configHidden(t configType, root, file string) bool {
|
|
keys, err := t.read(file)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
filter, err := p.filterFor(root, t.name)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return !slices.ContainsFunc(keys, func(k configKey) bool { return filter.secret(k.key, k.value) })
|
|
}
|
|
|
|
// configObjects lists the file's keys, those that don't look like secrets
|
|
// hidden.
|
|
func (p *Provider) configObjects(t configType, c provider.Container, b []byte) ([]provider.Object, error) {
|
|
keys, err := t.parse(c.ID, b)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", c.Name, err)
|
|
}
|
|
filter, err := p.filterFor(c.Source.ID, t.name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
format := provider.Attr{Label: "format", Value: t.name + " " + t.format(c.ID)}
|
|
var out []provider.Object
|
|
for _, k := range keys {
|
|
state, detail := provider.Attr{Label: "encrypted", Value: "no: plain text in the file"}, ""
|
|
switch {
|
|
case k.ref:
|
|
state, detail = provider.Attr{Label: "value", Value: "a reference: the secret is set elsewhere"}, "reference"
|
|
case k.jasypt():
|
|
state, detail = provider.Attr{Label: "encrypted", Value: "jasypt"}, "jasypt"
|
|
}
|
|
out = append(out, provider.Object{
|
|
Container: c, Type: provider.TypeSecret, ID: c.ID + "\x00" + k.name, Name: k.name, Detail: detail,
|
|
Hidden: !filter.secret(k.key, k.value), Attrs: []provider.Attr{format, state},
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// configDetails says how many of the file's keys aren't listed, and
|
|
// whether any listed one is in plain text.
|
|
func (p *Provider) configDetails(t configType, c provider.Container) (attrs []provider.Attr, plain bool) {
|
|
keys, err := t.read(c.ID)
|
|
if err != nil {
|
|
return []provider.Attr{{Label: "keys", Value: err.Error(), Warn: true}}, false
|
|
}
|
|
filter, err := p.filterFor(c.Source.ID, t.name)
|
|
if err != nil {
|
|
return []provider.Attr{{Label: "keys", Value: err.Error(), Warn: true}}, false
|
|
}
|
|
secrets, jasypt := 0, 0
|
|
for _, k := range keys {
|
|
if filter.secret(k.key, k.value) {
|
|
secrets++
|
|
plain = plain || k.plain()
|
|
}
|
|
if k.jasypt() {
|
|
jasypt++
|
|
}
|
|
}
|
|
return []provider.Attr{
|
|
{Label: "encryption", Value: jasyptShare(jasypt, len(keys))},
|
|
{Label: "hidden keys", Value: fmt.Sprintf("%d, not matched by the secret patterns", len(keys)-secrets)},
|
|
}, plain
|
|
}
|