Skip to content

Commit

Permalink
Refactor config parsing into a builder pattern. (Velocidex#381)
Browse files Browse the repository at this point in the history
Makes it clear how we fetch the config from different
commands. Depending on the command, some commands accept api config,
some rely on embedded config etc.

This refactor makes it much clearer which config each command support
and how to provide it.

Also added integration tests for repacking, autoexec and config loading order.
  • Loading branch information
scudette authored May 24, 2020
1 parent 99df9e4 commit 7b90f7a
Show file tree
Hide file tree
Showing 53 changed files with 1,100 additions and 648 deletions.
37 changes: 23 additions & 14 deletions bin/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
kingpin "gopkg.in/alecthomas/kingpin.v2"
actions_proto "www.velocidex.com/golang/velociraptor/actions/proto"
artifacts "www.velocidex.com/golang/velociraptor/artifacts"
"www.velocidex.com/golang/velociraptor/config"
config_proto "www.velocidex.com/golang/velociraptor/config/proto"
logging "www.velocidex.com/golang/velociraptor/logging"
"www.velocidex.com/golang/velociraptor/reporting"
Expand All @@ -50,11 +51,11 @@ var (

artifact_command_show_name = artifact_command_show.Arg(
"name", "Name to show.").
HintAction(listArtifacts).String()
HintAction(listArtifactsHint).String()

artifact_command_list_name = artifact_command_list.Arg(
"regex", "Regex of names to match.").
HintAction(listArtifacts).String()
HintAction(listArtifactsHint).String()

artifact_command_list_verbose_count = artifact_command_list.Flag(
"details", "Show more details (Use -d -dd for even more)").
Expand Down Expand Up @@ -82,15 +83,14 @@ var (

artifact_command_collect_name = artifact_command_collect.Arg(
"artifact_name", "The artifact name to collect.").
Required().HintAction(listArtifacts).Strings()
Required().HintAction(listArtifactsHint).Strings()

artifact_command_collect_args = artifact_command_collect.Flag(
"args", "Artifact args.").Strings()
)

func listArtifacts() []string {
config_obj := load_config_or_default()

func listArtifactsHint() []string {
config_obj := config.GetDefaultConfig()
result := []string{}

repository, err := artifacts.GetGlobalRepository(config_obj)
Expand Down Expand Up @@ -286,8 +286,8 @@ func valid_parameter(param_name string, repository *artifacts.Repository) bool {
}

func doArtifactCollect() {
config_obj := load_config_or_default()
load_config_artifacts(config_obj)
config_obj, err := DefaultConfigLoader.WithNullLoader().LoadAndValidate()
kingpin.FatalIfError(err, "Load Config ")

repository, err := getRepository(config_obj)
kingpin.FatalIfError(err, "Loading extra artifacts")
Expand Down Expand Up @@ -380,7 +380,8 @@ func getFilterRegEx(pattern string) (*regexp.Regexp, error) {
}

func doArtifactShow() {
config_obj := load_config_or_default()
config_obj, err := DefaultConfigLoader.WithNullLoader().LoadAndValidate()
kingpin.FatalIfError(err, "Load Config ")
repository, err := getRepository(config_obj)
kingpin.FatalIfError(err, "Loading extra artifacts")

Expand All @@ -394,7 +395,9 @@ func doArtifactShow() {
}

func doArtifactList() {
config_obj := load_config_or_default()
config_obj, err := DefaultConfigLoader.WithNullLoader().LoadAndValidate()
kingpin.FatalIfError(err, "Load Config ")

repository, err := getRepository(config_obj)
kingpin.FatalIfError(err, "Loading extra artifacts")

Expand Down Expand Up @@ -443,17 +446,23 @@ func doArtifactList() {
}
}

func load_config_artifacts(config_obj *config_proto.Config) {
func load_config_artifacts(config_obj *config_proto.Config) error {
if config_obj.Autoexec == nil {
return
return nil
}

repository, err := getRepository(config_obj)
kingpin.FatalIfError(err, "Loading extra artifacts")
if err != nil {
return err
}

for _, definition := range config_obj.Autoexec.ArtifactDefinitions {
_, err := repository.LoadProto(definition, true /* validate */)
kingpin.FatalIfError(err, "Unable to parse config artifact")
if err != nil {
return err
}
}
return nil
}

func init() {
Expand Down
4 changes: 3 additions & 1 deletion bin/artifacts_acquire.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,9 @@ func acquireArtifact(ctx context.Context, config_obj *config_proto.Config,
}

func doArtifactsAcquire() {
config_obj := load_config_or_default()
config_obj, err := DefaultConfigLoader.WithNullLoader().LoadAndValidate()
kingpin.FatalIfError(err, "Load Config ")

repository, err := getRepository(config_obj)
kingpin.FatalIfError(err, "Loading extra artifacts")

Expand Down
22 changes: 0 additions & 22 deletions bin/autoexec.go

This file was deleted.

232 changes: 232 additions & 0 deletions bin/binary_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
// Tests for the binary.

package main

import (
"bytes"
"fmt"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"runtime"
"testing"

"github.com/alecthomas/assert"
"github.com/stretchr/testify/require"
"github.com/stretchr/testify/suite"
"github.com/tink-ab/tempfile"
"www.velocidex.com/golang/velociraptor/config"
"www.velocidex.com/golang/velociraptor/constants"
)

type MainTestSuite struct {
suite.Suite
binary string
extension string
}

func (self *MainTestSuite) SetupTest() {
if runtime.GOOS == "windows" {
self.extension = ".exe"
}

// Search for a valid binary to run.
binaries, err := filepath.Glob(
"../output/velociraptor*" + constants.VERSION + "-" + runtime.GOOS +
"-" + runtime.GOARCH + self.extension)
assert.NoError(self.T(), err)

if len(binaries) == 0 {
binaries, _ = filepath.Glob("../output/velociraptor*" +
self.extension)
}

self.binary = binaries[0]
fmt.Printf("Found binary %v\n", self.binary)
}

func (self *MainTestSuite) TestHelp() {
cmd := exec.Command(self.binary, "--help")
out, err := cmd.CombinedOutput()
require.NoError(self.T(), err)
require.Contains(self.T(), string(out), "usage:")
}

const autoexec_file = `
autoexec:
argv:
- artifacts
- list
artifact_definitions:
- name: MySpecialArtifact
sources:
- query: SELECT * FROM info()
- name: Windows.Sys.Interfaces
description: Override a built in artifact in the config.
sources:
- query: SELECT "MySpecialInterface" FROM scope()
`

func (self *MainTestSuite) TestAutoexec() {
// Create a tempfile for the repacked binary.
exe, err := tempfile.TempFile("", "exe", self.extension)
assert.NoError(self.T(), err)

defer os.Remove(exe.Name())
exe.Close()

// A temp file for the config.
config_file, err := ioutil.TempFile("", "config")
assert.NoError(self.T(), err)

defer os.Remove(config_file.Name())
config_file.Write([]byte(autoexec_file))
config_file.Close()

// Repack the config in the binary.
cmd := exec.Command(self.binary, "config", "repack", config_file.Name(), exe.Name())
out, err := cmd.Output()
require.NoError(self.T(), err)

// Run the repacked binary with no args - it should run the
// `artifacts list` command.
cmd = exec.Command(exe.Name())
out, err = cmd.Output()
require.NoError(self.T(), err)

// The output should contain MySpecialArtifact as well as the
// standard artifacts.
require.Contains(self.T(), string(out), "MySpecialArtifact")
require.Contains(self.T(), string(out), "Windows.Sys.Users")

// If provided args it works normally.
cmd = exec.Command(exe.Name(),
"artifacts", "collect", "Windows.Sys.Interfaces", "--format", "jsonl")
out, err = cmd.Output()
require.NoError(self.T(), err)

// Config artifacts override built in artifacts.
require.Contains(self.T(), string(out), "MySpecialInterface")
}

func (self *MainTestSuite) TestGenerateConfigWithMerge() {
// A temp file for the generated config.
config_file, err := ioutil.TempFile("", "config")
assert.NoError(self.T(), err)

defer os.Remove(config_file.Name())
defer config_file.Close()

cmd := exec.Command(
self.binary, "config", "generate", "--merge",
`{"Client": {"nonce": "Foo", "writeback_linux": "some_location"}}`)
out, err := cmd.Output()
require.NoError(self.T(), err)

// Write the config to the tmp file
config_file_content := out
config_file.Write(out)
config_file.Close()

// Try to load it now.
config_obj, err := new(config.Loader).WithFileLoader(config_file.Name()).
WithRequiredFrontend().WithRequiredCA().WithRequiredClient().
LoadAndValidate()
require.NoError(self.T(), err)

require.Equal(self.T(), config_obj.Client.Nonce, "Foo")
require.Equal(self.T(), config_obj.Client.WritebackLinux, "some_location")

// Try to show a config without a flag.
cmd = exec.Command(self.binary, "config", "show")
cmd.Env = append(os.Environ(),
"VELOCIRAPTOR_CONFIG=",
)
out, err = cmd.Output()
require.Error(self.T(), err)

// Specify the config on the commandline
cmd = exec.Command(self.binary, "config", "show", "--config", config_file.Name())
cmd.Env = append(os.Environ(),
"VELOCIRAPTOR_CONFIG=",
)
out, err = cmd.Output()
require.NoError(self.T(), err)
require.Contains(self.T(), string(out), "Foo")

// Specify the config in the environment
cmd = exec.Command(self.binary, "config", "show")
cmd.Env = append(os.Environ(),
"VELOCIRAPTOR_CONFIG="+config_file.Name(),
)
out, err = cmd.Output()
require.NoError(self.T(), err)
require.Contains(self.T(), string(out), "Foo")

// Specifying invalid config in the flag is a hard stop - even
// if there is a valid environ.
cmd = exec.Command(self.binary, "config", "show", "--config", "XXXX")
cmd.Env = append(os.Environ(),
"VELOCIRAPTOR_CONFIG="+config_file.Name(),
)
out, err = cmd.Output()
require.Error(self.T(), err)

// Create a tempfile for the repacked binary.
exe, err := tempfile.TempFile("", "exe", self.extension)
assert.NoError(self.T(), err)

defer os.Remove(exe.Name())
exe.Close()

// Repack the config in the binary.
cmd = exec.Command(self.binary, "config", "repack", config_file.Name(), exe.Name())
out, err = cmd.Output()
require.NoError(self.T(), err)

// Run the repacked binary with invalid environ - config
// should come from embedded.
cmd = exec.Command(exe.Name(), "config", "show")
cmd.Env = append(os.Environ(),
"VELOCIRAPTOR_CONFIG=XXXX",
)
out, err = cmd.Output()
require.NoError(self.T(), err)
require.Contains(self.T(), string(out), "Foo")

// Make second copy of config file and store modified version
second_config_file, err := ioutil.TempFile("", "config")
assert.NoError(self.T(), err)

defer os.Remove(second_config_file.Name())

// Second file has no Foo in it
second_config_file_content := bytes.ReplaceAll(
config_file_content, []byte(`Foo`), []byte(`Bar`))
second_config_file.Write(second_config_file_content)
second_config_file.Close()

// Check that embedded binary with config will use its
// embedded version, even if an env var is specified.
cmd = exec.Command(exe.Name(), "config", "show")
cmd.Env = append(os.Environ(),
"VELOCIRAPTOR_CONFIG="+second_config_file.Name(),
)
out, err = cmd.Output()
require.NoError(self.T(), err)
require.Contains(self.T(), string(out), "Foo")

// If a config is explicitly specified, it will override even the
// embedded config.
cmd = exec.Command(exe.Name(), "config", "show", "--config", second_config_file.Name())
out, err = cmd.Output()
require.NoError(self.T(), err)

// loaded config contains Bar
require.Contains(self.T(), string(out), "Bar")
}

func TestMainProgram(t *testing.T) {
suite.Run(t, &MainTestSuite{})
}
6 changes: 3 additions & 3 deletions bin/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"sync"

kingpin "gopkg.in/alecthomas/kingpin.v2"
"www.velocidex.com/golang/velociraptor/config"
"www.velocidex.com/golang/velociraptor/crypto"
"www.velocidex.com/golang/velociraptor/executor"
"www.velocidex.com/golang/velociraptor/http_comms"
Expand All @@ -41,10 +40,11 @@ func RunClient(
config_path *string) {

// Include the writeback in the client's configuration.
config_obj, err := config.LoadConfigWithWriteback(*config_path)
config_obj, err := DefaultConfigLoader.WithRequiredClient().
WithWriteback().LoadAndValidate()
kingpin.FatalIfError(err, "Unable to load config file")

// Make sure the config is ok.
// Make sure the config crypto is ok.
err = crypto.VerifyConfig(config_obj)
if err != nil {
kingpin.FatalIfError(err, "Invalid config")
Expand Down
Loading

0 comments on commit 7b90f7a

Please sign in to comment.