Skip to content

Commit

Permalink
feat(scanner): add default clamd configuration (#1920)
Browse files Browse the repository at this point in the history
* fix(scanner): fix yaml invalid escape sequence

* fix(scanner): clam scanner incorrectly reports a clam command execution error if no vulnerabilities were found

* feat(scanner): genearet default clamd scan config

* refactor(scanner): move clamd config template into separate file

* feat(scanner): use clamd by default

* fix(scanner): alternative_freshclam_mirror_url cannot include servers under *.clamav.net

* style(scanner): rename variable

* refactor(scanner): args slice literal

Co-authored-by: Ramiz Polic <[email protected]>

* revert(scanner): revert using args slice literal

Co-authored-by: Ramiz Polic <[email protected]>

* fix(scanner): fix typo

* refactor(scanner): replace use_clam_daemon option with use_native_clamscan

* refactor(scanner): replace use_clam_daemon option with use_native_clamscan

* refactor(scanner): replace use_clam_daemon option with use_native_clamscan

* refactor(scanner): replace use_clam_daemon option with use_native_clamscan

* fix(scanner): add back json tags

* refactor(scanner): convert functions into to struct methods

* fix(scanner): remove the no-summary flag

---------

Co-authored-by: Ramiz Polic <[email protected]>
  • Loading branch information
zsoltkacsandi and ramizpolic committed Jul 22, 2024
1 parent fe54586 commit c2b984b
Show file tree
Hide file tree
Showing 6 changed files with 82 additions and 45 deletions.
6 changes: 3 additions & 3 deletions .families.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,13 @@ malware:
clam:
freshclam_binary_path: "/usr/local/bin/freshclam"
freshclam_config_path: "/etc/clamav/freshclam.conf"
alternative_freshclam_mirror_url: "database.clamav.net"
alternative_freshclam_mirror_url: "" # config option cannot include servers under *.clamav.net.
use_native_clamscan: false # scan using native (clamscan) command instead of daemon (clamdscan)
clamscan_binary_path: "/usr/local/bin/clamscan"
clamscan_exclude_files:
- "^.*\.log$"
- "^.*\\.log$"
clamscan_exclude_dirs:
- "^/sys"
use_clam_daemon: false # scan using daemon (clamdscan) command instead of native (clamscan)
clam_daemon_binary_path: "/usr/local/bin/clamd"
clam_daemon_config_path: "/etc/clamav/clamd.conf"
clam_daemon_client_binary_path: "/usr/local/bin/clamdscan"
Expand Down
2 changes: 1 addition & 1 deletion cli/state/testdata/effective-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@
"inputs": null,
"scanners_config": {
"clam": {
"use_clam_daemon": false,
"use_native_clamscan": false,
"clamscan_binary_path": "",
"clamscan_exclude_files": null,
"clamscan_exclude_dirs": null,
Expand Down
6 changes: 1 addition & 5 deletions orchestrator/watcher/assetscan/families.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,11 +168,7 @@ func withMalwareConfig(config *apitypes.MalwareConfig, opts *ScannerConfig) Scan
Inputs: nil, // rootfs directory will be determined by the CLI after mount.
ScannersConfig: malware.ScannersConfig{
Clam: clamconfig.Config{
// NOTE(ramizpolic): We disable scanning with daemon as we don't have proper
// default configuration in place. Once we have defined valid default
// configuration to use with clam daemon scan, we should re-enable this.
// https://github.com/openclarity/vmclarity/issues/1870
UseClamDaemon: false,
UseNativeClamscan: false,
AlternativeFreshclamMirrorURL: opts.AlternativeFreshclamMirrorURL,
},
Yara: yaraconfig.Config{
Expand Down
103 changes: 70 additions & 33 deletions scanner/families/malware/clam/clam.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,15 @@ package clam

import (
"context"
_ "embed"
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"slices"
"strings"
"text/template"

"github.com/openclarity/vmclarity/core/log"
"github.com/openclarity/vmclarity/scanner/common"
Expand All @@ -35,26 +39,29 @@ import (

const ScannerName = "clam"

//go:embed templates/clamd.conf
var clamdConfigTemplate string

type Scanner struct {
config config.Config
}

func New(ctx context.Context, _ string, config types.ScannersConfig) (families.Scanner[*types.ScannerResult], error) {
clamConfig := config.Clam
scanner := &Scanner{
config: config.Clam,
}

// Prepare freshclam to sync database data
if err := prepareFreshclam(ctx, clamConfig); err != nil {
if err := scanner.prepareFreshclam(ctx); err != nil {
return nil, fmt.Errorf("failed to run freshclam: %w", err)
}

// Prepare clam daemon
if err := prepareClamDaemon(ctx, clamConfig); err != nil {
if err := scanner.prepareClamDaemon(ctx); err != nil {
return nil, fmt.Errorf("failed to run freshclam: %w", err)
}

return &Scanner{
config: clamConfig,
}, nil
return scanner, nil
}

// nolint: cyclop
Expand All @@ -73,12 +80,12 @@ func (s *Scanner) Scan(ctx context.Context, inputType common.InputType, userInpu
// Scan path using clamscan or clamdscan
var scanOutput []byte
var scanTool string
if s.config.UseClamDaemon {
scanTool = "clamdscan"
scanOutput, err = s.runClamScanWithDaemon(ctx, fsScanPath)
} else {
if s.config.UseNativeClamscan {
scanTool = "clamscan"
scanOutput, err = s.runClamScan(ctx, fsScanPath)
} else {
scanTool = "clamdscan"
scanOutput, err = s.runClamScanWithDaemon(ctx, fsScanPath)
}
if err != nil {
return nil, fmt.Errorf("failed to run %s: %w", scanTool, err)
Expand Down Expand Up @@ -131,7 +138,7 @@ func (s *Scanner) runClamScan(ctx context.Context, fsScanPath string) ([]byte, e
/* If the error is that malware was found, this is not an actual error, Clam returns
a non 0 exit code when malware was found */
var runError utils.CmdRunError
if !errors.As(err, &runError) || !strings.Contains(string(runError.Stdout), constants.MalwareDetectedIndication) {
if !errors.As(err, &runError) || !strings.Contains(string(runError.Stdout), constants.ScanSummaryText) {
return nil, fmt.Errorf("failed to run clam command: %w", err)
}

Expand All @@ -154,7 +161,6 @@ func (s *Scanner) runClamScanWithDaemon(ctx context.Context, fsScanPath string)
args := []string{
"--multiscan",
"--stream",
"--no-summary",
"--infected",
}

Expand All @@ -176,7 +182,7 @@ func (s *Scanner) runClamScanWithDaemon(ctx context.Context, fsScanPath string)
/* If the error is that malware was found, this is not an actual error, Clamd returns
a non 0 exit code when malware was found */
var runError utils.CmdRunError
if !errors.As(err, &runError) || !strings.Contains(string(runError.Stdout), constants.MalwareDetectedIndication) {
if !errors.As(err, &runError) || !strings.Contains(string(runError.Stdout), constants.ScanSummaryText) {
return nil, fmt.Errorf("failed to run clamdscan command: %w", err)
}

Expand All @@ -186,27 +192,32 @@ func (s *Scanner) runClamScanWithDaemon(ctx context.Context, fsScanPath string)
return out, nil
}

func prepareClamDaemon(ctx context.Context, config config.Config) error {
func (s *Scanner) prepareClamDaemon(ctx context.Context) error {
// Check if daemon mode was requested
if !config.UseClamDaemon {
if s.config.UseNativeClamscan {
return nil
}

logger := log.GetLoggerFromContextOrDefault(ctx)

clamDaemonPath, err := exec.LookPath(config.GetClamDaemonBinaryPath())
clamDaemonPath, err := exec.LookPath(s.config.GetClamDaemonBinaryPath())
if err != nil {
return fmt.Errorf("failed to lookup executable %s: %w", config.ClamDaemonBinaryPath, err)
return fmt.Errorf("failed to lookup executable %s: %w", s.config.ClamDaemonBinaryPath, err)
}
logger.Debugf("found clamd binary at: %s", clamDaemonPath)

// Define default clamd args to run
var args []string

// Append custom config if provided
if config.ClamDaemonConfigPath != "" {
args = append(args, "--config-file", config.ClamDaemonConfigPath)
// Use default clamd configuration if it is not provided
if s.config.ClamDaemonConfigPath == "" {
s.config.ClamDaemonConfigPath = filepath.Join(os.TempDir(), "clamd.conf")
err = s.createDefaultClamdConfig()
if err != nil {
return fmt.Errorf("unable to create default clamd configuration: %w", err)
}
}
args = append(args, "--config-file", s.config.ClamDaemonConfigPath)

// Execute clamd command
logger.Infof("Starting clam daemon process...")
Expand All @@ -227,26 +238,25 @@ func prepareClamDaemon(ctx context.Context, config config.Config) error {
return nil
}

func prepareFreshclam(ctx context.Context, config config.Config) error {
func (s *Scanner) prepareFreshclam(ctx context.Context) error {
logger := log.GetLoggerFromContextOrDefault(ctx)

freshClamPath, err := exec.LookPath(config.GetFreshclamBinaryPath())
freshClamPath, err := exec.LookPath(s.config.GetFreshclamBinaryPath())
if err != nil {
return fmt.Errorf("failed to lookup executable %s: %w", config.FreshclamBinaryPath, err)
return fmt.Errorf("failed to lookup executable %s: %w", s.config.FreshclamBinaryPath, err)
}
logger.Debugf("found freshclam binary at: %s", freshClamPath)

// Sync freshclam configuration
logger.Infof("Syncing freshclam configuration...")

configPath := config.GetFreshclamConfigPath()
if err := syncFreshclamConfig(configPath, config.AlternativeFreshclamMirrorURL); err != nil {
if err := s.syncFreshclamConfig(); err != nil {
return fmt.Errorf("failed to sync freshclam config: %w", err)
}

// Define default freshclam args to run
args := []string{
"--config-file", configPath,
"--config-file", s.config.GetFreshclamConfigPath(),
}

// Execute freshclam command
Expand All @@ -264,25 +274,25 @@ func prepareFreshclam(ctx context.Context, config config.Config) error {
return nil
}

func syncFreshclamConfig(configPath, alternativeMirrorURL string) error {
func (s *Scanner) syncFreshclamConfig() error {
var configContents []byte
var configLines []string

// Attempt to read freshclam.conf file
_, err := os.Stat(configPath)
_, err := os.Stat(s.config.GetFreshclamConfigPath())
if os.IsNotExist(err) {
return fmt.Errorf("freshclam.conf not found at %s", configPath)
return fmt.Errorf("freshclam.conf not found at %s", s.config.GetFreshclamConfigPath())
} else if err != nil {
return fmt.Errorf("failed to check freshclam.conf: %w", err)
}

configContents, err = os.ReadFile(configPath)
configContents, err = os.ReadFile(s.config.GetFreshclamConfigPath())
if err != nil {
return fmt.Errorf("failed to read freshclam.conf: %w", err)
}

// Handle alternative freshclam mirror URL
if alternativeMirrorURL != "" {
if s.config.AlternativeFreshclamMirrorURL != "" {
configLines = strings.Split(string(configContents), "\n")

// Comment out any existing conflicting options
Expand All @@ -293,7 +303,7 @@ func syncFreshclamConfig(configPath, alternativeMirrorURL string) error {
}

// Add our mirror options
mirrorLine := fmt.Sprintf("%s %s", constants.PrivateMirrorConf, alternativeMirrorURL)
mirrorLine := fmt.Sprintf("%s %s", constants.PrivateMirrorConf, s.config.AlternativeFreshclamMirrorURL)
scriptedUpdatesLine := constants.ScriptedUpdatesConf + " no"

configLines = append(configLines, mirrorLine, scriptedUpdatesLine)
Expand All @@ -302,11 +312,38 @@ func syncFreshclamConfig(configPath, alternativeMirrorURL string) error {
configContents = []byte(strings.Join(configLines, "\n"))

writePermissions := 0o600
err = os.WriteFile(configPath, configContents, os.FileMode(writePermissions))
err = os.WriteFile(s.config.GetFreshclamConfigPath(), configContents, os.FileMode(writePermissions))
if err != nil {
return fmt.Errorf("failed to write freshclam.conf: %w", err)
}
}

return nil
}

func (s *Scanner) createDefaultClamdConfig() error {
t := template.New("clamd.conf")
t, err := t.Parse(clamdConfigTemplate)
if err != nil {
return fmt.Errorf("unable to parse template: %w", err)
}

f, err := os.Create(s.config.ClamDaemonConfigPath)
if err != nil {
return fmt.Errorf("unable to create file: %w", err)
}
defer f.Close()

err = t.Execute(f, struct {
LocalSocket string
ExcludedPaths []string
}{
filepath.Join(os.TempDir(), "clamd.sock"),
slices.Concat(s.config.ClamScanExcludeDirs, s.config.ClamScanExcludeFiles),
})
if err != nil {
return fmt.Errorf("unable te render config file template: %w", err)
}

return nil
}
6 changes: 3 additions & 3 deletions scanner/families/malware/clam/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,9 @@ const (
)

type Config struct {
// UseClamDaemon indicates that scanning should use daemon (clamdscan) command
// instead of native (clamscan) command.
UseClamDaemon bool `yaml:"use_clam_daemon" mapstructure:"use_clam_daemon" json:"use_clam_daemon"`
// UseNativeClamscan indicates that scanning should use the native (clamscan) command
// instead of daemon (clamdscan) command.
UseNativeClamscan bool `yaml:"use_native_clamscan" mapstructure:"use_native_clamscan" json:"use_native_clamscan"`
ClamScanBinaryPath string `yaml:"clamscan_binary_path" mapstructure:"clamscan_binary_path" json:"clamscan_binary_path"`
ClamScanExcludeFiles []string `yaml:"clamscan_exclude_files" mapstructure:"clamscan_exclude_files" json:"clamscan_exclude_files"`
ClamScanExcludeDirs []string `yaml:"clamscan_exclude_dirs" mapstructure:"clamscan_exclude_dirs" json:"clamscan_exclude_dirs"`
Expand Down
4 changes: 4 additions & 0 deletions scanner/families/malware/clam/templates/clamd.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
LocalSocket {{.LocalSocket}}
{{range .ExcludedPaths}}
ExcludePath {{.}}
{{end}}

0 comments on commit c2b984b

Please sign in to comment.