Skip to content

Commit

Permalink
feat(cmd/push): parse requirements and deps from rulesfile
Browse files Browse the repository at this point in the history
When the user does not provide requirements and dependencies
when pushing a rulefile artifact, it will try to parse the
rulefile and automatically set them in the config layer.
It only supports files in .yaml format.

Signed-off-by: Aldo Lacuku <[email protected]>
  • Loading branch information
alacuku committed Mar 8, 2024
1 parent 6e39c46 commit 8f7ad2f
Show file tree
Hide file tree
Showing 8 changed files with 546 additions and 19 deletions.
132 changes: 126 additions & 6 deletions cmd/registry/push/push.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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
}
10 changes: 4 additions & 6 deletions cmd/registry/push/push_suite_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
Expand Down
Loading

0 comments on commit 8f7ad2f

Please sign in to comment.