-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Use
/proc
fs to access mount namespace of files for hash calculation
The `/proc` fs is now used to acess the filesystem mount of a container, when enriching exec events with files hashes. This allows to drop the required config option to the containerd path.
- Loading branch information
1 parent
15bdb2b
commit 88fac20
Showing
8 changed files
with
292 additions
and
49 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,42 +1,149 @@ | ||
package enrichment | ||
|
||
import ( | ||
"context" | ||
"errors" | ||
"fmt" | ||
"io" | ||
"io/fs" | ||
"path/filepath" | ||
"strconv" | ||
"syscall" | ||
|
||
castpb "github.com/castai/kvisor/api/v1/runtime" | ||
"github.com/castai/kvisor/pkg/containers" | ||
"github.com/castai/kvisor/pkg/logging" | ||
"github.com/minio/sha256-simd" | ||
lru "github.com/hashicorp/golang-lru/v2" | ||
"github.com/minio/sha256-simd" | ||
) | ||
|
||
func EnrichWithFileHash(log *logging.Logger, fsys fs.FS) func(*castpb.Event) { | ||
return func(e *castpb.Event) { | ||
exec := e.GetExec() | ||
if exec == nil || exec.Path == "" { | ||
return | ||
} | ||
type fileHashCacheKey string | ||
type ContainerForCgroupGetter func(cgroup uint64) (*containers.Container, bool, error) | ||
|
||
func EnrichWithFileHash(log *logging.Logger, containerForCgroupGetter ContainerForCgroupGetter, procFS fs.StatFS) EventEnricher { | ||
cache, err := lru.New[fileHashCacheKey, []byte](1024) | ||
if err != nil { | ||
// This case can never happen, as err is only thrown if cache size is <0, which it isn't. | ||
panic(err) | ||
} | ||
|
||
return &fileHashEnricher{ | ||
log: log, | ||
containerForCgroupGetter: containerForCgroupGetter, | ||
procFS: procFS, | ||
cache: cache, | ||
} | ||
} | ||
|
||
type fileHashEnricher struct { | ||
log *logging.Logger | ||
containerForCgroupGetter ContainerForCgroupGetter | ||
procFS fs.StatFS | ||
cache *lru.Cache[fileHashCacheKey, []byte] | ||
} | ||
|
||
func (enricher *fileHashEnricher) Enrich(ctx context.Context, e *castpb.Event) { | ||
exec := e.GetExec() | ||
if exec == nil || exec.Path == "" { | ||
return | ||
} | ||
|
||
cont, found, err := enricher.containerForCgroupGetter(e.CgroupId) | ||
// if we do not have the container cached, we cannot easily accesss the FS and hence simply skip | ||
if err != nil || !found { | ||
return | ||
} | ||
|
||
path := filepath.Join(e.ContainerId, "rootfs", exec.Path) | ||
f, err := fsys.Open(path) | ||
for _, pid := range cont.PIDs { | ||
sha, err := enricher.calcFileHashForPID(cont, pid, exec.Path) | ||
// We search for the first PID we can successfully calculate a filehash for. | ||
if err != nil { | ||
return | ||
} | ||
defer f.Close() | ||
if errors.Is(err, ErrFileDoesNotExist) { | ||
// If the wanted file does not exist in the PID mount namespace, it will also not exist in the mounts of the other. | ||
// We can hence simply return, as we will not find the wanted file. | ||
return | ||
} | ||
|
||
h := sha256.New() | ||
if _, err := io.Copy(h, f); err != nil { | ||
log.Debugf("error while processing `%s`: %v", path, err) | ||
return | ||
continue | ||
} | ||
|
||
sum := h.Sum(nil) | ||
if exec.Meta == nil { | ||
exec.Meta = &castpb.ExecMetadata{ | ||
HashSha256: sum, | ||
} | ||
} else { | ||
exec.Meta.HashSha256 = sum | ||
exec.Meta = &castpb.ExecMetadata{} | ||
} | ||
|
||
exec.Meta.HashSha256 = sha | ||
return | ||
} | ||
} | ||
|
||
func (enricher *fileHashEnricher) calcFileHashForPID(cont *containers.Container, pid uint32, execPath string) ([]byte, error) { | ||
pidString := strconv.FormatInt(int64(pid), 10) | ||
|
||
_, err := enricher.procFS.Stat(pidString) | ||
if err != nil { | ||
// If the /proc/<pid> folder doesn't exist, there is nothing we can do. | ||
return nil, ErrProcFolderDoesNotExist | ||
} | ||
|
||
path := filepath.Join(pidString, "root", execPath) | ||
info, err := enricher.procFS.Stat(path) | ||
if err != nil { | ||
// If the wanted file does not exist inside the mount namespace, there is also nothing we can do. | ||
return nil, ErrFileDoesNotExist | ||
} | ||
|
||
key := enricher.buildCacheKey(cont, info) | ||
hash, found := enricher.checkCache(key) | ||
if found { | ||
return hash, nil | ||
} | ||
|
||
f, err := enricher.procFS.Open(path) | ||
if err != nil { | ||
return nil, ErrFileDoesNotExist | ||
} | ||
defer f.Close() | ||
|
||
h := sha256.New() | ||
if _, err := io.Copy(h, f); err != nil { | ||
return nil, err | ||
} | ||
|
||
hash = h.Sum(nil) | ||
enricher.cacheHash(key, hash) | ||
|
||
return hash, nil | ||
} | ||
|
||
var ( | ||
ErrCannotGetInode = errors.New("cannot get inode for path") | ||
ErrProcFolderDoesNotExist = errors.New("/proc/<pid> folder does not exist") | ||
ErrFileDoesNotExist = errors.New("wanted file does not exist") | ||
) | ||
|
||
func (enricher *fileHashEnricher) buildCacheKey(cont *containers.Container, info fs.FileInfo) fileHashCacheKey { | ||
stat, ok := info.Sys().(*syscall.Stat_t) | ||
if !ok { | ||
return "" | ||
} | ||
|
||
return fileHashCacheKey(fmt.Sprintf("%s:%d", cont.Cgroup.ContainerID, stat.Ino)) | ||
} | ||
|
||
func (enricher *fileHashEnricher) checkCache(key fileHashCacheKey) ([]byte, bool) { | ||
if key == "" { | ||
// An empty key indicates an error when calculating the hash key, hence we treat it as not cached. | ||
return nil, false | ||
} | ||
|
||
return enricher.cache.Get(key) | ||
} | ||
|
||
func (enricher *fileHashEnricher) cacheHash(key fileHashCacheKey, hash []byte) { | ||
if key == "" { | ||
// An empty key indicates an error when calculating the hash key, hence nothing will be cached | ||
return | ||
} | ||
|
||
enricher.cache.Add(key, hash) | ||
} |
Oops, something went wrong.