diff --git a/.github/workflows/component-tests.yaml b/.github/workflows/component-tests.yaml index d03d0d2e..dcd542c8 100644 --- a/.github/workflows/component-tests.yaml +++ b/.github/workflows/component-tests.yaml @@ -48,6 +48,7 @@ jobs: Test_06_KillProcessInTheMiddle, Test_07_RuleBindingApplyTest, Test_08_ApplicationProfilePatching, + Test_10_MalwareDetectionTest, # Test_10_DemoTest # Test_11_DuplicationTest ] diff --git a/clamav/Dockerfile b/clamav/Dockerfile new file mode 100644 index 00000000..e98dfd1a --- /dev/null +++ b/clamav/Dockerfile @@ -0,0 +1,16 @@ +ARG CLAMAV_VERSION + +FROM ubuntu:23.10 as builder +ARG SOCKS_PROXY +ENV SOCKS_PROXY=$SOCKS_PROXY +RUN apt-get update && apt-get install -y python3 python3-pip clamav +COPY create-filtered-clam-db.sh / +RUN /create-filtered-clam-db.sh + +FROM clamav/clamav-debian:${CLAMAV_VERSION} +RUN apt-get update && apt-get install -y netcat +COPY ./init.sh /init +RUN mkdir -p /var/lib/clamav || true +COPY --from=builder main.cud /var/lib/clamav/main.cud +RUN chmod +x /init && chown clamav:clamav /var/lib/clamav +ENV CLAMAV_NO_FRESHCLAMD=true \ No newline at end of file diff --git a/clamav/Makefile b/clamav/Makefile new file mode 100644 index 00000000..aca1016a --- /dev/null +++ b/clamav/Makefile @@ -0,0 +1,10 @@ +DOCKERFILE_PATH=./Dockerfile +BINARY_NAME=klamav + +IMAGE?=quay.io/kubescape/$(BINARY_NAME) +TAG?=1.2.0-6_base + +docker-build: + docker buildx build --build-arg CLAMAV_VERSION=$(TAG) --platform linux/amd64 -t $(IMAGE):$(TAG) -f $(DOCKERFILE_PATH) . +docker-push: + docker push $(IMAGE):$(TAG) diff --git a/clamav/README.md b/clamav/README.md new file mode 100644 index 00000000..965e38b8 --- /dev/null +++ b/clamav/README.md @@ -0,0 +1 @@ +# ClamAV \ No newline at end of file diff --git a/clamav/create-filtered-clam-db.sh b/clamav/create-filtered-clam-db.sh new file mode 100755 index 00000000..070f922b --- /dev/null +++ b/clamav/create-filtered-clam-db.sh @@ -0,0 +1,88 @@ +#!/bin/bash +set -x + +# Create a temporary directory: +mkdir -p tmp + +# Get into it +pushd tmp + +# Check if main.cvd exists +if [ -f ~/.cvdupdate/database/main.cvd ] +then + echo "main.cvd already exists" + cp ~/.cvdupdate/database/main.cvd . +else + echo "main.cvd does not exist, downloading it" + rm -rf ~/.cvdupdate + python3 -m pip install --user cvdupdate --break-system-packages + python3 -m cvdupdate update -V + return_code=$? + if [ $return_code -ne 0 ] + then + echo "Failed to download main.cvd (http code: $return_code)" + exit 1 + fi + cp ~/.cvdupdate/database/main.cvd . +fi + + +# unpack the main.cvd +sigtool --unpack main.cvd +if [ $? -ne 0 ] +then + echo "Failed to unpack main.cvd" + exit 1 +fi +rm main.cvd + +# Loop over all the files in the tmp directory +for file in * +do + # If the file has one line, skip + if [ $(wc -l < $file) -eq 1 ] + then + echo "Skipping $file" + continue + fi + + # If the file is the COPYING or main.cvd file, skip + if [ $(basename $file) == "main.cvd" ] + then + echo "Skipping $file" + continue + fi + if [ $(basename $file) == "COPYING" ] + then + echo "Skipping $file" + continue + fi + + # Filter out the lines that does not contain the word "Unix" or "Multios" + grep -v -E "Win\.|Osx\." $file > $file.tmp + mv $file.tmp $file + # If the file is empty, delete it + if [ $(wc -l < $file) -eq 0 ] + then + echo "Deleting $file" + rm $file + fi +done + + +sigtool --version +printf "slashben\n" | sigtool --build=main.cud --unsigned +if [ $? -ne 0 ] +then + echo "Failed to build main.cud" + exit 1 +fi + + +# Get back +popd + +cp tmp/main.cud main.cud + +# Clean up +rm -rf tmp diff --git a/clamav/init.sh b/clamav/init.sh new file mode 100755 index 00000000..3559497a --- /dev/null +++ b/clamav/init.sh @@ -0,0 +1,89 @@ +#!/usr/bin/env sh +# SPDX-License-Identifier: GPL-2.0-or-later +# +# Copyright (C) 2021 Olliver Schinagl +# Copyright (C) 2021-2023 Cisco Systems, Inc. and/or its affiliates. All rights reserved. +# +# A beginning user should be able to docker run image bash (or sh) without +# needing to learn about --entrypoint +# https://github.com/docker-library/official-images#consistency + +set -eu + +if [ ! -d "/run/clamav" ]; then + install -d -g "clamav" -m 775 -o "clamav" "/run/clamav" +fi + +# Assign ownership to the database directory, just in case it is a mounted volume +chown -R clamav:clamav /var/lib/clamav + +# run command if it is not starting with a "-" and is an executable in PATH +if [ "${#}" -gt 0 ] && \ + [ "${1#-}" = "${1}" ] && \ + command -v "${1}" > "/dev/null" 2>&1; then + # Ensure healthcheck always passes + CLAMAV_NO_CLAMD="true" exec "${@}" +else + if [ "${#}" -ge 1 ] && \ + [ "${1#-}" != "${1}" ]; then + # If an argument starts with "-" pass it to clamd specifically + exec clamd "${@}" + fi + # else default to running clamav's servers + + # Help tiny-init a little + mkdir -p "/run/lock" + ln -f -s "/run/lock" "/var/lock" + + # Ensure we have some virus data, otherwise clamd refuses to start + if [ "${CLAMAV_NO_FRESHCLAMD:-false}" != "true" ]; then + if [ ! -f "/var/lib/clamav/main.cvd" ]; then + echo "Updating initial database" + freshclam --foreground --stdout + fi + fi + + # Start freshclamd if not disabled + if [ "${CLAMAV_NO_FRESHCLAMD:-false}" != "true" ]; then + echo "Starting Freshclamd" + freshclam \ + --checks="${FRESHCLAM_CHECKS:-1}" \ + --daemon \ + --foreground \ + --stdout \ + --user="clamav" \ + & + fi + + if [ "${CLAMAV_NO_CLAMD:-false}" != "true" ]; then + echo "Starting ClamAV" + if [ -S "/run/clamav/clamd.sock" ]; then + unlink "/run/clamav/clamd.sock" + fi + if [ -S "/tmp/clamd.sock" ]; then + unlink "/tmp/clamd.sock" + fi + clamd --foreground & + while [ ! -S "/run/clamav/clamd.sock" ] && [ ! -S "/tmp/clamd.sock" ]; do + if [ "${_timeout:=0}" -gt "${CLAMD_STARTUP_TIMEOUT:=1800}" ]; then + echo + echo "Failed to start clamd" + exit 1 + fi + printf "\r%s" "Socket for clamd not found yet, retrying (${_timeout}/${CLAMD_STARTUP_TIMEOUT}) ..." + sleep 1 + _timeout="$((_timeout + 1))" + done + echo "socket found, clamd started." + fi + + if [ "${CLAMAV_NO_MILTERD:-true}" != "true" ]; then + echo "Starting clamav milterd" + clamav-milter & + fi + + # Wait forever (or until canceled) + exec tail -f "/dev/null" +fi + +exit 0 diff --git a/pkg/malwaremanager/v1/clamav/clamav.go b/pkg/malwaremanager/v1/clamav/clamav.go index 68e56b46..285fdaca 100644 --- a/pkg/malwaremanager/v1/clamav/clamav.go +++ b/pkg/malwaremanager/v1/clamav/clamav.go @@ -1,22 +1,13 @@ package malwaremanager import ( - "fmt" "node-agent/pkg/malwaremanager" - mmtypes "node-agent/pkg/malwaremanager/v1/types" - "node-agent/pkg/utils" nautils "node-agent/pkg/utils" - "time" - - "os" - "strings" "github.com/cenkalti/backoff/v4" - logger "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger" "github.com/kubescape/go-logger/helpers" - apitypes "github.com/armosec/armoapi-go/armotypes" - humanize "github.com/dustin/go-humanize" "github.com/dutchcoders/go-clamd" tracerexectype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/exec/types" traceropentype "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" @@ -24,7 +15,6 @@ import ( type ClamAVClient struct { clamd *clamd.Clamd - cfg *ClamAVConfig } var _ malwaremanager.MalwareScanner = (*ClamAVClient)(nil) @@ -33,16 +23,10 @@ const ( FixSuggestions = "Please remove the file from the system. If the file is required, please contact your security team for further investigation." ) -func CreateClamAVClient(cfg *ClamAVConfig) (*ClamAVClient, error) { - clamdAddress, err := cfg.GetNormalizedAddress() - if err != nil { - return nil, err - } - - clamd := clamd.NewClamd(clamdAddress) +func CreateClamAVClient(clamavSocket string) (*ClamAVClient, error) { + c := clamd.NewClamd(clamavSocket) clamavClient := ClamAVClient{ - clamd: clamd, - cfg: cfg, + clamd: c, } // Ping ClamAV with backoff to ensure it is up and running. @@ -58,186 +42,14 @@ func CreateClamAVClient(cfg *ClamAVConfig) (*ClamAVClient, error) { func (c *ClamAVClient) Scan(eventType nautils.EventType, event interface{}, containerPid uint32) malwaremanager.MalwareResult { // Check if the event is of type tracerexectype.Event or traceropentype.Event. - if eventType != nautils.ExecveEventType && eventType != nautils.OpenEventType { - return nil - } - - if eventType == nautils.ExecveEventType { + switch eventType { + case nautils.ExecveEventType: return c.handleExecEvent(event.(*tracerexectype.Event), containerPid) - } else if eventType == nautils.OpenEventType { + case nautils.OpenEventType: return c.handleOpenEvent(event.(*traceropentype.Event), containerPid) - } - - return nil -} - -func (c *ClamAVClient) handleExecEvent(execEvent *tracerexectype.Event, containerPid uint32) malwaremanager.MalwareResult { - if execEvent == nil { - return nil - } - - hostFilePath, err := nautils.GetHostFilePathFromEvent(execEvent, containerPid) - if err != nil { - logger.L().Error("Error getting host file path", helpers.Error(err)) + default: return nil } - - response, err := c.clamd.ScanFile(hostFilePath) - if err != nil { - logger.L().Error("Error scanning file", helpers.Error(err)) - return nil - } - - for result := range response { - if result.Status == clamd.RES_FOUND { - // A malware was found, send an alert. - sha256hash, err := nautils.CalculateSHA256FileHash(result.Path) - if err != nil { - logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) - } - sha1hash, err := nautils.CalculateSHA1FileHash(result.Path) - if err != nil { - logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) - } - md5hash, err := nautils.CalculateMD5FileHash(result.Path) - if err != nil { - logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) - } - size, err := nautils.GetFileSize(result.Path) - if err != nil { - logger.L().Error("Error getting file size of %s", helpers.String("path", result.Path), helpers.Error(err)) - } - path := strings.TrimPrefix(result.Path, os.Getenv("HOST_ROOT")) - - commandLine := fmt.Sprintf("%s %s", utils.GetExecPathFromEvent(execEvent), utils.GetExecArgsFromEvent(execEvent)) - return &mmtypes.GenericMalwareResult{ - BasicRuntimeAlert: apitypes.BaseRuntimeAlert{ - AlertName: result.Description, - InfectedPID: execEvent.Pid, - FixSuggestions: FixSuggestions, - SHA1Hash: sha1hash, - SHA256Hash: sha256hash, - MD5Hash: md5hash, - Severity: 10, // TODO: Get severity from api. - Size: humanize.IBytes(uint64(size)), - Timestamp: time.Unix(int64(execEvent.Timestamp), 0), - }, - RuntimeProcessDetails: apitypes.ProcessTree{ - ProcessTree: apitypes.Process{ - Comm: execEvent.Comm, - Path: path, - Gid: &execEvent.Gid, - PID: execEvent.Pid, - Uid: &execEvent.Uid, - UpperLayer: execEvent.UpperLayer, - PPID: execEvent.Ppid, - Pcomm: execEvent.Pcomm, - Cwd: execEvent.Cwd, - Hardlink: execEvent.ExePath, - Cmdline: commandLine, - }, - ContainerID: execEvent.Runtime.ContainerID, - }, - TriggerEvent: execEvent.Event, - MalwareRuntimeAlert: apitypes.MalwareAlert{ - MalwareDescription: result.Description, - }, - RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ - ContainerID: execEvent.Runtime.ContainerID, - ContainerName: execEvent.K8s.ContainerName, - Namespace: execEvent.GetNamespace(), - PodName: execEvent.GetPod(), - PodNamespace: execEvent.GetNamespace(), - HostNetwork: &execEvent.K8s.HostNetwork, - Image: execEvent.Runtime.ContainerImageName, - ImageDigest: execEvent.Runtime.ContainerImageDigest, - }, - } - } - } - - return nil -} - -func (c *ClamAVClient) handleOpenEvent(openEvent *traceropentype.Event, containerPid uint32) malwaremanager.MalwareResult { - if openEvent == nil { - return nil - } - - hostFilePath, err := nautils.GetHostFilePathFromEvent(openEvent, containerPid) - if err != nil { - logger.L().Error("Error getting host file path", helpers.Error(err)) - return nil - } - - response, err := c.clamd.ScanFile(hostFilePath) - if err != nil { - logger.L().Error("Error scanning file", helpers.Error(err)) - return nil - } - - for result := range response { - if result.Status == clamd.RES_FOUND { - // A malware was found, send an alert. - sha256hash, err := nautils.CalculateSHA256FileHash(result.Path) - if err != nil { - logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) - } - sha1hash, err := nautils.CalculateSHA1FileHash(result.Path) - if err != nil { - logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) - } - md5hash, err := nautils.CalculateMD5FileHash(result.Path) - if err != nil { - logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) - } - size, err := nautils.GetFileSize(result.Path) - if err != nil { - logger.L().Error("Error getting file size of %s", helpers.String("path", result.Path), helpers.Error(err)) - } - - path := strings.TrimPrefix(result.Path, os.Getenv("HOST_ROOT")) - return &mmtypes.GenericMalwareResult{ - BasicRuntimeAlert: apitypes.BaseRuntimeAlert{ - AlertName: result.Description, - InfectedPID: openEvent.Pid, - FixSuggestions: FixSuggestions, - SHA1Hash: sha1hash, - SHA256Hash: sha256hash, - MD5Hash: md5hash, - Severity: 10, // TODO: Get severity from api. - Size: humanize.IBytes(uint64(size)), - Timestamp: time.Unix(int64(openEvent.Timestamp), 0), - }, - RuntimeProcessDetails: apitypes.ProcessTree{ - ProcessTree: apitypes.Process{ - Comm: openEvent.Comm, - Path: path, - Gid: &openEvent.Gid, - PID: openEvent.Pid, - Uid: &openEvent.Uid, - }, - ContainerID: openEvent.Runtime.ContainerID, - }, - TriggerEvent: openEvent.Event, - MalwareRuntimeAlert: apitypes.MalwareAlert{ - MalwareDescription: result.Description, - }, - RuntimeAlertK8sDetails: apitypes.RuntimeAlertK8sDetails{ - ContainerID: openEvent.Runtime.ContainerID, - ContainerName: openEvent.K8s.ContainerName, - Namespace: openEvent.GetNamespace(), - PodName: openEvent.GetPod(), - PodNamespace: openEvent.GetNamespace(), - HostNetwork: &openEvent.K8s.HostNetwork, - Image: openEvent.Runtime.ContainerImageName, - ImageDigest: openEvent.Runtime.ContainerImageDigest, - }, - } - } - } - - return nil } // Ping ClamAV diff --git a/pkg/malwaremanager/v1/clamav/config.go b/pkg/malwaremanager/v1/clamav/config.go deleted file mode 100644 index d62e735a..00000000 --- a/pkg/malwaremanager/v1/clamav/config.go +++ /dev/null @@ -1,28 +0,0 @@ -package malwaremanager - -import ( - "fmt" - "net" -) - -type ClamAVConfig struct { - Address string `json:"address"` -} - -func (c *ClamAVConfig) GetNormalizedAddress() (string, error) { - host, port, err := c.parseAddress() - if err != nil { - return "", fmt.Errorf("failed to parse address: %w", err) - } - - return fmt.Sprintf("tcp://%s:%s", host, port), nil -} - -func (c *ClamAVConfig) parseAddress() (string, string, error) { - host, port, err := net.SplitHostPort(c.Address) - if err != nil { - return "", "", fmt.Errorf("failed to parse address: %w", err) - } - - return host, port, nil -} diff --git a/pkg/malwaremanager/v1/clamav/exec.go b/pkg/malwaremanager/v1/clamav/exec.go new file mode 100644 index 00000000..42aa8c79 --- /dev/null +++ b/pkg/malwaremanager/v1/clamav/exec.go @@ -0,0 +1,105 @@ +package malwaremanager + +import ( + "fmt" + "node-agent/pkg/malwaremanager" + malwaremanager2 "node-agent/pkg/malwaremanager/v1/types" + "node-agent/pkg/utils" + "os" + "strings" + "time" + + "github.com/armosec/armoapi-go/armotypes" + "github.com/dustin/go-humanize" + "github.com/dutchcoders/go-clamd" + "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/exec/types" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" +) + +func (c *ClamAVClient) handleExecEvent(event *types.Event, containerPid uint32) malwaremanager.MalwareResult { + if event == nil { + return nil + } + + hostFilePath, err := utils.GetHostFilePathFromEvent(event, containerPid) + if err != nil { + logger.L().Error("Error getting host file path", helpers.Error(err)) + return nil + } + + response, err := c.clamd.ScanFile(hostFilePath) + if err != nil { + logger.L().Error("Error scanning file", helpers.Error(err)) + return nil + } + + for result := range response { + if result.Status == clamd.RES_FOUND { + // A malware was found, send an alert. + sha256hash, err := utils.CalculateSHA256FileHash(result.Path) + if err != nil { + logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) + } + sha1hash, err := utils.CalculateSHA1FileHash(result.Path) + if err != nil { + logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) + } + md5hash, err := utils.CalculateMD5FileHash(result.Path) + if err != nil { + logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) + } + size, err := utils.GetFileSize(result.Path) + if err != nil { + logger.L().Error("Error getting file size of %s", helpers.String("path", result.Path), helpers.Error(err)) + } + path := strings.TrimPrefix(result.Path, os.Getenv("HOST_ROOT")) + + return &malwaremanager2.GenericMalwareResult{ + BasicRuntimeAlert: armotypes.BaseRuntimeAlert{ + AlertName: result.Description, + InfectedPID: event.Pid, + FixSuggestions: FixSuggestions, + SHA1Hash: sha1hash, + SHA256Hash: sha256hash, + MD5Hash: md5hash, + Severity: 10, // TODO: Get severity from api. + Size: humanize.IBytes(uint64(size)), + Timestamp: time.Unix(int64(event.Timestamp), 0), + }, + RuntimeProcessDetails: armotypes.ProcessTree{ + ProcessTree: armotypes.Process{ + Comm: event.Comm, + Path: path, + Gid: &event.Gid, + PID: event.Pid, + Uid: &event.Uid, + UpperLayer: event.UpperLayer, + PPID: event.Ppid, + Pcomm: event.Pcomm, + Cwd: event.Cwd, + Hardlink: event.ExePath, + Cmdline: fmt.Sprintf("%s %s", utils.GetExecPathFromEvent(event), utils.GetExecArgsFromEvent(event)), + }, + ContainerID: event.Runtime.ContainerID, + }, + TriggerEvent: event.Event, + MalwareRuntimeAlert: armotypes.MalwareAlert{ + MalwareDescription: result.Description, + }, + RuntimeAlertK8sDetails: armotypes.RuntimeAlertK8sDetails{ + ContainerID: event.Runtime.ContainerID, + ContainerName: event.K8s.ContainerName, + Namespace: event.GetNamespace(), + PodName: event.GetPod(), + PodNamespace: event.GetNamespace(), + HostNetwork: &event.K8s.HostNetwork, + Image: event.Runtime.ContainerImageName, + ImageDigest: event.Runtime.ContainerImageDigest, + }, + } + } + } + + return nil +} diff --git a/pkg/malwaremanager/v1/clamav/open.go b/pkg/malwaremanager/v1/clamav/open.go new file mode 100644 index 00000000..bbd492e0 --- /dev/null +++ b/pkg/malwaremanager/v1/clamav/open.go @@ -0,0 +1,104 @@ +package malwaremanager + +import ( + "node-agent/pkg/malwaremanager" + malwaremanager2 "node-agent/pkg/malwaremanager/v1/types" + "node-agent/pkg/utils" + "os" + "strings" + "time" + + "github.com/armosec/armoapi-go/armotypes" + "github.com/dustin/go-humanize" + "github.com/dutchcoders/go-clamd" + "github.com/inspektor-gadget/inspektor-gadget/pkg/gadgets/trace/open/types" + "github.com/kubescape/go-logger" + "github.com/kubescape/go-logger/helpers" + "golang.org/x/sys/unix" +) + +func (c *ClamAVClient) handleOpenEvent(event *types.Event, containerPid uint32) malwaremanager.MalwareResult { + if event == nil { + return nil + } + + // discard if it is an open for writing event + if event.FlagsRaw&unix.O_WRONLY != 0 { + return nil + } + + hostFilePath, err := utils.GetHostFilePathFromEvent(event, containerPid) + if err != nil { + logger.L().Error("Error getting host file path", helpers.Error(err)) + return nil + } + + response, err := c.clamd.ScanFile(hostFilePath) + if err != nil { + logger.L().Error("Error scanning file", helpers.Error(err)) + return nil + } + + for result := range response { + if result.Status == clamd.RES_FOUND { + // A malware was found, send an alert. + sha256hash, err := utils.CalculateSHA256FileHash(result.Path) + if err != nil { + logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) + } + sha1hash, err := utils.CalculateSHA1FileHash(result.Path) + if err != nil { + logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) + } + md5hash, err := utils.CalculateMD5FileHash(result.Path) + if err != nil { + logger.L().Error("Error calculating hash of %s", helpers.String("path", result.Path), helpers.Error(err)) + } + size, err := utils.GetFileSize(result.Path) + if err != nil { + logger.L().Error("Error getting file size of %s", helpers.String("path", result.Path), helpers.Error(err)) + } + path := strings.TrimPrefix(result.Path, os.Getenv("HOST_ROOT")) + + return &malwaremanager2.GenericMalwareResult{ + BasicRuntimeAlert: armotypes.BaseRuntimeAlert{ + AlertName: result.Description, + InfectedPID: event.Pid, + FixSuggestions: FixSuggestions, + SHA1Hash: sha1hash, + SHA256Hash: sha256hash, + MD5Hash: md5hash, + Severity: 10, // TODO: Get severity from api. + Size: humanize.IBytes(uint64(size)), + Timestamp: time.Unix(int64(event.Timestamp), 0), + }, + RuntimeProcessDetails: armotypes.ProcessTree{ + ProcessTree: armotypes.Process{ + Comm: event.Comm, + Path: path, + Gid: &event.Gid, + PID: event.Pid, + Uid: &event.Uid, + }, + ContainerID: event.Runtime.ContainerID, + }, + TriggerEvent: event.Event, + MalwareRuntimeAlert: armotypes.MalwareAlert{ + MalwareDescription: result.Description, + }, + RuntimeAlertK8sDetails: armotypes.RuntimeAlertK8sDetails{ + ContainerID: event.Runtime.ContainerID, + ContainerName: event.K8s.ContainerName, + Namespace: event.GetNamespace(), + PodName: event.GetPod(), + PodNamespace: event.GetNamespace(), + HostNetwork: &event.K8s.HostNetwork, + Image: event.Runtime.ContainerImageName, + ImageDigest: event.Runtime.ContainerImageDigest, + }, + } + } + } + + return nil +} diff --git a/pkg/malwaremanager/v1/malware_manager.go b/pkg/malwaremanager/v1/malware_manager.go index 6ab9c06e..1b6f218a 100644 --- a/pkg/malwaremanager/v1/malware_manager.go +++ b/pkg/malwaremanager/v1/malware_manager.go @@ -46,15 +46,12 @@ var _ malwaremanager.MalwareManagerClient = (*MalwareManager)(nil) func CreateMalwareManager(cfg config.Config, k8sClient k8sclient.K8sClientInterface, nodeName string, clusterName string, exporter exporters.Exporter, prometheusExporter metricsmanager.MetricsManager) (*MalwareManager, error) { // Create malware scanners - malwareScanners := []malwaremanager.MalwareScanner{} + var malwareScanners []malwaremanager.MalwareScanner // Create ClamAV scanner - // Check if ClamAV is enabled (CLAMAV_ADDRESS env var is set in the format :) - if clamavAddress, present := os.LookupEnv("CLAMAV_ADDRESS"); present { - clamavConfig := clamavv1.ClamAVConfig{ - Address: clamavAddress, - } - clamavScanner, err := clamavv1.CreateClamAVClient(&clamavConfig) + // Check if ClamAV is enabled (CLAMAV_SOCKET env var is set) + if clamavSocket, present := os.LookupEnv("CLAMAV_SOCKET"); present { + clamavScanner, err := clamavv1.CreateClamAVClient(clamavSocket) if err != nil { return nil, err } @@ -78,11 +75,11 @@ func (mm *MalwareManager) ContainerCallback(notif containercollection.PubSubEven case containercollection.EventTypeAddContainer: mm.containerIdToPid.Set(notif.Container.Runtime.ContainerID, notif.Container.Pid) if !mm.podToWlid.Has(notif.Container.K8s.PodName) { - wlid, err := mm.getWorkloadIdentifier(notif.Container.K8s.Namespace, notif.Container.K8s.PodName) + w, err := mm.getWorkloadIdentifier(notif.Container.K8s.Namespace, notif.Container.K8s.PodName) if err != nil { logger.L().Debug("MalwareManager - failed to get workload identifier", helpers.Error(err), helpers.String("k8s workload", notif.Container.K8s.PodName)) } else { - mm.podToWlid.Set(notif.Container.K8s.PodName, wlid) + mm.podToWlid.Set(notif.Container.K8s.PodName, w) } } shim, err := utils.GetProcessStat(int(notif.Container.Pid)) diff --git a/tests/chart/clamav/clamd.conf b/tests/chart/clamav/clamd.conf index 2fe8c371..acd2cf41 100644 --- a/tests/chart/clamav/clamd.conf +++ b/tests/chart/clamav/clamd.conf @@ -7,7 +7,5 @@ LogClean yes LocalSocket /run/clamav/clamd.sock LocalSocketGroup clamav LocalSocketMode 660 -TCPSocket 3310 -TCPAddr 127.0.0.1 MaxDirectoryRecursion 20 -ExtendedDetectionInfo yes \ No newline at end of file +ExtendedDetectionInfo yes diff --git a/tests/chart/clamav/freshclam.conf b/tests/chart/clamav/freshclam.conf index a0a44800..7c8e417a 100644 --- a/tests/chart/clamav/freshclam.conf +++ b/tests/chart/clamav/freshclam.conf @@ -13,4 +13,4 @@ CompressLocalDatabase no TestDatabases yes Checks 0 ConnectTimeout 30 -ReceiveTimeout 60 \ No newline at end of file +ReceiveTimeout 60 diff --git a/tests/chart/templates/node-agent/daemonset.yaml b/tests/chart/templates/node-agent/daemonset.yaml index 2dcf3cbd..328c0f6e 100644 --- a/tests/chart/templates/node-agent/daemonset.yaml +++ b/tests/chart/templates/node-agent/daemonset.yaml @@ -69,14 +69,6 @@ spec: capabilities: add: - SYS_PTRACE - livenessProbe: - exec: - command: - - /bin/sh - - -c - - "echo 'PING' | nc -w 1 localhost 3310 | grep PONG || exit 1" - initialDelaySeconds: 50 - periodSeconds: 10 resources: {{ toYaml .Values.clamav.resources | indent 12 }} {{- if .Values.clamav.volumeMounts }} @@ -99,8 +91,8 @@ spec: - name: KS_LOGGER_NAME value: "{{ .Values.logger.name }}" {{- if eq .Values.capabilities.malwareDetection "enable" }} - - name: CLAMAV_ADDRESS - value: "127.0.0.1:3310" + - name: CLAMAV_SOCKET + value: "/clamav/clamd.sock" - name: ENABLE_PROFILER value: "true" {{- end }} @@ -166,4 +158,4 @@ spec: tolerations: {{- if .Values.nodeAgent.tolerations }} {{- toYaml .Values.nodeAgent.tolerations | nindent 8 }} - {{- end }} \ No newline at end of file + {{- end }} diff --git a/tests/chart/values.yaml b/tests/chart/values.yaml index e5fc7f18..97fae313 100644 --- a/tests/chart/values.yaml +++ b/tests/chart/values.yaml @@ -56,7 +56,7 @@ nodeAgent: config: maxLearningPeriod: 2m learningPeriod: 1m - updatePeriod: 30s + updatePeriod: 30s prometheusExporter: enable httpExporterConfig: {} alertManagerExporterUrls: [ @@ -102,6 +102,9 @@ nodeAgent: name: bpffs - mountPath: /data name: data + - mountPath: /clamav + name: clamrun + readOnly: false volumes: - hostPath: @@ -139,9 +142,9 @@ nodeAgent: clamav: name: clamav image: - repository: quay.io/armosec/klamav - tag: beta5 - pullPolicy: Always + repository: quay.io/kubescape/klamav + tag: 1.2.0-6_base + pullPolicy: IfNotPresent resources: limits: cpu: 300m @@ -156,9 +159,14 @@ clamav: - mountPath: /etc/clamav name: etc readOnly: true + - mountPath: /run/clamav + name: clamrun + readOnly: false volumes: - name: clamdb emptyDir: {} + - name: clamrun + emptyDir: {} - name: etc configMap: name: clamav diff --git a/tests/component_test.go b/tests/component_test.go index a3df004b..f8465da6 100644 --- a/tests/component_test.go +++ b/tests/component_test.go @@ -9,6 +9,7 @@ import ( "node-agent/pkg/utils" "node-agent/tests/testutils" "path" + "slices" "testing" "time" @@ -557,6 +558,55 @@ func Test_09_FalsePositiveTest(t *testing.T) { assert.Equal(t, 0, len(alerts), "Expected no alerts to be generated, but got %d alerts", len(alerts)) } +func Test_10_MalwareDetectionTest(t *testing.T) { + start := time.Now() + defer tearDownTest(t, start) + + t.Log("Creating namespace") + ns := testutils.NewRandomNamespace() + + t.Log("Deploy container with malware") + exitCode := testutils.RunCommand("kubectl", "run", "-n", ns.Name, "malware-cryptominer", "--image=quay.io/petr_ruzicka/malware-cryptominer-container:2.0.2") + assert.Equalf(t, 0, exitCode, "expected no error when deploying malware container") + + // Wait for pod to be ready + exitCode = testutils.RunCommand("kubectl", "wait", "--for=condition=Ready", "pod", "malware-cryptominer", "-n", ns.Name, "--timeout=300s") + assert.Equalf(t, 0, exitCode, "expected no error when waiting for pod to be ready") + + // wait for application profile to be completed + time.Sleep(3 * time.Minute) + + _, _, err := testutils.ExecIntoPod("malware-cryptominer", ns.Name, []string{"ls", "-l", "/usr/share/nginx/html/xmrig"}, "") + assert.NoErrorf(t, err, "expected no error when executing command in malware container") + + // wait for the alerts to be generated + time.Sleep(20 * time.Second) + + alerts, err := testutils.GetMalwareAlerts(ns.Name) + if err != nil { + t.Errorf("Error getting alerts: %v", err) + } + + expectedMalwares := []string{ + "Multios.Coinminer.Miner-6781728-2.UNOFFICIAL", + } + + malwaresDetected := map[string]bool{} + + for _, alert := range alerts { + podName, podNameOk := alert.Labels["pod_name"] + malewareName, malewareNameOk := alert.Labels["malware_name"] + + if podNameOk && malewareNameOk { + if podName == "malware-cryptominer" && slices.Contains(expectedMalwares, malewareName) { + malwaresDetected[malewareName] = true + } + } + } + + assert.Equal(t, len(expectedMalwares), len(malwaresDetected), "Expected %d malwares to be detected, but got %d malwares", len(expectedMalwares), len(malwaresDetected)) +} + // func Test_10_DemoTest(t *testing.T) { // start := time.Now() // defer tearDownTest(t, start) diff --git a/tests/testutils/alertmanager.go b/tests/testutils/alertmanager.go index 276aa96b..0edda649 100644 --- a/tests/testutils/alertmanager.go +++ b/tests/testutils/alertmanager.go @@ -36,6 +36,23 @@ func GetAlerts(namespace string) ([]Alert, error) { return alerts, nil } +func GetMalwareAlerts(namespace string) ([]Alert, error) { + url := alertManagerURL + if envURL, exists := os.LookupEnv("ALERTMANAGER_URL"); exists { + url = envURL + } + + alerts, err := getActiveAlerts(url) + if err != nil { + return nil, fmt.Errorf("could not get alerts: %v", err) + } + + alerts = filterAlertsByLabel(alerts, "alertname", "KubescapeMalwareDetected") + alerts = filterAlertsByLabel(alerts, "namespace", namespace) + + return alerts, nil +} + // getActiveAlerts fetches the active alerts from Alertmanager func getActiveAlerts(alertManagerURL string) ([]Alert, error) { endpoint := fmt.Sprintf("%s/api/v2/alerts?active=true", alertManagerURL) diff --git a/tests/testutils/k8s.go b/tests/testutils/k8s.go index 44e0d126..934a033f 100644 --- a/tests/testutils/k8s.go +++ b/tests/testutils/k8s.go @@ -90,6 +90,10 @@ func (w *TestWorkload) ExecIntoPod(command []string, container string) (string, } pod := pods[0] + return ExecIntoPod(pod.Name, w.Namespace, command, container) +} + +func ExecIntoPod(podName, podNamespace string, command []string, container string) (string, string, error) { k8sClient := k8sinterface.NewKubernetesApi() buf := &bytes.Buffer{} @@ -109,9 +113,9 @@ func (w *TestWorkload) ExecIntoPod(command []string, container string) (string, request := k8sClient.KubernetesClient.CoreV1().RESTClient(). Post(). - Namespace(pod.Namespace). + Namespace(podNamespace). Resource("pods"). - Name(pod.Name). + Name(podName). SubResource("exec"). VersionedParams(podExecOpts, scheme.ParameterCodec) exec, err := remotecommand.NewSPDYExecutor(k8sClient.K8SConfig, "POST", request.URL()) @@ -123,7 +127,7 @@ func (w *TestWorkload) ExecIntoPod(command []string, container string) (string, Stderr: errBuf, }) if err != nil { - return "", "", fmt.Errorf("%w Failed executing command %s on %v/%v", err, command, pod.Namespace, pod.Name) + return "", "", fmt.Errorf("%w Failed executing command %s on %v/%v", err, command, podNamespace, podName) } return buf.String(), errBuf.String(), nil