diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b166aa..3e48e67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ ## Unreleased +### Added + +- `--filter` (`-f`) regex to include the matching secrets in `encrypt` cmd. +- `--filter` (`-f`) regex to include the matching secrets in `decrypt` cmd. +- `--filter` (`-f`) regex to include the matching secrets in `untrack` cmd. + ## [v0.1.1] - 2021-03-11 ### Added diff --git a/README.md b/README.md index 46d4cef..2fc8d8a 100644 --- a/README.md +++ b/README.md @@ -50,12 +50,24 @@ Encrypt (and track) a directory in dry-run to see what would be encrypted before agebox encrypt ./secrets --dry-run ``` +Encrypt (and track) a directory and only (filter regex used) the `secret` named yaml files. + +```bash +agebox encrypt ./manifests --filter ".*secret(\.yaml|\.yml)$" +``` + Decrypt a subset of tracked secrets and a file. ```bash agebox decrypt ./secrets/team-1 ./secrets/secret1.yaml ``` +Decrypt only (filter regex used) `team-a` tracked files. + +```bash +agebox decrypt ./secrets --filter ".*team-a.*" +``` + Validate all tracked encrypted files exist and decryption is possible. ```bash diff --git a/cmd/agebox/commands/decrypt.go b/cmd/agebox/commands/decrypt.go index 3dfbc6a..6dbbf6f 100644 --- a/cmd/agebox/commands/decrypt.go +++ b/cmd/agebox/commands/decrypt.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "regexp" "gopkg.in/alecthomas/kingpin.v2" @@ -22,6 +23,7 @@ type decryptCommand struct { DecryptAll bool Force bool DryRun bool + RegexFilter *regexp.Regexp } // NewDecryptCommand returns the decrypt command. @@ -32,6 +34,7 @@ func NewDecryptCommand(app *kingpin.Application) Command { cmd.Flag("all", "Decrypts all tracked files.").Short('a').BoolVar(&c.DecryptAll) cmd.Flag("dry-run", "Enables dry run mode, write operations will be ignored").BoolVar(&c.DryRun) cmd.Flag("force", "Forces the decryption even if decrypted file exists").BoolVar(&c.Force) + cmd.Flag("filter", "Decrypts only the filenames (without encrypted extension) that match the provided regex").Short('f').RegexpVar(&c.RegexFilter) cmd.Arg("files", "Files to decrypt.").StringsVar(&c.Files) return c @@ -79,6 +82,7 @@ func (d decryptCommand) Run(ctx context.Context, config RootConfig) error { secretIDProc := process.NewIDProcessorChain( process.NewPathSanitizer(""), process.NewIgnoreAlreadyProcessed(map[string]struct{}{}), // This should be after pathSanitizer. + process.NewIncludeRegexMatch(d.RegexFilter, logger), process.NewDecryptionPathState(d.Force, secretRepo, logger), ) diff --git a/cmd/agebox/commands/encrypt.go b/cmd/agebox/commands/encrypt.go index 6c90adb..2b0a53e 100644 --- a/cmd/agebox/commands/encrypt.go +++ b/cmd/agebox/commands/encrypt.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "regexp" "gopkg.in/alecthomas/kingpin.v2" @@ -21,6 +22,7 @@ type encryptCommand struct { Files []string EncryptAll bool DryRun bool + RegexFilter *regexp.Regexp } // NewEncryptCommand returns the encrypt command. @@ -30,6 +32,7 @@ func NewEncryptCommand(app *kingpin.Application) Command { cmd.Flag("public-keys", "Path to public keys.").Default("keys").Short('p').StringVar(&c.PubKeysPath) cmd.Flag("all", "Encrypts all tracked files.").Short('a').BoolVar(&c.EncryptAll) cmd.Flag("dry-run", "Enables dry run mode, write operations will be ignored").BoolVar(&c.DryRun) + cmd.Flag("filter", "Encrypts only the filenames (without encrypted extension) that match the provided regex").Short('f').RegexpVar(&c.RegexFilter) cmd.Arg("files", "Files to encrypt.").StringsVar(&c.Files) return c @@ -85,6 +88,7 @@ func (e encryptCommand) Run(ctx context.Context, config RootConfig) error { secretIDProc := process.NewIDProcessorChain( process.NewPathSanitizer(""), process.NewIgnoreAlreadyProcessed(map[string]struct{}{}), // This should be after pathSanitizer. + process.NewIncludeRegexMatch(e.RegexFilter, logger), process.NewEncryptionPathState(secretRepo, logger), ) diff --git a/cmd/agebox/commands/untrack.go b/cmd/agebox/commands/untrack.go index 8487335..7a9dcdb 100644 --- a/cmd/agebox/commands/untrack.go +++ b/cmd/agebox/commands/untrack.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "os" + "regexp" "gopkg.in/alecthomas/kingpin.v2" @@ -15,9 +16,10 @@ import ( ) type untrackCommand struct { - Files []string - Delete bool - DryRun bool + Files []string + Delete bool + DryRun bool + RegexFilter *regexp.Regexp } // NewUntrackCommand returns the untrack command. @@ -27,6 +29,7 @@ func NewUntrackCommand(app *kingpin.Application) Command { cmd.Alias("rm") cmd.Flag("dry-run", "Enables dry run mode, write operations will be ignored").BoolVar(&c.DryRun) cmd.Flag("delete", "Deletes the untracked files, encrypted or decrypted").BoolVar(&c.Delete) + cmd.Flag("filter", "Untracks only the filenames (without encrypted extension) that match the provided regex").Short('f').RegexpVar(&c.RegexFilter) cmd.Arg("files", "Files to decrypt.").StringsVar(&c.Files) return c @@ -71,6 +74,7 @@ func (u untrackCommand) Run(ctx context.Context, config RootConfig) error { secretIDProc := process.NewIDProcessorChain( process.NewPathSanitizer(""), process.NewIgnoreAlreadyProcessed(map[string]struct{}{}), // This should be after pathSanitizer. + process.NewIncludeRegexMatch(u.RegexFilter, logger), process.NewTrackedState(tracked.EncryptedSecrets, true, logger), ) diff --git a/internal/secret/process/process.go b/internal/secret/process/process.go index b30fa3b..9eb9129 100644 --- a/internal/secret/process/process.go +++ b/internal/secret/process/process.go @@ -3,6 +3,7 @@ package process import ( "context" "fmt" + "regexp" "sync" "github.com/slok/agebox/internal/log" @@ -100,3 +101,23 @@ func NewTrackedState(trackedSecretIDs map[string]struct{}, ignoreMissing bool, l return "", fmt.Errorf("%q secret untracked", secretID) }) } + +// NewIncludeRegexMatch will ignore all the secrets that don't match the provided regex. +// If the regex is nil it will match all (Noop). +func NewIncludeRegexMatch(regex *regexp.Regexp, logger log.Logger) IDProcessor { + // Match all. + if regex == nil { + return NoopIDProcessor + } + + return IDProcessorFunc(func(ctx context.Context, secretID string) (string, error) { + logger := logger.WithValues(log.Kv{"secret-id": secretID}) + + if regex.MatchString(secretID) { + return secretID, nil + } + + logger.Debugf("secret ignored by regex unmatch") + return "", nil + }) +} diff --git a/internal/secret/process/process_test.go b/internal/secret/process/process_test.go index 33d76f9..d99cd70 100644 --- a/internal/secret/process/process_test.go +++ b/internal/secret/process/process_test.go @@ -3,6 +3,7 @@ package process_test import ( "context" "fmt" + "regexp" "testing" "github.com/stretchr/testify/assert" @@ -192,3 +193,50 @@ func TestTrackedState(t *testing.T) { }) } } + +func TestIncludeRegexMatch(t *testing.T) { + tests := map[string]struct { + regex *regexp.Regexp + secret string + expSecret string + expErr bool + }{ + "A nil regex should match everything.": { + secret: "test1", + expSecret: "test1", + }, + + "A matching secret should be allowed (all).": { + regex: regexp.MustCompile(".*"), + secret: "test1", + expSecret: "test1", + }, + + "A matching secret should be allowed.": { + regex: regexp.MustCompile("^test[0-9]$"), + secret: "test2", + expSecret: "test2", + }, + + "A not matching secret should be ignored.": { + regex: regexp.MustCompile("^notest[0-9]$"), + secret: "test1", + expSecret: "", + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + p := process.NewIncludeRegexMatch(test.regex, log.Noop) + gotSecret, err := p.ProcessID(context.TODO(), test.secret) + + if test.expErr { + assert.Error(err) + } else if assert.NoError(err) { + assert.Equal(test.expSecret, gotSecret) + } + }) + } +}