Skip to content

Commit 34a73b6

Browse files
committed
feat(cli): the new lacework-cli MVP 🔥🔥
This is the MVP of the `lacework-cli` tool! Signed-off-by: Salim Afiune Maya <[email protected]>
1 parent 4ae8b8e commit 34a73b6

File tree

8 files changed

+489
-67
lines changed

8 files changed

+489
-67
lines changed

api/client.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,3 +93,8 @@ func WithURL(baseURL string) Option {
9393
return nil
9494
})
9595
}
96+
97+
// URL returns the base url configured
98+
func (c *Client) URL() string {
99+
return c.baseURL.String()
100+
}

cli/VERSION

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
0.1.0

cli/cmd/api.go

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
//
2+
// Author:: Salim Afiune Maya (<[email protected]>)
3+
// Copyright:: Copyright 2020, Lacework Inc.
4+
// License:: Apache License, Version 2.0
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
package cmd
20+
21+
import (
22+
"fmt"
23+
"strings"
24+
25+
"github.com/pkg/errors"
26+
"github.com/spf13/cobra"
27+
28+
"github.com/lacework/go-sdk/api"
29+
"github.com/lacework/go-sdk/internal/array"
30+
)
31+
32+
var (
33+
// list of valid API methods
34+
validApiMethods = []string{"get", "post", "delete", "patch"}
35+
36+
// data to send for POST/PATCH request
37+
apiData string
38+
39+
// apiCmd represents the api command
40+
apiCmd = &cobra.Command{
41+
Use: "api <method> <path>",
42+
Short: "Helper to call Lacework's ResfulAPI",
43+
Long: `Use this helper to call any available Lacework API endpoint.
44+
45+
An example, list all integrations configured in your account:
46+
47+
lacework-cli api get /external/integrations
48+
49+
For a complete list of available API endpoints visit:
50+
51+
https://<ACCOUNT>.lacework.net/api/v1/external/docs
52+
`,
53+
Args: argsApiValidator,
54+
RunE: runApiCommand,
55+
}
56+
)
57+
58+
func init() {
59+
rootCmd.AddCommand(apiCmd)
60+
61+
apiCmd.Flags().StringVarP(&apiData,
62+
"data", "d", "",
63+
"data to send only for post and patch requests",
64+
)
65+
}
66+
67+
func runApiCommand(cmd *cobra.Command, args []string) error {
68+
switch args[0] {
69+
case "post", "patch":
70+
if apiData == "" {
71+
return fmt.Errorf("missing '--data' parameter for post or patch requests")
72+
}
73+
case "delete", "get":
74+
if apiData != "" {
75+
return fmt.Errorf("use '--data' only for post and patch requests")
76+
}
77+
}
78+
79+
lacework, err := api.NewClient(cli.Account,
80+
api.WithApiKeys(cli.KeyID, cli.Secret),
81+
)
82+
if err != nil {
83+
return errors.Wrap(err, "unable to generate Lacework API client")
84+
}
85+
86+
cli.Log.Debugw("api client generated",
87+
"version", lacework.ApiVersion(),
88+
"base_url", lacework.URL(),
89+
)
90+
91+
response := new(map[string]interface{})
92+
err = lacework.RequestDecoder(
93+
strings.ToUpper(args[0]),
94+
strings.TrimPrefix(args[1], "/"),
95+
strings.NewReader(apiData),
96+
response,
97+
)
98+
if err != nil {
99+
return errors.Wrap(err, "unable to send the request")
100+
}
101+
102+
pretty, err := cli.JsonF.Marshal(*response)
103+
if err != nil {
104+
cli.Log.Debugw("api response", "raw", response)
105+
return errors.Wrap(err, "unable to format json response")
106+
}
107+
108+
fmt.Println(string(pretty))
109+
return nil
110+
}
111+
112+
func argsApiValidator(_ *cobra.Command, args []string) error {
113+
if len(args) != 2 {
114+
return errors.New("requires 2 argument. (method and path)")
115+
}
116+
if !array.ContainsStr(validApiMethods, args[0]) {
117+
return fmt.Errorf(
118+
"invalid method specified: '%s' (valid methods are %s)",
119+
args[0], validApiMethods,
120+
)
121+
}
122+
return nil
123+
}

cli/cmd/integration.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
//
2+
// Author:: Salim Afiune Maya (<[email protected]>)
3+
// Copyright:: Copyright 2020, Lacework Inc.
4+
// License:: Apache License, Version 2.0
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
package cmd
20+
21+
import (
22+
"fmt"
23+
24+
"github.com/pkg/errors"
25+
"github.com/spf13/cobra"
26+
27+
"github.com/lacework/go-sdk/api"
28+
)
29+
30+
var (
31+
// integrationCmd represents the integration command
32+
integrationCmd = &cobra.Command{
33+
Use: "integration",
34+
Short: "Manage external integrations",
35+
}
36+
37+
// integrationListCmd represents the list sub-command inside the integration command
38+
instegrationListCmd = &cobra.Command{
39+
Use: "list",
40+
Short: "List all available external integrations",
41+
RunE: func(cmd *cobra.Command, args []string) error {
42+
lacework, err := api.NewClient(cli.Account,
43+
api.WithApiKeys(cli.KeyID, cli.Secret),
44+
)
45+
if err != nil {
46+
return errors.Wrap(err, "unable to generate Lacework API client")
47+
}
48+
49+
cli.Log.Debugw("api client generated",
50+
"version", lacework.ApiVersion(),
51+
"base_url", lacework.URL(),
52+
)
53+
54+
integrations, err := lacework.Integrations.List()
55+
if err != nil {
56+
return errors.Wrap(err, "unable to get integrations")
57+
}
58+
59+
fmt.Println(integrations.String())
60+
return nil
61+
},
62+
}
63+
64+
// integrationCreateCmd represents the create sub-command inside the integration command
65+
instegrationCreateCmd = &cobra.Command{
66+
Use: "create",
67+
Short: "Create an external integrations",
68+
RunE: func(cmd *cobra.Command, args []string) error {
69+
return nil
70+
},
71+
}
72+
73+
// integrationDeleteCmd represents the delete sub-command inside the integration command
74+
instegrationDeleteCmd = &cobra.Command{
75+
Use: "delete",
76+
Short: "Delete an external integrations",
77+
RunE: func(cmd *cobra.Command, args []string) error {
78+
return nil
79+
},
80+
}
81+
)
82+
83+
func init() {
84+
// add the integration command
85+
rootCmd.AddCommand(integrationCmd)
86+
87+
// add sub-commands to the integration command
88+
integrationCmd.AddCommand(instegrationListCmd)
89+
integrationCmd.AddCommand(instegrationCreateCmd)
90+
integrationCmd.AddCommand(instegrationDeleteCmd)
91+
}

cli/cmd/root.go

Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
//
2+
// Author:: Salim Afiune Maya (<[email protected]>)
3+
// Copyright:: Copyright 2020, Lacework Inc.
4+
// License:: Apache License, Version 2.0
5+
//
6+
// Licensed under the Apache License, Version 2.0 (the "License");
7+
// you may not use this file except in compliance with the License.
8+
// You may obtain a copy of the License at
9+
//
10+
// http://www.apache.org/licenses/LICENSE-2.0
11+
//
12+
// Unless required by applicable law or agreed to in writing, software
13+
// distributed under the License is distributed on an "AS IS" BASIS,
14+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15+
// See the License for the specific language governing permissions and
16+
// limitations under the License.
17+
//
18+
19+
package cmd
20+
21+
import (
22+
"fmt"
23+
"os"
24+
25+
"github.com/spf13/cobra"
26+
27+
prettyjson "github.com/hokaccha/go-prettyjson"
28+
homedir "github.com/mitchellh/go-homedir"
29+
"github.com/spf13/viper"
30+
"go.uber.org/zap"
31+
)
32+
33+
type cliState struct {
34+
Account string
35+
KeyID string
36+
Secret string
37+
Token string
38+
LogLevel string
39+
40+
JsonF *prettyjson.Formatter
41+
Log *zap.SugaredLogger
42+
}
43+
44+
// rootCmd represents the base command when called without any subcommands
45+
var (
46+
cfgFile string
47+
cli = cliState{
48+
JsonF: prettyjson.NewFormatter(),
49+
}
50+
rootCmd = &cobra.Command{
51+
Use: "lacework-cli",
52+
Short: "A tool to manage your Lacework cloud security platform.",
53+
PersistentPreRun: loadStateFromViper,
54+
SilenceErrors: true,
55+
Long: `
56+
The Lacework Command Line Interface is a tool that helps you manage your
57+
Lacework cloud security platform. You can use it to manage compliance
58+
reports, external integrations, vulnerability scans, and other operations.`,
59+
}
60+
)
61+
62+
// Execute adds all child commands to the root command and sets flags appropriately.
63+
// This is called by main.main(). It only needs to happen once to the rootCmd.
64+
func Execute() {
65+
if err := rootCmd.Execute(); err != nil {
66+
fmt.Printf("Error: %s\n", err)
67+
os.Exit(1)
68+
}
69+
}
70+
71+
func init() {
72+
cobra.OnInitialize(initConfig)
73+
74+
rootCmd.PersistentFlags().StringVarP(&cfgFile,
75+
"config", "c", "",
76+
"config file (default is $HOME/.lacework.toml)",
77+
)
78+
rootCmd.PersistentFlags().Bool("debug", false,
79+
"turn on debug logging",
80+
)
81+
rootCmd.PersistentFlags().StringP("api_key", "k", "",
82+
"access key id",
83+
)
84+
rootCmd.PersistentFlags().StringP("api_secret", "s", "",
85+
"access secret key",
86+
)
87+
rootCmd.PersistentFlags().StringP("account", "a", "",
88+
"account subdomain of URL (i.e. <ACCOUNT>.lacework.net)",
89+
)
90+
91+
checkBindError(viper.BindPFlag("account", rootCmd.PersistentFlags().Lookup("account")))
92+
checkBindError(viper.BindPFlag("api_key", rootCmd.PersistentFlags().Lookup("api_key")))
93+
checkBindError(viper.BindPFlag("api_secret", rootCmd.PersistentFlags().Lookup("api_secret")))
94+
checkBindError(viper.BindPFlag("debug", rootCmd.PersistentFlags().Lookup("debug")))
95+
}
96+
97+
// initConfig reads in config file and ENV variables if set
98+
func initConfig() {
99+
if cfgFile != "" {
100+
// Use config file from flag
101+
viper.SetConfigFile(cfgFile)
102+
} else {
103+
// Find home directory
104+
home, err := homedir.Dir()
105+
if err != nil {
106+
fmt.Println(err)
107+
os.Exit(1)
108+
}
109+
110+
// Search config in home directory with name ".lacework" (without extension)
111+
viper.AddConfigPath(home)
112+
viper.SetConfigName(".lacework")
113+
}
114+
115+
viper.SetConfigType("toml") // set TOML as the config format
116+
viper.SetEnvPrefix("LACEWORK") // set prefix for all env variables LW_ABC
117+
viper.AutomaticEnv() // read in environment variables that match
118+
119+
// Initialize zap logger
120+
initializeCliLogger()
121+
122+
// If a config file is found, read it in
123+
if err := viper.ReadInConfig(); err == nil {
124+
cli.Log.Debugw("using config file",
125+
"path", viper.ConfigFileUsed(),
126+
)
127+
}
128+
}
129+
130+
// initializeCliLogger initializes the cli logger, by default we assume production,
131+
// but if debug mode is turned on, we switch to development
132+
func initializeCliLogger() {
133+
var (
134+
log *zap.Logger
135+
err error
136+
)
137+
138+
if viper.GetBool("debug") {
139+
log, err = zap.NewDevelopment()
140+
} else {
141+
log, err = zap.NewProduction()
142+
}
143+
144+
// if we find any error initializing zap, default to a standard logger
145+
if err != nil {
146+
fmt.Printf("Error: unable to initialize zap logger: %v\n", err)
147+
cli.Log = zap.NewExample().Sugar()
148+
} else {
149+
cli.Log = log.Sugar()
150+
}
151+
}
152+
153+
func checkBindError(err error) {
154+
if err != nil {
155+
cli.Log.Debugw("unable to bind parameter", "error", err.Error())
156+
}
157+
}
158+
159+
func loadStateFromViper(_ *cobra.Command, _ []string) {
160+
cli.KeyID = viper.GetString("api_key")
161+
cli.Secret = viper.GetString("api_secret")
162+
cli.Account = viper.GetString("account")
163+
if viper.GetBool("debug") {
164+
cli.LogLevel = "info"
165+
} else {
166+
cli.LogLevel = "debug"
167+
}
168+
}

0 commit comments

Comments
 (0)