feat: .NET appsettings files in repos #88
No files matched your search
@@ -23,7 +23,7 @@ A terminal UI for finding, reading and sharing secrets. Inspired by the fantasti
|
|||||||
- **Your way**: rebind any key, pick a theme, and add custom commands to run
|
- **Your way**: rebind any key, pick a theme, and add custom commands to run
|
||||||
on a secret.
|
on a secret.
|
||||||
- **Try it first**: `lazysecrets --demo` runs on built-in sample data.
|
- **Try it first**: `lazysecrets --demo` runs on built-in sample data.
|
||||||
- **Sources**: Azure Key Vault, Databricks secret scopes, and SOPS, `.env`, Spring Boot and Docker Compose files in your git repos.
|
- **Sources**: Azure Key Vault, Databricks secret scopes, and SOPS, `.env`, Spring Boot, Docker Compose and .NET appsettings files in your git repos.
|
||||||
|
|
||||||
> **Early development:** expect rough edges. See [CHANGELOG.md](CHANGELOG.md) for what's new.
|
> **Early development:** expect rough edges. See [CHANGELOG.md](CHANGELOG.md) for what's new.
|
||||||
|
|
||||||
@@ -87,7 +87,7 @@ pickers and scripts: see [Integration](docs/integration.md#an-fzf-picker).
|
|||||||
- [Keys](docs/keys.md): every key, Go to, the layout, and rebinding keys
|
- [Keys](docs/keys.md): every key, Go to, the layout, and rebinding keys
|
||||||
- [Azure Key Vault](docs/azure.md): accounts, permissions, pinned vaults
|
- [Azure Key Vault](docs/azure.md): accounts, permissions, pinned vaults
|
||||||
- [Databricks](docs/databricks.md): profiles and Key Vault-backed scopes
|
- [Databricks](docs/databricks.md): profiles and Key Vault-backed scopes
|
||||||
- [SOPS, .env, Spring and compose files](docs/repos.md): which repos and files, decrypting
|
- [SOPS, .env, Spring, compose and .NET files](docs/repos.md): which repos and files, decrypting
|
||||||
- [Sharing](docs/sharing.md): links to secrets, encrypted env files, and
|
- [Sharing](docs/sharing.md): links to secrets, encrypted env files, and
|
||||||
opening links, also from the browser and chat
|
opening links, also from the browser and chat
|
||||||
- [Integration](docs/integration.md): how lazysecrets can integrate into other systems
|
- [Integration](docs/integration.md): how lazysecrets can integrate into other systems
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ func listCmd(providers []provider.Provider, st *store.Store, cachePath string, t
|
|||||||
expiry := fs.Bool("expiry", true, "mark and say which have expired (✗ expired) or expire within ui.expiryWarningDays (⚠ expires in 12d) (text)")
|
expiry := fs.Bool("expiry", true, "mark and say which have expired (✗ expired) or expire within ui.expiryWarningDays (⚠ expires in 12d) (text)")
|
||||||
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
|
kinds := fs.String("kind", "", "only these kinds, comma-separated: "+strings.Join(listing.Kinds, ", ")+" (vault: also scopes and repo files)")
|
||||||
showErrors := fs.Bool("errors", true, "list the lists that can't be read, with why (text: ✗ lines; json: \"error\")")
|
showErrors := fs.Bool("errors", true, "list the lists that can't be read, with why (text: ✗ lines; json: \"error\")")
|
||||||
hidden := fs.Bool("hidden", false, "list what's hidden too, as H does in the TUI: Spring and compose keys that don't look secret, example files, and files and repos with only those (json: \"hidden\")")
|
hidden := fs.Bool("hidden", false, "list what's hidden too, as H does in the TUI: config file keys (Spring, compose, .NET) that don't look secret, example files, and files and repos with only those (json: \"hidden\")")
|
||||||
if err := fs.Parse(args); err != nil {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -347,7 +347,7 @@ Flags:
|
|||||||
repos := repo.New(repo.Settings{
|
repos := repo.New(repo.Settings{
|
||||||
Dir: startDir, Paths: pc.Repos.Paths, Roots: valueOr(pc.Repos.Roots, repo.DefaultRoots),
|
Dir: startDir, Paths: pc.Repos.Paths, Roots: valueOr(pc.Repos.Roots, repo.DefaultRoots),
|
||||||
Starred: starredRepos(st), Include: pc.Repos.Include, Exclude: pc.Repos.Exclude,
|
Starred: starredRepos(st), Include: pc.Repos.Include, Exclude: pc.Repos.Exclude,
|
||||||
SecretPatterns: pc.Repos.SecretPatterns, Types: map[string]repo.FileType{"spring": pc.Repos.Spring, "compose": pc.Repos.Compose},
|
SecretPatterns: pc.Repos.SecretPatterns, Types: map[string]repo.FileType{"spring": pc.Repos.Spring, "compose": pc.Repos.Compose, "dotnet": pc.Repos.Dotnet},
|
||||||
})
|
})
|
||||||
// The repo it started in (shown on top as Repo) and the
|
// The repo it started in (shown on top as Repo) and the
|
||||||
// others, checked for secret files in the background like
|
// others, checked for secret files in the background like
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ theme:
|
|||||||
repoColor: ["#c099ff"]
|
repoColor: ["#c099ff"]
|
||||||
springColor: ["#c3e88d"] # Spring Boot config files
|
springColor: ["#c3e88d"] # Spring Boot config files
|
||||||
composeColor: ["#82aaff"] # Docker Compose files
|
composeColor: ["#82aaff"] # Docker Compose files
|
||||||
|
dotnetColor: ["#c099ff"] # .NET appsettings files
|
||||||
```
|
```
|
||||||
|
|
||||||
A role lazysecrets doesn't know, a typo or one from a newer version, is
|
A role lazysecrets doesn't know, a typo or one from a newer version, is
|
||||||
|
|||||||
+2
-2
@@ -20,7 +20,7 @@ The rule: a letter in the lists, ctrl+ that letter where you type a filter
|
|||||||
| `[` / `]` | previous / next tab of the focused pane: Sources and Favourites in Sources; Secrets, Keys, Certs in Secrets |
|
| `[` / `]` | previous / next tab of the focused pane: Sources and Favourites in Sources; Secrets, Keys, Certs in Secrets |
|
||||||
| `/` | filter the focused list by name or note ([how it matches](#searching)); `enter` keeps it, `esc` clears it. While typing: `ctrl+j`/`ctrl+k` move, `ctrl+y` copies, `ctrl+r` reveals, `ctrl+f` stars |
|
| `/` | filter the focused list by name or note ([how it matches](#searching)); `enter` keeps it, `esc` clears it. While typing: `ctrl+j`/`ctrl+k` move, `ctrl+y` copies, `ctrl+r` reveals, `ctrl+f` stars |
|
||||||
| `` ` `` | switch the Sources pane between grouped by provider and a flat list; `enter`/`space` on a provider folds it |
|
| `` ` `` | switch the Sources pane between grouped by provider and a flat list; `enter`/`space` on a provider folds it |
|
||||||
| `H` | show / hide what's hidden: [Spring and compose keys](repos.md#spring-boot-config) that don't look secret, example files, and files and repos with only those; shown dimmed |
|
| `H` | show / hide what's hidden: [config file keys](repos.md#spring-boot-config) (Spring, compose, .NET) that don't look secret, example files, and files and repos with only those; shown dimmed |
|
||||||
| `r` | refresh the focused pane |
|
| `r` | refresh the focused pane |
|
||||||
| `q`, `ctrl+c` | quit |
|
| `q`, `ctrl+c` | quit |
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ match case exactly.
|
|||||||
| `Y` | copy the reference (`@Microsoft.KeyVault(...)`, key/certificate URL, `{{secrets/scope/key}}`) |
|
| `Y` | copy the reference (`@Microsoft.KeyVault(...)`, key/certificate URL, `{{secrets/scope/key}}`) |
|
||||||
| `v` | versions of the selected item (Azure); `space`/`y`/`Y` then act on that version; `esc`, `⌫` or `v` to go back |
|
| `v` | versions of the selected item (Azure); `space`/`y`/`Y` then act on that version; `esc`, `⌫` or `v` to go back |
|
||||||
| `o` | open the selected item in the browser (over SSH: copy the link) |
|
| `o` | open the selected item in the browser (over SSH: copy the link) |
|
||||||
| `e` | open the selected SOPS, `.env`, Spring or compose file, or a secret's file at its line ([how](repos.md#opening-files)) |
|
| `e` | open the selected SOPS, `.env`, Spring, compose or .NET file, or a secret's file at its line ([how](repos.md#opening-files)) |
|
||||||
| `s` / `S` | copy a [link](sharing.md) to the selected item / share menu (also env files, [encrypted](sharing.md#sharing-env-files)) |
|
| `s` / `S` | copy a [link](sharing.md) to the selected item / share menu (also env files, [encrypted](sharing.md#sharing-env-files)) |
|
||||||
| `ctrl+o` | open a link or reference (or just paste one) |
|
| `ctrl+o` | open a link or reference (or just paste one) |
|
||||||
| `x` | [custom commands](custom-commands.md) |
|
| `x` | [custom commands](custom-commands.md) |
|
||||||
|
|||||||
+18
-6
@@ -1,9 +1,10 @@
|
|||||||
# SOPS, .env, Spring and compose files in git repos
|
# SOPS, .env, Spring, compose and .NET files in git repos
|
||||||
|
|
||||||
lazysecrets shows git repositories under **Repos**: their
|
lazysecrets shows git repositories under **Repos**: their
|
||||||
[SOPS](https://github.com/getsops/sops)-encrypted files (YAML, JSON,
|
[SOPS](https://github.com/getsops/sops)-encrypted files (YAML, JSON,
|
||||||
`.env`, INI), plain env files, [Spring Boot config](#spring-boot-config)
|
`.env`, INI), plain env files, [Spring Boot config](#spring-boot-config)
|
||||||
and [Docker Compose files](#docker-compose) as vaults, and the keys in them (`db.password`, `list[0]`) as secrets. A dot or bracket inside a key is
|
[Docker Compose files](#docker-compose) and [.NET appsettings](#net-appsettings)
|
||||||
|
as vaults, and the keys in them (`db.password`, `list[0]`) as secrets. A dot or bracket inside a key is
|
||||||
escaped, `app\.name`, so it doesn't read as nesting.
|
escaped, `app\.name`, so it doesn't read as nesting.
|
||||||
|
|
||||||
## Which repos
|
## Which repos
|
||||||
@@ -47,7 +48,7 @@ Folders of dependencies and caches are never looked in, tracked or not:
|
|||||||
3. where the repo's `.sops.yaml` `creation_rules` say SOPS files go,
|
3. where the repo's `.sops.yaml` `creation_rules` say SOPS files go,
|
||||||
4. the whole repo.
|
4. the whole repo.
|
||||||
|
|
||||||
`include`/`exclude` apply to env, Spring and compose files too.
|
`include`/`exclude` apply to env, Spring, compose and .NET files too.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
providers:
|
providers:
|
||||||
@@ -106,8 +107,8 @@ providers:
|
|||||||
```
|
```
|
||||||
|
|
||||||
A repo's `.lazysecrets.yaml` can set them too, the same way
|
A repo's `.lazysecrets.yaml` can set them too, the same way
|
||||||
(`secretPatterns:`, `spring: secretPatterns:`, `compose:
|
(`secretPatterns:`, `spring:`, `compose:` or `dotnet: secretPatterns:`),
|
||||||
secretPatterns:`), for everyone. Each list
|
for everyone. Each list
|
||||||
comes from the first that sets it: the repo's for the type, the repo's,
|
comes from the first that sets it: the repo's for the type, the repo's,
|
||||||
the config's for the type, the config's, the defaults.
|
the config's for the type, the config's, the defaults.
|
||||||
|
|
||||||
@@ -133,6 +134,17 @@ without a value (`DB_PASSWORD:`, `- DB_PASSWORD`), taken from where
|
|||||||
compose runs, are marked as references; `${DB_PASSWORD:-x}` has its
|
compose runs, are marked as references; `${DB_PASSWORD:-x}` has its
|
||||||
default in the file.
|
default in the file.
|
||||||
|
|
||||||
|
## .NET appsettings
|
||||||
|
|
||||||
|
`appsettings.json` and `appsettings.<Environment>.json`, tracked by git.
|
||||||
|
Keys are named as .NET's configuration names them, sections joined by
|
||||||
|
colons: `ConnectionStrings:Default`, `Servers:0:Name`. The same
|
||||||
|
[secret patterns](#spring-boot-config) pick them, the rest hidden, with
|
||||||
|
`dotnet: secretPatterns:` for .NET files' own; a connection string with
|
||||||
|
`Password=` is listed by its value. An App Service Key Vault reference,
|
||||||
|
`@Microsoft.KeyVault(…)`, is marked as a reference. Jasypt values decrypt
|
||||||
|
with `$JASYPT_ENCRYPTOR_PASSWORD` only.
|
||||||
|
|
||||||
## Decrypting
|
## Decrypting
|
||||||
|
|
||||||
Listing never decrypts: SOPS and Jasypt encrypt values only, so keys and
|
Listing never decrypts: SOPS and Jasypt encrypt values only, so keys and
|
||||||
@@ -151,7 +163,7 @@ repo's history, or easily could be.
|
|||||||
|
|
||||||
### Jasypt
|
### Jasypt
|
||||||
|
|
||||||
A value like `ENC(...)` in a plain `.env`, Spring or compose file is
|
A value like `ENC(...)` in a plain `.env`, Spring, compose or .NET file is
|
||||||
[Jasypt](http://www.jasypt.org)'s, decrypted when it's revealed or copied
|
[Jasypt](http://www.jasypt.org)'s, decrypted when it's revealed or copied
|
||||||
with jasypt-spring-boot's default algorithm (3.0+:
|
with jasypt-spring-boot's default algorithm (3.0+:
|
||||||
`PBEWITHHMACSHA512ANDAES_256`). The password is, as in Spring:
|
`PBEWITHHMACSHA512ANDAES_256`). The password is, as in Spring:
|
||||||
|
|||||||
@@ -154,11 +154,12 @@ type Repos struct {
|
|||||||
// a repo's .sops.yaml creation_rules say where to look.
|
// a repo's .sops.yaml creation_rules say where to look.
|
||||||
Include []string `yaml:"include"`
|
Include []string `yaml:"include"`
|
||||||
Exclude []string `yaml:"exclude"`
|
Exclude []string `yaml:"exclude"`
|
||||||
// SecretPatterns say which keys of config files are secrets; Spring
|
// SecretPatterns say which keys of config files are secrets; Spring,
|
||||||
// and Compose override them for their files, list by list.
|
// Compose and Dotnet override them for their files, list by list.
|
||||||
SecretPatterns repo.SecretPatterns `yaml:"secretPatterns"`
|
SecretPatterns repo.SecretPatterns `yaml:"secretPatterns"`
|
||||||
Spring repo.FileType `yaml:"spring"`
|
Spring repo.FileType `yaml:"spring"`
|
||||||
Compose repo.FileType `yaml:"compose"`
|
Compose repo.FileType `yaml:"compose"`
|
||||||
|
Dotnet repo.FileType `yaml:"dotnet"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Azure struct {
|
type Azure struct {
|
||||||
@@ -300,7 +301,8 @@ func (c Config) validate(path string) error {
|
|||||||
}
|
}
|
||||||
if err := errors.Join(c.Providers.Repos.SecretPatterns.Check("providers.repos.secretPatterns"),
|
if err := errors.Join(c.Providers.Repos.SecretPatterns.Check("providers.repos.secretPatterns"),
|
||||||
c.Providers.Repos.Spring.SecretPatterns.Check("providers.repos.spring.secretPatterns"),
|
c.Providers.Repos.Spring.SecretPatterns.Check("providers.repos.spring.secretPatterns"),
|
||||||
c.Providers.Repos.Compose.SecretPatterns.Check("providers.repos.compose.secretPatterns")); err != nil {
|
c.Providers.Repos.Compose.SecretPatterns.Check("providers.repos.compose.secretPatterns"),
|
||||||
|
c.Providers.Repos.Dotnet.SecretPatterns.Check("providers.repos.dotnet.secretPatterns")); err != nil {
|
||||||
return fmt.Errorf("%s: %w", path, err)
|
return fmt.Errorf("%s: %w", path, err)
|
||||||
}
|
}
|
||||||
for i, v := range c.Providers.Azure.Vaults {
|
for i, v := range c.Providers.Azure.Vaults {
|
||||||
|
|||||||
@@ -22,14 +22,14 @@ type configType struct {
|
|||||||
format func(file string) string // yaml, properties
|
format func(file string) string // yaml, properties
|
||||||
parse func(file string, b []byte) ([]configKey, error)
|
parse func(file string, b []byte) ([]configKey, error)
|
||||||
// jasyptSetting is the key that holds the Jasypt password for the
|
// jasyptSetting is the key that holds the Jasypt password for the
|
||||||
// key named name.
|
// key named name; nil when only $JASYPT_ENCRYPTOR_PASSWORD can.
|
||||||
jasyptSetting func(name string) string
|
jasyptSetting func(name string) string
|
||||||
// placeholder reads a value that only refers to another,
|
// placeholder reads a value that only refers to another,
|
||||||
// ${DB_PASSWORD}: what it names, and its default if it has one.
|
// ${DB_PASSWORD}: what it names, and its default if it has one.
|
||||||
placeholder func(v string) (name, def string, hasDef, ok bool)
|
placeholder func(v string) (name, def string, hasDef, ok bool)
|
||||||
}
|
}
|
||||||
|
|
||||||
var configTypes = []configType{springType, composeType}
|
var configTypes = []configType{springType, composeType, dotnetType}
|
||||||
|
|
||||||
func isConfig(f string) bool {
|
func isConfig(f string) bool {
|
||||||
_, ok := configTypeOf(f)
|
_, ok := configTypeOf(f)
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"go.yaml.in/yaml/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
// .NET config files: appsettings.json and appsettings.<Environment>.json.
|
||||||
|
// Keys are named as .NET's configuration names them, sections joined by
|
||||||
|
// colons: ConnectionStrings:Default, Servers:0:Name.
|
||||||
|
|
||||||
|
var dotnetType = configType{
|
||||||
|
name: "dotnet", is: IsDotnet, format: func(string) string { return "json" }, parse: parseDotnet,
|
||||||
|
placeholder: dotnetPlaceholder,
|
||||||
|
}
|
||||||
|
|
||||||
|
var dotnetName = regexp.MustCompile(`^appsettings(\.[^/]+)?\.json$`)
|
||||||
|
|
||||||
|
// IsDotnet reports whether f is named like a .NET appsettings file.
|
||||||
|
func IsDotnet(f string) bool {
|
||||||
|
return dotnetName.MatchString(strings.ToLower(path.Base(f)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// dotnetPlaceholder reads an App Service Key Vault reference,
|
||||||
|
// @Microsoft.KeyVault(SecretUri=…): the secret is in the vault.
|
||||||
|
func dotnetPlaceholder(v string) (name, def string, hasDef, ok bool) {
|
||||||
|
if strings.HasPrefix(v, "@Microsoft.KeyVault(") && strings.HasSuffix(v, ")") {
|
||||||
|
return v, "", false, true
|
||||||
|
}
|
||||||
|
return "", "", false, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseDotnet(_ string, b []byte) ([]configKey, error) {
|
||||||
|
var doc yaml.Node // JSON is YAML, and the YAML parser says where each key is
|
||||||
|
if err := yaml.Unmarshal(b, &doc); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var out []configKey
|
||||||
|
if len(doc.Content) > 0 {
|
||||||
|
dotnetWalk(doc.Content[0], "", &out)
|
||||||
|
}
|
||||||
|
for i := range out {
|
||||||
|
out[i].name = out[i].key
|
||||||
|
out[i].ref = reference(dotnetPlaceholder, out[i].value)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func dotnetWalk(n *yaml.Node, name string, out *[]configKey) {
|
||||||
|
join := func(child string) string {
|
||||||
|
if name == "" {
|
||||||
|
return child
|
||||||
|
}
|
||||||
|
return name + ":" + child
|
||||||
|
}
|
||||||
|
switch n.Kind {
|
||||||
|
case yaml.MappingNode:
|
||||||
|
for i := 0; i+1 < len(n.Content); i += 2 {
|
||||||
|
dotnetWalk(n.Content[i+1], join(n.Content[i].Value), out)
|
||||||
|
}
|
||||||
|
case yaml.SequenceNode:
|
||||||
|
for i, c := range n.Content {
|
||||||
|
dotnetWalk(c, join(fmt.Sprint(i)), out)
|
||||||
|
}
|
||||||
|
case yaml.ScalarNode:
|
||||||
|
if name != "" {
|
||||||
|
*out = append(*out, configKey{key: name, value: n.Value, line: n.Line})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"maps"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.kebcloud.no/keb/lazysecrets/internal/provider"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIsDotnet(t *testing.T) {
|
||||||
|
for f, want := range map[string]bool{
|
||||||
|
"appsettings.json": true,
|
||||||
|
"src/Api/appsettings.Production.json": true,
|
||||||
|
"appsettings.Example.json": true, // listed hidden
|
||||||
|
"appsettings.json.bak": false,
|
||||||
|
"settings.json": false,
|
||||||
|
"package.json": false,
|
||||||
|
} {
|
||||||
|
if IsDotnet(f) != want {
|
||||||
|
t.Errorf("%s: %t", f, !want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParseDotnet(t *testing.T) {
|
||||||
|
keys, err := parseDotnet("", []byte("\xef\xbb\xbf"+`{
|
||||||
|
"ConnectionStrings": { "Default": "Server=db;Password=hunter2" },
|
||||||
|
"AzureAd": { "ClientId": "abc", "ClientSecret": "@Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/ad)" },
|
||||||
|
"Servers": [ { "Name": "a", "ApiKey": "k" } ],
|
||||||
|
"Logging": { "LogLevel": { "Default": "Information" } }
|
||||||
|
}`))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
got := map[string]string{}
|
||||||
|
for _, k := range keys {
|
||||||
|
ref := ""
|
||||||
|
if k.ref {
|
||||||
|
ref = " (ref)"
|
||||||
|
}
|
||||||
|
got[k.name] = k.value + ref
|
||||||
|
}
|
||||||
|
want := map[string]string{
|
||||||
|
"ConnectionStrings:Default": "Server=db;Password=hunter2",
|
||||||
|
"AzureAd:ClientId": "abc",
|
||||||
|
"AzureAd:ClientSecret": "@Microsoft.KeyVault(SecretUri=https://kv.vault.azure.net/secrets/ad) (ref)",
|
||||||
|
"Servers:0:Name": "a",
|
||||||
|
"Servers:0:ApiKey": "k",
|
||||||
|
"Logging:LogLevel:Default": "Information",
|
||||||
|
}
|
||||||
|
if !maps.Equal(got, want) {
|
||||||
|
t.Errorf("got %v\nwant %v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDotnetFiles(t *testing.T) {
|
||||||
|
root := gitRepo(t, map[string]string{
|
||||||
|
"src/Api/appsettings.json": `{
|
||||||
|
"Logging": { "LogLevel": { "Default": "Information" } },
|
||||||
|
"ConnectionStrings": { "Default": "Server=db;User Id=app;Password=hunter2" },
|
||||||
|
"AzureAd": { "ClientSecret": "@Microsoft.KeyVault(VaultName=kv;SecretName=ad)" }
|
||||||
|
}`,
|
||||||
|
"src/Api/appsettings.Development.json": `{ "Logging": { "LogLevel": { "Default": "Debug" } } }`,
|
||||||
|
})
|
||||||
|
t.Setenv("JASYPT_ENCRYPTOR_PASSWORD", "")
|
||||||
|
ctx := context.Background()
|
||||||
|
p := New(Settings{Dir: root})
|
||||||
|
srcs, _ := p.Sources(ctx)
|
||||||
|
cs, err := p.Containers(ctx, srcs[0])
|
||||||
|
if err != nil || len(cs) != 2 {
|
||||||
|
t.Fatalf("files: %+v %v", cs, err)
|
||||||
|
}
|
||||||
|
for _, c := range cs {
|
||||||
|
if c.Detail != "dotnet · json" || c.Hidden != (c.Name == "src/Api/appsettings.Development.json") {
|
||||||
|
t.Errorf("%s: %q hidden %t", c.Name, c.Detail, c.Hidden)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
settings := cs[1]
|
||||||
|
|
||||||
|
keys, _ := p.Objects(ctx, settings, provider.TypeSecret)
|
||||||
|
got := map[string]string{}
|
||||||
|
for _, k := range shown(keys) {
|
||||||
|
got[k.Name] = k.Detail
|
||||||
|
}
|
||||||
|
// The connection string by its value, the client secret by its name.
|
||||||
|
if want := map[string]string{"ConnectionStrings:Default": "", "AzureAd:ClientSecret": "reference"}; !maps.Equal(got, want) {
|
||||||
|
t.Errorf("keys %v, want %v", got, want)
|
||||||
|
}
|
||||||
|
link := provider.Object{Container: provider.Container{Source: srcs[0], Name: "src/Api/appsettings.json"}, Name: "ConnectionStrings:Default"}
|
||||||
|
if v, err := p.Value(ctx, link); err != nil || v.Reveal() != "Server=db;User Id=app;Password=hunter2" {
|
||||||
|
t.Errorf("value: %q %v", v.Reveal(), err)
|
||||||
|
}
|
||||||
|
if f, _ := p.File(settings, &link); f.Line != 3 {
|
||||||
|
t.Errorf("line %d", f.Line)
|
||||||
|
}
|
||||||
|
if d, plain := p.configDetails(dotnetType, settings); !plain || d[1].Value != "1, not matched by the secret patterns" {
|
||||||
|
t.Errorf("details: %+v %t", d, plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,6 +46,9 @@ func jasyptPassword(file, name string) (password, from string, err error) {
|
|||||||
}
|
}
|
||||||
setting, read, placeholder := jasyptEnv, plainValue, composePlaceholder // env files interpolate like compose
|
setting, read, placeholder := jasyptEnv, plainValue, composePlaceholder // env files interpolate like compose
|
||||||
if t, ok := configTypeOf(file); ok {
|
if t, ok := configTypeOf(file); ok {
|
||||||
|
if t.jasyptSetting == nil {
|
||||||
|
return "", "", fmt.Errorf("jasypt: no password: set $%s", jasyptEnv)
|
||||||
|
}
|
||||||
setting, read, placeholder = t.jasyptSetting(name), t.value, t.placeholder
|
setting, read, placeholder = t.jasyptSetting(name), t.value, t.placeholder
|
||||||
}
|
}
|
||||||
v, err := read(file, setting)
|
v, err := read(file, setting)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
// Package repo browses the encrypted secret files in git repositories:
|
// Package repo browses the encrypted secret files in git repositories:
|
||||||
// a repo is a source, its secret files the containers (vault level), and
|
// a repo is a source, its secret files the containers (vault level), and
|
||||||
// their keys the secrets. Secret files are SOPS files, plain env files and
|
// their keys the secrets. Secret files are SOPS files, plain env files and
|
||||||
// config files: Spring Boot's and Docker Compose's (configfile.go).
|
// config files: Spring Boot's, Docker Compose's and .NET's (configfile.go).
|
||||||
//
|
//
|
||||||
// Listing never decrypts: SOPS encrypts values only, so a file's keys and
|
// Listing never decrypts: SOPS encrypts values only, so a file's keys and
|
||||||
// metadata are read directly. A value is decrypted when it's revealed or
|
// metadata are read directly. A value is decrypted when it's revealed or
|
||||||
@@ -640,6 +640,7 @@ type lazysecretsFile struct {
|
|||||||
SecretPatterns SecretPatterns `yaml:"secretPatterns"`
|
SecretPatterns SecretPatterns `yaml:"secretPatterns"`
|
||||||
Spring FileType `yaml:"spring"`
|
Spring FileType `yaml:"spring"`
|
||||||
Compose FileType `yaml:"compose"`
|
Compose FileType `yaml:"compose"`
|
||||||
|
Dotnet FileType `yaml:"dotnet"`
|
||||||
Sharing struct {
|
Sharing struct {
|
||||||
// Key is the link to the secret that encrypts the repo's
|
// Key is the link to the secret that encrypts the repo's
|
||||||
// shared env files (package share).
|
// shared env files (package share).
|
||||||
@@ -648,7 +649,7 @@ type lazysecretsFile struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (f lazysecretsFile) types() map[string]FileType {
|
func (f lazysecretsFile) types() map[string]FileType {
|
||||||
return map[string]FileType{"spring": f.Spring, "compose": f.Compose}
|
return map[string]FileType{"spring": f.Spring, "compose": f.Compose, "dotnet": f.Dotnet}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ShareKey is the sharing.key of the repo's .lazysecrets.yaml, if any.
|
// ShareKey is the sharing.key of the repo's .lazysecrets.yaml, if any.
|
||||||
|
|||||||
@@ -145,8 +145,8 @@ func (m Model) objectRender() func(styles.Styles, provider.Object, int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// fileIcon is a repo file's icon, in its colour: a lock for an encrypted
|
// fileIcon is a repo file's icon, in its colour: a lock for an encrypted
|
||||||
// one, a leaf for Spring config, a whale for compose, a file in the
|
// one, a leaf for Spring config, a whale for compose, .NET's for
|
||||||
// warning colour for plain text; "" for other vaults, or without icons.
|
// appsettings, a file in the warning colour for plain text; "" for other vaults, or without icons.
|
||||||
func (m Model) fileIcon(sty styles.Styles, c provider.Container) string {
|
func (m Model) fileIcon(sty styles.Styles, c provider.Container) string {
|
||||||
if c.Source.Kind != provider.KindRepo {
|
if c.Source.Kind != provider.KindRepo {
|
||||||
return ""
|
return ""
|
||||||
@@ -157,6 +157,8 @@ func (m Model) fileIcon(sty styles.Styles, c provider.Container) string {
|
|||||||
icon, style = m.icons.Spring, sty.Spring
|
icon, style = m.icons.Spring, sty.Spring
|
||||||
case strings.HasPrefix(c.Detail, "compose"):
|
case strings.HasPrefix(c.Detail, "compose"):
|
||||||
icon, style = m.icons.Compose, sty.Compose
|
icon, style = m.icons.Compose, sty.Compose
|
||||||
|
case strings.HasPrefix(c.Detail, "dotnet"):
|
||||||
|
icon, style = m.icons.Dotnet, sty.Dotnet
|
||||||
case !strings.HasPrefix(c.Detail, "sops"):
|
case !strings.HasPrefix(c.Detail, "sops"):
|
||||||
icon, style = m.icons.PlainFile, sty.Warning
|
icon, style = m.icons.PlainFile, sty.Warning
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ type Set struct {
|
|||||||
PlainFile string // a plain text secrets file (.env)
|
PlainFile string // a plain text secrets file (.env)
|
||||||
Spring string // a Spring Boot config file
|
Spring string // a Spring Boot config file
|
||||||
Compose string // a Docker Compose file
|
Compose string // a Docker Compose file
|
||||||
|
Dotnet string // a .NET appsettings file
|
||||||
Note string // has a note (lazysecrets list)
|
Note string // has a note (lazysecrets list)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,6 +30,7 @@ var NerdFont = Set{
|
|||||||
PlainFile: "\U000F0214", // md-file
|
PlainFile: "\U000F0214", // md-file
|
||||||
Spring: "\U000F032A", // md-leaf
|
Spring: "\U000F032A", // md-leaf
|
||||||
Compose: "\U000F0868", // md-docker
|
Compose: "\U000F0868", // md-docker
|
||||||
|
Dotnet: "\U000F0AAE", // md-dot_net
|
||||||
Note: "\U000F09ED",
|
Note: "\U000F09ED",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ func (m *Map) Actions() []Action {
|
|||||||
a(Lists, "prevTab", &m.PrevTab, "prev tab", "lists", "Previous tab of the focused pane: Sources and Favourites in Sources; Secrets, Keys, Certs in Secrets."),
|
a(Lists, "prevTab", &m.PrevTab, "prev tab", "lists", "Previous tab of the focused pane: Sources and Favourites in Sources; Secrets, Keys, Certs in Secrets."),
|
||||||
a(Lists, "nextTab", &m.NextTab, "next tab", "lists", "Next tab of the focused pane."),
|
a(Lists, "nextTab", &m.NextTab, "next tab", "lists", "Next tab of the focused pane."),
|
||||||
a(Lists, "sourcesView", &m.SourcesView, "grouped/flat sources", "lists", "Switch the Sources pane between grouped by provider and a flat list."),
|
a(Lists, "sourcesView", &m.SourcesView, "grouped/flat sources", "lists", "Switch the Sources pane between grouped by provider and a flat list."),
|
||||||
a(Lists, "toggleHidden", &m.ToggleHidden, "show/hide hidden", "lists", "Show or hide what's hidden: Spring and compose keys that don't look like secrets, example files (.env.example), and files and repos with only those. Shown, they're dimmed; Go to finds them too."),
|
a(Lists, "toggleHidden", &m.ToggleHidden, "show/hide hidden", "lists", "Show or hide what's hidden: config file keys (Spring, compose, .NET) that don't look like secrets, example files (.env.example), and files and repos with only those. Shown, they're dimmed; Go to finds them too."),
|
||||||
a(Lists, "star", &m.Favourite, "star", "lists", "Star or unstar the selected subscription, vault or secret, for the account showing it. Starred items get a ★ and sort first."),
|
a(Lists, "star", &m.Favourite, "star", "lists", "Star or unstar the selected subscription, vault or secret, for the account showing it. Starred items get a ★ and sort first."),
|
||||||
a(Lists, "goTo", &m.Search, "search", "lists", "Go to anything: search every vault and secret loaded and jump to one. Again in Go to: only the source or group you're in, and back to all. On the left, the scope: All, an account (or Databricks, or Repos), or one source (○ ◐ ● for how much of it is loaded)."),
|
a(Lists, "goTo", &m.Search, "search", "lists", "Go to anything: search every vault and secret loaded and jump to one. Again in Go to: only the source or group you're in, and back to all. On the left, the scope: All, an account (or Databricks, or Repos), or one source (○ ◐ ● for how much of it is loaded)."),
|
||||||
a(Lists, "favourites", &m.Favourites, "favourites", "lists", "Go to, on Favourites: everything you've starred, to search and jump to."),
|
a(Lists, "favourites", &m.Favourites, "favourites", "lists", "Go to, on Favourites: everything you've starred, to search and jump to."),
|
||||||
|
|||||||
@@ -36,6 +36,7 @@ type Styles struct {
|
|||||||
KindRepo lipgloss.Style
|
KindRepo lipgloss.Style
|
||||||
Spring lipgloss.Style // Spring Boot config files
|
Spring lipgloss.Style // Spring Boot config files
|
||||||
Compose lipgloss.Style // Docker Compose files
|
Compose lipgloss.Style // Docker Compose files
|
||||||
|
Dotnet lipgloss.Style // .NET appsettings files
|
||||||
|
|
||||||
// themed marks the selection colours as set by the theme, so
|
// themed marks the selection colours as set by the theme, so
|
||||||
// OnBackground leaves them.
|
// OnBackground leaves them.
|
||||||
@@ -74,6 +75,7 @@ func Default() Styles {
|
|||||||
KindRepo: lipgloss.NewStyle().Foreground(lipgloss.Magenta),
|
KindRepo: lipgloss.NewStyle().Foreground(lipgloss.Magenta),
|
||||||
Spring: lipgloss.NewStyle().Foreground(lipgloss.Green),
|
Spring: lipgloss.NewStyle().Foreground(lipgloss.Green),
|
||||||
Compose: lipgloss.NewStyle().Foreground(lipgloss.Blue),
|
Compose: lipgloss.NewStyle().Foreground(lipgloss.Blue),
|
||||||
|
Dotnet: lipgloss.NewStyle().Foreground(lipgloss.BrightMagenta),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ var Roles = map[string]string{
|
|||||||
"repoColor": "repos and their secret files (SOPS)",
|
"repoColor": "repos and their secret files (SOPS)",
|
||||||
"springColor": "Spring Boot config files",
|
"springColor": "Spring Boot config files",
|
||||||
"composeColor": "Docker Compose files",
|
"composeColor": "Docker Compose files",
|
||||||
|
"dotnetColor": ".NET appsettings files",
|
||||||
}
|
}
|
||||||
|
|
||||||
// FromTheme returns the default styles with the theme's roles applied, and
|
// FromTheme returns the default styles with the theme's roles applied, and
|
||||||
@@ -105,6 +106,8 @@ func FromTheme(theme map[string][]string) (Styles, color.Color, error) {
|
|||||||
st.Spring = v.fg(lipgloss.NewStyle())
|
st.Spring = v.fg(lipgloss.NewStyle())
|
||||||
case "composeColor":
|
case "composeColor":
|
||||||
st.Compose = v.fg(lipgloss.NewStyle())
|
st.Compose = v.fg(lipgloss.NewStyle())
|
||||||
|
case "dotnetColor":
|
||||||
|
st.Dotnet = v.fg(lipgloss.NewStyle())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return st, fg, nil
|
return st, fg, nil
|
||||||
|
|||||||
Reference in new issue
Block a user