diff --git a/pkg/config/config.go b/pkg/config/config.go index 5cfa291c..0363a8e7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -1,8 +1,11 @@ package config import ( + "context" "fmt" + "io" "io/ioutil" + "net/http" "os" "path/filepath" @@ -27,7 +30,13 @@ func NewConfig(filename string) (*Config, error) { var c Config if len(filename) > 0 { var err error - c, err = loadConfig(filename) + + if isValidURL(filename) { + c, err = loadRemoteConfig(filename) + } else { + c, err = loadConfig(filename) + } + if err != nil { return nil, err } @@ -117,13 +126,42 @@ func (c *Config) RemoveRule(i int) { func loadConfig(filename string) (c Config, err error) { yamlFile, err := ioutil.ReadFile(filename) + log.Debug().Str("filename", filename).Msg("Adding custom ruleset from") if err != nil { return c, err } - return c, yaml.Unmarshal(yamlFile, &c) } +// gets the remote config from the url provided and returns config +func loadRemoteConfig(url string) (c Config, err error) { + log.Debug().Str("url", url).Msg("Downloading file from") + client := &http.Client{} + ctx := context.Background() + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return c, err + } + resp, err := client.Do(req) + if err != nil { + return c, err + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return c, err + } + defer resp.Body.Close() + + // only parse response body if it is in the response is in the 2xx range + statusOK := resp.StatusCode >= 200 && resp.StatusCode <= 299 + if !statusOK { + return c, fmt.Errorf("unable to download remote config from url. Response code: %v. Response body: %c", resp.StatusCode, body) + } + + log.Debug().Int("HTTP Response Status:", resp.StatusCode).Msg("Valid URL Response") + return c, yaml.Unmarshal(body, &c) +} + func relative(filename string) string { // viper provides an absolute path to the config file, but we want the relative // path to the config file from the current directory to make it easy for woke to ignore it diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index 6711d4d3..8d5d6415 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -42,12 +42,14 @@ func TestNewConfig(t *testing.T) { enabledRules[i] = fmt.Sprintf("%q", c.Rules[i].Name) } + loadedRemoteConfigMsg := `{"level":"debug","filename":"testdata/good.yaml","message":"Adding custom ruleset from"}` + loadedRemoteConfig := `{"level":"debug","filename":"testdata/good.yaml","message":"Adding custom ruleset from"}` loadedConfigMsg := `{"level":"debug","config":"testdata/good.yaml","message":"loaded config file"}` configRulesMsg := fmt.Sprintf(`{"level":"debug","rules":[%s],"message":"config rules"}`, strings.Join(configRules, ",")) defaultRulesMsg := fmt.Sprintf(`{"level":"debug","rules":[%s],"message":"default rules"}`, strings.Join(defaultRules, ",")) allRulesMsg := fmt.Sprintf(`{"level":"debug","rules":[%s],"message":"all enabled rules"}`, strings.Join(enabledRules, ",")) assert.Equal(t, - loadedConfigMsg+"\n"+configRulesMsg+"\n"+defaultRulesMsg+"\n"+allRulesMsg+"\n", + loadedRemoteConfigMsg+"\n"+loadedRemoteConfig+"\n"+loadedConfigMsg+"\n"+configRulesMsg+"\n"+defaultRulesMsg+"\n"+allRulesMsg+"\n", out.String()) }) @@ -206,8 +208,29 @@ func TestNewConfig(t *testing.T) { assert.EqualValues(t, expected.Rules, c.Rules) assert.Equal(t, "No findings found.", c.GetSuccessExitMessage()) }) -} + t.Run("load-config-with-bad-url", func(t *testing.T) { + _, err := NewConfig("https://raw.githubusercontent.com/get-woke/woke/main/example") + assert.Error(t, err) + }) + + t.Run("load-config-with-url", func(t *testing.T) { + c, err := NewConfig("https://raw.githubusercontent.com/get-woke/woke/main/example.yaml") + assert.NoError(t, err) + assert.NotNil(t, c) + }) + + t.Run("load-remote-config-valid-url", func(t *testing.T) { + c, err := loadRemoteConfig("https://raw.githubusercontent.com/get-woke/woke/main/example.yaml") + assert.NoError(t, err) + assert.NotNil(t, c) + }) + + t.Run("load-remote-config-invalid-url", func(t *testing.T) { + _, err := loadRemoteConfig("https://raw.githubusercontent.com/get-woke/woke/main/example") + assert.Error(t, err) + }) +} func Test_relative(t *testing.T) { cwd, err := os.Getwd() assert.NoError(t, err) diff --git a/pkg/config/remote.go b/pkg/config/remote.go new file mode 100644 index 00000000..b048bbde --- /dev/null +++ b/pkg/config/remote.go @@ -0,0 +1,22 @@ +package config + +import ( + "net/url" + + "github.com/rs/zerolog/log" +) + +// isValidUrl tests a string to determine if it is a valid URL or not +func isValidURL(toTest string) bool { + _, err := url.ParseRequestURI(toTest) + if err != nil { + return false + } + + u, err := url.Parse(toTest) + if err != nil || u.Scheme == "" || u.Host == "" { + return false + } + log.Debug().Str("remoteConfig", toTest).Msg("Valid URL for remote config.") + return true +} diff --git a/pkg/config/remote_test.go b/pkg/config/remote_test.go new file mode 100644 index 00000000..cc101277 --- /dev/null +++ b/pkg/config/remote_test.go @@ -0,0 +1,34 @@ +package config + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func Test_isValidURL(t *testing.T) { + t.Run("valid-url-test1", func(t *testing.T) { + boolResponse := isValidURL("https://raw.githubusercontent.com/get-woke/woke/main/example.yaml") + assert.True(t, boolResponse) + }) + + t.Run("invalid-url-test1", func(t *testing.T) { + boolResponse := isValidURL("Users/Document/test.yaml") + assert.False(t, boolResponse) + }) + + t.Run("invalid-url-test2", func(t *testing.T) { + boolResponse := isValidURL("/Users/Document/test.yaml") + assert.False(t, boolResponse) + }) + + t.Run("invalid-url-test3", func(t *testing.T) { + boolResponse := isValidURL("C:User\testpath\test.yaml") + assert.False(t, boolResponse) + }) + + t.Run("invalid-url-test4", func(t *testing.T) { + boolResponse := isValidURL("C:\\directory.com\test.yaml") + assert.False(t, boolResponse) + }) +}