From 5ee26b984b4bdcaeb709d4578037490981c2aa5d Mon Sep 17 00:00:00 2001 From: Alexander Kadyrov Date: Sat, 18 May 2024 16:02:53 +0400 Subject: [PATCH] Combine all apps into one single app Resolves #3 --- Makefile | 15 -- README.md | 90 +++++++--- cmd/dotenvanalyzer/main.go | 55 ------- cmd/envanalyzer/main.go | 55 ------- cmd/web3safe/main.go | 30 ++-- cmd/yamlanalyzer/main.go | 55 ------- internal/app/app.go | 57 +++++++ internal/app/error.go | 21 +++ internal/commands/generate-config/command.go | 164 +++++++++++++++++++ internal/commands/runner.go | 93 +++++++++++ internal/commands/scan-dotenv/command.go | 125 ++++++++++++++ internal/commands/scan-shell-env/command.go | 89 ++++++++++ internal/commands/scan-yaml/command.go | 125 ++++++++++++++ internal/config/config.go | 63 +------ internal/dotenvanalyzer/dotenvanalyzer.go | 117 ++++++++----- internal/dotenvanalyzer/flags.go | 18 -- internal/flags/flags.go | 16 -- internal/utils/exitCodes.go | 7 + internal/utils/utils.go | 53 ++++++ internal/yamlanalyzer/flags.go | 18 -- internal/yamlanalyzer/yamlanalyzer.go | 106 +++++++----- 21 files changed, 964 insertions(+), 408 deletions(-) delete mode 100644 cmd/dotenvanalyzer/main.go delete mode 100644 cmd/envanalyzer/main.go delete mode 100644 cmd/yamlanalyzer/main.go create mode 100644 internal/app/app.go create mode 100644 internal/app/error.go create mode 100644 internal/commands/generate-config/command.go create mode 100644 internal/commands/runner.go create mode 100644 internal/commands/scan-dotenv/command.go create mode 100644 internal/commands/scan-shell-env/command.go create mode 100644 internal/commands/scan-yaml/command.go delete mode 100644 internal/dotenvanalyzer/flags.go delete mode 100644 internal/flags/flags.go create mode 100644 internal/utils/exitCodes.go create mode 100644 internal/utils/utils.go delete mode 100644 internal/yamlanalyzer/flags.go diff --git a/Makefile b/Makefile index a37dfeb..310dc93 100644 --- a/Makefile +++ b/Makefile @@ -10,27 +10,12 @@ BIN_DIR=./bin WEB3SAFE_BINARY_NAME=web3safe WEB3SAFE_CMD_DIR=$(CMD_DIR)/$(WEB3SAFE_BINARY_NAME) -DOTENVANALYZER_BINARY_NAME=dotenvanalyzer -DOTENVANALYZER_CMD_DIR=$(CMD_DIR)/$(DOTENVANALYZER_BINARY_NAME) - -ENVANALYZER_BINARY_NAME=envanalyzer -ENVANALYZER_CMD_DIR=$(CMD_DIR)/$(ENVANALYZER_BINARY_NAME) - -YAMLANALYZER_BINARY_NAME=yamlanalyzer -YAMLANALYZER_CMD_DIR=$(CMD_DIR)/$(YAMLANALYZER_BINARY_NAME) - build: clean $(GOBUILD) -o $(BIN_DIR)/$(WEB3SAFE_BINARY_NAME) $(WEB3SAFE_CMD_DIR)/... - $(GOBUILD) -o $(BIN_DIR)/$(DOTENVANALYZER_BINARY_NAME) $(DOTENVANALYZER_CMD_DIR)/... - $(GOBUILD) -o $(BIN_DIR)/$(ENVANALYZER_BINARY_NAME) $(ENVANALYZER_CMD_DIR)/... - $(GOBUILD) -o $(BIN_DIR)/$(YAMLANALYZER_BINARY_NAME) $(YAMLANALYZER_CMD_DIR)/... clean: $(GOCLEAN) rm -f $(WEB3SAFE_BINARY_NAME) - rm -f $(DOTENVANALYZER_BINARY_NAME) - rm -f $(ENVANALYZER_BINARY_NAME) - rm -f $(YAMLANALYZER_BINARY_NAME) deps: $(GOCMD) mod tidy diff --git a/README.md b/README.md index 04f3411..d0b8a32 100644 --- a/README.md +++ b/README.md @@ -45,55 +45,91 @@ Web3Safe is a command-line tool written in Go. To install it, follow these steps make build ``` -3. All apps will be placed inside `bin` directory: +3. App will be placed inside `bin` directory: ``` - dotenvanalyzer - envanalyzer - yamlanalyzer web3safe ``` -Web3Safe includes three tools: Web3Safe itself, Shell ENV Analyzer and Dotenv -Analyzer. +### Docker -### Web3Safe +TBD -This tool is designed for creating a configuration file for the other apps. +## Usage -- `-help`: Show all the available commands. -- `-generateConfig`: Generate a new configuration file. +### Create a new configuration file -### EnvAnalyzer +```sh +web3safe config -create [-config "/path/to/config.yml"] [-force] +``` -This tool scans the current user's environment variables and display any +### Print the default config (or a given config) to your terminal + +```sh +web3safe config -print [-config "/path/to/config.yml"] +``` + +### Analyze shell ENV variables + +This tool scans the current user's shell environment variables and display any sensitive information found. -You can also customize the analysis by providing additional flags: +```sh +web3safe shellenv [-config "/path/to/config.yml"] +``` + +Example: + +```sh +$ MNEMONIC=test web3safe shellenv -- `-help`: Show all the available commands. -- `-config`: Specify a custom configuration file for rule customization. +Shell ENV has a sensitive variable: MNEMONIC +``` -### DotEnvAnalyzer +### Analyze dotenv (.env) files -By default, this tool scans .env files starting from a given directory -recursively and display any sensitive information found inside `.env` files. +```sh +web3safe dotenv [-config "/path/to/config.yml"] +``` You can also customize the analysis by providing additional flags: -- `-help`: Show all the available commands. -- `-config`: Specify a custom configuration file for rule customization. -- `-path`: Path to start scan from (default: current dir). +- `-dir`: Path to the directory to scan +- `-recursive`: If set, the directory will be scanned recursively +- `-file`: Path to the file to scan -### YamlAnalyzer +Example: -By default, this tool scans YAML files (`yml` and `yaml`) starting from a given -directory recursively and display any sensitive information found inside files. +```sh +$ web3safe dotenv -dir . -recursive + +samples/.env:5: found sensitive variable MNEMONIC_WORDS +samples/.env:7: found sensitive variable private_key +samples/.env.export:1: found sensitive variable PRIVATE_KEY +samples/.env.export:2: found sensitive variable BINANCE_ACCOUNT_PRIVATE_KEY +``` + +### Analyze YAML files + +```sh +web3safe yaml [-config "/path/to/config.yml"] +``` You can also customize the analysis by providing additional flags: -- `-help`: Show all the available commands. -- `-config`: Specify a custom configuration file for rule customization. -- `-path`: Path to start scan from (default: current dir). +- `-dir`: Path to the directory to scan +- `-recursive`: If set, the directory will be scanned recursively +- `-file`: Path to the file to scan + +Example: + +```sh +$ web3safe yaml -dir . -recursive + +samples/config.yml: found sensitive key "PASSWORD" in .nested.inside.PASSWORD +samples/config.yml: found sensitive key "MNEMONIC" in .nested.inside.MNEMONIC +samples/playbook.yml: found sensitive key "password" in [0].password +samples/playbook.yml: found sensitive key "mnemonic" in [0].env.mnemonic +``` ## Contributing diff --git a/cmd/dotenvanalyzer/main.go b/cmd/dotenvanalyzer/main.go deleted file mode 100644 index ff99980..0000000 --- a/cmd/dotenvanalyzer/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/gruz0/web3safe/internal/config" - "github.com/gruz0/web3safe/internal/dotenvanalyzer" -) - -func main() { - flags := dotenvanalyzer.ParseFlags() - - cfg := loadConfig(flags.ConfigFilePath) - - dotenvAnalyzer := dotenvanalyzer.NewDotEnvAnalyzer(flags.PathToScan, cfg) - - if err := dotenvAnalyzer.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Unable to run dotenvAnalyzer: %v\n", err) - - os.Exit(1) - } - - dotenvAnalyzerReport := dotenvAnalyzer.Report() - - if len(dotenvAnalyzerReport) == 0 { - fmt.Fprintf(os.Stdout, "Nothing found. Great!\n") - - os.Exit(0) - } - - for _, message := range dotenvAnalyzerReport { - fmt.Fprintln(os.Stderr, message) - } - - os.Exit(1) -} - -func loadConfig(configFilePath string) config.Config { - if configFilePath == "" { - fmt.Fprintf(os.Stdout, "No config file provided. We will use the default configuration.\n\n") - - return config.GetDefaultConfig() - } - - fmt.Fprintf(os.Stdout, "Loading configuration file: %s\n", configFilePath) - - loadedConfig, loadConfigErr := config.LoadConfig(configFilePath) - if loadConfigErr != nil { - fmt.Fprintf(os.Stderr, "Unable to load config: %v\n", loadConfigErr) - os.Exit(1) - } - - return loadedConfig -} diff --git a/cmd/envanalyzer/main.go b/cmd/envanalyzer/main.go deleted file mode 100644 index 486f3e3..0000000 --- a/cmd/envanalyzer/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/gruz0/web3safe/internal/config" - "github.com/gruz0/web3safe/internal/envanalyzer" -) - -func main() { - flags := envanalyzer.ParseFlags() - - cfg := loadConfig(flags.ConfigFilePath) - - envAnalyzer := envanalyzer.NewEnvAnalyzer(cfg) - - if err := envAnalyzer.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Unable to run envAnalyzer: %v\n", err) - - os.Exit(1) - } - - envAnalyzerReport := envAnalyzer.Report() - - if len(envAnalyzerReport) == 0 { - fmt.Fprintf(os.Stdout, "Nothing found in ENV. Great!\n") - - os.Exit(0) - } - - for _, message := range envAnalyzerReport { - fmt.Fprintln(os.Stderr, message) - } - - os.Exit(1) -} - -func loadConfig(configFilePath string) config.Config { - if configFilePath == "" { - fmt.Fprintf(os.Stdout, "No config file provided. We will use the default configuration.\n\n") - - return config.GetDefaultConfig() - } - - fmt.Fprintf(os.Stdout, "Loading configuration file: %s\n", configFilePath) - - loadedConfig, loadConfigErr := config.LoadConfig(configFilePath) - if loadConfigErr != nil { - fmt.Fprintf(os.Stderr, "Unable to load config: %v\n", loadConfigErr) - os.Exit(1) - } - - return loadedConfig -} diff --git a/cmd/web3safe/main.go b/cmd/web3safe/main.go index 1b46557..9bddbb8 100644 --- a/cmd/web3safe/main.go +++ b/cmd/web3safe/main.go @@ -1,29 +1,31 @@ package main import ( + "errors" "fmt" "os" - "github.com/gruz0/web3safe/internal/config" - "github.com/gruz0/web3safe/internal/flags" + "github.com/gruz0/web3safe/internal/app" + "github.com/gruz0/web3safe/internal/utils" ) func main() { - flags := flags.ParseFlags() + success, err := app.Run(os.Args) + if err != nil { + var appError *app.Error - if flags.GenerateConfig { - generateConfig() - } -} + if errors.As(err, &appError) { + fmt.Fprintln(os.Stderr, appError.Error()) + os.Exit(appError.ExitCode()) + } -func generateConfig() { - newConfigFilePath := config.GetDefaultConfigFilePath() + fmt.Fprintf(os.Stderr, "Unhandled error: %v", err) + os.Exit(utils.ExitError) + } - if err := config.GenerateConfig(newConfigFilePath); err != nil { - fmt.Fprintf(os.Stderr, "Error generating config: %v\n", err) - os.Exit(1) + if success { + os.Exit(utils.ExitSuccess) } - fmt.Fprintf(os.Stdout, "New configuration file generated at %s\n", newConfigFilePath) - os.Exit(0) + os.Exit(utils.ExitError) } diff --git a/cmd/yamlanalyzer/main.go b/cmd/yamlanalyzer/main.go deleted file mode 100644 index 45dea54..0000000 --- a/cmd/yamlanalyzer/main.go +++ /dev/null @@ -1,55 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/gruz0/web3safe/internal/config" - "github.com/gruz0/web3safe/internal/yamlanalyzer" -) - -func main() { - flags := yamlanalyzer.ParseFlags() - - cfg := loadConfig(flags.ConfigFilePath) - - yamlAnalyzer := yamlanalyzer.NewYamlAnalyzer(flags.PathToScan, cfg) - - if err := yamlAnalyzer.Run(); err != nil { - fmt.Fprintf(os.Stderr, "Unable to run yamlAnalyzer: %v\n", err) - - os.Exit(1) - } - - yamlAnalyzerReport := yamlAnalyzer.Report() - - if len(yamlAnalyzerReport) == 0 { - fmt.Fprintf(os.Stdout, "Nothing found. Great!\n") - - os.Exit(0) - } - - for _, message := range yamlAnalyzerReport { - fmt.Fprintln(os.Stderr, message) - } - - os.Exit(1) -} - -func loadConfig(configFilePath string) config.Config { - if configFilePath == "" { - fmt.Fprintf(os.Stdout, "No config file provided. We will use the default configuration.\n\n") - - return config.GetDefaultConfig() - } - - fmt.Fprintf(os.Stdout, "Loading configuration file: %s\n", configFilePath) - - loadedConfig, loadConfigErr := config.LoadConfig(configFilePath) - if loadConfigErr != nil { - fmt.Fprintf(os.Stderr, "Unable to load config: %v\n", loadConfigErr) - os.Exit(1) - } - - return loadedConfig -} diff --git a/internal/app/app.go b/internal/app/app.go new file mode 100644 index 0000000..44615c7 --- /dev/null +++ b/internal/app/app.go @@ -0,0 +1,57 @@ +package app + +import ( + "errors" + "fmt" + + "github.com/gruz0/web3safe/internal/commands" + generateconfig "github.com/gruz0/web3safe/internal/commands/generate-config" + scandotenv "github.com/gruz0/web3safe/internal/commands/scan-dotenv" + scanuserenv "github.com/gruz0/web3safe/internal/commands/scan-shell-env" + scanyaml "github.com/gruz0/web3safe/internal/commands/scan-yaml" + "github.com/gruz0/web3safe/internal/utils" +) + +const minimumArgsRequired = 2 + +func Run(args []string) (bool, error) { + runner := commands.NewRunner() + + cmds := []commands.CommandHandler{ + generateconfig.New(), + scanuserenv.New(), + scandotenv.New(), + scanyaml.New(), + } + + for _, cmd := range cmds { + if err := runner.Register(cmd); err != nil { + return false, NewError( + fmt.Sprintf("failed to register command: %v", err), + utils.ExitError, + ) + } + } + + if len(args) < minimumArgsRequired { + runner.Help() + + return false, nil + } + + success, err := runner.Run(args[1:]) + if err != nil { + var cmdErr *commands.CommandError + + if errors.As(err, &cmdErr) { + return false, NewError( + err.Error(), + cmdErr.ExitCode(), + ) + } + + return false, fmt.Errorf("Error: %w", err) + } + + return success, nil +} diff --git a/internal/app/error.go b/internal/app/error.go new file mode 100644 index 0000000..f7b8c30 --- /dev/null +++ b/internal/app/error.go @@ -0,0 +1,21 @@ +package app + +type Error struct { + message string + exitCode int +} + +func NewError(message string, exitCode int) *Error { + return &Error{ + message: message, + exitCode: exitCode, + } +} + +func (e *Error) Error() string { + return e.message +} + +func (e *Error) ExitCode() int { + return e.exitCode +} diff --git a/internal/commands/generate-config/command.go b/internal/commands/generate-config/command.go new file mode 100644 index 0000000..dccfad3 --- /dev/null +++ b/internal/commands/generate-config/command.go @@ -0,0 +1,164 @@ +package generateconfig + +import ( + "flag" + "fmt" + "os" + "path/filepath" + + "github.com/gruz0/web3safe/internal/commands" + "github.com/gruz0/web3safe/internal/config" + "github.com/gruz0/web3safe/internal/utils" + "gopkg.in/yaml.v2" +) + +const ( + defaultConfigFilename = "web3safe.yaml" + configDirPermission = 0o755 + configPermission = 0o600 +) + +type Command struct { + fs *flag.FlagSet + printHelp bool + configFilePath string + createConfig bool + printConfig bool + force bool +} + +func New() *Command { + command := &Command{ + fs: flag.NewFlagSet("config", flag.ContinueOnError), + printHelp: false, + configFilePath: "", + createConfig: false, + printConfig: false, + force: false, + } + + command.fs.StringVar(&command.configFilePath, "config", "", "Path to the configuration file") + command.fs.BoolVar(&command.printHelp, "help", false, "Show help") + command.fs.BoolVar(&command.createConfig, "create", false, "Create a config by its default location") + command.fs.BoolVar(&command.printConfig, "print", false, "Print a config") + command.fs.BoolVar(&command.force, "force", false, "Overwrite config") + + return command +} + +func (c *Command) Name() string { + return c.fs.Name() +} + +func (c *Command) PrintHelp() { + c.fs.PrintDefaults() +} + +func (c *Command) ParseArgs(args []string) error { + if err := c.fs.Parse(args); err != nil { + return commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to parse args: %v", err), + utils.ExitInvalidArguments, + ) + } + + return nil +} + +func (c *Command) Run() (bool, error) { + if c.createConfig { + return c.handleCreateConfig(c.configFilePath) + } + + if c.printConfig { + return c.handlePrintConfig(c.configFilePath) + } + + c.PrintHelp() + + return false, nil +} + +func (c *Command) handleCreateConfig(configFilePath string) (bool, error) { + if configFilePath == "" { + configFilePath = filepath.Join(utils.GetDefaultConfigDirectory(), defaultConfigFilename) + } + + if utils.IsFileExist(configFilePath) && !c.force { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("file %s already exists. Please use -force flag to override the config", configFilePath), + utils.ExitError, + ) + } + + yamlData, err := yaml.Marshal(config.GetDefaultConfig()) + if err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to encode config: %v", err), + utils.ExitError, + ) + } + + configDir := filepath.Dir(configFilePath) + + if err := os.MkdirAll(configDir, configDirPermission); err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to create a config directory %s: %v", configDir, err), + utils.ExitError, + ) + } + + err = os.WriteFile(configFilePath, yamlData, configPermission) + if err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to write file %s: %v", configFilePath, err), + utils.ExitError, + ) + } + + fmt.Fprintf(os.Stdout, "New configuration file saved to %s\n", configFilePath) + + return true, nil +} + +func (c *Command) handlePrintConfig(configFilePath string) (bool, error) { + cfg := config.GetDefaultConfig() + + if configFilePath != "" { + loadedConfig, err := config.Load(configFilePath) + if err != nil { + return false, commands.NewCommandError( + c.Name(), + err.Error(), + utils.ExitError, + ) + } + + cfg = loadedConfig + } + + yamlData, err := yaml.Marshal(cfg) + if err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to encode config: %v", err), + utils.ExitError, + ) + } + + _, err = os.Stdout.Write(yamlData) + if err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to write config: %v", err), + utils.ExitError, + ) + } + + return true, nil +} diff --git a/internal/commands/runner.go b/internal/commands/runner.go new file mode 100644 index 0000000..f7fd2ba --- /dev/null +++ b/internal/commands/runner.go @@ -0,0 +1,93 @@ +package commands + +import ( + "errors" + "fmt" + "os" + "strings" + + "github.com/gruz0/web3safe/internal/utils" +) + +var ( + ErrCommandAlreadyRegistered = errors.New("command already registered") + ErrUnknownCommand = errors.New("unknown command") +) + +type CommandError struct { + name string + message string + exitCode int +} + +func NewCommandError(name, message string, exitCode int) *CommandError { + return &CommandError{ + name: name, + message: message, + exitCode: exitCode, + } +} + +func (e *CommandError) Error() string { + return fmt.Sprintf("%s: %v", e.name, e.message) +} + +func (e *CommandError) ExitCode() int { + return e.exitCode +} + +type CommandHandler interface { + ParseArgs(args []string) error + Run() (bool, error) + Name() string + PrintHelp() +} + +type Runner struct { + commands map[string]CommandHandler +} + +func NewRunner() *Runner { + return &Runner{ + commands: make(map[string]CommandHandler), + } +} + +func (r *Runner) Register(command CommandHandler) error { + if _, ok := r.commands[command.Name()]; ok { + return fmt.Errorf("%w: %s", ErrCommandAlreadyRegistered, command.Name()) + } + + r.commands[strings.ToLower(command.Name())] = command + + return nil +} + +func (r *Runner) Help() { + fmt.Fprintf(os.Stdout, "Usage: %s subcommand arguments\n", utils.AppName) + + for name, cmd := range r.commands { + fmt.Fprintf(os.Stdout, "\nSubcommand: %s\n", name) + cmd.PrintHelp() + } +} + +func (r *Runner) Run(args []string) (bool, error) { + commandName := strings.ToLower(args[0]) + + cmd, ok := r.commands[commandName] + if !ok { + return false, fmt.Errorf("%w: %s", ErrUnknownCommand, commandName) + } + + if err := cmd.ParseArgs(args[1:]); err != nil { + return false, fmt.Errorf("%w", err) + } + + success, err := cmd.Run() + if err != nil { + return false, fmt.Errorf("%w", err) + } + + return success, nil +} diff --git a/internal/commands/scan-dotenv/command.go b/internal/commands/scan-dotenv/command.go new file mode 100644 index 0000000..17fdfd4 --- /dev/null +++ b/internal/commands/scan-dotenv/command.go @@ -0,0 +1,125 @@ +package scandotenv + +import ( + "flag" + "fmt" + "os" + + "github.com/gruz0/web3safe/internal/commands" + "github.com/gruz0/web3safe/internal/config" + "github.com/gruz0/web3safe/internal/dotenvanalyzer" + "github.com/gruz0/web3safe/internal/utils" +) + +type Command struct { + fs *flag.FlagSet + configFilePath string + directoryToScan string + fileToScan string + recursive bool +} + +func New() *Command { + command := &Command{ + fs: flag.NewFlagSet("dotenv", flag.ContinueOnError), + configFilePath: "", + directoryToScan: "", + fileToScan: "", + recursive: false, + } + + command.fs.StringVar(&command.configFilePath, "config", "", "Path to configuration file") + command.fs.StringVar(&command.directoryToScan, "dir", "", "Path to the directory to scan") + command.fs.StringVar(&command.fileToScan, "file", "", "Path to the file to scan") + command.fs.BoolVar(&command.recursive, "recursive", false, "Scan directory recursively") + + return command +} + +func (c *Command) Name() string { + return c.fs.Name() +} + +func (c *Command) PrintHelp() { + c.fs.PrintDefaults() +} + +func (c *Command) ParseArgs(args []string) error { + if err := c.fs.Parse(args); err != nil { + return commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to parse args: %v", err), + utils.ExitInvalidArguments, + ) + } + + return nil +} + +func (c *Command) Run() (bool, error) { + if c.directoryToScan == "" && c.fileToScan == "" { + c.PrintHelp() + + return false, nil + } + + cfg, err := c.loadConfig() + if err != nil { + return false, err + } + + dotenvAnalyzer := dotenvanalyzer.NewDotEnvAnalyzer(cfg) + + if c.directoryToScan != "" { + if err := dotenvAnalyzer.ScanDirectory(c.directoryToScan, c.recursive); err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("Unable to analyze dotenv files: %v", err), + utils.ExitError, + ) + } + } + + if c.fileToScan != "" { + if err := dotenvAnalyzer.ScanFile(c.fileToScan); err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("Unable to analyze dotenv file: %v", err), + utils.ExitError, + ) + } + } + + dotenvAnalyzerReport := dotenvAnalyzer.Report() + + if len(dotenvAnalyzerReport) == 0 { + fmt.Fprintln(os.Stdout, "Nothing found. Great!") + + return true, nil + } + + for _, message := range dotenvAnalyzerReport { + fmt.Fprintln(os.Stderr, message) + } + + return false, nil +} + +func (c *Command) loadConfig() (config.Config, error) { + cfg := config.GetDefaultConfig() + + if c.configFilePath != "" { + loadedConfig, err := config.Load(c.configFilePath) + if err != nil { + return cfg, commands.NewCommandError( + c.Name(), + err.Error(), + utils.ExitError, + ) + } + + cfg = loadedConfig + } + + return cfg, nil +} diff --git a/internal/commands/scan-shell-env/command.go b/internal/commands/scan-shell-env/command.go new file mode 100644 index 0000000..dd082ed --- /dev/null +++ b/internal/commands/scan-shell-env/command.go @@ -0,0 +1,89 @@ +package scanshellenv + +import ( + "flag" + "fmt" + "os" + + "github.com/gruz0/web3safe/internal/commands" + "github.com/gruz0/web3safe/internal/config" + "github.com/gruz0/web3safe/internal/envanalyzer" + "github.com/gruz0/web3safe/internal/utils" +) + +type Command struct { + fs *flag.FlagSet + configFilePath string +} + +func New() *Command { + command := &Command{ + fs: flag.NewFlagSet("shellenv", flag.ContinueOnError), + configFilePath: "", + } + + command.fs.StringVar(&command.configFilePath, "config", "", "Path to the configuration file") + + return command +} + +func (c *Command) Name() string { + return c.fs.Name() +} + +func (c *Command) PrintHelp() { + c.fs.PrintDefaults() +} + +func (c *Command) ParseArgs(args []string) error { + if err := c.fs.Parse(args); err != nil { + return commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to parse args: %v", err), + utils.ExitInvalidArguments, + ) + } + + return nil +} + +func (c *Command) Run() (bool, error) { + cfg := config.GetDefaultConfig() + + if c.configFilePath != "" { + loadedConfig, err := config.Load(c.configFilePath) + if err != nil { + return false, commands.NewCommandError( + c.Name(), + err.Error(), + utils.ExitError, + ) + } + + cfg = loadedConfig + } + + envAnalyzer := envanalyzer.NewEnvAnalyzer(cfg) + + if err := envAnalyzer.Run(); err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("Unable to analyze shell envs: %v", err), + utils.ExitError, + ) + } + + envAnalyzerReport := envAnalyzer.Report() + + if len(envAnalyzerReport) == 0 { + fmt.Fprintln(os.Stdout, "Nothing found in ENV. Great!") + + return true, nil + } + + for _, message := range envAnalyzerReport { + fmt.Fprintln(os.Stderr, message) + } + + return false, nil +} diff --git a/internal/commands/scan-yaml/command.go b/internal/commands/scan-yaml/command.go new file mode 100644 index 0000000..59a922e --- /dev/null +++ b/internal/commands/scan-yaml/command.go @@ -0,0 +1,125 @@ +package scanyaml + +import ( + "flag" + "fmt" + "os" + + "github.com/gruz0/web3safe/internal/commands" + "github.com/gruz0/web3safe/internal/config" + "github.com/gruz0/web3safe/internal/utils" + "github.com/gruz0/web3safe/internal/yamlanalyzer" +) + +type Command struct { + fs *flag.FlagSet + configFilePath string + directoryToScan string + fileToScan string + recursive bool +} + +func New() *Command { + command := &Command{ + fs: flag.NewFlagSet("yaml", flag.ContinueOnError), + configFilePath: "", + directoryToScan: "", + fileToScan: "", + recursive: false, + } + + command.fs.StringVar(&command.configFilePath, "config", "", "Path to configuration file") + command.fs.StringVar(&command.directoryToScan, "dir", "", "Path to the directory to scan") + command.fs.StringVar(&command.fileToScan, "file", "", "Path to the file to scan") + command.fs.BoolVar(&command.recursive, "recursive", false, "Scan directory recursively") + + return command +} + +func (c *Command) Name() string { + return c.fs.Name() +} + +func (c *Command) PrintHelp() { + c.fs.PrintDefaults() +} + +func (c *Command) ParseArgs(args []string) error { + if err := c.fs.Parse(args); err != nil { + return commands.NewCommandError( + c.Name(), + fmt.Sprintf("failed to parse args: %v", err), + utils.ExitInvalidArguments, + ) + } + + return nil +} + +func (c *Command) Run() (bool, error) { + if c.directoryToScan == "" && c.fileToScan == "" { + c.PrintHelp() + + return false, nil + } + + cfg, err := c.loadConfig() + if err != nil { + return false, err + } + + yamlAnalyzer := yamlanalyzer.NewYamlAnalyzer(cfg) + + if c.directoryToScan != "" { + if err := yamlAnalyzer.ScanDirectory(c.directoryToScan, c.recursive); err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("Unable to analyze yaml files: %v", err), + utils.ExitError, + ) + } + } + + if c.fileToScan != "" { + if err := yamlAnalyzer.ScanFile(c.fileToScan); err != nil { + return false, commands.NewCommandError( + c.Name(), + fmt.Sprintf("Unable to analyze yaml file: %v", err), + utils.ExitError, + ) + } + } + + yamlAnalyzerReport := yamlAnalyzer.Report() + + if len(yamlAnalyzerReport) == 0 { + fmt.Fprintln(os.Stdout, "Nothing found. Great!") + + return true, nil + } + + for _, message := range yamlAnalyzerReport { + fmt.Fprintln(os.Stderr, message) + } + + return false, nil +} + +func (c *Command) loadConfig() (config.Config, error) { + cfg := config.GetDefaultConfig() + + if c.configFilePath != "" { + loadedConfig, err := config.Load(c.configFilePath) + if err != nil { + return cfg, commands.NewCommandError( + c.Name(), + err.Error(), + utils.ExitError, + ) + } + + cfg = loadedConfig + } + + return cfg, nil +} diff --git a/internal/config/config.go b/internal/config/config.go index ae23606..8f2feb9 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -3,23 +3,13 @@ package config import ( "errors" "os" - "path/filepath" - "runtime" "gopkg.in/yaml.v2" ) -const ( - appName = "web3safe" - configDirPermission = 0o755 - configPermission = 0o600 -) - var ( - ErrFailedToReadFile = errors.New("failed to read file") - ErrFailedToParseFile = errors.New("failed to parse file") - ErrFailedToEncodeFile = errors.New("failed to encode file") - ErrFailedToWriteFile = errors.New("failed to write file") + ErrFailedToReadFile = errors.New("failed to read config") + ErrFailedToParseFile = errors.New("failed to parse config") ) type Rule struct { @@ -40,17 +30,17 @@ type Config struct { IgnoreYAMLFiles []string `yaml:"ignoreYamlFiles"` } -func LoadConfig(configFilePath string) (Config, error) { - var config Config - +func Load(configFilePath string) (Config, error) { yamlFile, err := os.ReadFile(configFilePath) if err != nil { - return config, errors.Join(ErrFailedToReadFile, err) + return Config{}, ErrFailedToReadFile } + var config Config + err = yaml.Unmarshal(yamlFile, &config) if err != nil { - return config, errors.Join(ErrFailedToParseFile, err) + return config, ErrFailedToParseFile } return config, nil @@ -77,20 +67,6 @@ func GetDefaultConfig() Config { return config } -func GenerateConfig(filePath string) error { - yamlData, err := yaml.Marshal(GetDefaultConfig()) - if err != nil { - return errors.Join(ErrFailedToEncodeFile, err) - } - - err = os.WriteFile(filePath, yamlData, configPermission) - if err != nil { - return errors.Join(ErrFailedToWriteFile, err) - } - - return nil -} - func addDefaultRule(key string) Rule { return Rule{ Key: key, @@ -100,28 +76,3 @@ func addDefaultRule(key string) Rule { Include: true, } } - -func GetDefaultConfigFilePath() string { - var configDir string - - switch osType := runtime.GOOS; osType { - case "windows": - appData := os.Getenv("APPDATA") - configDir = filepath.Join(appData, appName) - case "darwin", "linux": - homeDir, err := os.UserHomeDir() - if err != nil { - panic("Unable to get home directory") - } - - configDir = filepath.Join(homeDir, ".config", appName) - default: - panic("Unsupported operating system") - } - - if err := os.MkdirAll(configDir, configDirPermission); err != nil { - panic(err) - } - - return filepath.Join(configDir, "config.yaml") -} diff --git a/internal/dotenvanalyzer/dotenvanalyzer.go b/internal/dotenvanalyzer/dotenvanalyzer.go index 2dac971..1f31bfc 100644 --- a/internal/dotenvanalyzer/dotenvanalyzer.go +++ b/internal/dotenvanalyzer/dotenvanalyzer.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/gruz0/web3safe/internal/config" + "github.com/gruz0/web3safe/internal/utils" ) const ( @@ -15,75 +16,116 @@ const ( reportMessageFormat = "%s:%d: found sensitive variable %s" ) -var ( - ErrFailedToAccessPath = errors.New("failed to access path") - ErrFailedToAnalyzeVariables = errors.New("failed to analyze variables") - ErrEnvVariableHasInvalidFormat = errors.New("variable has invalid ENV format") -) +var ErrEnvVariableHasInvalidFormat = errors.New("variable has invalid ENV format") type DotEnvAnalyzer struct { - pathToScan string - config config.Config - report []string + config config.Config + report []string + filesToIgnore map[string]bool } -func NewDotEnvAnalyzer(pathToScan string, config config.Config) *DotEnvAnalyzer { +func NewDotEnvAnalyzer(config config.Config) *DotEnvAnalyzer { + filesToIgnore := make(map[string]bool) + for _, fileToIgnore := range config.IgnoreEnvFiles { + filesToIgnore[fileToIgnore] = true + } + return &DotEnvAnalyzer{ - pathToScan: pathToScan, - config: config, - report: make([]string, 0), + config: config, + report: make([]string, 0), + filesToIgnore: filesToIgnore, } } -func (s *DotEnvAnalyzer) Run() error { - filesToIgnore := make(map[string]bool) - for _, fileToIgnore := range s.config.IgnoreEnvFiles { - filesToIgnore[fileToIgnore] = true +func (s *DotEnvAnalyzer) ScanDirectory(directoryPath string, recursive bool) error { + if recursive { + return s.scanDirectoryRecursively(directoryPath) } - err := filepath.Walk(s.pathToScan, func(path string, info os.FileInfo, err error) error { - if err != nil { - return errors.Join(ErrFailedToAccessPath, err) - } + return s.scanDirectory(directoryPath) +} - if !info.Mode().IsRegular() { - return nil +func (s *DotEnvAnalyzer) ScanFile(filePath string) error { + filename := filepath.Base(filePath) + + if s.isFileSkippable(filename) { + return nil + } + + return s.analyzeVariables(filePath) +} + +func (s *DotEnvAnalyzer) Report() []string { + return s.report +} + +func (s *DotEnvAnalyzer) scanDirectoryRecursively(directoryPath string) error { + err := filepath.WalkDir(directoryPath, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("failed to access directory: %w", err) } - if !s.hasEnvPrefix(info.Name()) { + if entry.IsDir() { return nil } - if filesToIgnore[info.Name()] { + if s.isFileSkippable(entry.Name()) { return nil } if err := s.analyzeVariables(path); err != nil { - return errors.Join(ErrFailedToAnalyzeVariables, err) + return fmt.Errorf("failed to analyze file: %w", err) } return nil }) if err != nil { - return fmt.Errorf("failed to walk directory: %w", err) + return fmt.Errorf("failed to walk directory recursively: %w", err) } return nil } -func (s *DotEnvAnalyzer) Report() []string { - return s.report +func (s *DotEnvAnalyzer) scanDirectory(directoryPath string) error { + files, err := os.ReadDir(directoryPath) + if err != nil { + return fmt.Errorf("failed to walk directory: %w", err) + } + + for _, entry := range files { + if entry.IsDir() { + continue + } + + if s.isFileSkippable(entry.Name()) { + continue + } + + if err := s.analyzeVariables(filepath.Join(directoryPath, entry.Name())); err != nil { + return fmt.Errorf("failed to analyze file: %w", err) + } + } + + return nil } -func (s *DotEnvAnalyzer) hasEnvPrefix(name string) bool { - return strings.HasPrefix(name, ".env") +func (s *DotEnvAnalyzer) isFileSkippable(fileName string) bool { + if !strings.HasPrefix(fileName, ".env") { + return true + } + + if s.filesToIgnore[fileName] { + return true + } + + return false } //nolint:funlen,gocognit,cyclop func (s *DotEnvAnalyzer) analyzeVariables(filePath string) error { - lines, err := getFileContent(filePath) + lines, err := utils.GetFileContent(filePath) if err != nil { - return err + return fmt.Errorf("%w", err) } for idx, line := range lines { @@ -97,7 +139,7 @@ func (s *DotEnvAnalyzer) analyzeVariables(filePath string) error { parts := strings.SplitN(line, "=", envVariablePartsCount) if len(parts) != envVariablePartsCount { - return ErrEnvVariableHasInvalidFormat + return fmt.Errorf("%w: %s", ErrEnvVariableHasInvalidFormat, line) } // If value is empty, just skip @@ -167,12 +209,3 @@ func (s *DotEnvAnalyzer) analyzeVariables(filePath string) error { return nil } - -func getFileContent(filePath string) ([]string, error) { - content, err := os.ReadFile(filePath) - if err != nil { - return nil, fmt.Errorf("failed to read file %s: %w", filePath, err) - } - - return strings.Split(string(content), "\n"), nil -} diff --git a/internal/dotenvanalyzer/flags.go b/internal/dotenvanalyzer/flags.go deleted file mode 100644 index a0abfe5..0000000 --- a/internal/dotenvanalyzer/flags.go +++ /dev/null @@ -1,18 +0,0 @@ -package dotenvanalyzer - -import "flag" - -type Flags struct { - PathToScan string - ConfigFilePath string -} - -func ParseFlags() Flags { - var flags Flags - - flag.StringVar(&flags.PathToScan, "path", ".", "Path to scan") - flag.StringVar(&flags.ConfigFilePath, "config", "", "Path to the configuration file") - flag.Parse() - - return flags -} diff --git a/internal/flags/flags.go b/internal/flags/flags.go deleted file mode 100644 index f6ef133..0000000 --- a/internal/flags/flags.go +++ /dev/null @@ -1,16 +0,0 @@ -package flags - -import "flag" - -type Flags struct { - GenerateConfig bool -} - -func ParseFlags() Flags { - var flags Flags - - flag.BoolVar(&flags.GenerateConfig, "generateConfig", false, "Generate a new configuration file") - flag.Parse() - - return flags -} diff --git a/internal/utils/exitCodes.go b/internal/utils/exitCodes.go new file mode 100644 index 0000000..50e218b --- /dev/null +++ b/internal/utils/exitCodes.go @@ -0,0 +1,7 @@ +package utils + +const ( + ExitSuccess = 0 + ExitError = 1 + ExitInvalidArguments = 2 +) diff --git a/internal/utils/utils.go b/internal/utils/utils.go new file mode 100644 index 0000000..a9de919 --- /dev/null +++ b/internal/utils/utils.go @@ -0,0 +1,53 @@ +package utils + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" +) + +const AppName = "web3safe" + +func GetDefaultConfigDirectory() string { + var configDir string + + switch osType := runtime.GOOS; osType { + case "windows": + appData := os.Getenv("APPDATA") + configDir = filepath.Join(appData, AppName) + case "darwin", "linux": + homeDir, err := os.UserHomeDir() + if err != nil { + panic("Unable to get home directory") + } + + configDir = filepath.Join(homeDir, ".config", AppName) + default: + panic("Unsupported operating system") + } + + return configDir +} + +func IsFileExist(filePath string) bool { + if _, err := os.Stat(filePath); err != nil { + if os.IsNotExist(err) { + return false + } + + panic(fmt.Sprintf("unable to check file existence: %v", err)) + } + + return true +} + +func GetFileContent(filePath string) ([]string, error) { + content, err := os.ReadFile(filePath) + if err != nil { + return nil, fmt.Errorf("failed to read file %s: %w", filePath, err) + } + + return strings.Split(string(content), "\n"), nil +} diff --git a/internal/yamlanalyzer/flags.go b/internal/yamlanalyzer/flags.go deleted file mode 100644 index c963b6b..0000000 --- a/internal/yamlanalyzer/flags.go +++ /dev/null @@ -1,18 +0,0 @@ -package yamlanalyzer - -import "flag" - -type Flags struct { - PathToScan string - ConfigFilePath string -} - -func ParseFlags() Flags { - var flags Flags - - flag.StringVar(&flags.PathToScan, "path", ".", "Path to scan") - flag.StringVar(&flags.ConfigFilePath, "config", "", "Path to the configuration file") - flag.Parse() - - return flags -} diff --git a/internal/yamlanalyzer/yamlanalyzer.go b/internal/yamlanalyzer/yamlanalyzer.go index 109e48a..826acc3 100644 --- a/internal/yamlanalyzer/yamlanalyzer.go +++ b/internal/yamlanalyzer/yamlanalyzer.go @@ -1,7 +1,6 @@ package yamlanalyzer import ( - "errors" "fmt" "os" "path/filepath" @@ -13,19 +12,12 @@ import ( ) const ( - reportMessageFormat = "%s: found sensitive key \"%s\" in %s" -) - -var ( - ErrFailedToAccessPath = errors.New("failed to access path") - ErrFailedToParseFile = errors.New("failed to parse file") - ErrFailedToAnalyzeVariables = errors.New("failed to analyze variables") + reportMessageFormat = "%s: found sensitive key %q in %s" ) type YamlAnalyzer struct { - pathToScan string - config config.Config - report []string + config config.Config + report []string } type match struct { @@ -33,57 +25,97 @@ type match struct { Variable string } -func NewYamlAnalyzer(pathToScan string, config config.Config) *YamlAnalyzer { +func NewYamlAnalyzer(config config.Config) *YamlAnalyzer { return &YamlAnalyzer{ - pathToScan: pathToScan, - config: config, - report: make([]string, 0), + config: config, + report: make([]string, 0), } } -func (s *YamlAnalyzer) Run() error { - err := filepath.Walk(s.pathToScan, func(path string, info os.FileInfo, err error) error { - if err != nil { - return errors.Join(ErrFailedToAccessPath, err) - } +func (s *YamlAnalyzer) ScanDirectory(directoryPath string, recursive bool) error { + if recursive { + return s.scanDirectoryRecursively(directoryPath) + } - if !info.Mode().IsRegular() { - return nil - } + return s.scanDirectory(directoryPath) +} - if !s.isYAML(info.Name()) { - return nil +func (s *YamlAnalyzer) ScanFile(filePath string) error { + filename := filepath.Base(filePath) + + if s.isFileSkippable(filename) { + return nil + } + + return s.analyzeKeys(filePath) +} + +func (s *YamlAnalyzer) Report() []string { + return s.report +} + +func (s *YamlAnalyzer) scanDirectoryRecursively(directoryPath string) error { + err := filepath.WalkDir(directoryPath, func(path string, entry os.DirEntry, err error) error { + if err != nil { + return fmt.Errorf("failed to access directory: %w", err) } - if info.Size() == 0 { + if entry.IsDir() { return nil } - for _, fileToIgnore := range s.config.IgnoreYAMLFiles { - if strings.Contains(info.Name(), fileToIgnore) { - return nil - } + if s.isFileSkippable(entry.Name()) { + return nil } if err := s.analyzeKeys(path); err != nil { - return errors.Join(ErrFailedToAnalyzeVariables, err) + return fmt.Errorf("failed to analyze file: %w", err) } return nil }) if err != nil { - return fmt.Errorf("failed to walk directory: %w", err) + return fmt.Errorf("failed to walk directory recursively: %w", err) } return nil } -func (s *YamlAnalyzer) Report() []string { - return s.report +func (s *YamlAnalyzer) scanDirectory(directoryPath string) error { + files, err := os.ReadDir(directoryPath) + if err != nil { + return fmt.Errorf("failed to walk directory: %w", err) + } + + for _, entry := range files { + if entry.IsDir() { + continue + } + + if s.isFileSkippable(entry.Name()) { + continue + } + + if err := s.analyzeKeys(filepath.Join(directoryPath, entry.Name())); err != nil { + return fmt.Errorf("failed to analyze file: %w", err) + } + } + + return nil } -func (s *YamlAnalyzer) isYAML(name string) bool { - return strings.HasSuffix(name, ".yaml") || strings.HasSuffix(name, ".yml") +func (s *YamlAnalyzer) isFileSkippable(fileName string) bool { + if !strings.HasSuffix(fileName, ".yaml") && !strings.HasSuffix(fileName, ".yml") { + return true + } + + for _, fileToIgnore := range s.config.IgnoreYAMLFiles { + if strings.Contains(fileName, fileToIgnore) { + return true + } + } + + return false } func (s *YamlAnalyzer) analyzeKeys(filePath string) error { @@ -94,7 +126,7 @@ func (s *YamlAnalyzer) analyzeKeys(filePath string) error { var data interface{} if err := yaml.Unmarshal(yamlData, &data); err != nil { - return errors.Join(ErrFailedToParseFile, err) + return fmt.Errorf("failed to parse file %s: %w", filePath, err) } keys := traverse(data)