diff --git a/cmd/artifact/follow/follow.go b/cmd/artifact/follow/follow.go index a6e59c513..5c9aec7f7 100644 --- a/cmd/artifact/follow/follow.go +++ b/cmd/artifact/follow/follow.go @@ -32,6 +32,7 @@ import ( "github.com/falcosecurity/falcoctl/internal/follower" "github.com/falcosecurity/falcoctl/internal/utils" "github.com/falcosecurity/falcoctl/pkg/index" + "github.com/falcosecurity/falcoctl/pkg/oci" "github.com/falcosecurity/falcoctl/pkg/options" "github.com/falcosecurity/falcoctl/pkg/output" ) @@ -88,6 +89,7 @@ type artifactFollowOptions struct { versions config.FalcoVersions timeout time.Duration closeChan chan bool + allowedTypes oci.ArtifactTypeSlice } // NewArtifactFollowCmd returns the artifact follow command. @@ -184,6 +186,21 @@ func NewArtifactFollowCmd(ctx context.Context, opt *options.CommonOptions) *cobr } } + // Override "allowed-types" flag with viper config if not set by user. + f = cmd.Flags().Lookup("allowed-types") + if f == nil { + // should never happen + o.Printer.CheckErr(fmt.Errorf("unable to retrieve flag allowed-types")) + } else if !f.Changed && viper.IsSet(config.ArtifactAllowedTypesKey) { + val, err := config.ArtifactAllowedTypes() + if err != nil { + o.Printer.CheckErr(err) + } + if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil { + o.Printer.CheckErr(fmt.Errorf("unable to overwrite \"allowed-types\" flag: %w", err)) + } + } + if o.every != 0 && o.cron != "" { o.Printer.CheckErr(fmt.Errorf("can't set both \"cron\" and \"every\" flags")) } @@ -214,6 +231,7 @@ func NewArtifactFollowCmd(ctx context.Context, opt *options.CommonOptions) *cobr "Where to retrieve versions, it can be either an URL or a path to a file") cmd.Flags().DurationVar(&o.timeout, "timeout", defaultBackoffConfig.MaxDelay, "Timeout for initial connection to the Falco versions endpoint") + cmd.Flags().Var(&o.allowedTypes, "allowed-types", "whitelist of artifacts type that can be followed") return cmd } @@ -285,6 +303,7 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st CloseChan: o.closeChan, TmpDir: o.tmpDir, FalcoVersions: o.versions, + AllowedTypes: o.allowedTypes, } fol, err := follower.New(ctx, ref, o.Printer, cfg) if err != nil { diff --git a/cmd/artifact/install/install.go b/cmd/artifact/install/install.go index 581785b42..976555159 100644 --- a/cmd/artifact/install/install.go +++ b/cmd/artifact/install/install.go @@ -71,6 +71,7 @@ type artifactInstallOptions struct { *options.RegistryOptions rulesfilesDir string pluginsDir string + allowedTypes oci.ArtifactTypeSlice } // NewArtifactInstallCmd returns the artifact install command. @@ -117,6 +118,21 @@ func NewArtifactInstallCmd(ctx context.Context, opt *options.CommonOptions) *cob if err := utils.ExistsAndIsWritable(f.Value.String()); err != nil { o.Printer.CheckErr(fmt.Errorf("plugins-dir: %w", err)) } + + // Override "allowed-types" flag with viper config if not set by user. + f = cmd.Flags().Lookup("allowed-types") + if f == nil { + // should never happen + o.Printer.CheckErr(fmt.Errorf("unable to retrieve flag allowed-types")) + } else if !f.Changed && viper.IsSet(config.ArtifactAllowedTypesKey) { + val, err := config.ArtifactAllowedTypes() + if err != nil { + o.Printer.CheckErr(err) + } + if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil { + o.Printer.CheckErr(fmt.Errorf("unable to overwrite \"allowed-types\" flag: %w", err)) + } + } }, Run: func(cmd *cobra.Command, args []string) { o.Printer.CheckErr(o.RunArtifactInstall(ctx, args)) @@ -128,6 +144,7 @@ func NewArtifactInstallCmd(ctx context.Context, opt *options.CommonOptions) *cob "directory where to install rules. Defaults to /etc/falco") cmd.Flags().StringVarP(&o.pluginsDir, "plugins-dir", "", config.PluginsDir, "directory where to install plugins. Defaults to /usr/share/falco/plugins") + cmd.Flags().Var(&o.allowedTypes, "allowed-types", "whitelist of artifacts type that can be installed") return cmd } @@ -137,7 +154,7 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] // Retrieve configuration for installer configuredInstaller, err := config.Installer() if err != nil { - o.Printer.CheckErr(fmt.Errorf("unable to retrieved the configured installer: %w", err)) + o.Printer.CheckErr(fmt.Errorf("unable to retrieve the configured installer: %w", err)) } // Set args as configured if no arg was passed @@ -233,6 +250,10 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] return err } + if err := puller.CheckAllowedType(ctx, ref, o.allowedTypes.Types); err != nil { + return err + } + // Install will always install artifact for the current OS and architecture result, err := puller.Pull(ctx, ref, tmpDir, runtime.GOOS, runtime.GOARCH) if err != nil { diff --git a/internal/config/config.go b/internal/config/config.go index 74cb81e67..89e9ed9a2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -27,6 +27,8 @@ import ( "github.com/docker/docker/pkg/homedir" "github.com/mitchellh/mapstructure" "github.com/spf13/viper" + + "github.com/falcosecurity/falcoctl/pkg/oci" ) var ( @@ -92,6 +94,8 @@ const ( ArtifactInstallRulesfilesDirKey = "artifact.install.rulesfilesdir" // ArtifactInstallPluginsDirKey is the Viper key for follower "pluginsDir" configuration. ArtifactInstallPluginsDirKey = "artifact.install.pluginsdir" + // ArtifactAllowedTypesKey is the Viper key for the whitelist of artifacts to be installed in the system. + ArtifactAllowedTypesKey = "artifact.allowedTypes" ) // Index represents a configured index. @@ -161,6 +165,9 @@ func Load(path string) error { // Set default index viper.SetDefault(IndexesKey, []Index{DefaultIndex}) + // Set default artifact types + viper.SetDefault(ArtifactAllowedTypesKey, []string{oci.Rulesfile.String()}) + err = viper.ReadInConfig() if errors.As(err, &viper.ConfigFileNotFoundError{}) || os.IsNotExist(err) { // If the config is not found, we create the file with the @@ -414,6 +421,32 @@ func Installer() (Install, error) { }, nil } +// ArtifactAllowedTypes retrieves the allowed types section of the config file. +func ArtifactAllowedTypes() (*oci.ArtifactTypeSlice, error) { + allowedTypes := viper.GetStringSlice(ArtifactAllowedTypesKey) + if len(allowedTypes) == 1 { // in this case it might come from the env + if !CommaSeparatedRegexp.MatchString(allowedTypes[0]) { + return nil, fmt.Errorf("env variable not correctly set, should match %q, got %q", SemicolonSeparatedRegexp.String(), allowedTypes[0]) + } + allowedTypes = strings.Split(allowedTypes[0], ",") + } + + var allowedArtifactTypes []oci.ArtifactType + for _, t := range allowedTypes { + var at oci.ArtifactType + if err := at.Set(t); err != nil { + return nil, fmt.Errorf("unrecognized artifact type in config: %q", t) + } + + allowedArtifactTypes = append(allowedArtifactTypes, at) + } + + return &oci.ArtifactTypeSlice{ + Types: allowedArtifactTypes, + CommaSeparatedString: strings.Join(allowedTypes, ","), + }, nil +} + // UpdateConfigFile is used to update a section of the config file. // We create a brand new viper instance for doing it so that we are sure that modifications // are scoped to the passed key with no side effects (e.g user forgot to unset one env variable for diff --git a/internal/follower/follower.go b/internal/follower/follower.go index 8e1e015f8..476b1dc14 100644 --- a/internal/follower/follower.go +++ b/internal/follower/follower.go @@ -74,6 +74,8 @@ type Config struct { // FalcoVersions is a struct containing all the required Falco versions that this follower // has to take into account when installing artifacts. FalcoVersions config.FalcoVersions + // AllowedTypes specify a list of artifacts that we are allowed to download. + AllowedTypes oci.ArtifactTypeSlice } var ( @@ -236,6 +238,11 @@ func (f *Follower) follow(ctx context.Context) { // pull downloads, extracts, and installs the artifact. func (f *Follower) pull(ctx context.Context) (filePaths []string, res *oci.RegistryResult, err error) { + f.Verbosef("check if pulling an allowed type of artifact") + if err := f.Puller.CheckAllowedType(ctx, f.ref, f.Config.AllowedTypes.Types); err != nil { + return nil, nil, err + } + // Pull the artifact from the repository. f.Verbosef("pulling artifact %q", f.ref) res, err = f.Pull(ctx, f.ref, f.tmpDir, runtime.GOOS, runtime.GOARCH) diff --git a/pkg/oci/puller/puller.go b/pkg/oci/puller/puller.go index 44a5bf41d..359e8b648 100644 --- a/pkg/oci/puller/puller.go +++ b/pkg/oci/puller/puller.go @@ -156,8 +156,8 @@ func manifestFromDesc(ctx context.Context, target oras.Target, desc *v1.Descript return &manifest, nil } -// PullConfigLayer fetches only the config layer from a given ref. -func (p *Puller) PullConfigLayer(ctx context.Context, ref string) (*oci.ArtifactConfig, error) { +// manifestFromRef retieves the manifest of an artifact, also taking care of resolving to it walking through indexes. +func (p *Puller) manifestFromRef(ctx context.Context, ref string) (*v1.Manifest, error) { repo, err := repository.NewRepository(ref, repository.WithClient(p.Client), repository.WithPlainHTTP(p.plainHTTP)) if err != nil { return nil, err @@ -214,6 +214,21 @@ func (p *Puller) PullConfigLayer(ctx context.Context, ref string) (*oci.Artifact return nil, fmt.Errorf("unable to unmarshal manifest: %w", err) } + return &manifest, nil +} + +// PullConfigLayer fetches only the config layer from a given ref. +func (p *Puller) PullConfigLayer(ctx context.Context, ref string) (*oci.ArtifactConfig, error) { + repo, err := repository.NewRepository(ref, repository.WithClient(p.Client), repository.WithPlainHTTP(p.plainHTTP)) + if err != nil { + return nil, err + } + + manifest, err := p.manifestFromRef(ctx, ref) + if err != nil { + return nil, err + } + configRef := manifest.Config.Digest.String() descriptor, err := repo.Blobs().Resolve(ctx, configRef) @@ -223,7 +238,7 @@ func (p *Puller) PullConfigLayer(ctx context.Context, ref string) (*oci.Artifact rc, err := repo.Fetch(ctx, descriptor) if err != nil { - return nil, fmt.Errorf("unable to fetch descriptor with digest: %s", desc.Digest.String()) + return nil, fmt.Errorf("unable to fetch descriptor with digest: %s", descriptor.Digest.String()) } configBytes, err := io.ReadAll(rc) @@ -238,3 +253,33 @@ func (p *Puller) PullConfigLayer(ctx context.Context, ref string) (*oci.Artifact return &artifactConfig, nil } + +// CheckAllowedType does a preliminary check on the manifest to state whether we are allowed +// or not to download this type of artifact. +func (p *Puller) CheckAllowedType(ctx context.Context, ref string, allowedTypes []oci.ArtifactType) error { + if len(allowedTypes) == 0 { + return fmt.Errorf("cannot download any artifact types because any was allowed") + } + + manifest, err := p.manifestFromRef(ctx, ref) + if err != nil { + return err + } + + if len(manifest.Layers) == 0 { + return fmt.Errorf("malformed artifact, expected to find at least one layer for ref %q", ref) + } + + var allowedMediaTypes []string + for _, t := range allowedTypes { + allowedMediaTypes = append(allowedMediaTypes, t.ToMediaType()) + } + + for _, t := range allowedMediaTypes { + if manifest.Layers[0].MediaType == t { + return nil + } + } + + return fmt.Errorf("cannot download artifact of type %q: not permitted", manifest.Layers[0].MediaType) +} diff --git a/pkg/oci/types.go b/pkg/oci/types.go index b66c61b0d..05528bc59 100644 --- a/pkg/oci/types.go +++ b/pkg/oci/types.go @@ -17,6 +17,7 @@ package oci import ( "errors" "fmt" + "regexp" "sort" "strings" @@ -36,8 +37,8 @@ const ( // The following functions are necessary to use ArtifactType with Cobra. // String returns a string representation of ArtifactType. -func (e *ArtifactType) String() string { - return string(*e) +func (e ArtifactType) String() string { + return string(e) } // Set an ArtifactType. @@ -56,6 +57,57 @@ func (e *ArtifactType) Type() string { return "ArtifactType" } +// ToMediaType converts type to its corresponding media type. +// Ensure this is called after a Set(). +func (e *ArtifactType) ToMediaType() string { + switch *e { + case Rulesfile: + return FalcoRulesfileLayerMediaType + case Plugin: + return FalcoPluginLayerMediaType + } + + // should never happen + return "" +} + +// ArtifactTypeSlice is a slice of ArtifactType, can be passed as comma separated values. +type ArtifactTypeSlice struct { + Types []ArtifactType + CommaSeparatedString string +} + +// String returns a string representation of ArtifactTypeSlice. +func (e ArtifactTypeSlice) String() string { + return e.CommaSeparatedString +} + +// Set an ArtifactType. +func (e *ArtifactTypeSlice) Set(v string) error { + commaSeparatedRegexp := regexp.MustCompile(`^([^,]+)(,[^,]+)*$`) + if !commaSeparatedRegexp.MatchString(v) { + return fmt.Errorf("%q is not a valid comma separated string", v) + } + + e.CommaSeparatedString = v + + tokens := strings.Split(v, ",") + for _, token := range tokens { + var at ArtifactType + if err := at.Set(token); err != nil { + return fmt.Errorf("not valid token: %w", err) + } + e.Types = append(e.Types, at) + } + + return nil +} + +// Type returns a string representing this type. +func (e *ArtifactTypeSlice) Type() string { + return "ArtifactTypeSlice" +} + // RegistryResult represents a generic result that is generated when // interacting with a remote OCI registry. type RegistryResult struct {