Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor search and implement replace command #69

Merged
merged 5 commits into from
Feb 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Core features are:

- recursive operations on paths with `cp`, `mv` or `rm`
- search with `grep` (substring or regular-expression)
- substitute patterns in keys and/or values (substring or regular-expression) with `replace`
- transparency towards differences between KV1 and KV2, i.e., you can freely move/copy secrets between both
- non-interactive mode for automation (`vsh -c "<cmd>"`)
- merging keys with different strategies through `append`
Expand All @@ -37,17 +38,18 @@ Download latest static binaries from [release page](https://github.com/fishi0x01
## Supported commands

```text
mv <from-path> <to-path>
cp <from-path> <to-path>
append <from-secret> <to-secret> [flag]
rm <dir-path or filel-path>
ls <dir-path // optional>
grep <search> <path> [-e|--regexp] [-k|--keys] [-v|--values]
cd <dir-path>
cat <file-path>
cd <dir-path>
cp <from-path> <to-path>
grep <search> <path> [-e|--regexp] [-k|--keys] [-v|--values]
ls <dir-path // optional>
mv <from-path> <to-path>
replace <search> <replacement> <path> [-e|--regexp] [-k|--keys] [-v|--values] [-y|--confirm] [-n|--dry-run]
rm <dir-path or file-path>
```

`cp`, `rm` and `grep` command always have the `-r/-R` flag implied, i.e., every operation works recursively.
`cp`, `grep`, `replace` and `rm` command always have the `-r/-R` flag implied, i.e., every operation works recursively.

### append

Expand Down Expand Up @@ -128,6 +130,10 @@ tree=oak
`grep` recursively searches the given substring in key and value pairs. To treat the search string as a regular-expression, add `-e` or `--regexp` to the end of the command. By default, both keys and values will be searched. If you would like to limit the search, you may add `-k` or `--keys` to the end of the command to search only a path's keys, or `-v` or `--values` to search only a path's values.
If you are looking for copies or just trying to find the path to a certain string, this command might come in handy.

### replace

`replace` works similarly to `grep` above, but has the ability to mutate data inside Vault. By default, confirmation is required before writing data. You may skip confirmation by using the `-y`/`--confirm` flags. Conversely, you may use the `-n`/`--dry-run` flags to skip both confirmation and any writes. Changes that would be made are presented in red (delete) and green (add) coloring.

## Setting the vault token

In order to get a valid token, `vsh` uses vault's TokenHelper mechanism (`github.com/hashicorp/vault/command/config`).
Expand Down
35 changes: 18 additions & 17 deletions cli/command.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cli

import (
"os"
"path/filepath"
"strings"

Expand All @@ -19,27 +18,29 @@ type Command interface {

// Commands contains all available commands
type Commands struct {
Mv *MoveCommand
Cp *CopyCommand
Append *AppendCommand
Rm *RemoveCommand
Ls *ListCommand
Cd *CdCommand
Cat *CatCommand
Grep *GrepCommand
Mv *MoveCommand
Cp *CopyCommand
Append *AppendCommand
Rm *RemoveCommand
Ls *ListCommand
Cd *CdCommand
Cat *CatCommand
Grep *GrepCommand
Replace *ReplaceCommand
}

// NewCommands returns a Commands struct with all available commands
func NewCommands(client *client.Client) *Commands {
return &Commands{
Mv: NewMoveCommand(client),
Cp: NewCopyCommand(client),
Append: NewAppendCommand(client),
Rm: NewRemoveCommand(client),
Ls: NewListCommand(client),
Cd: NewCdCommand(client),
Cat: NewCatCommand(client),
Grep: NewGrepCommand(client, os.Stdout, os.Stderr),
Mv: NewMoveCommand(client),
Cp: NewCopyCommand(client),
Append: NewAppendCommand(client),
Rm: NewRemoveCommand(client),
Ls: NewListCommand(client),
Cd: NewCdCommand(client),
Cat: NewCatCommand(client),
Grep: NewGrepCommand(client),
Replace: NewReplaceCommand(client),
}
}

Expand Down
184 changes: 27 additions & 157 deletions cli/grep.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,27 @@ package cli

import (
"fmt"
"index/suffixarray"
"io"
"path/filepath"
"regexp"
"sort"
"os"

"github.com/fatih/color"
"github.com/fishi0x01/vsh/client"
"github.com/fishi0x01/vsh/log"
)

// GrepMode defines the scope of which parts of a path to search (keys and/or values)
type GrepMode int

const (
// ModeKeys only searches keys
ModeKeys GrepMode = 1
// ModeValues only searches values
ModeValues GrepMode = 2
)

// GrepCommand container for all 'grep' parameters
type GrepCommand struct {
name string

client *client.Client
stderr io.Writer
stdout io.Writer
Path string
Search string
Regexp *regexp.Regexp
Mode GrepMode
}

// Match structure to keep indices of matched terms
type Match struct {
path string
term string
key string
value string
// sorted slices of indices of match starts and length
keyIndex [][]int
valueIndex [][]int
client *client.Client
Path string
searcher *Searcher
SearchParameters
}

// NewGrepCommand creates a new GrepCommand parameter container
func NewGrepCommand(c *client.Client, stdout io.Writer, stderr io.Writer) *GrepCommand {
func NewGrepCommand(c *client.Client) *GrepCommand {
return &GrepCommand{
name: "grep",
client: c,
stdout: stdout,
stderr: stderr,
}
}

Expand All @@ -72,11 +41,6 @@ func (cmd *GrepCommand) PrintUsage() {
log.UserInfo("Usage:\ngrep <search> <path> [-e|--regexp] [-k|--keys] [-v|--values]")
}

// IsMode returns true if the specified mode is enabled
func (cmd *GrepCommand) IsMode(mode GrepMode) bool {
return cmd.Mode&mode == mode
}

// Parse given arguments and return status
func (cmd *GrepCommand) Parse(args []string) error {
if len(args) < 3 {
Expand All @@ -89,11 +53,7 @@ func (cmd *GrepCommand) Parse(args []string) error {
for _, v := range flags {
switch v {
case "-e", "--regexp":
re, err := regexp.Compile(cmd.Search)
if err != nil {
return fmt.Errorf("cannot parse regex pattern")
}
cmd.Regexp = re
cmd.IsRegexp = true
case "-k", "--keys":
cmd.Mode |= ModeKeys
case "-v", "--values":
Expand All @@ -105,24 +65,21 @@ func (cmd *GrepCommand) Parse(args []string) error {
if cmd.Mode == 0 {
cmd.Mode = ModeKeys + ModeValues
}
searcher, err := NewSearcher(cmd)
if err != nil {
return err
}
cmd.searcher = searcher

return nil
}

// Run executes 'grep' with given RemoveCommand's parameters
// Run executes 'grep' with given GrepCommand's parameters
func (cmd *GrepCommand) Run() int {
path := cmdPath(cmd.client.Pwd, cmd.Path)
var filePaths []string

switch t := cmd.client.GetType(path); t {
case client.LEAF:
filePaths = append(filePaths, filepath.Clean(path))
case client.NODE:
for _, traversedPath := range cmd.client.Traverse(path) {
filePaths = append(filePaths, traversedPath)
}
default:
log.UserError("Not a valid path for operation: %s", path)
filePaths, err := cmd.client.SubpathsForPath(path)
if err != nil {
log.UserError(fmt.Sprintf("%s", err))
return 1
}

Expand All @@ -132,12 +89,21 @@ func (cmd *GrepCommand) Run() int {
return 1
}
for _, match := range matches {
match.print(cmd.stdout)
match.print(os.Stdout, false)
}
}
return 0
}

// GetSearchParams returns the search parameters the command was run with
func (cmd *GrepCommand) GetSearchParams() SearchParameters {
return SearchParameters{
Search: cmd.Search,
Mode: cmd.Mode,
IsRegexp: cmd.IsRegexp,
}
}

func (cmd *GrepCommand) grepFile(search string, path string) (matches []*Match, err error) {
matches = []*Match{}

Expand All @@ -148,105 +114,9 @@ func (cmd *GrepCommand) grepFile(search string, path string) (matches []*Match,
}

for k, v := range secret.GetData() {
matches = append(matches, cmd.doMatch(path, k, fmt.Sprintf("%v", v), search)...)
matches = append(matches, cmd.searcher.DoSearch(path, k, fmt.Sprintf("%v", v))...)
}
}

return matches, nil
}

func (cmd *GrepCommand) doMatch(path string, k string, v string, search string) (m []*Match) {
if cmd.Regexp != nil {
return cmd.regexpMatch(path, k, v, cmd.Regexp)
}
return cmd.substrMatch(path, k, v, search)
}

// find all indices for matches in key and value
func (cmd *GrepCommand) substrMatch(path string, k string, v string, substr string) (m []*Match) {
substrLength := len(substr)
keyMatchPairs := make([][]int, 0)
if cmd.IsMode(ModeKeys) {
keyIndex := suffixarray.New([]byte(k))
keyMatches := keyIndex.Lookup([]byte(substr), -1)
sort.Ints(keyMatches)
for _, offset := range keyMatches {
keyMatchPairs = append(keyMatchPairs, []int{offset, substrLength})
}
}

valueMatchPairs := make([][]int, 0)
if cmd.IsMode(ModeValues) {
valueIndex := suffixarray.New([]byte(v))
valueMatches := valueIndex.Lookup([]byte(substr), -1)
sort.Ints(valueMatches)
for _, offset := range valueMatches {
valueMatchPairs = append(valueMatchPairs, []int{offset, substrLength})
}
}

if len(keyMatchPairs) > 0 || len(valueMatchPairs) > 0 {
m = []*Match{
{
path: path,
term: substr,
key: k,
value: v,
keyIndex: keyMatchPairs,
valueIndex: valueMatchPairs,
},
}
}
return m
}

func (cmd *GrepCommand) regexpMatch(path string, k string, v string, pattern *regexp.Regexp) (m []*Match) {
keyMatches := make([][]int, 0)
if cmd.IsMode(ModeKeys) {
keyMatches = pattern.FindAllIndex([]byte(k), -1)
}
valueMatches := make([][]int, 0)
if cmd.IsMode(ModeValues) {
valueMatches = pattern.FindAllIndex([]byte(v), -1)
}

if len(keyMatches) > 0 || len(valueMatches) > 0 {
m = []*Match{
{
path: path,
term: pattern.String(),
key: k,
value: v,
keyIndex: keyMatches,
valueIndex: valueMatches,
},
}
}
return m
}

func (match *Match) print(out io.Writer) {
fmt.Fprint(out, match.path, "> ")
highlightMatches(match.key, match.keyIndex, out)
fmt.Fprintf(out, " = ")
highlightMatches(match.value, match.valueIndex, out)
fmt.Fprintf(out, "\n")
}

func highlightMatches(s string, index [][]int, out io.Writer) {
matchColor := color.New(color.FgYellow).SprintFunc()
cur := 0
if len(index) > 0 {
for _, pair := range index {
next := pair[0]
length := pair[1]
end := next + length
fmt.Fprint(out, s[cur:next])
fmt.Fprint(out, matchColor(s[next:end]))
cur = end
}
fmt.Fprint(out, s[cur:])
} else {
fmt.Fprint(out, s)
}
}
16 changes: 16 additions & 0 deletions cli/kv_mode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package cli

// KeyValueMode defines the scope of which parts of a path to search (keys and/or values)
type KeyValueMode int

const (
// ModeKeys only searches keys
ModeKeys KeyValueMode = 1
// ModeValues only searches values
ModeValues KeyValueMode = 2
)

// KeyValueCommand interface to describe a command that supports Key and/or Value scoping
type KeyValueCommand interface {
IsMode(mode KeyValueMode) bool
}
Loading