2020-12-09 15:31:34 +00:00
|
|
|
package cmd
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"os"
|
|
|
|
"os/signal"
|
|
|
|
"strings"
|
|
|
|
"syscall"
|
|
|
|
|
|
|
|
"github.com/cloudskiff/driftctl/pkg"
|
2020-12-16 12:02:02 +00:00
|
|
|
"github.com/cloudskiff/driftctl/pkg/alerter"
|
2020-12-09 15:31:34 +00:00
|
|
|
"github.com/cloudskiff/driftctl/pkg/cmd/scan/output"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/filter"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/iac/config"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/iac/supplier"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/iac/terraform/state/backend"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/remote"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/resource"
|
|
|
|
"github.com/cloudskiff/driftctl/pkg/terraform"
|
|
|
|
"github.com/jmespath/go-jmespath"
|
|
|
|
"github.com/sirupsen/logrus"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
|
|
|
type ScanOptions struct {
|
|
|
|
Coverage bool
|
|
|
|
Detect bool
|
|
|
|
From config.SupplierConfig
|
|
|
|
To string
|
|
|
|
Output output.OutputConfig
|
|
|
|
Filter *jmespath.JMESPath
|
|
|
|
}
|
|
|
|
|
|
|
|
func NewScanCmd() *cobra.Command {
|
|
|
|
opts := &ScanOptions{}
|
|
|
|
|
|
|
|
cmd := &cobra.Command{
|
|
|
|
Use: "scan",
|
|
|
|
Short: "Scan",
|
|
|
|
Long: "Scan",
|
|
|
|
Args: cobra.NoArgs,
|
|
|
|
PreRunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
from, _ := cmd.Flags().GetString("from")
|
|
|
|
|
|
|
|
iacSource, err := parseFromFlag(from)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
opts.From = *iacSource
|
|
|
|
|
|
|
|
to, _ := cmd.Flags().GetString("to")
|
|
|
|
if !remote.IsSupported(to) {
|
|
|
|
return fmt.Errorf(
|
|
|
|
"unsupported cloud provider '%s'\nValid values are: %s",
|
|
|
|
to,
|
|
|
|
strings.Join(remote.GetSupportedRemotes(), ","),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
outputFlag, _ := cmd.Flags().GetString("output")
|
|
|
|
out, err := parseOutputFlag(outputFlag)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
opts.Output = *out
|
|
|
|
|
|
|
|
filterFlag, _ := cmd.Flags().GetString("filter")
|
|
|
|
if filterFlag != "" {
|
|
|
|
expr, err := filter.BuildExpression(filterFlag)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("unable to parse filter expression: %s", err.Error())
|
|
|
|
}
|
|
|
|
opts.Filter = expr
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
},
|
|
|
|
RunE: func(cmd *cobra.Command, args []string) error {
|
|
|
|
return scanRun(opts)
|
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
fl := cmd.Flags()
|
|
|
|
fl.StringP(
|
|
|
|
"filter",
|
|
|
|
"",
|
|
|
|
"",
|
|
|
|
"JMESPath expression to filter on\n"+
|
|
|
|
"Examples : \n"+
|
|
|
|
" - Type == 'aws_s3_bucket' (will filter only s3 buckets)\n"+
|
|
|
|
" - Type =='aws_s3_bucket && Id != 'my_bucket' (excludes s3 bucket 'my_bucket')\n"+
|
|
|
|
" - Attr.Tags.Terraform == 'true' (include only resources that have Tag Terraform equal to 'true')\n",
|
|
|
|
)
|
|
|
|
fl.StringP(
|
|
|
|
"output",
|
|
|
|
"o",
|
|
|
|
output.Example(output.ConsoleOutputType),
|
|
|
|
"Output format, by default it will write to the console\n"+
|
|
|
|
"Accepted formats are: "+strings.Join(output.SupportedOutputsExample(), ",")+"\n",
|
|
|
|
)
|
|
|
|
fl.StringP(
|
|
|
|
"from",
|
|
|
|
"f",
|
|
|
|
"tfstate://terraform.tfstate",
|
|
|
|
"IaC source, by default try to find local terraform.tfstate file\n"+
|
|
|
|
"Accepted schemes are: "+strings.Join(supplier.GetSupportedSchemes(), ",")+"\n",
|
|
|
|
)
|
|
|
|
supportedRemotes := remote.GetSupportedRemotes()
|
|
|
|
fl.StringVarP(
|
|
|
|
&opts.To,
|
|
|
|
"to",
|
|
|
|
"t",
|
|
|
|
supportedRemotes[0],
|
|
|
|
"Cloud provider source\n"+
|
|
|
|
"Accepted values are: "+strings.Join(supportedRemotes, ",")+"\n",
|
|
|
|
)
|
|
|
|
|
|
|
|
return cmd
|
|
|
|
}
|
|
|
|
|
|
|
|
func scanRun(opts *ScanOptions) error {
|
|
|
|
c := make(chan os.Signal)
|
|
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
|
|
|
2020-12-16 12:02:02 +00:00
|
|
|
alerter := alerter.NewAlerter()
|
|
|
|
|
|
|
|
err := remote.Activate(opts.To, alerter)
|
2020-12-09 15:31:34 +00:00
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
|
|
|
|
// Teardown
|
|
|
|
defer func() {
|
|
|
|
logrus.Trace("Exiting scan cmd")
|
|
|
|
terraform.Cleanup()
|
|
|
|
logrus.Trace("Exited")
|
|
|
|
}()
|
|
|
|
|
2020-12-16 12:02:02 +00:00
|
|
|
scanner := pkg.NewScanner(resource.Suppliers(), alerter)
|
2020-12-09 15:31:34 +00:00
|
|
|
|
|
|
|
iacSupplier, err := supplier.GetIACSupplier(opts.From)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
logrus.WithFields(logrus.Fields{
|
|
|
|
"supplier": opts.From.Key,
|
|
|
|
"backend": opts.From.Backend,
|
|
|
|
"path": opts.From.Path,
|
|
|
|
}).Debug("Found IAC provider")
|
2020-12-16 12:02:02 +00:00
|
|
|
ctl := pkg.NewDriftCTL(scanner, iacSupplier, opts.Filter, alerter)
|
2020-12-09 15:31:34 +00:00
|
|
|
|
|
|
|
go func() {
|
|
|
|
<-c
|
|
|
|
logrus.Warn("Detected interrupt, cleanup ...")
|
|
|
|
ctl.Stop()
|
|
|
|
}()
|
|
|
|
|
|
|
|
analysis := ctl.Run()
|
|
|
|
|
|
|
|
if analysis == nil {
|
2020-12-19 08:48:56 +00:00
|
|
|
return errors.New("unable to run driftctl")
|
2020-12-09 15:31:34 +00:00
|
|
|
}
|
|
|
|
out := output.GetOutput(opts.Output)
|
|
|
|
return out.Write(analysis)
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseFromFlag(from string) (*config.SupplierConfig, error) {
|
|
|
|
|
|
|
|
schemePath := strings.Split(from, "://")
|
|
|
|
if len(schemePath) != 2 || schemePath[1] == "" || schemePath[0] == "" {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Unable to parse from flag: %s\nAccepted schemes are: %s",
|
|
|
|
from,
|
|
|
|
strings.Join(supplier.GetSupportedSchemes(), ","),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
scheme := schemePath[0]
|
|
|
|
path := schemePath[1]
|
|
|
|
supplierBackend := strings.Split(scheme, "+")
|
|
|
|
if len(supplierBackend) > 2 {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Unable to parse from scheme: %s\nAccepted schemes are: %s",
|
|
|
|
scheme,
|
|
|
|
strings.Join(supplier.GetSupportedSchemes(), ","),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
supplierKey := supplierBackend[0]
|
|
|
|
if !supplier.IsSupplierSupported(supplierKey) {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Unsupported IaC source: %s\nAccepted values are: %s",
|
|
|
|
supplierKey,
|
|
|
|
strings.Join(supplier.GetSupportedSuppliers(), ","),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
backendString := ""
|
|
|
|
if len(supplierBackend) == 2 {
|
|
|
|
backendString = supplierBackend[1]
|
|
|
|
if !backend.IsSupported(backendString) {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Unsupported IaC backend: %s\nAccepted values are: %s",
|
|
|
|
backendString,
|
|
|
|
strings.Join(backend.GetSupportedBackends(), ","),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return &config.SupplierConfig{
|
|
|
|
Key: supplierKey,
|
|
|
|
Backend: backendString,
|
|
|
|
Path: path,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseOutputFlag(out string) (*output.OutputConfig, error) {
|
|
|
|
schemeOpts := strings.Split(out, "://")
|
|
|
|
if len(schemeOpts) < 2 || schemeOpts[0] == "" {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Unable to parse output flag: %s\nAccepted formats are: %s",
|
|
|
|
out,
|
|
|
|
strings.Join(output.SupportedOutputsExample(), ","),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
o := schemeOpts[0]
|
|
|
|
if !output.IsSupported(o) {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Unsupported output '%s'\nValid formats are: %s",
|
|
|
|
o,
|
|
|
|
strings.Join(output.SupportedOutputsExample(), ","),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
opts := schemeOpts[1:]
|
|
|
|
options := map[string]string{}
|
|
|
|
|
|
|
|
switch o {
|
|
|
|
case output.JSONOutputType:
|
|
|
|
if len(opts) != 1 || opts[0] == "" {
|
|
|
|
return nil, fmt.Errorf(
|
|
|
|
"Invalid json output '%s'\nMust be of kind: %s",
|
|
|
|
out,
|
|
|
|
output.Example(output.JSONOutputType),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
options["path"] = opts[0]
|
|
|
|
}
|
|
|
|
|
|
|
|
return &output.OutputConfig{
|
|
|
|
Key: o,
|
|
|
|
Options: options,
|
|
|
|
}, nil
|
|
|
|
}
|