From a7fa79b325aed43896239a4a5171be93ef9ebc89 Mon Sep 17 00:00:00 2001 From: Alexis-Maurer Fortin Date: Wed, 8 May 2024 12:09:30 -0400 Subject: [PATCH] [Breaking Changes] Switch to Use Cobra/Viper for CLI and Config Handling (#64) --- README.md | 22 ++--- analyze/analyze.go | 30 ++---- cmd/analyzeLocal.go | 45 +++++++++ cmd/analyzeOrg.go | 52 +++++++++++ cmd/analyzeRepo.go | 42 +++++++++ cmd/root.go | 178 +++++++++++++++++++++++++++++++++++ cmd/version.go | 21 +++++ go.mod | 20 ++++ go.sum | 45 +++++++++ poutine.go | 221 ++------------------------------------------ 10 files changed, 428 insertions(+), 248 deletions(-) create mode 100644 cmd/analyzeLocal.go create mode 100644 cmd/analyzeOrg.go create mode 100644 cmd/analyzeRepo.go create mode 100644 cmd/root.go create mode 100644 cmd/version.go diff --git a/README.md b/README.md index b5baf4f..9fe31f1 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ jobs: ### Usage ``` bash -poutine [options] [command] [arguments] +poutine [command] [arguments] [options] ``` #### Analyze a local repository @@ -77,31 +77,31 @@ poutine analyze_local . #### Analyze a remote GitHub repository ```bash -poutine -token "$GH_TOKEN" analyze_repo org/repo +poutine analyze_repo org/repo --token "$GH_TOKEN" ``` #### Analyze all repositories in a GitHub organization ```bash -poutine -token "$GH_TOKEN" analyze_org org +poutine analyze_org org --token "$GH_TOKEN" ``` #### Analyze all projects in a self-hosted Gitlab instance ``` bash -poutine -token "$GL_TOKEN" -scm gitlab -scm-base-uri https://gitlab.example.com analyze_org my-org/project +poutine analyze_org my-org/project --token "$GL_TOKEN" --scm gitlab --scm-base-uri https://gitlab.example.com ``` ### Configuration Options ``` --token SCM access token (required for the commands analyze_repo, analyze_org) (env: GH_TOKEN) --format Output format (default: pretty, json, sarif) --scm SCM platform (default: github, gitlab) --scm-base-uri Base URI of the self-hosted SCM instance --threads Number of threads to use (default: 2) --verbose Enable debug logging +--token SCM access token (required for the commands analyze_repo, analyze_org) (env: GH_TOKEN) +--format Output format (default: pretty, json, sarif) +--scm SCM platform (default: github, gitlab) +--scm-base-uri Base URI of the self-hosted SCM instance +--threads Number of threads to use (default: 2) +--verbose Enable debug logging ``` ## Building from source @@ -120,7 +120,7 @@ For examples of vulnerabilities in GitHub Actions workflows, you can explore the To get started with some hints, try using `poutine` to analyze the `messypoutine` organization: ``` bash -poutine -token `gh auth token` analyze_org messypoutine +poutine analyze_org messypoutine --token `gh auth token` ``` You may submit the flags you find in a [private vulnerability disclosure](https://github.com/messypoutine/.github/security/advisories/new). diff --git a/analyze/analyze.go b/analyze/analyze.go index aad8648..58d4b06 100644 --- a/analyze/analyze.go +++ b/analyze/analyze.go @@ -11,17 +11,14 @@ import ( "sync" "time" - "github.com/rs/zerolog/log" - "gopkg.in/yaml.v3" - "github.com/boostsecurityio/poutine/opa" "github.com/boostsecurityio/poutine/providers/pkgsupply" "github.com/boostsecurityio/poutine/scanner" + "github.com/rs/zerolog/log" "github.com/schollz/progressbar/v3" ) const TEMP_DIR_PREFIX = "poutine-*" -const CONFIG_PATH = ".poutine.yml" type Repository interface { GetProviderName() string @@ -53,12 +50,15 @@ type GitClient interface { GetRepoHeadBranchName(ctx context.Context, repoPath string) (string, error) } -func NewAnalyzer(scmClient ScmClient, gitClient GitClient, formatter Formatter) *Analyzer { +func NewAnalyzer(scmClient ScmClient, gitClient GitClient, formatter Formatter, config *models.Config) *Analyzer { + if config == nil { + config = &models.Config{} + } return &Analyzer{ ScmClient: scmClient, GitClient: gitClient, Formatter: formatter, - Config: &models.Config{}, + Config: config, } } @@ -267,24 +267,6 @@ func (a *Analyzer) AnalyzeLocalRepo(ctx context.Context, repoPath string) error return a.finalizeAnalysis(ctx, inventory) } -func (a *Analyzer) LoadConfig(path string) error { - f, err := os.Open(path) - if err != nil { - if os.IsNotExist(err) { - log.Debug().Msgf("Config file `%s` not found, using default config", path) - return nil - } - return err - } - - err = yaml.NewDecoder(f).Decode(a.Config) - if err != nil { - return err - } - - return nil -} - type Formatter interface { Format(ctx context.Context, report *opa.FindingsResult, packages []*models.PackageInsights) error } diff --git a/cmd/analyzeLocal.go b/cmd/analyzeLocal.go new file mode 100644 index 0000000..47340c4 --- /dev/null +++ b/cmd/analyzeLocal.go @@ -0,0 +1,45 @@ +package cmd + +import ( + "fmt" + "github.com/boostsecurityio/poutine/analyze" + "github.com/boostsecurityio/poutine/providers/gitops" + "github.com/boostsecurityio/poutine/providers/local" + + "github.com/spf13/cobra" +) + +// analyzeLocalCmd represents the analyzeLocal command +var analyzeLocalCmd = &cobra.Command{ + Use: "analyze_local", + Short: "Analyzes a local repository for supply chain vulnerabilities", + Long: `Analyzes a local repository for supply chain vulnerabilities +Example: poutine analyze_local /path/to/repo`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + ctx := cmd.Context() + repoPath := args[0] + + formatter := GetFormatter() + + localScmClient, err := local.NewGitSCMClient(ctx, repoPath, nil) + if err != nil { + return fmt.Errorf("failed to create local SCM client: %w", err) + } + + localGitClient := gitops.NewLocalGitClient(nil) + + analyzer := analyze.NewAnalyzer(localScmClient, localGitClient, formatter, config) + + err = analyzer.AnalyzeLocalRepo(ctx, repoPath) + if err != nil { + return fmt.Errorf("failed to analyze repoPath %s: %w", repoPath, err) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(analyzeLocalCmd) +} diff --git a/cmd/analyzeOrg.go b/cmd/analyzeOrg.go new file mode 100644 index 0000000..c41f7d9 --- /dev/null +++ b/cmd/analyzeOrg.go @@ -0,0 +1,52 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +var threads int + +// analyzeOrgCmd represents the analyzeOrg command +var analyzeOrgCmd = &cobra.Command{ + Use: "analyze_org", + Short: "Analyzes an organization's repositories for supply chain vulnerabilities", + Long: `Analyzes an organization's repositories for supply chain vulnerabilities +Example: poutine analyze_org org --token "$GH_TOKEN" + +Analyze All Projects in a Self-Hosted Gitlab Organization: +poutine analyze_org my-org/project --token "$GL_TOKEN" --scm gitlab --scm-base-uri https://gitlab.example.com + +Note: This command will scan all repositories in the organization except those that are Archived. +`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + token = viper.GetString("token") + ctx := cmd.Context() + analyzer, err := GetAnalyzer(ctx, "analyze_org") + if err != nil { + return err + } + + org := args[0] + + err = analyzer.AnalyzeOrg(ctx, org, &threads) + if err != nil { + return fmt.Errorf("failed to analyze org %s: %w", org, err) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(analyzeOrgCmd) + + analyzeOrgCmd.Flags().StringVarP(&token, "token", "t", "", "SCM access token (env: GH_TOKEN)") + + analyzeOrgCmd.Flags().IntVarP(&threads, "threads", "j", 2, "Parallelization factor for scanning organizations") + + viper.BindPFlag("token", analyzeOrgCmd.Flags().Lookup("token")) + viper.BindEnv("token", "GH_TOKEN") +} diff --git a/cmd/analyzeRepo.go b/cmd/analyzeRepo.go new file mode 100644 index 0000000..66c3c54 --- /dev/null +++ b/cmd/analyzeRepo.go @@ -0,0 +1,42 @@ +package cmd + +import ( + "fmt" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +// analyzeRepoCmd represents the analyzeRepo command +var analyzeRepoCmd = &cobra.Command{ + Use: "analyze_repo", + Short: "Analyzes a remote repository for supply chain vulnerabilities", + Long: `Analyzes a remote repository for supply chain vulnerabilities +Example Scanning a remote Github Repository: poutine analyze_repo org/repo --token "$GH_TOKEN"`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + token = viper.GetString("token") + ctx := cmd.Context() + analyzer, err := GetAnalyzer(ctx, "analyze_repo") + if err != nil { + return err + } + + repo := args[0] + + err = analyzer.AnalyzeRepo(ctx, repo) + if err != nil { + return fmt.Errorf("failed to analyze repo %s: %w", repo, err) + } + + return nil + }, +} + +func init() { + rootCmd.AddCommand(analyzeRepoCmd) + + analyzeRepoCmd.Flags().StringVarP(&token, "token", "t", "", "SCM access token (env: GH_TOKEN)") + + viper.BindPFlag("token", analyzeOrgCmd.Flags().Lookup("token")) + viper.BindEnv("token", "GH_TOKEN") +} diff --git a/cmd/root.go b/cmd/root.go new file mode 100644 index 0000000..b177ea2 --- /dev/null +++ b/cmd/root.go @@ -0,0 +1,178 @@ +package cmd + +import ( + "context" + "fmt" + "github.com/boostsecurityio/poutine/analyze" + "github.com/boostsecurityio/poutine/formatters/json" + "github.com/boostsecurityio/poutine/formatters/pretty" + "github.com/boostsecurityio/poutine/formatters/sarif" + "github.com/boostsecurityio/poutine/models" + "github.com/boostsecurityio/poutine/opa" + "github.com/boostsecurityio/poutine/providers/gitops" + "github.com/boostsecurityio/poutine/providers/scm" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" + "github.com/spf13/viper" + "os" + "os/signal" + "path/filepath" + "strings" + "syscall" + + "github.com/spf13/cobra" +) + +var Format string +var Verbose bool +var ScmProvider string +var ScmBaseURL string +var ( + Version string + Commit string + Date string +) +var token string +var cfgFile string +var config *models.Config + +var legacyFlags = []string{"-token", "-format", "-verbose", "-scm", "-scm-base-uri", "-threads"} + +const ( + exitCodeErr = 1 + exitCodeInterrupt = 2 +) + +// rootCmd represents the base command when called without any subcommands +var rootCmd = &cobra.Command{ + Use: "poutine", + Short: "A Supply Chain Vulnerability Scanner for Build Pipelines", + Long: `A Supply Chain Vulnerability Scanner for Build Pipelines +By BoostSecurity.io - https://github.com/boostsecurityio/poutine `, +} + +// Execute adds all child commands to the root command and sets flags appropriately. +// This is called by main.main(). It only needs to happen once to the rootCmd. +func Execute() { + zerolog.SetGlobalLevel(zerolog.InfoLevel) + if Verbose { + zerolog.SetGlobalLevel(zerolog.DebugLevel) + } + output := zerolog.ConsoleWriter{Out: os.Stderr} + output.FormatLevel = func(i interface{}) string { + return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) + } + log.Logger = log.Output(output) + + ctx := context.Background() + ctx, cancel := context.WithCancel(ctx) + signalChan := make(chan os.Signal, 1) + signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) + defer func() { + signal.Stop(signalChan) + cancel() + }() + + go func() { + select { + case <-signalChan: // first signal, cancel context + cancel() + cleanup() + case <-ctx.Done(): + return + } + <-signalChan // second signal, hard exit + os.Exit(exitCodeInterrupt) + }() + + err := rootCmd.ExecuteContext(ctx) + if err != nil { + log.Error().Err(err).Msg("") + os.Exit(exitCodeErr) + } +} + +func init() { + cobra.OnInitialize(initConfig) + + for _, arg := range os.Args { + for _, legacyFlag := range legacyFlags { + if strings.Contains(arg, legacyFlag) { + fmt.Println("Error: Flags now come after the command and require '--' instead of a single '-', use poutine --help for more information.") + os.Exit(exitCodeErr) + } + } + } + + rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is .poutine.yml in the current directory)") + rootCmd.PersistentFlags().StringVarP(&Format, "format", "f", "pretty", "Output format (pretty, json, sarif)") + rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "Enable verbose logging") + rootCmd.PersistentFlags().StringVarP(&ScmProvider, "scm", "s", "github", "SCM platform (github, gitlab)") + rootCmd.PersistentFlags().StringVarP(&ScmBaseURL, "scm-base-url", "b", "", "Base URI of the self-hosted SCM instance (optional)") +} + +func initConfig() { + viper.AutomaticEnv() + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + viper.AddConfigPath(".") + viper.SetConfigFile(".poutine.yml") + } + + if err := viper.ReadInConfig(); err != nil { + if _, ok := err.(viper.ConfigFileNotFoundError); ok { + return + } else { + log.Error().Err(err).Msg("Can't read config") + os.Exit(1) + } + } + + if err := viper.Unmarshal(&config); err != nil { + log.Error().Err(err).Msg("Unable to unmarshal config") + os.Exit(1) + } +} + +func cleanup() { + log.Debug().Msg("Cleaning up temp directories") + globPattern := filepath.Join(os.TempDir(), analyze.TEMP_DIR_PREFIX) + matches, err := filepath.Glob(globPattern) + if err != nil { + log.Error().Err(err).Msg("Failed to match temp folders") + } + for _, match := range matches { + if err := os.RemoveAll(match); err != nil { + log.Error().Err(err).Msgf("Failed to remove %q", match) + } + } + log.Debug().Msg("Finished cleaning up temp directories") +} + +func GetFormatter() analyze.Formatter { + switch Format { + case "pretty": + return &pretty.Format{} + case "json": + opaClient, _ := opa.NewOpa() + return json.NewFormat(opaClient, Format, os.Stdout) + case "sarif": + return sarif.NewFormat(os.Stdout) + } + return &pretty.Format{} +} + +func GetAnalyzer(ctx context.Context, command string) (*analyze.Analyzer, error) { + scmClient, err := scm.NewScmClient(ctx, ScmProvider, ScmBaseURL, token, command) + if err != nil { + return nil, fmt.Errorf("failed to create SCM client: %w", err) + } + + formatter := GetFormatter() + + gitClient := gitops.NewGitClient(nil) + + analyzer := analyze.NewAnalyzer(scmClient, gitClient, formatter, config) + return analyzer, nil +} diff --git a/cmd/version.go b/cmd/version.go new file mode 100644 index 0000000..c9af4dd --- /dev/null +++ b/cmd/version.go @@ -0,0 +1,21 @@ +package cmd + +import ( + "fmt" + + "github.com/spf13/cobra" +) + +// versionCmd represents the version command +var versionCmd = &cobra.Command{ + Use: "version", + Short: "prints the version of poutine", + Run: func(cmd *cobra.Command, args []string) { + fmt.Printf("Version: %s\nCommit: %s\nBuilt At: %s\n", Version, Commit, Date) + return + }, +} + +func init() { + rootCmd.AddCommand(versionCmd) +} diff --git a/go.mod b/go.mod index b02f2fe..2945be8 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fatih/color v1.14.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/go-ini/ini v1.67.0 // indirect github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -37,10 +38,15 @@ require ( github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-hclog v1.5.0 // indirect github.com/hashicorp/go-retryablehttp v0.7.2 // indirect + github.com/hashicorp/hcl v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/magiconair/properties v1.8.7 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.1.0 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect @@ -48,8 +54,17 @@ require ( github.com/prometheus/procfs v0.12.0 // indirect github.com/rcrowley/go-metrics v0.0.0-20200313005456-10cdbea86bc0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/sagikazarmark/locafero v0.4.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 // indirect github.com/sirupsen/logrus v1.9.3 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.11.0 // indirect + github.com/spf13/cast v1.6.0 // indirect + github.com/spf13/cobra v1.8.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/viper v1.18.2 // indirect + github.com/subosito/gotenv v1.6.0 // indirect github.com/tchap/go-patricia/v2 v2.3.1 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect @@ -58,10 +73,15 @@ require ( go.opentelemetry.io/otel/metric v1.21.0 // indirect go.opentelemetry.io/otel/sdk v1.21.0 // indirect go.opentelemetry.io/otel/trace v1.21.0 // indirect + go.uber.org/atomic v1.9.0 // indirect + go.uber.org/multierr v1.9.0 // indirect + golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sys v0.18.0 // indirect golang.org/x/term v0.18.0 // indirect + golang.org/x/text v0.14.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/protobuf v1.33.0 // indirect + gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect sigs.k8s.io/yaml v1.4.0 // indirect ) diff --git a/go.sum b/go.sum index 0b620dc..cba4248 100644 --- a/go.sum +++ b/go.sum @@ -16,6 +16,7 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -37,6 +38,8 @@ github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8 github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/foxcpp/go-mockdns v1.1.0 h1:jI0rD8M0wuYAxL7r/ynTrCQQq0BVqfB99Vgk7DlmewI= github.com/foxcpp/go-mockdns v1.1.0/go.mod h1:IhLeSFGed3mJIAXPH2aiRQB+kqz7oqu8ld2qVbOu7Wk= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A= github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= @@ -87,6 +90,10 @@ github.com/hashicorp/go-retryablehttp v0.7.2 h1:AcYqCvkpalPnPF2pn0KamgwamS42TqUD github.com/hashicorp/go-retryablehttp v0.7.2/go.mod h1:Jy/gPYAdjqffZ/yFGCFV2doI5wjtH1ewM9u8iYVjtX8= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= +github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/k0kubun/go-ansi v0.0.0-20180517002512-3bf9e2903213/go.mod h1:vNUNkEQ1e29fT/6vq2aBdFsgNPmy8qMdSay1npru+Sw= github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= @@ -97,6 +104,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -113,6 +122,8 @@ github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM= github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/open-policy-agent/opa v0.63.0 h1:ztNNste1v8kH0/vJMJNquE45lRvqwrM5mY9Ctr9xIXw= @@ -122,6 +133,8 @@ github.com/owenrumney/go-sarif/v2 v2.3.1 h1:77opmuqxQZE1UF6TylFz5XllVEI72Wijgwpw github.com/owenrumney/go-sarif/v2 v2.3.1/go.mod h1:MSqMMx9WqlBSY7pXoOZWgEsVB4FDNfhcaXDA1j6Sr+w= github.com/package-url/packageurl-go v0.1.2 h1:0H2DQt6DHd/NeRlVwW4EZ4oEI6Bn40XlNPRqegcxuo4= github.com/package-url/packageurl-go v0.1.2/go.mod h1:uQd4a7Rh3ZsVg5j0lNyAfyxIeGde9yrlhjF78GzeW0c= +github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= +github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -144,6 +157,11 @@ github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncj github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= github.com/rs/zerolog v1.32.0 h1:keLypqrlIjaFsbmJOBdB/qvyF8KEtCWHwobLp5l/mQ0= github.com/rs/zerolog v1.32.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ= +github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= github.com/schollz/progressbar/v3 v3.14.2 h1:EducH6uNLIWsr560zSV1KrTeUb/wZGAHqyMFIEa99ks= github.com/schollz/progressbar/v3 v3.14.2/go.mod h1:aQAZQnhF4JGFtRJiw/eobaXpsqpVQAftEQ+hLGXaRc4= github.com/shurcooL/githubv4 v0.0.0-20240120211514-18a1ae0e79dc h1:vH0NQbIDk+mJLvBliNGfcQgUmhlniWBDXC79oRxfZA0= @@ -152,13 +170,32 @@ github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466 h1:17JxqqJY66GmZV github.com/shurcooL/graphql v0.0.0-20230722043721-ed46e5a46466/go.mod h1:9dIRpgIY7hVhoqfe0/FcYp0bpInZaT7dc3BYOprrIUE= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8= +github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY= +github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= +github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.18.2 h1:LUXCnvUvSM6FXAsj6nnfc8Q2tp1dIgUfY9Kc8GsSOiQ= +github.com/spf13/viper v1.18.2/go.mod h1:EKmWIqdnk5lOcmR72yw6hS+8OPYcwD0jteitLMVB+yk= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/tchap/go-patricia/v2 v2.3.1 h1:6rQp39lgIYZ+MHmdEq4xzuk1t7OdC35z/xm0BGhTkes= github.com/tchap/go-patricia/v2 v2.3.1/go.mod h1:VZRHKAb53DLaG+nA9EaYYiaEx6YztwDlLElMsnSHD4k= github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= @@ -190,7 +227,13 @@ go.opentelemetry.io/otel/trace v1.21.0 h1:WD9i5gzvoUPuXIXH24ZNBudiarZDKuekPqi/E8 go.opentelemetry.io/otel/trace v1.21.0/go.mod h1:LGbsEB0f9LGjN+OZaQQ26sohbOmiMR+BaslueVtS/qQ= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= +go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= +go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= +golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= @@ -241,6 +284,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= +gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/poutine.go b/poutine.go index 09c81f6..ee1e8af 100644 --- a/poutine.go +++ b/poutine.go @@ -1,223 +1,18 @@ package main import ( - "context" - "flag" - "fmt" - "github.com/boostsecurityio/poutine/providers/gitops" - "os" - "os/signal" - "path/filepath" - "strings" - "syscall" - - "github.com/boostsecurityio/poutine/analyze" - "github.com/boostsecurityio/poutine/formatters/json" - "github.com/boostsecurityio/poutine/formatters/pretty" - "github.com/boostsecurityio/poutine/formatters/sarif" - "github.com/boostsecurityio/poutine/opa" - "github.com/boostsecurityio/poutine/providers/local" - "github.com/boostsecurityio/poutine/providers/scm" - "github.com/rs/zerolog" - "github.com/rs/zerolog/log" -) - -const ( - exitCodeErr = 1 - exitCodeInterrupt = 2 + "github.com/boostsecurityio/poutine/cmd" ) -func usage() { - fmt.Fprintf(os.Stderr, `poutine - A Supply Chain Vulnerability Scanner for Build Pipelines -By BoostSecurity.io - https://github.com/boostsecurityio/poutine - -Usage: - poutine [options] [] - -Commands: - analyze_org - analyze_repo / - analyze_local - version - -Options: -`) - - flag.PrintDefaults() - os.Exit(exitCodeInterrupt) -} - var ( - format = flag.String("format", "pretty", "Output format (pretty, json, sarif)") - token = flag.String("token", "", "SCM access token (required for the commands analyze_org, analyze_repo) (env: GH_TOKEN)") - scmProvider = flag.String("scm", "github", "SCM platform (github, gitlab)") - scmBaseURL = flag.String("scm-base-url", "", "Base URI of the self-hosted SCM instance (optional)") - threads = flag.Int("threads", 2, "Parallelization factor for scanning organizations") - verbose = flag.Bool("verbose", false, "Enable verbose logging") - config = flag.String("config", analyze.CONFIG_PATH, "Path to the configuration file") - version = "development" - commit = "none" - date = "unknown" + Version = "development" + Commit = "none" + Date = "unknown" ) func main() { - // Parse flags. - flag.Usage = usage - flag.Parse() - - // Ensure the command is correct. - args := flag.Args() - if len(args) < 1 { - usage() - } - - zerolog.SetGlobalLevel(zerolog.InfoLevel) - if *verbose { - zerolog.SetGlobalLevel(zerolog.DebugLevel) - } - output := zerolog.ConsoleWriter{Out: os.Stderr} - output.FormatLevel = func(i interface{}) string { - return strings.ToUpper(fmt.Sprintf("| %-6s|", i)) - } - log.Logger = log.Output(output) - - ctx := context.Background() - ctx, cancel := context.WithCancel(ctx) - signalChan := make(chan os.Signal, 1) - signal.Notify(signalChan, os.Interrupt, syscall.SIGTERM) - defer func() { - signal.Stop(signalChan) - cancel() - }() - - go func() { - select { - case <-signalChan: // first signal, cancel context - cancel() - cleanup() - case <-ctx.Done(): - return - } - <-signalChan // second signal, hard exit - os.Exit(exitCodeInterrupt) - }() - - err := run(ctx, args) - if err != nil { - log.Error().Err(err).Msg("") - os.Exit(exitCodeErr) - } -} - -func run(ctx context.Context, args []string) error { - command := args[0] - if command == "version" { - fmt.Printf("Version: %s\nCommit: %s\nBuilt At: %s\n", version, commit, date) - return nil - } - if len(args) != 2 { - usage() - } - scmToken := getToken() - scmClient, err := scm.NewScmClient(ctx, *scmProvider, *scmBaseURL, scmToken, command) - if err != nil { - return fmt.Errorf("failed to create SCM client: %w", err) - } - - formatter := getFormatter() - - gitClient := gitops.NewGitClient(nil) - - analyzer := analyze.NewAnalyzer(scmClient, gitClient, formatter) - err = analyzer.LoadConfig(*config) - if err != nil { - return err - } - - switch command { - case "analyze_org": - return analyzeOrg(ctx, args[1], analyzer) - case "analyze_repo": - return analyzeRepo(ctx, args[1], analyzer) - case "analyze_local": - return analyzeLocal(ctx, args[1], analyzer, formatter) - default: - return fmt.Errorf("unknown command %q", command) - } -} - -func analyzeOrg(ctx context.Context, org string, analyzer *analyze.Analyzer) error { - if org == "" { - return fmt.Errorf("invalid organization name %q", org) - } - - err := analyzer.AnalyzeOrg(ctx, org, threads) - if err != nil { - return fmt.Errorf("failed to analyze org %s: %w", org, err) - } - - return nil -} - -func analyzeRepo(ctx context.Context, repo string, analyzer *analyze.Analyzer) error { - err := analyzer.AnalyzeRepo(ctx, repo) - if err != nil { - return fmt.Errorf("failed to analyze repo %s: %w", repo, err) - } - - return nil -} - -func analyzeLocal(ctx context.Context, repoPath string, analyzer *analyze.Analyzer, formatter analyze.Formatter) error { - localScmClient, err := local.NewGitSCMClient(ctx, repoPath, nil) - if err != nil { - return fmt.Errorf("failed to create local SCM client: %w", err) - } - - localGitClient := gitops.NewLocalGitClient(nil) - local := analyze.NewAnalyzer(localScmClient, localGitClient, formatter) - local.Config = analyzer.Config - - err = local.AnalyzeLocalRepo(ctx, repoPath) - if err != nil { - return fmt.Errorf("failed to analyze repoPath %s: %w", repoPath, err) - } - return nil -} - -func getToken() string { - ghToken := *token - if ghToken == "" { - ghToken = os.Getenv("GH_TOKEN") - } - return ghToken -} - -func getFormatter() analyze.Formatter { - format := *format - switch format { - case "pretty": - return &pretty.Format{} - case "json": - opaClient, _ := opa.NewOpa() - return json.NewFormat(opaClient, format, os.Stdout) - case "sarif": - return sarif.NewFormat(os.Stdout) - } - return &pretty.Format{} -} - -func cleanup() { - log.Debug().Msg("Cleaning up temp directories") - globPattern := filepath.Join(os.TempDir(), analyze.TEMP_DIR_PREFIX) - matches, err := filepath.Glob(globPattern) - if err != nil { - log.Error().Err(err).Msg("Failed to match temp folders") - } - for _, match := range matches { - if err := os.RemoveAll(match); err != nil { - log.Error().Err(err).Msgf("Failed to remove %q", match) - } - } - log.Debug().Msg("Finished cleaning up temp directories") + cmd.Commit = Commit + cmd.Version = Version + cmd.Date = Date + cmd.Execute() }