Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

pkg/cwhub: improve support for k8s config maps with custom items #3154

Merged
merged 5 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions pkg/cwhub/relativepath.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cwhub

import (
"path/filepath"
"strings"
)

// relativePathComponents returns the list of path components after baseDir.
// If path is not inside baseDir, it returns an empty slice.
func relativePathComponents(path string, baseDir string) []string {
absPath, err := filepath.Abs(path)
if err != nil {
return []string{}
}

Check warning on line 14 in pkg/cwhub/relativepath.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/relativepath.go#L13-L14

Added lines #L13 - L14 were not covered by tests
absBaseDir, err := filepath.Abs(baseDir)
if err != nil {
return []string{}
}

Check warning on line 18 in pkg/cwhub/relativepath.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/relativepath.go#L17-L18

Added lines #L17 - L18 were not covered by tests

// is path inside baseDir?
relPath, err := filepath.Rel(absBaseDir, absPath)
if err != nil || strings.HasPrefix(relPath, "..") || relPath == "." {
return []string{}
}

return strings.Split(relPath, string(filepath.Separator))
}

72 changes: 72 additions & 0 deletions pkg/cwhub/relativepath_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package cwhub

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRelativePathComponents(t *testing.T) {
tests := []struct {
name string
path string
baseDir string
expected []string
}{
{
name: "Path within baseDir",
path: "/home/user/project/src/file.go",
baseDir: "/home/user/project",
expected: []string{"src", "file.go"},
},
{
name: "Path is baseDir",
path: "/home/user/project",
baseDir: "/home/user/project",
expected: []string{},
},
{
name: "Path outside baseDir",
path: "/home/user/otherproject/src/file.go",
baseDir: "/home/user/project",
expected: []string{},
},
{
name: "Path is subdirectory of baseDir",
path: "/home/user/project/src/",
baseDir: "/home/user/project",
expected: []string{"src"},
},
{
name: "Relative paths",
path: "project/src/file.go",
baseDir: "project",
expected: []string{"src", "file.go"},
},
{
name: "BaseDir with trailing slash",
path: "/home/user/project/src/file.go",
baseDir: "/home/user/project/",
expected: []string{"src", "file.go"},
},
{
name: "Empty baseDir",
path: "/home/user/project/src/file.go",
baseDir: "",
expected: []string{},
},
{
name: "Empty path",
path: "",
baseDir: "/home/user/project",
expected: []string{},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := relativePathComponents(tt.path, tt.baseDir)
assert.Equal(t, tt.expected, result)
})
}
}
198 changes: 131 additions & 67 deletions pkg/cwhub/sync.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,22 +20,49 @@
return strings.HasSuffix(path, ".yaml") || strings.HasSuffix(path, ".yml")
}

// linkTarget returns the target of a symlink, or empty string if it's dangling.
func linkTarget(path string, logger *logrus.Logger) (string, error) {
hubpath, err := os.Readlink(path)
if err != nil {
return "", fmt.Errorf("unable to read symlink: %s", path)
// resolveSymlink returns the ultimate target path of a symlink
// returns error if the symlink is dangling or too many symlinks are followed
func resolveSymlink(path string) (string, error) {
const maxSymlinks = 10 // Prevent infinite loops
for i := 0; i < maxSymlinks; i++ {
fi, err := os.Lstat(path)
if err != nil {
return "", err // dangling link
}

Check warning on line 31 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L31

Added line #L31 was not covered by tests

if fi.Mode()&os.ModeSymlink == 0 {
// found the target
return path, nil
}

path, err = os.Readlink(path)
if err != nil {
return "", err
}

Check warning on line 41 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L40-L41

Added lines #L40 - L41 were not covered by tests

// relative to the link's directory?
if !filepath.IsAbs(path) {
path = filepath.Join(filepath.Dir(path), path)
}

Check warning on line 46 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L45-L46

Added lines #L45 - L46 were not covered by tests
}

logger.Tracef("symlink %s -> %s", path, hubpath)
return "", errors.New("too many levels of symbolic links")
}

_, err = os.Lstat(hubpath)
if os.IsNotExist(err) {
logger.Warningf("link target does not exist: %s -> %s", path, hubpath)
return "", nil
// isPathInside checks if a path is inside the given directory
// it can return false negatives if the filesystem is case insensitive
func isPathInside(path, dir string) (bool, error) {
absFilePath, err := filepath.Abs(path)
if err != nil {
return false, err
}

Check warning on line 58 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L57-L58

Added lines #L57 - L58 were not covered by tests

absDir, err := filepath.Abs(dir)
if err != nil {
return false, err

Check warning on line 62 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L62

Added line #L62 was not covered by tests
}

return hubpath, nil
return strings.HasPrefix(absFilePath, absDir), nil
}

// information used to create a new Item, from a file path.
Expand All @@ -53,58 +80,76 @@
hubDir := h.local.HubDir
installDir := h.local.InstallDir

subs := strings.Split(path, string(os.PathSeparator))
subsHub := relativePathComponents(path, hubDir)
subsInstall := relativePathComponents(path, installDir)

logger.Tracef("path:%s, hubdir:%s, installdir:%s", path, hubDir, installDir)
logger.Tracef("subs:%v", subs)
// we're in hub (~/.hub/hub/)
if strings.HasPrefix(path, hubDir) {
switch {
case len(subsHub) > 0:
logger.Tracef("in hub dir")

// .../hub/parsers/s00-raw/crowdsec/skip-pretag.yaml
// .../hub/scenarios/crowdsec/ssh_bf.yaml
// .../hub/profiles/crowdsec/linux.yaml
if len(subs) < 4 {
return nil, fmt.Errorf("path is too short: %s (%d)", path, len(subs))
// .../hub/parsers/s00-raw/crowdsecurity/skip-pretag.yaml
// .../hub/scenarios/crowdsecurity/ssh_bf.yaml
// .../hub/profiles/crowdsecurity/linux.yaml
if len(subsHub) < 3 {
return nil, fmt.Errorf("path is too short: %s (%d)", path, len(subsHub))
}

Check warning on line 95 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L94-L95

Added lines #L94 - L95 were not covered by tests

ftype := subsHub[0]
if !slices.Contains(ItemTypes, ftype) {
// this doesn't really happen anymore, because we only scan the {hubtype} directories
return nil, fmt.Errorf("unknown configuration type '%s'", ftype)
}

Check warning on line 101 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L99-L101

Added lines #L99 - L101 were not covered by tests

stage := ""
fauthor := subsHub[1]
fname := subsHub[2]

if ftype == PARSERS || ftype == POSTOVERFLOWS {
stage = subsHub[1]
fauthor = subsHub[2]
fname = subsHub[3]
}

ret = &itemFileInfo{
inhub: true,
fname: subs[len(subs)-1],
fauthor: subs[len(subs)-2],
stage: subs[len(subs)-3],
ftype: subs[len(subs)-4],
ftype: ftype,
stage: stage,
fauthor: fauthor,
fname: fname,
}
} else if strings.HasPrefix(path, installDir) { // we're in install /etc/crowdsec/<type>/...

case len(subsInstall) > 0:
logger.Tracef("in install dir")

if len(subs) < 3 {
return nil, fmt.Errorf("path is too short: %s (%d)", path, len(subs))
}
// .../config/parser/stage/file.yaml
// .../config/postoverflow/stage/file.yaml
// .../config/scenarios/scenar.yaml
// .../config/collections/linux.yaml //file is empty
ret = &itemFileInfo{
inhub: false,
fname: subs[len(subs)-1],
stage: subs[len(subs)-2],
ftype: subs[len(subs)-3],
fauthor: "",

if len(subsInstall) < 2 {
return nil, fmt.Errorf("path is too short: %s (%d)", path, len(subsInstall))

Check warning on line 130 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L130

Added line #L130 was not covered by tests
}
} else {
return nil, fmt.Errorf("file '%s' is not from hub '%s' nor from the configuration directory '%s'", path, hubDir, installDir)
}

logger.Tracef("stage:%s ftype:%s", ret.stage, ret.ftype)
// this can be in any number of subdirs, we join them to compose the item name

ftype := subsInstall[0]
stage := ""
fname := strings.Join(subsInstall[1:], "/")

if ret.ftype != PARSERS && ret.ftype != POSTOVERFLOWS {
if !slices.Contains(ItemTypes, ret.stage) {
return nil, errors.New("unknown configuration type")
if ftype == PARSERS || ftype == POSTOVERFLOWS {
stage = subsInstall[1]
fname = strings.Join(subsInstall[2:], "/")
}

ret.ftype = ret.stage
ret.stage = ""
ret = &itemFileInfo{
inhub: false,
ftype: ftype,
stage: stage,
fauthor: "",
fname: fname,
}
default:
return nil, fmt.Errorf("file '%s' is not from hub '%s' nor from the configuration directory '%s'", path, hubDir, installDir)

Check warning on line 152 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L151-L152

Added lines #L151 - L152 were not covered by tests
}

logger.Tracef("CORRECTED [%s] by [%s] in stage [%s] of type [%s]", ret.fname, ret.fauthor, ret.stage, ret.ftype)
Expand Down Expand Up @@ -176,8 +221,6 @@
}

func (h *Hub) itemVisit(path string, f os.DirEntry, err error) error {
hubpath := ""

if err != nil {
h.logger.Debugf("while syncing hub dir: %s", err)
// there is a path error, we ignore the file
Expand All @@ -190,8 +233,26 @@
return err
}

// permission errors, files removed while reading, etc.
if f == nil {
return nil
}

Check warning on line 239 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L238-L239

Added lines #L238 - L239 were not covered by tests

if f.IsDir() {
// if a directory starts with a dot, we don't traverse it
// - single dot prefix is hidden by unix convention
// - double dot prefix is used by k8s to mount config maps
if strings.HasPrefix(f.Name(), ".") {
h.logger.Tracef("skipping hidden directory %s", path)
return filepath.SkipDir
}

Check warning on line 248 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L248

Added line #L248 was not covered by tests

// keep traversing
return nil
}

// we only care about YAML files
if f == nil || f.IsDir() || !isYAMLFileName(f.Name()) {
if !isYAMLFileName(f.Name()) {
return nil
}

Expand All @@ -201,35 +262,38 @@
return nil
}

// non symlinks are local user files or hub files
if f.Type()&os.ModeSymlink == 0 {
h.logger.Tracef("%s is not a symlink", path)

if !info.inhub {
h.logger.Tracef("%s is a local file, skip", path)
// follow the link to see if it falls in the hub directory
// if it's not a link, target == path
target, err := resolveSymlink(path)
if err != nil {
// target does not exist, the user might have removed the file
// or switched to a hub branch without it; or symlink loop

Check warning on line 270 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L269-L270

Added lines #L269 - L270 were not covered by tests
h.logger.Warningf("Ignoring file %s: %s", path, err)
return nil
}

Check warning on line 273 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L273

Added line #L273 was not covered by tests

item, err := newLocalItem(h, path, info)
if err != nil {
return err
}
targetInHub, err := isPathInside(target, h.local.HubDir)
if err != nil {
h.logger.Warningf("Ignoring file %s: %s", path, err)
return nil
}

Check warning on line 279 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L277-L279

Added lines #L277 - L279 were not covered by tests

h.addItem(item)
// local (custom) item if the file or link target is not inside the hub dir
if !targetInHub {
h.logger.Tracef("%s is a local file, skip", path)

return nil
}
} else {
hubpath, err = linkTarget(path, h.logger)
item, err := newLocalItem(h, path, info)
if err != nil {
return err
}

if hubpath == "" {
// target does not exist, the user might have removed the file
// or switched to a hub branch without it
return nil
}
h.addItem(item)

Check warning on line 291 in pkg/cwhub/sync.go

View check run for this annotation

Codecov / codecov/patch

pkg/cwhub/sync.go#L291

Added line #L291 was not covered by tests
return nil
}

hubpath := target

// try to find which configuration item it is
h.logger.Tracef("check [%s] of %s", info.fname, info.ftype)

Expand Down
Loading