diff --git a/README.md b/README.md index fadc1f25..846ff073 100644 --- a/README.md +++ b/README.md @@ -338,7 +338,7 @@ Currently, *falcoctl* supports only two types of artifacts: **plugin** and **rul * `--annotation-source`: set annotation source for the artifact; * `--depends-on`: set an artifact dependency (can be specified multiple times). Example: `--depends-on my-plugin:1.2.3` * `--tag`: additional artifact tag. Can be repeated multiple time -* `--type`: type of artifact to be pushed. Allowed values: `rulesfile`, `plugin` +* `--type`: type of artifact to be pushed. Allowed values: `rulesfile`, `plugin`, `asset` ### Falcoctl registry pull Pulling **artifacts** involves specifying the reference. The type of **artifact** is not required since the tool will implicitly extract it from the OCI **artifact**: diff --git a/cmd/artifact/follow/follow.go b/cmd/artifact/follow/follow.go index a11d2b55..da96bdf1 100644 --- a/cmd/artifact/follow/follow.go +++ b/cmd/artifact/follow/follow.go @@ -81,8 +81,7 @@ Example - Install and follow "cloudtrail" plugins using a fully qualified refere type artifactFollowOptions struct { *options.Common *options.Registry - rulesfilesDir string - pluginsDir string + *options.Directory tmpDir string every time.Duration cron string @@ -101,6 +100,7 @@ func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Comma o := artifactFollowOptions{ Common: opt, Registry: &options.Registry{}, + Directory: &options.Directory{}, closeChan: make(chan bool), versions: config.FalcoVersions{}, } @@ -147,26 +147,38 @@ func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Comma } // Override "rulesfiles-dir" flag with viper config if not set by user. - f = cmd.Flags().Lookup(install.FlagRulesFilesDir) + f = cmd.Flags().Lookup(options.FlagRulesFilesDir) if f == nil { // should never happen - return fmt.Errorf("unable to retrieve flag %q", install.FlagRulesFilesDir) + return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir) } else if !f.Changed && viper.IsSet(config.ArtifactFollowRulesfilesDirKey) { val := viper.Get(config.ArtifactFollowRulesfilesDirKey) if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil { - return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagRulesFilesDir, err) + return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagRulesFilesDir, err) } } // Override "plugins-dir" flag with viper config if not set by user. - f = cmd.Flags().Lookup(install.FlagPluginsFilesDir) + f = cmd.Flags().Lookup(options.FlagPluginsFilesDir) if f == nil { // should never happen - return fmt.Errorf("unable to retrieve flag %q", install.FlagPluginsFilesDir) + return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir) } else if !f.Changed && viper.IsSet(config.ArtifactFollowPluginsDirKey) { val := viper.Get(config.ArtifactFollowPluginsDirKey) if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil { - return fmt.Errorf("unable to overwrite %q flag: %w", install.FlagPluginsFilesDir, err) + return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagPluginsFilesDir, err) + } + } + + // Override "assets-dir" flag with viper config if not set by user. + f = cmd.Flags().Lookup(options.FlagAssetsFilesDir) + if f == nil { + // should never happen + return fmt.Errorf("unable to retrieve flag %q", options.FlagAssetsFilesDir) + } else if !f.Changed && viper.IsSet(config.ArtifactFollowAssetsDirKey) { + val := viper.Get(config.ArtifactFollowAssetsDirKey) + if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil { + return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagAssetsFilesDir, err) } } @@ -222,15 +234,11 @@ func NewArtifactFollowCmd(ctx context.Context, opt *options.Common) *cobra.Comma } o.Registry.AddFlags(cmd) + o.Directory.AddFlags(cmd) cmd.Flags().DurationVarP(&o.every, "every", "e", config.FollowResync, "Time interval how often it checks for a new version of the "+ "artifact. Cannot be used together with 'cron' option.") cmd.Flags().StringVar(&o.cron, "cron", "", "Cron-like string to specify interval how often it checks for a new version of the artifact."+ " Cannot be used together with 'every' option.") - // TODO (alacuku): move it in a dedicate data structure since they are in common with artifactInstall command. - cmd.Flags().StringVarP(&o.rulesfilesDir, install.FlagRulesFilesDir, "", config.RulesfilesDir, - "Directory where to install rules") - cmd.Flags().StringVarP(&o.pluginsDir, install.FlagPluginsFilesDir, "", config.PluginsDir, - "Directory where to install plugins") cmd.Flags().StringVar(&o.tmpDir, "tmp-dir", "", "Directory where to save temporary files") cmd.Flags().StringVar(&o.falcoVersions, "falco-versions", "http://localhost:8765/versions", "Where to retrieve versions, it can be either an URL or a path to a file") @@ -298,8 +306,9 @@ func (o *artifactFollowOptions) RunArtifactFollow(ctx context.Context, args []st cfg := &follower.Config{ WaitGroup: &wg, Resync: sched, - RulesfilesDir: o.rulesfilesDir, - PluginsDir: o.pluginsDir, + RulesfilesDir: o.RulesfilesDir, + PluginsDir: o.PluginsDir, + AssetsDir: o.AssetsDir, ArtifactReference: ref, PlainHTTP: o.PlainHTTP, CloseChan: o.closeChan, diff --git a/cmd/artifact/install/constants.go b/cmd/artifact/install/constants.go index ea969a56..7cc69551 100644 --- a/cmd/artifact/install/constants.go +++ b/cmd/artifact/install/constants.go @@ -16,13 +16,6 @@ package install const ( - - // FlagRulesFilesDir is the name of the flag to specify the directory path of the rules files. - FlagRulesFilesDir = "rulesfiles-dir" - - // FlagPluginsFilesDir is the name of the flag to specify the directory path of the plugins. - FlagPluginsFilesDir = "plugins-dir" - // FlagAllowedTypes is the name of the flag to specify allowed artifact types. FlagAllowedTypes = "allowed-types" diff --git a/cmd/artifact/install/install.go b/cmd/artifact/install/install.go index ddb2d1f0..b8f49a26 100644 --- a/cmd/artifact/install/install.go +++ b/cmd/artifact/install/install.go @@ -72,18 +72,18 @@ Example - Install "cloudtrail" plugins using a fully qualified reference: type artifactInstallOptions struct { *options.Common *options.Registry - rulesfilesDir string - pluginsDir string - allowedTypes oci.ArtifactTypeSlice - resolveDeps bool - noVerify bool + *options.Directory + allowedTypes oci.ArtifactTypeSlice + resolveDeps bool + noVerify bool } // NewArtifactInstallCmd returns the artifact install command. func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Command { o := artifactInstallOptions{ - Common: opt, - Registry: &options.Registry{}, + Common: opt, + Registry: &options.Registry{}, + Directory: &options.Directory{}, } cmd := &cobra.Command{ @@ -93,26 +93,38 @@ func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Comm Long: longInstall, PreRunE: func(cmd *cobra.Command, args []string) error { // Override "rulesfiles-dir" flag with viper config if not set by user. - f := cmd.Flags().Lookup(FlagRulesFilesDir) + f := cmd.Flags().Lookup(options.FlagRulesFilesDir) if f == nil { // should never happen - return fmt.Errorf("unable to retrieve flag %q", FlagRulesFilesDir) + return fmt.Errorf("unable to retrieve flag %q", options.FlagRulesFilesDir) } else if !f.Changed && viper.IsSet(config.ArtifactInstallRulesfilesDirKey) { val := viper.Get(config.ArtifactInstallRulesfilesDirKey) if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil { - return fmt.Errorf("unable to overwrite %q flag: %w", FlagRulesFilesDir, err) + return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagRulesFilesDir, err) } } // Override "plugins-dir" flag with viper config if not set by user. - f = cmd.Flags().Lookup(FlagPluginsFilesDir) + f = cmd.Flags().Lookup(options.FlagPluginsFilesDir) if f == nil { // should never happen - return fmt.Errorf("unable to retrieve flag %q", FlagPluginsFilesDir) + return fmt.Errorf("unable to retrieve flag %q", options.FlagPluginsFilesDir) } else if !f.Changed && viper.IsSet(config.ArtifactInstallPluginsDirKey) { val := viper.Get(config.ArtifactInstallPluginsDirKey) if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil { - return fmt.Errorf("unable to overwrite %q flag: %w", FlagPluginsFilesDir, err) + return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagPluginsFilesDir, err) + } + } + + // Override "assets-dir" flag with viper config if not set by user. + f = cmd.Flags().Lookup(options.FlagAssetsFilesDir) + if f == nil { + // should never happen + return fmt.Errorf("unable to retrieve flag %q", options.FlagAssetsFilesDir) + } else if !f.Changed && viper.IsSet(config.ArtifactFollowAssetsDirKey) { + val := viper.Get(config.ArtifactFollowAssetsDirKey) + if err := cmd.Flags().Set(f.Name, fmt.Sprintf("%v", val)); err != nil { + return fmt.Errorf("unable to overwrite %q flag: %w", options.FlagAssetsFilesDir, err) } } @@ -161,10 +173,7 @@ func NewArtifactInstallCmd(ctx context.Context, opt *options.Common) *cobra.Comm } o.Registry.AddFlags(cmd) - cmd.Flags().StringVarP(&o.rulesfilesDir, FlagRulesFilesDir, "", config.RulesfilesDir, - "directory where to install rules.") - cmd.Flags().StringVarP(&o.pluginsDir, FlagPluginsFilesDir, "", config.PluginsDir, - "directory where to install plugins.") + o.Directory.AddFlags(cmd) cmd.Flags().Var(&o.allowedTypes, FlagAllowedTypes, fmt.Sprintf(`list of artifact types that can be installed. If not specified or configured, all types are allowed. It accepts comma separated values or it can be repeated multiple times. @@ -300,9 +309,13 @@ func (o *artifactInstallOptions) RunArtifactInstall(ctx context.Context, args [] var destDir string switch result.Type { case oci.Plugin: - destDir = o.pluginsDir + destDir = o.PluginsDir case oci.Rulesfile: - destDir = o.rulesfilesDir + destDir = o.RulesfilesDir + case oci.Asset: + destDir = o.AssetsDir + default: + return fmt.Errorf("unrecognized result type %q while pulling artifact", result.Type) } // Check if directory exists and is writable. diff --git a/cmd/artifact/list/artifact_list.go b/cmd/artifact/list/artifact_list.go index 432d7288..7e8c7e4e 100644 --- a/cmd/artifact/list/artifact_list.go +++ b/cmd/artifact/list/artifact_list.go @@ -52,7 +52,7 @@ func NewArtifactListCmd(ctx context.Context, opt *options.Common) *cobra.Command }, } - cmd.Flags().Var(&o.artifactType, "type", `Only list artifacts with a specific type. Allowed values: "rulesfile", "plugin""`) + cmd.Flags().Var(&o.artifactType, "type", `Only list artifacts with a specific type. Allowed values: "rulesfile", "plugin", "asset"`) cmd.Flags().StringVar(&o.index, "index", "", "Only display artifacts from a configured index") return cmd diff --git a/cmd/artifact/search/artifact_search.go b/cmd/artifact/search/artifact_search.go index d750c2a8..6cccb0b5 100644 --- a/cmd/artifact/search/artifact_search.go +++ b/cmd/artifact/search/artifact_search.go @@ -69,7 +69,7 @@ func NewArtifactSearchCmd(ctx context.Context, opt *options.Common) *cobra.Comma cmd.Flags().Float64VarP(&o.minScore, "min-score", "", defaultMinScore, "the minimum score used to match artifact names with search keywords") - cmd.Flags().Var(&o.artifactType, "type", `Only search artifacts with a specific type. Allowed values: "rulesfile", "plugin""`) + cmd.Flags().Var(&o.artifactType, "type", `Only search artifacts with a specific type. Allowed values: "rulesfile", "plugin", "asset"`) return cmd } diff --git a/cmd/registry/push/push.go b/cmd/registry/push/push.go index f016c6d2..8727caee 100644 --- a/cmd/registry/push/push.go +++ b/cmd/registry/push/push.go @@ -194,6 +194,8 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error { opts = append(opts, ocipusher.WithFilepathsAndPlatforms(paths, o.Platforms)) case oci.Rulesfile: opts = append(opts, ocipusher.WithFilepaths(paths)) + case oci.Asset: + opts = append(opts, ocipusher.WithFilepaths(paths)) } res, err := pusher.Push(ctx, o.ArtifactType, ref, opts...) diff --git a/cmd/registry/push/push_test.go b/cmd/registry/push/push_test.go index 4afc4fba..3a730ed3 100644 --- a/cmd/registry/push/push_test.go +++ b/cmd/registry/push/push_test.go @@ -45,7 +45,7 @@ Flags: --platform stringArray os and architecture of the artifact in OS/ARCH format (only for plugins artifacts) -r, --requires stringArray set an artifact requirement (can be specified multiple times). Example: "--requires plugin_api_version:1.2.3" -t, --tag stringArray additional artifact tag. Can be repeated multiple times - --type ArtifactType type of artifact to be pushed. Allowed values: "rulesfile", "plugin" (default ) + --type ArtifactType type of artifact to be pushed. Allowed values: "rulesfile", "plugin", "asset" (default ) --version string set the version of the artifact Global Flags: @@ -99,7 +99,7 @@ Flags: --platform stringArray os and architecture of the artifact in OS/ARCH format (only for plugins artifacts) -r, --requires stringArray set an artifact requirement (can be specified multiple times). Example: "--requires plugin_api_version:1.2.3" -t, --tag stringArray additional artifact tag. Can be repeated multiple times - --type ArtifactType type of artifact to be pushed. Allowed values: "rulesfile", "plugin" + --type ArtifactType type of artifact to be pushed. Allowed values: "rulesfile", "plugin", "asset" --version string set the version of the artifact Global Flags: @@ -192,10 +192,10 @@ var registryPushTests = Describe("push", func() { When("multiple rulesfiles", func() { BeforeEach(func() { - args = []string{registryCmd, pushCmd, rulesRepo, "--config", configFile, rulesfiletgz, rulesfiletgz, - "--type", "rulesfile", "--version", "1.1.1", "--plain-http"} + args = []string{registryCmd, pushCmd, "--config", configFile, + "--type", "rulesfile", "--version", "1.1.1", "--plain-http", rulesRepo, rulesfiletgz, rulesfiletgz} }) - pushAssertFailedBehavior(registryPushUsage, "ERROR expecting 1 rulesfile object received 2: invalid number of rulesfiles") + pushAssertFailedBehavior(registryPushUsage, "ERROR expecting 1 rulesfile object, received 2: invalid number of rulesfiles") }) When("unreachable registry", func() { @@ -253,7 +253,8 @@ var registryPushTests = Describe("push", func() { args = []string{registryCmd, pushCmd, pluginsRepo, pluginsRepo, "--config", configFile, "--type", "wrongType", "--version", "1.1.1", "--plain-http"} }) - pushAssertFailedBehavior(registryPushUsage, "ERROR invalid argument \"wrongType\" for \"--type\" flag: must be one of \"rulesfile\", \"plugin\"") + pushAssertFailedBehavior(registryPushUsage, "ERROR invalid argument \"wrongType\" for \"--type\" "+ + "flag: must be one of \"rulesfile\", \"plugin\", \"asset") }) }) diff --git a/internal/config/config.go b/internal/config/config.go index c75dcdb6..7367fc6a 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -69,11 +69,15 @@ const ( PluginsDir = "/usr/share/falco/plugins" // RulesfilesDir default path where rulesfiles are installed. RulesfilesDir = "/etc/falco" + // AssetsDir default path where assets are installed. + AssetsDir = "/etc/falco/assets" // FollowResync time interval how often it checks for newer version of the artifact. // Default values is set every 24 hours. FollowResync = time.Hour * 24 + // // Viper configuration keys. + // // RegistryCredentialConfigKey is the Viper key for the credentials store path configuration. //#nosec G101 -- false positive @@ -84,8 +88,10 @@ const ( RegistryAuthBasicKey = "registry.auth.basic" // RegistryAuthGcpKey is the Viper key for gcp authentication configuration. RegistryAuthGcpKey = "registry.auth.gcp" + // IndexesKey is the Viper key for indexes configuration. IndexesKey = "indexes" + // ArtifactFollowEveryKey is the Viper key for follower "every" configuration. ArtifactFollowEveryKey = "artifact.follow.every" // ArtifactFollowCronKey is the Viper key for follower "cron" configuration. @@ -98,16 +104,22 @@ const ( ArtifactFollowRulesfilesDirKey = "artifact.follow.rulesfilesdir" // ArtifactFollowPluginsDirKey is the Viper key for follower "pluginsDir" configuration. ArtifactFollowPluginsDirKey = "artifact.follow.pluginsdir" + // ArtifactFollowAssetsDirKey is the Viper key for follower "pluginsDir" configuration. + ArtifactFollowAssetsDirKey = "artifact.follow.assetsdir" // ArtifactFollowTmpDirKey is the Viper key for follower "pluginsDir" configuration. ArtifactFollowTmpDirKey = "artifact.follow.tmpdir" + // ArtifactInstallArtifactsKey is the Viper key for installer "artifacts" configuration. ArtifactInstallArtifactsKey = "artifact.install.refs" - // ArtifactInstallRulesfilesDirKey is the Viper key for follower "rulesFilesDir" configuration. + // ArtifactInstallRulesfilesDirKey is the Viper key for installer "rulesFilesDir" configuration. ArtifactInstallRulesfilesDirKey = "artifact.install.rulesfilesdir" - // ArtifactInstallPluginsDirKey is the Viper key for follower "pluginsDir" configuration. + // ArtifactInstallPluginsDirKey is the Viper key for installer "pluginsDir" configuration. ArtifactInstallPluginsDirKey = "artifact.install.pluginsdir" + // ArtifactInstallAssetsDirKey is the Viper key for installer "pluginsDir" configuration. + ArtifactInstallAssetsDirKey = "artifact.install.assetsdir" // ArtifactInstallResolveDepsKey is the Viper key for installer "resolveDeps" configuration. ArtifactInstallResolveDepsKey = "artifact.install.resolveDeps" + // ArtifactAllowedTypesKey is the Viper key for the whitelist of artifacts to be installed in the system. ArtifactAllowedTypesKey = "artifact.allowedTypes" // ArtifactNoVerifyKey is the Viper key for skipping signature verification. diff --git a/internal/follower/follower.go b/internal/follower/follower.go index 677d8ae4..398c6fcf 100644 --- a/internal/follower/follower.go +++ b/internal/follower/follower.go @@ -65,10 +65,12 @@ type Config struct { CloseChan <-chan bool // Resync time after which periodically it checks for new a new version. Resync cron.Schedule - // RulesfileDir directory where the rulesfile are stored. + // RulesfilesDir directory where the rulesfile are stored. RulesfilesDir string - // PluginsDir directory where the plugins are stored. + // PluginsDir directory where plugins are stored. PluginsDir string + // AssetsDir directory where assets are stored. + AssetsDir string // ArtifactReference reference to the artifact in a remote repository. ArtifactReference string // PlainHTTP is set to true if all registry interaction must be in plain http. @@ -311,6 +313,8 @@ func (f *Follower) destinationDir(res *oci.RegistryResult) string { dir = f.PluginsDir case oci.Rulesfile: dir = f.RulesfilesDir + case oci.Asset: + dir = f.AssetsDir } return dir } diff --git a/internal/utils/compress.go b/internal/utils/compress.go index 5c96a93e..252235fe 100644 --- a/internal/utils/compress.go +++ b/internal/utils/compress.go @@ -20,6 +20,7 @@ import ( "compress/gzip" "fmt" "io" + "io/fs" "os" "path/filepath" "strings" @@ -48,11 +49,6 @@ func CreateTarGzArchive(path string) (file string, err error) { return "", err } - header, err := tar.FileInfoHeader(fInfo, fInfo.Name()) - if err != nil { - return "", err - } - // Create new writer for gzip. gzw := gzip.NewWriter(outFile) defer func() { @@ -76,20 +72,63 @@ func CreateTarGzArchive(path string) (file string, err error) { } }() + if fInfo.IsDir() { + // write header of the directory + header, err := tar.FileInfoHeader(fInfo, path) + if err != nil { + return "", err + } + + if err = tw.WriteHeader(header); err != nil { + return "", err + } + + // walk files in the directory and copy to .tar.gz + err = filepath.Walk(path, func(path string, info fs.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + return nil + } + + return copyToTarGz(path, tw, info) + }) + if err != nil { + return "", err + } + } else { + if err = copyToTarGz(path, tw, fInfo); err != nil { + return "", err + } + } + + return outFile.Name(), err +} + +func copyToTarGz(path string, tw *tar.Writer, info fs.FileInfo) error { + header := &tar.Header{ + Name: path, + Size: info.Size(), + Mode: int64(info.Mode()), + Typeflag: tar.TypeReg, + } + // write the header - if err = tw.WriteHeader(header); err != nil { - return "", err + if err := tw.WriteHeader(header); err != nil { + return err } f, err := os.Open(path) if err != nil { - return "", err + return err } // copy file data into tar writer - if _, err = io.Copy(tw, f); err != nil { - return "", err + if _, err = io.CopyN(tw, f, info.Size()); err != nil { + return err } - return outFile.Name(), err + return nil } diff --git a/internal/utils/compress_test.go b/internal/utils/compress_test.go new file mode 100644 index 00000000..38a49725 --- /dev/null +++ b/internal/utils/compress_test.go @@ -0,0 +1,146 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package utils + +import ( + "archive/tar" + "compress/gzip" + "errors" + "fmt" + "io" + "os" + "path/filepath" + "testing" +) + +const ( + filename1 = "file1" + filename2 = "file2" +) + +func TestCreateTarGzArchiveFile(t *testing.T) { + dir := t.TempDir() + f1, err := os.Create(filepath.Join(dir, filename1)) + if err != nil { + t.Fatalf(err.Error()) + } + defer f1.Close() + + tarball, err := CreateTarGzArchive(filepath.Join(dir, filename1)) + if err != nil { + t.Fatalf(err.Error()) + } + defer os.Remove(tarball) + + file, err := os.Open(tarball) + if err != nil { + t.Fatalf(err.Error()) + } + + paths, err := listHeaders(file) + fmt.Println(paths) + if err != nil { + t.Fatalf(err.Error()) + } + + if len(paths) != 1 { + t.Fatalf("Expected 1 path, got %d", len(paths)) + } + + base := filepath.Base(paths[0]) + if base != filename1 { + t.Errorf("Expected file1, got %s", base) + } +} + +func TestCreateTarGzArchiveDir(t *testing.T) { + // Test that we can compress directories + dir := t.TempDir() + + // add some files + f1, err := os.Create(filepath.Join(dir, filename1)) + if err != nil { + t.Fatalf(err.Error()) + } + defer f1.Close() + f2, err := os.Create(filepath.Join(dir, filename2)) + if err != nil { + t.Fatalf(err.Error()) + } + defer f2.Close() + + tarball, err := CreateTarGzArchive(dir) + if err != nil { + t.Fatalf(err.Error()) + } + defer os.Remove(tarball) + + file, err := os.Open(tarball) + if err != nil { + t.Fatalf(err.Error()) + } + defer file.Close() + + paths, err := listHeaders(file) + if err != nil { + t.Fatalf(err.Error()) + } + + if len(paths) != 3 { + t.Fatalf("Expected 3 paths, got %d", len(paths)) + } + + p := filepath.Base(paths[0]) + if p != filepath.Base(dir) { + t.Errorf("Expected %s, got %s", filepath.Base(dir), p) + } + + p = filepath.Base(paths[1]) + if p != filename1 { + t.Errorf("Expected file1, got %s", p) + } + + p = filepath.Base(paths[2]) + if p != filename2 { + t.Errorf("Expected file2, got %s", p) + } +} + +func listHeaders(gzipStream io.Reader) ([]string, error) { + uncompressedStream, err := gzip.NewReader(gzipStream) + if err != nil { + return nil, err + } + + tarReader := tar.NewReader(uncompressedStream) + + var files []string + for { + header, err := tarReader.Next() + + if errors.Is(err, io.EOF) { + break + } + + if err != nil { + return nil, err + } + + files = append(files, header.Name) + } + + return files, nil +} diff --git a/pkg/oci/constants.go b/pkg/oci/constants.go index 05ed4206..4e953cb7 100644 --- a/pkg/oci/constants.go +++ b/pkg/oci/constants.go @@ -29,6 +29,12 @@ const ( // FalcoPluginLayerMediaType is the MediaType for plugins. FalcoPluginLayerMediaType = "application/vnd.cncf.falco.plugin.layer.v1+tar.gz" + // FalcoAssetConfigMediaType is the MediaType for asset's config layer. + FalcoAssetConfigMediaType = "application/vnd.cncf.falco.asset.config.v1+json" + + // FalcoAssetLayerMediaType is the MediaType for assets. + FalcoAssetLayerMediaType = "application/vnd.cncf.falco.asset.layer.v1+tar.gz" + // DefaultTag is the default tag reference to be used when none is provided. DefaultTag = "latest" ) diff --git a/pkg/oci/puller/puller.go b/pkg/oci/puller/puller.go index d716f744..b2b9c713 100644 --- a/pkg/oci/puller/puller.go +++ b/pkg/oci/puller/puller.go @@ -107,6 +107,8 @@ func (p *Puller) Pull(ctx context.Context, ref, destDir, os, arch string) (*oci. artifactType = oci.Plugin case oci.FalcoRulesfileLayerMediaType: artifactType = oci.Rulesfile + case oci.FalcoAssetLayerMediaType: + artifactType = oci.Asset default: return nil, fmt.Errorf("unknown media type: %q", manifest.Layers[0].MediaType) } diff --git a/pkg/oci/pusher/pusher.go b/pkg/oci/pusher/pusher.go index cddb0450..47380bd3 100644 --- a/pkg/oci/pusher/pusher.go +++ b/pkg/oci/pusher/pusher.go @@ -49,6 +49,8 @@ var ( ErrMismatchFilepathAndPlatform = errors.New("number of filepaths and platform should be the same") // ErrInvalidNumberRulesfiles error when the number of rulesfiles is not the one expected. ErrInvalidNumberRulesfiles = errors.New("invalid number of rulesfiles") + // ErrInvalidNumberAssets error when the number of assets is not the one expected. + ErrInvalidNumberAssets = errors.New("invalid number of assets") // ErrInvalidDependenciesFormat error when the dependencies are invalid. ErrInvalidDependenciesFormat = errors.New("invalid dependency format") ) @@ -87,9 +89,11 @@ func (p *Pusher) Push(ctx context.Context, artifactType oci.ArtifactType, return nil, err } - // First thing check that we do not have multiple rulesfiles. + // First thing check that we do not have multiple rulesfiles or multiple assets. if artifactType == oci.Rulesfile && len(o.Filepaths) != 1 { - return nil, fmt.Errorf("expecting 1 rulesfile object received %d: %w", len(o.Filepaths), ErrInvalidNumberRulesfiles) + return nil, fmt.Errorf("expecting 1 rulesfile object, received %d: %w", len(o.Filepaths), ErrInvalidNumberRulesfiles) + } else if artifactType == oci.Asset && len(o.Filepaths) != 1 { + return nil, fmt.Errorf("expecting 1 asset object, received %d: %w", len(o.Filepaths), ErrInvalidNumberAssets) } repo, err := repository.NewRepository(ref, @@ -164,8 +168,8 @@ func (p *Pusher) Push(ctx context.Context, artifactType oci.ArtifactType, } } - if artifactType == oci.Rulesfile { - // We should have only one manifestDesc. + if artifactType == oci.Rulesfile || artifactType == oci.Asset { + // We should have only one manifestDesc for any not arch dependent artifact. rootDesc = manifestDescs[0] } else { // Here we are in the case when we are dealing with a plugin. @@ -212,6 +216,10 @@ func (p *Pusher) storeMainLayer(ctx context.Context, fileStore *file.Store, layerMediaType = oci.FalcoRulesfileLayerMediaType case oci.Plugin: layerMediaType = oci.FalcoPluginLayerMediaType + case oci.Asset: + layerMediaType = oci.FalcoAssetLayerMediaType + default: + return nil, fmt.Errorf("unknown media type for main layer: %s", artifactType) } // Add the content of the principal layer to the file store. @@ -231,6 +239,10 @@ func (p *Pusher) storeConfigLayer(ctx context.Context, fileStore *file.Store, layerMediaType = oci.FalcoRulesfileConfigMediaType case oci.Plugin: layerMediaType = oci.FalcoPluginConfigMediaType + case oci.Asset: + layerMediaType = oci.FalcoAssetConfigMediaType + default: + return nil, fmt.Errorf("unknown media type for config layer: %s", artifactType) } // todo: this is likely unnecessary, since the json marshaller should do that. double-check diff --git a/pkg/oci/pusher/pusher_test.go b/pkg/oci/pusher/pusher_test.go index a70fd266..dca8097b 100644 --- a/pkg/oci/pusher/pusher_test.go +++ b/pkg/oci/pusher/pusher_test.go @@ -324,6 +324,57 @@ var _ = Describe("Pusher", func() { }) }) + Context("handling asset artifacts", func() { + BeforeEach(func() { + artifactType = oci.Asset + }) + + When("only one asset tarball is given", func() { + BeforeEach(func() { + filePaths = ocipusher.WithFilepaths([]string{testRuleTarball}) // not interested in the content + options = []ocipusher.Option{filePaths} + // Repo and default tag for the artifact + repoAndTag = "/one-asset-test:1.2.3" + repo, err = localRegistry.Repository(ctx, "one-asset-test") + Expect(err).To(BeNil()) + }) + It("should succeed", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(result).ToNot(BeNil()) + d, reader, err := repo.FetchReference(ctx, ref) + Expect(err).ToNot(HaveOccurred()) + manifest, err := test.ManifestFromReader(reader) + Expect(err).ToNot(HaveOccurred()) + // Being the artifact of type asset we expect that the retrieved descriptor is of type manifest. + Expect(d.MediaType).To(Equal(v1.MediaTypeImageManifest)) + // Checking the digest is correct. + Expect(d.Digest.String()).To(Equal(result.Digest)) + // It must have only one layer since no config layer is configured. + Expect(manifest.Layers).To(HaveLen(1)) + // The layer has to be of type asset. + Expect(manifest.Layers[0].MediaType).To(Equal(oci.FalcoAssetLayerMediaType)) + // It must have the asset config layer media type. + Expect(manifest.Config.MediaType).To(Equal(oci.FalcoAssetConfigMediaType)) + }) + }) + + When("multiple assets tarballs are given", func() { + BeforeEach(func() { + filePaths = ocipusher.WithFilepaths([]string{testRuleTarball, testRuleTarball}) // not interested in the content + options = []ocipusher.Option{filePaths} + // Repo and default tag for the artifact + repoAndTag = "/multiple-assets-test:1.2.3" + repo, err = localRegistry.Repository(ctx, "multiple-assets-test") + Expect(err).To(BeNil()) + }) + It("should error", func() { + Expect(err).To(HaveOccurred()) + Expect(errors.Is(err, ocipusher.ErrInvalidNumberAssets)).To(BeTrue()) + Expect(result).To(BeNil()) + }) + }) + }) + Context("generic error handling", func() { When("file does not exist", func() { BeforeEach(func() { diff --git a/pkg/oci/types.go b/pkg/oci/types.go index 0c7a2433..5609070d 100644 --- a/pkg/oci/types.go +++ b/pkg/oci/types.go @@ -33,6 +33,8 @@ const ( Rulesfile ArtifactType = "rulesfile" // Plugin represents a plugin artifact. Plugin ArtifactType = "plugin" + // Asset represents an artifact consumed by another plugin. + Asset ArtifactType = "asset" ) // The following functions are necessary to use ArtifactType with Cobra. @@ -45,11 +47,11 @@ func (e ArtifactType) String() string { // Set an ArtifactType. func (e *ArtifactType) Set(v string) error { switch v { - case "rulesfile", "plugin": + case "rulesfile", "plugin", "asset": *e = ArtifactType(v) return nil default: - return errors.New(`must be one of "rulesfile", "plugin"`) + return errors.New(`must be one of "rulesfile", "plugin", "asset"`) } } @@ -66,6 +68,8 @@ func (e *ArtifactType) ToMediaType() string { return FalcoRulesfileLayerMediaType case Plugin: return FalcoPluginLayerMediaType + case Asset: + return FalcoAssetLayerMediaType } // should never happen @@ -80,6 +84,8 @@ func HumanReadableMediaType(s string) string { return string(Rulesfile) case FalcoPluginLayerMediaType: return string(Plugin) + case FalcoAssetLayerMediaType: + return string(Asset) } // should never happen diff --git a/pkg/options/artifact.go b/pkg/options/artifact.go index 4a61a3a6..7ff5a431 100644 --- a/pkg/options/artifact.go +++ b/pkg/options/artifact.go @@ -65,7 +65,7 @@ func (art *Artifact) AddFlags(cmd *cobra.Command) error { "additional artifact tag. Can be repeated multiple times") cmd.Flags().Var(&art.ArtifactType, "type", - `type of artifact to be pushed. Allowed values: "rulesfile", "plugin"`) + `type of artifact to be pushed. Allowed values: "rulesfile", "plugin", "asset"`) if err := cmd.MarkFlagRequired("type"); err != nil { // this should never happen. return fmt.Errorf("unable to mark flag \"type\" as required: %w", err) diff --git a/pkg/options/directory_options.go b/pkg/options/directory_options.go new file mode 100644 index 00000000..b2a61cd0 --- /dev/null +++ b/pkg/options/directory_options.go @@ -0,0 +1,53 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (C) 2023 The Falco Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package options + +import ( + "github.com/spf13/cobra" + + "github.com/falcosecurity/falcoctl/internal/config" +) + +const ( + // FlagRulesFilesDir is the name of the flag to specify the directory path of rules files. + FlagRulesFilesDir = "rulesfiles-dir" + + // FlagPluginsFilesDir is the name of the flag to specify the directory path of plugins. + FlagPluginsFilesDir = "plugins-dir" + + // FlagAssetsFilesDir is the name of the flag to specify the directory path of assets. + FlagAssetsFilesDir = "assets-dir" +) + +// Directory options for install directories for artifacts. +type Directory struct { + // RulesfilesDir path where rule are installed + RulesfilesDir string + // PluginsDir path where plugins are installed + PluginsDir string + // AssetsDire path where assets are installed + AssetsDir string +} + +// AddFlags registers the directories flags. +func (o *Directory) AddFlags(cmd *cobra.Command) { + cmd.Flags().StringVarP(&o.RulesfilesDir, FlagRulesFilesDir, "", config.RulesfilesDir, + "Directory where to install rules") + cmd.Flags().StringVarP(&o.PluginsDir, FlagPluginsFilesDir, "", config.PluginsDir, + "Directory where to install plugins") + cmd.Flags().StringVarP(&o.AssetsDir, FlagAssetsFilesDir, "", config.AssetsDir, + "Directory where to install assets") +}