diff --git a/cmd/registry/push/push.go b/cmd/registry/push/push.go index 2d829d97..b97e2906 100644 --- a/cmd/registry/push/push.go +++ b/cmd/registry/push/push.go @@ -22,7 +22,10 @@ import ( "os" "path/filepath" + "github.com/blang/semver/v4" + "github.com/pterm/pterm" "github.com/spf13/cobra" + "gopkg.in/yaml.v3" "github.com/falcosecurity/falcoctl/internal/utils" "github.com/falcosecurity/falcoctl/pkg/oci" @@ -148,12 +151,22 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error { } }() + config := &oci.ArtifactConfig{ + Name: o.Name, + Version: o.Version, + } + for i, p := range paths { if err = utils.IsTarGz(filepath.Clean(p)); err != nil && !errors.Is(err, utils.ErrNotTarGz) { return err } else if err == nil { continue } else { + if o.ArtifactType == oci.Rulesfile { + if config, err = rulesConfigLayer(o.Printer.Logger, p, o.Artifact); err != nil { + return err + } + } path, err := utils.CreateTarGzArchive(p) if err != nil { return err @@ -165,11 +178,6 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error { } } - // Setup OCI artifact configuration - config := oci.ArtifactConfig{ - Name: o.Name, - Version: o.Version, - } if config.Name == "" { // extract artifact name from ref, if not provided by the user if config.Name, err = utils.NameFromRef(ref); err != nil { @@ -186,7 +194,7 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error { opts := ocipusher.Options{ ocipusher.WithTags(o.Tags...), ocipusher.WithAnnotationSource(o.AnnotationSource), - ocipusher.WithArtifactConfig(config), + ocipusher.WithArtifactConfig(*config), } switch o.ArtifactType { @@ -207,3 +215,115 @@ func (o *pushOptions) runPush(ctx context.Context, args []string) error { return nil } + +const ( + depsKey = "required_plugin_versions" + // engineKey is the key in the rulesfiles. + engineKey = "required_engine_version" + // engineRequirementKey is used as name for the engine requirement in the config layer for the rulesfile artifacts. + engineRequirementKey = "engine_version_semver" +) + +func rulesConfigLayer(logger *pterm.Logger, filePath string, artifactOptions *options.Artifact) (*oci.ArtifactConfig, error) { + var data []map[string]interface{} + + // Setup OCI artifact configuration + config := oci.ArtifactConfig{ + Name: artifactOptions.Name, + Version: artifactOptions.Version, + } + + yamlFile, err := os.ReadFile(filepath.Clean(filePath)) + if err != nil { + return nil, fmt.Errorf("unable to open rulesfile %s: %w", filePath, err) + } + + if err := yaml.Unmarshal(yamlFile, &data); err != nil { + return nil, fmt.Errorf("unable to unmarshal rulesfile %s: %w", filePath, err) + } + + // Parse the plugin dependency. + // Check if the user has provided any. + if len(artifactOptions.Dependencies) != 0 { + logger.Info("Dependencies provided by user") + if err = config.ParseDependencies(artifactOptions.Dependencies...); err != nil { + return nil, err + } + } else { + // If no user provided then try to parse them from the rulesfile. + var found bool + logger.Info("Parsing dependencies from: ", logger.Args("rulesfile", filePath)) + var requiredPluginVersionsEntry interface{} + var ok bool + for _, entry := range data { + if requiredPluginVersionsEntry, ok = entry[depsKey]; !ok { + continue + } + + var deps []oci.ArtifactDependency + byteData, err := yaml.Marshal(requiredPluginVersionsEntry) + if err != nil { + return nil, fmt.Errorf("unable to parse dependencies from rulesfile: %w", err) + } + err = yaml.Unmarshal(byteData, &deps) + if err != nil { + return nil, fmt.Errorf("unable to parse dependencies from rulesfile: %w", err) + } + logger.Info("Dependencies correctly parsed from rulesfile") + // Set the deps. + config.Dependencies = deps + found = true + break + } + if !found { + logger.Warn("No dependencies were provided by the user and none were found in the rulesfile.") + } + } + + // Parse the requirements. + // Check if the user has provided any. + if len(artifactOptions.Requirements) != 0 { + logger.Info("Requirements provided by user") + if err = config.ParseRequirements(artifactOptions.Requirements...); err != nil { + return nil, err + } + } else { + var found bool + var engineVersion string + logger.Info("Parsing requirements from: ", logger.Args("rulesfile", filePath)) + // If no user provided requirements then try to parse them from the rulesfile. + for _, entry := range data { + if requiredEngineVersionEntry, ok := entry[engineKey]; ok { + // Check if the version is an int. This is for backward compatibility. The engine version used to be an + // int but internally used by falco as a semver minor version. + // 15 -> 0.15.0 + if engVersionInt, ok := requiredEngineVersionEntry.(int); ok { + engineVersion = fmt.Sprintf("0.%d.0", engVersionInt) + } else { + engineVersion, ok = requiredEngineVersionEntry.(string) + if !ok { + return nil, fmt.Errorf("%s must be an int or a string respecting the semver specification, got type %T", engineKey, requiredEngineVersionEntry) + } + + // Check if it is in semver format. + if _, err := semver.Parse(engineVersion); err != nil { + return nil, fmt.Errorf("%s must be in semver format: %w", engineVersion, err) + } + } + + // Set the requirements. + config.Requirements = []oci.ArtifactRequirement{{ + Name: engineRequirementKey, + Version: engineVersion, + }} + found = true + break + } + } + if !found { + logger.Warn("No requirements were provided by the user and none were found in the rulesfile.") + } + } + + return &config, nil +} diff --git a/cmd/registry/push/push_suite_test.go b/cmd/registry/push/push_suite_test.go index 71ac47e0..cc5ff027 100644 --- a/cmd/registry/push/push_suite_test.go +++ b/cmd/registry/push/push_suite_test.go @@ -37,14 +37,13 @@ import ( testutils "github.com/falcosecurity/falcoctl/pkg/test" ) -//nolint:unused // false positive const ( - rulesfiletgz = "../../../pkg/test/data/rules.tar.gz" - rulesfileyaml = "../../../pkg/test/data/rules.yaml" - plugintgz = "../../../pkg/test/data/plugin.tar.gz" + rulesfiletgz = "../../../pkg/test/data/rules.tar.gz" + rulesfileyaml = "../../../pkg/test/data/rulesWithoutReqAndDeps.yaml" + rulesFileWithDepsAndReq = "../../../pkg/test/data/rules.yaml" + plugintgz = "../../../pkg/test/data/plugin.tar.gz" ) -//nolint:unused // false positive var ( registry string ctx = context.Background() @@ -102,7 +101,6 @@ var _ = AfterSuite(func() { Expect(os.RemoveAll(configDir)).Should(Succeed()) }) -//nolint:unused // false positive func executeRoot(args []string) error { rootCmd.SetArgs(args) rootCmd.SetOut(output) diff --git a/cmd/registry/push/push_test.go b/cmd/registry/push/push_test.go index 3a730ed3..4bd7b29b 100644 --- a/cmd/registry/push/push_test.go +++ b/cmd/registry/push/push_test.go @@ -32,7 +32,6 @@ import ( testutils "github.com/falcosecurity/falcoctl/pkg/test" ) -//nolint:lll,unused // no need to check for line length. var registryPushUsage = `Usage: falcoctl registry push hostname/repo[:tag|@digest] file [flags] @@ -108,7 +107,6 @@ Global Flags: --log-level string Set level for logs (info, warn, debug, trace) (default "info") ` -//nolint:unused // false positive var pushAssertFailedBehavior = func(usage, specificError string) { It("check that fails and the usage is not printed", func() { Expect(err).To(HaveOccurred()) @@ -117,14 +115,12 @@ var pushAssertFailedBehavior = func(usage, specificError string) { }) } -//nolint:unused // false positive var randomRulesRepoName = func(registry, repo string) (string, string) { rName := fmt.Sprintf("%s-%d", repo, rand.Int()) return rName, fmt.Sprintf("%s/%s", registry, rName) } -//nolint:unused // false positive -var registryPushTests = Describe("push", func() { +var _ = Describe("push", func() { var ( registryCmd = "registry" pushCmd = "push" @@ -258,7 +254,7 @@ var registryPushTests = Describe("push", func() { }) }) - Context("success", func() { + Context("success with rules without deps and requirements", func() { const ( rulesRepoBaseName = "push-rulesfile" pluginsRepoBaseName = "push-plugins" @@ -556,4 +552,365 @@ var registryPushTests = Describe("push", func() { }) }) }) + + Context("rulesfile deps and requirements", func() { + const ( + rulesRepoBaseName = "push-rulesfile" + pluginsRepoBaseName = "push-plugins" + ) + + var ( + version = "1.1.1" + // registry/rulesRepoBaseName-randomInt + fullRepoName string + // rulesRepoBaseName-randomInt + repoName string + // It is set in the config layer. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + + // Variables passed as arguments to the push command. Each test case updates them + // to point to the file on disk living in pkg/test/data. + rulesfile string + rulesfileData *testutils.RulesfileArtifact + ) + + Context("user provided deps", func() { + JustBeforeEach(func() { + // This runs after the push command, so check the returned error before proceeding. + Expect(err).ShouldNot(HaveOccurred()) + rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry) + Expect(err).ShouldNot(HaveOccurred()) + }) + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + rulesfile = rulesFileWithDepsAndReq + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--depends-on", dep1, "--depends-on", dep2, "--requires", req, "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + It("deps should be the ones provided by the user", func() { + Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Dependencies[0].Name, + rulesfileData.Layer.Config.Dependencies[0].Version)).Should(Equal(dep1)) + Expect(fmt.Sprintf("%s:%s|%s:%s", rulesfileData.Layer.Config.Dependencies[1].Name, + rulesfileData.Layer.Config.Dependencies[1].Version, rulesfileData.Layer.Config.Dependencies[1].Alternatives[0].Name, + rulesfileData.Layer.Config.Dependencies[1].Alternatives[0].Version)).Should(Equal(dep2)) + }) + }) + + Context("parsed from file deps", func() { + JustBeforeEach(func() { + // This runs after the push command, so check the returned error before proceeding. + Expect(err).ShouldNot(HaveOccurred()) + rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry) + Expect(err).ShouldNot(HaveOccurred()) + }) + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + rulesfile = rulesFileWithDepsAndReq + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + It("deps should be same as in rulesfile", func() { + Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Dependencies[0].Name, + rulesfileData.Layer.Config.Dependencies[0].Version)).Should(Equal("cloudtrail:0.2.3")) + Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Dependencies[1].Name, + rulesfileData.Layer.Config.Dependencies[1].Version)).Should(Equal("json:0.2.2")) + }) + }) + + Context("parsed from file deps with alternatives", func() { + var data = ` +- required_plugin_versions: + - name: k8saudit + version: 0.7.0 + alternatives: + - name: k8saudit-eks + version: 0.4.0 + - name: json + version: 0.7.0 +` + JustBeforeEach(func() { + // This runs after the push command, so check the returned error before proceeding. + Expect(err).ShouldNot(HaveOccurred()) + rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry) + Expect(err).ShouldNot(HaveOccurred()) + }) + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + tmpDir := GinkgoT().TempDir() + rulesfile, err = testutils.WriteToTmpFile(data, tmpDir) + Expect(err).ToNot(HaveOccurred()) + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + It("deps should be same as in rulesfile", func() { + Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Dependencies[0].Name, + rulesfileData.Layer.Config.Dependencies[0].Version)).Should(Equal("k8saudit:0.7.0")) + Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Dependencies[1].Name, + rulesfileData.Layer.Config.Dependencies[1].Version)).Should(Equal("json:0.7.0")) + Expect(fmt.Sprintf("%s:%s|%s:%s", rulesfileData.Layer.Config.Dependencies[0].Name, + rulesfileData.Layer.Config.Dependencies[0].Version, rulesfileData.Layer.Config.Dependencies[0].Alternatives[0].Name, + rulesfileData.Layer.Config.Dependencies[0].Alternatives[0].Version)).Should(Equal("k8saudit:0.7.0|k8saudit-eks:0.4.0")) + }) + }) + + Context("no deps at all", func() { + JustBeforeEach(func() { + // This runs after the push command, so check the returned error before proceeding. + Expect(err).ShouldNot(HaveOccurred()) + rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry) + Expect(err).ShouldNot(HaveOccurred()) + }) + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + rulesfile = rulesfileyaml + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + It("deps should be same as in rulesfile", func() { + Expect(rulesfileData.Layer.Config.Dependencies).Should(HaveLen(0)) + }) + }) + + Context("user provided requirement", func() { + JustBeforeEach(func() { + // This runs after the push command, so check the returned error before proceeding. + Expect(err).ShouldNot(HaveOccurred()) + rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry) + Expect(err).ShouldNot(HaveOccurred()) + }) + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + rulesfile = rulesFileWithDepsAndReq + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--requires", req, "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + It("reqs should be the ones provided by the user", func() { + Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Requirements[0].Name, + rulesfileData.Layer.Config.Requirements[0].Version)).Should(Equal(req)) + }) + }) + + Context("requirement parsed from file in semver format", func() { + JustBeforeEach(func() { + // This runs after the push command, so check the returned error before proceeding. + Expect(err).ShouldNot(HaveOccurred()) + rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry) + Expect(err).ShouldNot(HaveOccurred()) + }) + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + rulesfile = rulesFileWithDepsAndReq + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + It("reqs should be the ones provided by the user", func() { + Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Requirements[0].Name, + rulesfileData.Layer.Config.Requirements[0].Version)).Should(Equal("engine_version_semver:0.10.0")) + }) + }) + + Context("requirement parsed from file in int format", func() { + var rulesfileContent = ` +- required_engine_version: 10 +` + JustBeforeEach(func() { + // This runs after the push command, so check the returned error before proceeding. + Expect(err).ShouldNot(HaveOccurred()) + rulesfileData, err = testutils.FetchRulesfileFromRegistry(ctx, repoName, pushedTags[0], orasRegistry) + Expect(err).ShouldNot(HaveOccurred()) + }) + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + tmpDir := GinkgoT().TempDir() + rulesfile, err = testutils.WriteToTmpFile(rulesfileContent, tmpDir) + Expect(err).ToNot(HaveOccurred()) + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + AfterEach(func() { + Expect(os.RemoveAll(filepath.Dir(rulesfile))).ToNot(HaveOccurred()) + }) + + It("reqs should be the ones provided by the user", func() { + Expect(fmt.Sprintf("%s:%s", rulesfileData.Layer.Config.Requirements[0].Name, + rulesfileData.Layer.Config.Requirements[0].Version)).Should(Equal("engine_version_semver:0.10.0")) + }) + }) + + Context("requirement parsed from file -- invalid format (float)", func() { + var rulesFile = ` +- required_engine_version: 10.0 +` + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + tmpDir := GinkgoT().TempDir() + rulesfile, err = testutils.WriteToTmpFile(rulesFile, tmpDir) + Expect(err).ToNot(HaveOccurred()) + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + AfterEach(func() { + Expect(os.RemoveAll(filepath.Dir(rulesfile))).ToNot(HaveOccurred()) + }) + + It("reqs should be the ones provided by the user", func() { + Expect(err).Should(HaveOccurred()) + Expect(output).Should(gbytes.Say(regexp.QuoteMeta("required_engine_version must be an int or a string respecting " + + "the semver specification, got type float64"))) + }) + }) + + Context("requirement parsed from file -- invalid format (not semver)", func() { + var rulesFile = ` +- required_engine_version: 10.0notsemver +` + + JustAfterEach(func() { + // This variable could be changed by single tests. + // Make sure to set them at their default values. + artifactNameInConfigLayer = "test-rulesfile" + pushedTags = []string{"tag1", "tag2", "latest"} + }) + + BeforeEach(func() { + repoName, fullRepoName = randomRulesRepoName(registry, rulesRepoBaseName) + tmpDir := GinkgoT().TempDir() + rulesfile, err = testutils.WriteToTmpFile(rulesFile, tmpDir) + Expect(err).ToNot(HaveOccurred()) + args = []string{registryCmd, pushCmd, fullRepoName, rulesfile, "--config", configFile, "--type", "rulesfile", "--version", version, + "--plain-http", "--annotation-source", anSource, + "--tag", pushedTags[0], "--tag", pushedTags[1], "--tag", pushedTags[2], "--name", artifactNameInConfigLayer} + // Set name to the expected one. + artifactNameInConfigLayer = repoName + // We expect that latest tag is pushed, so set it in the pushed tags. + pushedTags = []string{"latest"} + }) + + AfterEach(func() { + Expect(os.RemoveAll(filepath.Dir(rulesfile))).ToNot(HaveOccurred()) + }) + + It("reqs should be the ones provided by the user", func() { + Expect(err).Should(HaveOccurred()) + Expect(output).Should(gbytes.Say(regexp.QuoteMeta("10.0notsemver must be in semver format: No Major.Minor.Patch elements found"))) + }) + }) + }) +}) + +var _ = Describe("rulesConfigLayer", func() { + }) diff --git a/go.mod b/go.mod index cc3ab597..ff6f425e 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.21 require ( cloud.google.com/go/storage v1.39.0 github.com/blang/semver v3.5.1+incompatible + github.com/blang/semver/v4 v4.0.0 github.com/distribution/distribution/v3 v3.0.0-alpha.1 github.com/docker/cli v25.0.3+incompatible github.com/docker/docker v25.0.3+incompatible diff --git a/go.sum b/go.sum index f030ebab..a30fb33b 100644 --- a/go.sum +++ b/go.sum @@ -181,6 +181,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/blang/semver v3.5.1+incompatible h1:cQNTCjp13qL8KC3Nbxr/y2Bqb63oX6wdnnjpJbkM4JQ= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/bshuster-repo/logrus-logstash-hook v1.1.0 h1:o2FzZifLg+z/DN1OFmzTWzZZx/roaqt8IPZCIVco8r4= github.com/bshuster-repo/logrus-logstash-hook v1.1.0/go.mod h1:Q2aXOe7rNuPgbBtPCOzYyWDvKX7+FpxE5sRdvcPoui0= github.com/bsm/ginkgo/v2 v2.7.0/go.mod h1:AiKlXPm7ItEHNc/2+OkrNG4E0ITzojb9/xWzvQ9XZ9w= diff --git a/pkg/test/data/rules.yaml b/pkg/test/data/rules.yaml index 5f256e9b..40e375d7 100644 --- a/pkg/test/data/rules.yaml +++ b/pkg/test/data/rules.yaml @@ -17,7 +17,7 @@ # # All rules files related to plugins should require at least engine version 10 -- required_engine_version: 10 +- required_engine_version: 0.10.0 - required_plugin_versions: - name: cloudtrail diff --git a/pkg/test/data/rulesWithoutReqAndDeps.yaml b/pkg/test/data/rulesWithoutReqAndDeps.yaml new file mode 100644 index 00000000..f61e91a4 --- /dev/null +++ b/pkg/test/data/rulesWithoutReqAndDeps.yaml @@ -0,0 +1,31 @@ +- rule: All Cloudtrail Events + desc: Match all cloudtrail events. + condition: + evt.num > 0 + output: Some Cloudtrail Event (evtnum=%evt.num info=%evt.plugininfo ts=%evt.time.iso8601 id=%ct.id error=%ct.error) + priority: DEBUG + tags: + - cloud + - aws + source: aws_cloudtrail + enabled: false + +- rule: Console Login Through Assume Role + desc: Detect a console login through Assume Role. + condition: + ct.name="ConsoleLogin" and not ct.error exists + and ct.user.identitytype="AssumedRole" + and json.value[/responseElements/ConsoleLogin]="Success" + output: + Detected a console login through Assume Role + (principal=%ct.user.principalid, + assumedRole=%ct.user.arn, + requesting IP=%ct.srcip, + AWS region=%ct.region) + priority: WARNING + tags: + - cloud + - aws + - aws_console + - aws_iam + source: aws_cloudtrail diff --git a/pkg/test/filesystem.go b/pkg/test/filesystem.go index 3f1f21ca..083104d9 100644 --- a/pkg/test/filesystem.go +++ b/pkg/test/filesystem.go @@ -36,3 +36,21 @@ func CreateEmptyFile(name string) (string, error) { return configFile, nil } + +// WriteToTmpFile writes data to a temporary file in the specified path. +func WriteToTmpFile(data, dirPath string) (string, error) { + tmpFile, err := os.CreateTemp(dirPath, "rulesfiles-test") + if err != nil { + return "", err + } + if _, err = tmpFile.WriteString(data); err != nil { + return "", err + } + + // Get path. + info, err := tmpFile.Stat() + if err != nil { + return "", err + } + return filepath.Join(dirPath, info.Name()), tmpFile.Close() +}