From c2b984b231893c5957530a8411054a79524e5572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Zsolt=20Kacs=C3=A1ndi?= Date: Mon, 22 Jul 2024 14:22:14 +0200 Subject: [PATCH] feat(scanner): add default clamd configuration (#1920) * 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 <32913827+ramizpolic@users.noreply.github.com> * revert(scanner): revert using args slice literal Co-authored-by: Ramiz Polic <32913827+ramizpolic@users.noreply.github.com> * 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 <32913827+ramizpolic@users.noreply.github.com> --- .families.yaml | 6 +- cli/state/testdata/effective-config.json | 2 +- orchestrator/watcher/assetscan/families.go | 6 +- scanner/families/malware/clam/clam.go | 103 ++++++++++++------ .../families/malware/clam/config/config.go | 6 +- .../malware/clam/templates/clamd.conf | 4 + 6 files changed, 82 insertions(+), 45 deletions(-) create mode 100644 scanner/families/malware/clam/templates/clamd.conf diff --git a/.families.yaml b/.families.yaml index 7a24a90fc..489f7aee6 100644 --- a/.families.yaml +++ b/.families.yaml @@ -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" diff --git a/cli/state/testdata/effective-config.json b/cli/state/testdata/effective-config.json index 43f86885c..89df2bacb 100644 --- a/cli/state/testdata/effective-config.json +++ b/cli/state/testdata/effective-config.json @@ -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, diff --git a/orchestrator/watcher/assetscan/families.go b/orchestrator/watcher/assetscan/families.go index 29021854e..6d11341dc 100644 --- a/orchestrator/watcher/assetscan/families.go +++ b/orchestrator/watcher/assetscan/families.go @@ -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{ diff --git a/scanner/families/malware/clam/clam.go b/scanner/families/malware/clam/clam.go index 47f9dc0a5..279c74f9c 100644 --- a/scanner/families/malware/clam/clam.go +++ b/scanner/families/malware/clam/clam.go @@ -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" @@ -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 @@ -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) @@ -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) } @@ -154,7 +161,6 @@ func (s *Scanner) runClamScanWithDaemon(ctx context.Context, fsScanPath string) args := []string{ "--multiscan", "--stream", - "--no-summary", "--infected", } @@ -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) } @@ -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...") @@ -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 @@ -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 @@ -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) @@ -302,7 +312,7 @@ 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) } @@ -310,3 +320,30 @@ func syncFreshclamConfig(configPath, alternativeMirrorURL string) error { 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 +} diff --git a/scanner/families/malware/clam/config/config.go b/scanner/families/malware/clam/config/config.go index 77001a6bc..b00d01444 100644 --- a/scanner/families/malware/clam/config/config.go +++ b/scanner/families/malware/clam/config/config.go @@ -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"` diff --git a/scanner/families/malware/clam/templates/clamd.conf b/scanner/families/malware/clam/templates/clamd.conf new file mode 100644 index 000000000..52197329d --- /dev/null +++ b/scanner/families/malware/clam/templates/clamd.conf @@ -0,0 +1,4 @@ +LocalSocket {{.LocalSocket}} +{{range .ExcludedPaths}} +ExcludePath {{.}} +{{end}}