diff --git a/.families.yaml b/.families.yaml index 6f890e55b..9898551d9 100644 --- a/.families.yaml +++ b/.families.yaml @@ -1,5 +1,5 @@ sbom: - enabled: true + enabled: false analyzers_list: - "syft" - "gomod" @@ -26,7 +26,7 @@ sbom: token: "token" vulnerabilities: - enabled: true + enabled: false scanners_list: - "grype" inputs: @@ -68,7 +68,7 @@ secrets: binary_path: "/usr/local/bin/gitleaks" exploits: - enabled: true + enabled: false scanners_list: - "exploitdb" inputs: [] @@ -78,10 +78,20 @@ exploits: base_url: "http://localhost:1326" misconfiguration: - enabled: true + enabled: false scanners_list: - "fake" inputs: - input: "./" input_type: "rootfs" scanners_configs: {} + + +infofinder: + enabled: true + scanners_list: + - "sshTopology" + inputs: + - input: "/" + input_type: "rootfs" + scanners_configs: {} diff --git a/Dockerfile.cli b/Dockerfile.cli index 090b1640a..14477c4d9 100644 --- a/Dockerfile.cli +++ b/Dockerfile.cli @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.2 -ARG VMCLARITY_TOOLS_BASE=ghcr.io/openclarity/vmclarity-tools-base:v0.2.0@sha256:0e0ed706dc297366af44d736c71aefa350b54a0214290aa81b3603462e39872b +ARG VMCLARITY_TOOLS_BASE=ghcr.io/openclarity/vmclarity-tools-base:v0.3.0@sha256:c592419f5f3f184909363e080b01aea5deef63374b856aed74334afa3bdef793 FROM --platform=$BUILDPLATFORM golang:1.20.7-alpine AS builder RUN apk add --update --no-cache ca-certificates git diff --git a/pkg/cli/presenter/default.go b/pkg/cli/presenter/default.go index 9d04f93d8..a7cb537db 100644 --- a/pkg/cli/presenter/default.go +++ b/pkg/cli/presenter/default.go @@ -22,6 +22,7 @@ import ( "github.com/openclarity/vmclarity/pkg/shared/families" "github.com/openclarity/vmclarity/pkg/shared/families/exploits" + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder" "github.com/openclarity/vmclarity/pkg/shared/families/malware" "github.com/openclarity/vmclarity/pkg/shared/families/rootkits" "github.com/openclarity/vmclarity/pkg/shared/families/sbom" @@ -54,6 +55,8 @@ func (p *DefaultPresenter) ExportFamilyResult(ctx context.Context, res families. err = p.ExportRootkitResult(ctx, res) case types.Malware: err = p.ExportMalwareResult(ctx, res) + case types.InfoFinder: + err = p.ExportInfoFinderResult(ctx, res) } return err @@ -167,3 +170,21 @@ func (p *DefaultPresenter) ExportRootkitResult(_ context.Context, res families.F } return nil } + +func (p *DefaultPresenter) ExportInfoFinderResult(_ context.Context, res families.FamilyResult) error { + infoFinderResults, ok := res.Result.(*infofinder.Results) + if !ok { + return fmt.Errorf("failed to convert to infofinder results") + } + + bytes, err := json.Marshal(infoFinderResults) + if err != nil { + return fmt.Errorf("failed to marshal infofinder results: %w", err) + } + + if err = p.Write(bytes, "infofinder.json"); err != nil { + return fmt.Errorf("failed to output infofinder results: %w", err) + } + + return nil +} diff --git a/pkg/cli/presenter/vmclarity.go b/pkg/cli/presenter/vmclarity.go index 042f16212..6b58c2167 100644 --- a/pkg/cli/presenter/vmclarity.go +++ b/pkg/cli/presenter/vmclarity.go @@ -61,6 +61,8 @@ func (v *VMClarityPresenter) ExportFamilyResult(ctx context.Context, res familie err = v.ExportRootkitResult(ctx, res) case types.Malware: err = v.ExportMalwareResult(ctx, res) + case types.InfoFinder: + err = fmt.Errorf("InfoFinder family is unsupported") } return err diff --git a/pkg/cli/state/local.go b/pkg/cli/state/local.go index 1015a2508..a39c389a2 100644 --- a/pkg/cli/state/local.go +++ b/pkg/cli/state/local.go @@ -52,6 +52,8 @@ func (l *LocalState) MarkFamilyScanInProgress(ctx context.Context, familyType ty logger.Info("Rootkit scan is in progress") case types.Malware: logger.Info("Malware scan is in progress") + case types.InfoFinder: + logger.Info("InfoFinder scan is in progress") } return nil } diff --git a/pkg/cli/state/vmclarity.go b/pkg/cli/state/vmclarity.go index 3126ddc42..2b4b811fa 100644 --- a/pkg/cli/state/vmclarity.go +++ b/pkg/cli/state/vmclarity.go @@ -172,6 +172,8 @@ func (v *VMClarityState) MarkFamilyScanInProgress(ctx context.Context, familyTyp err = v.markRootkitsScanInProgress(ctx) case types.Malware: err = v.markMalwareScanInProgress(ctx) + case types.InfoFinder: + err = fmt.Errorf("InfoFinder family is unsupported") } return err } diff --git a/pkg/shared/families/config.go b/pkg/shared/families/config.go index bf8cb5566..de9fd6e2a 100644 --- a/pkg/shared/families/config.go +++ b/pkg/shared/families/config.go @@ -19,6 +19,7 @@ import ( kubeclarityutils "github.com/openclarity/kubeclarity/shared/pkg/utils" "github.com/openclarity/vmclarity/pkg/shared/families/exploits" + infofinderTypes "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/types" "github.com/openclarity/vmclarity/pkg/shared/families/malware" misconfigurationTypes "github.com/openclarity/vmclarity/pkg/shared/families/misconfiguration/types" "github.com/openclarity/vmclarity/pkg/shared/families/rootkits" @@ -39,6 +40,7 @@ type Config struct { Rootkits rootkits.Config `json:"rootkits" yaml:"rootkits" mapstructure:"rootkits"` Malware malware.Config `json:"malware" yaml:"malware" mapstructure:"malware"` Misconfiguration misconfigurationTypes.Config `json:"misconfiguration" yaml:"misconfiguration" mapstructure:"misconfiguration"` + InfoFinder infofinderTypes.Config `json:"infofinder" yaml:"infofinder" mapstructure:"infofinder"` // Enrichers Exploits exploits.Config `json:"exploits" yaml:"exploits" mapstructure:"exploits"` @@ -111,6 +113,17 @@ func SetMountPointsForFamiliesInput(mountPoints []string, familiesConfig *Config }, ) } + + if familiesConfig.InfoFinder.Enabled { + familiesConfig.InfoFinder.Inputs = append( + familiesConfig.InfoFinder.Inputs, + types.Input{ + StripPathFromResult: utils.PointerTo(true), + Input: mountDir, + InputType: string(kubeclarityutils.ROOTFS), + }, + ) + } } return familiesConfig } diff --git a/pkg/shared/families/infofinder/family.go b/pkg/shared/families/infofinder/family.go new file mode 100644 index 000000000..a75107f04 --- /dev/null +++ b/pkg/shared/families/infofinder/family.go @@ -0,0 +1,97 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infofinder + +import ( + "context" + "fmt" + "time" + + "github.com/openclarity/kubeclarity/shared/pkg/job_manager" + "github.com/openclarity/kubeclarity/shared/pkg/utils" + + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/job" + infofinderTypes "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/types" + "github.com/openclarity/vmclarity/pkg/shared/families/interfaces" + "github.com/openclarity/vmclarity/pkg/shared/families/results" + "github.com/openclarity/vmclarity/pkg/shared/families/types" + familiesutils "github.com/openclarity/vmclarity/pkg/shared/families/utils" + "github.com/openclarity/vmclarity/pkg/shared/log" +) + +type InfoFinder struct { + conf infofinderTypes.Config +} + +func (i InfoFinder) Run(ctx context.Context, _ *results.Results) (interfaces.IsResults, error) { + logger := log.GetLoggerFromContextOrDiscard(ctx).WithField("family", "info finder") + logger.Info("InfoFinder Run...") + + infoFinderResults := NewResults() + + manager := job_manager.New(i.conf.ScannersList, i.conf.ScannersConfig, logger, job.Factory) + for _, input := range i.conf.Inputs { + startTime := time.Now() + managerResults, err := manager.Run(utils.SourceType(input.InputType), input.Input) + if err != nil { + return nil, fmt.Errorf("failed to scan input %q for info: %v", input.Input, err) + } + endTime := time.Now() + inputSize, err := familiesutils.GetInputSize(input) + if err != nil { + logger.Warnf("Failed to calculate input %v size: %v", input, err) + } + + // Merge results. + for name, result := range managerResults { + logger.Infof("Merging result from %q", name) + if assetScan, ok := result.(*infofinderTypes.ScannerResult); ok { + if familiesutils.ShouldStripInputPath(input.StripPathFromResult, i.conf.StripInputPaths) { + assetScan = stripPathFromResult(assetScan, input.Input) + } + infoFinderResults.AddScannerResult(assetScan) + } else { + return nil, fmt.Errorf("received bad scanner result type %T, expected infofinderTypes.ScannerResult", result) + } + } + infoFinderResults.Metadata.InputScans = append(infoFinderResults.Metadata.InputScans, types.CreateInputScanMetadata(startTime, endTime, inputSize, input)) + } + + logger.Info("InfoFinder Done...") + + return infoFinderResults, nil +} + +// stripPathFromResult strip input path from results wherever it is found. +func stripPathFromResult(result *infofinderTypes.ScannerResult, path string) *infofinderTypes.ScannerResult { + for i := range result.Infos { + result.Infos[i].Path = familiesutils.TrimMountPath(result.Infos[i].Path, path) + } + return result +} + +func (i InfoFinder) GetType() types.FamilyType { + return types.InfoFinder +} + +// ensure types implement the requisite interfaces. +var _ interfaces.Family = &InfoFinder{} + +func New(conf infofinderTypes.Config) *InfoFinder { + return &InfoFinder{ + conf: conf, + } +} diff --git a/pkg/shared/families/infofinder/job/job.go b/pkg/shared/families/infofinder/job/job.go new file mode 100644 index 000000000..5c0c180bf --- /dev/null +++ b/pkg/shared/families/infofinder/job/job.go @@ -0,0 +1,28 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package job + +import ( + "github.com/openclarity/kubeclarity/shared/pkg/job_manager" + + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/sshtopology" +) + +var Factory = job_manager.NewJobFactory() + +func init() { + Factory.Register(sshtopology.ScannerName, sshtopology.New) +} diff --git a/pkg/shared/families/infofinder/result.go b/pkg/shared/families/infofinder/result.go new file mode 100644 index 000000000..9b13baabe --- /dev/null +++ b/pkg/shared/families/infofinder/result.go @@ -0,0 +1,68 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package infofinder + +import ( + "time" + + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/types" + familiestypes "github.com/openclarity/vmclarity/pkg/shared/families/types" +) + +type FlattenedInfos struct { + ScannerName string `json:"ScannerName"` + types.Info +} + +type Results struct { + Metadata familiestypes.Metadata `json:"Metadata"` + Infos []FlattenedInfos `json:"Infos"` +} + +func NewResults() *Results { + return &Results{ + Metadata: familiestypes.Metadata{ + Timestamp: time.Now(), + Scanners: []string{}, + }, + Infos: []FlattenedInfos{}, + } +} + +func (*Results) IsResults() {} + +func (r *Results) addScannerNameToMetadata(name string) { + for _, scannerName := range r.Metadata.Scanners { + if scannerName == name { + return + } + } + r.Metadata.Scanners = append(r.Metadata.Scanners, name) +} + +func (r *Results) AddScannerResult(scannerResult *types.ScannerResult) { + r.addScannerNameToMetadata(scannerResult.ScannerName) + + for _, info := range scannerResult.Infos { + r.Infos = append(r.Infos, FlattenedInfos{ + ScannerName: scannerResult.ScannerName, + Info: info, + }) + } + + // bump the timestamp as there are new results + r.Metadata.Timestamp = time.Now() +} diff --git a/pkg/shared/families/infofinder/sshtopology/scanner.go b/pkg/shared/families/infofinder/sshtopology/scanner.go new file mode 100644 index 000000000..dbbb802e8 --- /dev/null +++ b/pkg/shared/families/infofinder/sshtopology/scanner.go @@ -0,0 +1,371 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sshtopology + +import ( + "bufio" + "errors" + "fmt" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + "sync" + + "github.com/openclarity/kubeclarity/shared/pkg/job_manager" + "github.com/openclarity/kubeclarity/shared/pkg/utils" + log "github.com/sirupsen/logrus" + + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/types" + sharedUtils "github.com/openclarity/vmclarity/pkg/shared/utils" +) + +const ScannerName = "sshTopology" + +type Scanner struct { + name string + logger *log.Entry + config types.SSHTopologyConfig + resultChan chan job_manager.Result +} + +func New(c job_manager.IsConfig, logger *log.Entry, resultChan chan job_manager.Result) job_manager.Job { + conf := c.(types.ScannersConfig) // nolint:forcetypeassert + return &Scanner{ + name: ScannerName, + logger: logger.Dup().WithField("scanner", ScannerName), + config: conf.SSHTopology, + resultChan: resultChan, + } +} + +// nolint:cyclop,gocognit +func (s *Scanner) Run(sourceType utils.SourceType, userInput string) error { + go func() { + s.logger.Debugf("Running with input=%v and source type=%v", userInput, sourceType) + retResults := types.ScannerResult{ + ScannerName: ScannerName, + } + + // Validate this is an input type supported by the scanner, + // otherwise return skipped. + if !s.isValidInputType(sourceType) { + s.sendResults(retResults, nil) + return + } + + var errs []error + homeUserDirs, err := getHomeUserDirs(userInput) + if err != nil { + // Collect the error and continue. + errs = append(errs, fmt.Errorf("failed to get home user dirs: %v", err)) + } + s.logger.Debugf("Found home user dirs %+v", homeUserDirs) + + errorsChan := make(chan error) + fingerprintsChan := make(chan []types.Info) + + go func() { + for fingerprints := range fingerprintsChan { + retResults.Infos = append(retResults.Infos, fingerprints...) + } + }() + + go func() { + for e := range errorsChan { + errs = append(errs, e) + } + }() + + var wg sync.WaitGroup + wg.Add(1) + go func() { + defer wg.Done() + if sshDaemonKeysFingerprints, err := s.getSSHDaemonKeysFingerprints(userInput); err != nil { + errorsChan <- fmt.Errorf("failed to get ssh daemon keys: %v", err) + } else { + fingerprintsChan <- sshDaemonKeysFingerprints + } + }() + + for i := range homeUserDirs { + dir := homeUserDirs[i] + + wg.Add(1) + go func() { + defer wg.Done() + if sshPrivateKeysFingerprints, err := s.getSSHPrivateKeysFingerprints(dir); err != nil { + errorsChan <- fmt.Errorf("failed to get ssh private keys: %v", err) + } else { + fingerprintsChan <- sshPrivateKeysFingerprints + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + if sshAuthorizedKeysFingerprints, err := s.getSSHAuthorizedKeysFingerprints(dir); err != nil { + errorsChan <- fmt.Errorf("failed to get ssh authorized keys: %v", err) + } else { + fingerprintsChan <- sshAuthorizedKeysFingerprints + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + if sshKnownHostsFingerprints, err := s.getSSHKnownHostsFingerprints(dir); err != nil { + errorsChan <- fmt.Errorf("failed to get ssh known hosts: %v", err) + } else { + fingerprintsChan <- sshKnownHostsFingerprints + } + }() + } + + wg.Wait() + close(errorsChan) + close(fingerprintsChan) + + retErr := errors.Join(errs...) + if retErr != nil { + retResults.Error = retErr + } + + if len(retResults.Infos) > 0 && retResults.Error != nil { + // Since we have findings, we want to share what we've got and only print the errors here. + // Maybe we need to support to send both errors and findings in a higher level. + s.logger.Error(retResults.Error) + retResults.Error = nil + } + + s.sendResults(retResults, nil) + }() + + return nil +} + +func getHomeUserDirs(rootDir string) ([]string, error) { + var dirs []string + + // Set root home if exists. + rootHome := path.Join(rootDir, "root") + if _, err := os.Stat(rootHome); err == nil { + dirs = append(dirs, rootHome) + } + + homeDirPath := path.Join(rootDir, "home") + files, err := os.ReadDir(homeDirPath) + if err != nil { + return nil, fmt.Errorf("failed to read dir (%v): %v", homeDirPath, err) + } + + for _, f := range files { + if f.IsDir() { + dirs = append(dirs, path.Join(homeDirPath, f.Name())) + } + } + + return dirs, nil +} + +func (s *Scanner) getSSHDaemonKeysFingerprints(rootPath string) ([]types.Info, error) { + paths, err := s.getPrivateKeysPaths(path.Join(rootPath, "/etc/ssh"), false) + if err != nil { + return nil, fmt.Errorf("failed to get private keys paths: %v", err) + } + s.logger.Debugf("Found ssh daemon private keys paths %+v", paths) + + fingerprints, err := s.getFingerprints(paths, types.SSHDaemonKeyFingerprint) + if err != nil { + return nil, fmt.Errorf("failed to get ssh daemon private keys fingerprints: %v", err) + } + s.logger.Debugf("Found ssh daemon private keys fingerprints %+v", fingerprints) + + return fingerprints, nil +} + +func (s *Scanner) getSSHPrivateKeysFingerprints(homeUserDir string) ([]types.Info, error) { + paths, err := s.getPrivateKeysPaths(homeUserDir, true) + if err != nil { + return nil, fmt.Errorf("failed to get private keys paths: %v", err) + } + s.logger.Debugf("Found ssh private keys paths %+v", paths) + + infos, err := s.getFingerprints(paths, types.SSHPrivateKeyFingerprint) + if err != nil { + return nil, fmt.Errorf("failed to get ssh private keys fingerprints: %v", err) + } + s.logger.Debugf("Found ssh private keys fingerprints %+v", infos) + + return infos, nil +} + +func (s *Scanner) getSSHAuthorizedKeysFingerprints(homeUserDir string) ([]types.Info, error) { + infos, err := s.getFingerprints([]string{path.Join(homeUserDir, ".ssh/authorized_keys")}, types.SSHAuthorizedKeyFingerprint) + if err != nil { + return nil, fmt.Errorf("failed to get ssh authorized keys fingerprints: %v", err) + } + s.logger.Debugf("Found ssh authorized keys fingerprints %+v", infos) + + return infos, nil +} + +func (s *Scanner) getSSHKnownHostsFingerprints(homeUserDir string) ([]types.Info, error) { + infos, err := s.getFingerprints([]string{path.Join(homeUserDir, ".ssh/known_hosts")}, types.SSHKnownHostFingerprint) + if err != nil { + return nil, fmt.Errorf("failed to get ssh known hosts fingerprints: %v", err) + } + s.logger.Debugf("Found ssh known hosts fingerprints %+v", infos) + + return infos, nil +} + +func (s *Scanner) getFingerprints(paths []string, infoType types.InfoType) ([]types.Info, error) { + var infos []types.Info + + for _, p := range paths { + _, err := os.Stat(p) + if os.IsNotExist(err) { + s.logger.Debugf("File (%v) does not exist.", p) + continue + } else if err != nil { + return nil, fmt.Errorf("failed to check file: %v", err) + } + + var output []byte + if output, err = s.executeSSHKeyGenFingerprintCommand("sha256", p); err != nil { + return nil, fmt.Errorf("failed to execute ssh-keygen command: %v", err) + } + + infos = append(infos, parseSSHKeyGenFingerprintCommandOutput(string(output), infoType, p)...) + } + + return infos, nil +} + +func (s *Scanner) getPrivateKeysPaths(rootPath string, recursive bool) ([]string, error) { + var paths []string + err := filepath.Walk(rootPath, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + if info.IsDir() { + if path != rootPath && !recursive { + return filepath.SkipDir + } + return nil + } + + isPrivateKeyFile, err := isPrivateKey(path) + if err != nil { + s.logger.Errorf("failed to verify if file (%v) is private key file - skipping: %v", path, err) + return nil + } + + if isPrivateKeyFile { + paths = append(paths, path) + } + + return nil + }) + if err != nil { + return nil, fmt.Errorf("failed to walks the file tree rooted at %v: %v", rootPath, err) + } + + return paths, nil +} + +func isPrivateKey(path string) (bool, error) { + f, err := os.Open(path) + if err != nil { + return false, fmt.Errorf("failed to open file: %v", err) + } + defer f.Close() + + // Splits on newlines by default. + scanner := bufio.NewScanner(f) + + for scanner.Scan() { + // We only need to look at the first line to find PEM private keys. + return strings.Contains(scanner.Text(), "PRIVATE KEY"), nil + } + + if err = scanner.Err(); err != nil { + return false, fmt.Errorf("failed to scan file: %v", err) + } + + return false, nil +} + +func parseSSHKeyGenFingerprintCommandOutput(output string, infoType types.InfoType, path string) []types.Info { + lines := strings.Split(output, "\n") + infos := make([]types.Info, 0, len(lines)) + for i := range lines { + if lines[i] == "" { + continue + } + infos = append(infos, types.Info{ + Type: infoType, + Path: path, + Data: lines[i], + }) + } + return infos +} + +func (s *Scanner) executeSSHKeyGenFingerprintCommand(hashAlgo string, filePath string) ([]byte, error) { + args := []string{ + "-E", + hashAlgo, + "-l", + "-f", + filePath, + } + cmd := exec.Command("ssh-keygen", args...) + s.logger.Infof("Running command: %v", cmd.String()) + output, err := sharedUtils.RunCommand(cmd) + if err != nil { + return nil, fmt.Errorf("failed to run command: %v", err) + } + + return output, nil +} + +func (s *Scanner) isValidInputType(sourceType utils.SourceType) bool { + switch sourceType { + case utils.ROOTFS: + return true + case utils.DIR, utils.FILE, utils.IMAGE, utils.SBOM: + s.logger.Infof("Source type %v is not supported for %s, skipping.", ScannerName, sourceType) + default: + s.logger.Infof("Unknown source type %v, skipping.", sourceType) + } + return false +} + +func (s *Scanner) sendResults(results types.ScannerResult, err error) { + if err != nil { + s.logger.Error(err) + results.Error = err + } + select { + case s.resultChan <- &results: + default: + s.logger.Error("Failed to send results on channel") + } +} diff --git a/pkg/shared/families/infofinder/sshtopology/scanner_test.go b/pkg/shared/families/infofinder/sshtopology/scanner_test.go new file mode 100644 index 000000000..fdeb50b10 --- /dev/null +++ b/pkg/shared/families/infofinder/sshtopology/scanner_test.go @@ -0,0 +1,274 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package sshtopology + +import ( + "reflect" + "testing" + + log "github.com/sirupsen/logrus" + + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder/types" +) + +var testScanner = &Scanner{ + logger: log.NewEntry(log.StandardLogger()), +} + +func Test_getHomeUserDirs(t *testing.T) { + type args struct { + rootDir string + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "sanity", + args: args{ + rootDir: "../testdata/rootfolder", + }, + want: []string{"../testdata/rootfolder/home/dir1", "../testdata/rootfolder/home/dir2"}, + wantErr: false, + }, + { + name: "root folder with root home folder", + args: args{ + rootDir: "../testdata/rootfolderwithroothome", + }, + want: []string{"../testdata/rootfolderwithroothome/root", "../testdata/rootfolderwithroothome/home/dir1", "../testdata/rootfolderwithroothome/home/dir2"}, + wantErr: false, + }, + { + name: "missing dir", + args: args{ + rootDir: "../testdata/missingrootfolder", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := getHomeUserDirs(tt.args.rootDir) + if (err != nil) != tt.wantErr { + t.Errorf("getHomeUserDirs() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getHomeUserDirs() got = %v, want %v", got, tt.want) + } + }) + } +} + +func TestScanner_getPrivateKeysPaths(t *testing.T) { + type args struct { + rootPath string + recursive bool + } + tests := []struct { + name string + args args + want []string + wantErr bool + }{ + { + name: "not recursive - found nothing", + args: args{ + rootPath: "../testdata/rootfolder", + recursive: false, + }, + want: nil, + wantErr: false, + }, + { + name: "not recursive - found keys in folder", + args: args{ + rootPath: "../testdata/rootfolder/home/dir1", + recursive: false, + }, + want: []string{"../testdata/rootfolder/home/dir1/private_key"}, + wantErr: false, + }, + { + name: "recursive - found keys in all sub folders", + args: args{ + rootPath: "../testdata/rootfolder", + recursive: true, + }, + want: []string{ + "../testdata/rootfolder/.ssh/private_key", + "../testdata/rootfolder/etc/ssh/ssh_dummy_key", + "../testdata/rootfolder/etc/ssh/ssh_dummy_key2", + "../testdata/rootfolder/home/dir1/dir3/private_key", + "../testdata/rootfolder/home/dir1/private_key", + "../testdata/rootfolder/home/dir2/private_key", + }, + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + s := testScanner + got, err := s.getPrivateKeysPaths(tt.args.rootPath, tt.args.recursive) + if (err != nil) != tt.wantErr { + t.Errorf("getPrivateKeysPaths() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("getPrivateKeysPaths() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_isPrivateKey(t *testing.T) { + type args struct { + path string + } + tests := []struct { + name string + args args + want bool + wantErr bool + }{ + { + name: "private key", + args: args{ + path: "../testdata/rootfolder/.ssh/private_key", + }, + want: true, + wantErr: false, + }, + { + name: "not private key", + args: args{ + path: "../testdata/rootfolder/.ssh/not_a_key", + }, + want: false, + wantErr: false, + }, + { + name: "empty_file", + args: args{ + path: "../testdata/rootfolder/.ssh/empty_file", + }, + want: false, + wantErr: false, + }, + { + name: "folder as an input should return an error", + args: args{ + path: "../testdata/rootfolder/.ssh", + }, + want: false, + wantErr: true, + }, + { + name: "path does not exists should return an error", + args: args{ + path: "../testdata/dummyrootfolder/.ssh", + }, + want: false, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := isPrivateKey(tt.args.path) + if (err != nil) != tt.wantErr { + t.Errorf("isPrivateKey() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("isPrivateKey() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_parseSSHKeyGenCommandOutput(t *testing.T) { + type args struct { + output string + infoType types.InfoType + path string + } + tests := []struct { + name string + args args + want []types.Info + }{ + { + name: "single line output", + args: args{ + output: "256 SHA256:gv6snCwAl5+6fY2g5VkmETWb9Mv0zLRkMz8aQyQWAVc ec2-3-64-214-52.eu-central-1.compute.amazonaws.com (ED25519)", + infoType: types.SSHKnownHostFingerprint, + path: "/home/user/.ssh/known_hosts", + }, + want: []types.Info{ + { + Type: types.SSHKnownHostFingerprint, + Path: "/home/user/.ssh/known_hosts", + Data: "256 SHA256:gv6snCwAl5+6fY2g5VkmETWb9Mv0zLRkMz8aQyQWAVc ec2-3-64-214-52.eu-central-1.compute.amazonaws.com (ED25519)", + }, + }, + }, + { + name: "single line output with new line", + args: args{ + output: "256 SHA256:gv6snCwAl5+6fY2g5VkmETWb9Mv0zLRkMz8aQyQWAVc ec2-3-64-214-52.eu-central-1.compute.amazonaws.com (ED25519)\n", + infoType: types.SSHKnownHostFingerprint, + path: "/home/user/.ssh/known_hosts", + }, + want: []types.Info{ + { + Type: types.SSHKnownHostFingerprint, + Path: "/home/user/.ssh/known_hosts", + Data: "256 SHA256:gv6snCwAl5+6fY2g5VkmETWb9Mv0zLRkMz8aQyQWAVc ec2-3-64-214-52.eu-central-1.compute.amazonaws.com (ED25519)", + }, + }, + }, + { + name: "multiple lines output", + args: args{ + output: "256 SHA256:gv6snCwAl5+6fY2g5VkmETWb9Mv0zLRkMz8aQyQWAVc ec2-3-64-214-52.eu-central-1.compute.amazonaws.com (ED25519)\n256 SHA256:cDmm4+e/BNwQpsk/Qhh39i2qiT6HcIs6qTLtIiMWzPg ec2-3-64-214-52.eu-central-1.compute.amazonaws.com (ECDSA)", + infoType: types.SSHKnownHostFingerprint, + path: "/home/user/.ssh/known_hosts", + }, + want: []types.Info{ + { + Type: types.SSHKnownHostFingerprint, + Path: "/home/user/.ssh/known_hosts", + Data: "256 SHA256:gv6snCwAl5+6fY2g5VkmETWb9Mv0zLRkMz8aQyQWAVc ec2-3-64-214-52.eu-central-1.compute.amazonaws.com (ED25519)", + }, + { + Type: types.SSHKnownHostFingerprint, + Path: "/home/user/.ssh/known_hosts", + Data: "256 SHA256:cDmm4+e/BNwQpsk/Qhh39i2qiT6HcIs6qTLtIiMWzPg ec2-3-64-214-52.eu-central-1.compute.amazonaws.com (ECDSA)", + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := parseSSHKeyGenFingerprintCommandOutput(tt.args.output, tt.args.infoType, tt.args.path); !reflect.DeepEqual(got, tt.want) { + t.Errorf("parseSSHKeyGenFingerprintCommandOutput() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/.ssh/empty_file b/pkg/shared/families/infofinder/testdata/rootfolder/.ssh/empty_file new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/.ssh/not_a_key b/pkg/shared/families/infofinder/testdata/rootfolder/.ssh/not_a_key new file mode 100644 index 000000000..269d38cbc --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolder/.ssh/not_a_key @@ -0,0 +1 @@ +THIS IS NOT A KEY \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/.ssh/private_key b/pkg/shared/families/infofinder/testdata/rootfolder/.ssh/private_key new file mode 100644 index 000000000..4cdef8c42 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolder/.ssh/private_key @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/not_a_key b/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/not_a_key new file mode 100644 index 000000000..269d38cbc --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/not_a_key @@ -0,0 +1 @@ +THIS IS NOT A KEY \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/ssh_dummy_key b/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/ssh_dummy_key new file mode 100644 index 000000000..4cdef8c42 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/ssh_dummy_key @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/ssh_dummy_key2 b/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/ssh_dummy_key2 new file mode 100644 index 000000000..ce3dc5700 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolder/etc/ssh/ssh_dummy_key2 @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY2 +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/home/dir1/dir3/private_key b/pkg/shared/families/infofinder/testdata/rootfolder/home/dir1/dir3/private_key new file mode 100644 index 000000000..4cdef8c42 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolder/home/dir1/dir3/private_key @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/home/dir1/private_key b/pkg/shared/families/infofinder/testdata/rootfolder/home/dir1/private_key new file mode 100644 index 000000000..4cdef8c42 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolder/home/dir1/private_key @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/home/dir2/private_key b/pkg/shared/families/infofinder/testdata/rootfolder/home/dir2/private_key new file mode 100644 index 000000000..4cdef8c42 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolder/home/dir2/private_key @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/home/file1 b/pkg/shared/families/infofinder/testdata/rootfolder/home/file1 new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/shared/families/infofinder/testdata/rootfolder/home/file2 b/pkg/shared/families/infofinder/testdata/rootfolder/home/file2 new file mode 100644 index 000000000..e69de29bb diff --git a/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/home/dir1/private_key b/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/home/dir1/private_key new file mode 100644 index 000000000..4cdef8c42 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/home/dir1/private_key @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/home/dir2/private_key b/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/home/dir2/private_key new file mode 100644 index 000000000..4cdef8c42 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/home/dir2/private_key @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/root/dir1/private_key b/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/root/dir1/private_key new file mode 100644 index 000000000..4cdef8c42 --- /dev/null +++ b/pkg/shared/families/infofinder/testdata/rootfolderwithroothome/root/dir1/private_key @@ -0,0 +1,3 @@ +-----BEGIN OPENSSH PRIVATE KEY----- +NOT REALLY A KEY +-----END OPENSSH PRIVATE KEY----- \ No newline at end of file diff --git a/pkg/shared/families/infofinder/types/config.go b/pkg/shared/families/infofinder/types/config.go new file mode 100644 index 000000000..3359f485f --- /dev/null +++ b/pkg/shared/families/infofinder/types/config.go @@ -0,0 +1,43 @@ +// Copyright © 2023 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +import ( + "github.com/openclarity/vmclarity/pkg/shared/families/types" +) + +type Config struct { + Enabled bool `json:"enabled" yaml:"enabled" mapstructure:"enabled"` + ScannersList []string `yaml:"scanners_list" mapstructure:"scanners_list"` + StripInputPaths bool `yaml:"strip_input_paths" mapstructure:"strip_input_paths"` + Inputs []types.Input `yaml:"inputs" mapstructure:"inputs"` + ScannersConfig ScannersConfig `yaml:"scanners_config" mapstructure:"scanners_config"` +} + +// ScannersConfig +// Add scanner specific configurations here, where the key is the scanner name, +// and the value is the scanner specific configuration. +// +// For example if the scanner name is "sshTopology": +// +// SSHTopology SSHTopologyConfig `yaml:"ssh_topology" mapstructure:"ssh_topology"` +type ScannersConfig struct { + SSHTopology SSHTopologyConfig `yaml:"ssh_topology" mapstructure:"ssh_topology"` +} + +func (ScannersConfig) IsConfig() {} + +type SSHTopologyConfig struct{} diff --git a/pkg/shared/families/infofinder/types/scannerResult.go b/pkg/shared/families/infofinder/types/scannerResult.go new file mode 100644 index 000000000..65f432c1a --- /dev/null +++ b/pkg/shared/families/infofinder/types/scannerResult.go @@ -0,0 +1,41 @@ +// Copyright © 2022 Cisco Systems, Inc. and its affiliates. +// All rights reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package types + +type InfoType string + +const ( + SSHKnownHostFingerprint InfoType = "sshKnownHostFingerprint" + SSHAuthorizedKeyFingerprint InfoType = "sshAuthorizedKeyFingerprint" + SSHPrivateKeyFingerprint InfoType = "sshPrivateKeyFingerprint" + SSHDaemonKeyFingerprint InfoType = "sshDaemonKeyFingerprint" +) + +type Info struct { + Type InfoType `json:"type"` + Path string `json:"path"` + Data string `json:"data"` +} + +type ScannerResult struct { + ScannerName string + Infos []Info + Error error +} + +func (sr ScannerResult) GetError() error { + return sr.Error +} diff --git a/pkg/shared/families/manager.go b/pkg/shared/families/manager.go index 93a11a23e..c9fb7d139 100644 --- a/pkg/shared/families/manager.go +++ b/pkg/shared/families/manager.go @@ -20,6 +20,7 @@ import ( "fmt" "github.com/openclarity/vmclarity/pkg/shared/families/exploits" + "github.com/openclarity/vmclarity/pkg/shared/families/infofinder" "github.com/openclarity/vmclarity/pkg/shared/families/interfaces" "github.com/openclarity/vmclarity/pkg/shared/families/malware" "github.com/openclarity/vmclarity/pkg/shared/families/misconfiguration" @@ -65,6 +66,9 @@ func New(config *Config) *Manager { if config.Misconfiguration.Enabled { manager.families = append(manager.families, misconfiguration.New(config.Misconfiguration)) } + if config.InfoFinder.Enabled { + manager.families = append(manager.families, infofinder.New(config.InfoFinder)) + } // Enrichers. // Exploits MUST be after Vulnerabilities to support the case it is configured to use the output from Vulnerabilities. @@ -128,6 +132,7 @@ func (m *Manager) Run(ctx context.Context, notifier FamilyNotifier) []error { case r := <-result: logger.Debugf("received result from family %q: %v", family.GetType(), r) if r.Err != nil { + logger.Errorf("received error result from family %q: %v", family.GetType(), r.Err) oneOrMoreFamilyFailed = true } else { familyResults.SetResults(r.Result) diff --git a/pkg/shared/families/types/types.go b/pkg/shared/families/types/types.go index 25efc654a..809be9ded 100644 --- a/pkg/shared/families/types/types.go +++ b/pkg/shared/families/types/types.go @@ -25,6 +25,7 @@ const ( Rootkits FamilyType = "rootkits" Malware FamilyType = "malware" Misconfiguration FamilyType = "misconfiguration" + InfoFinder FamilyType = "infofinder" Exploits FamilyType = "exploits" )