diff --git a/detector/detector.go b/detector/detector.go index a2f8aa3c24..ed988afdbc 100644 --- a/detector/detector.go +++ b/detector/detector.go @@ -22,6 +22,7 @@ import ( "github.com/future-architect/vuls/oval" "github.com/future-architect/vuls/reporter" "github.com/future-architect/vuls/util" + hver "github.com/hashicorp/go-version" cvemodels "github.com/vulsio/go-cve-dictionary/models" ) @@ -54,6 +55,10 @@ func Detect(rs []models.ScanResult, dir string) ([]models.ScanResult, error) { return nil, xerrors.Errorf("Failed to detect Pkg CVE: %w", err) } + if err := DetectSnapCves(&r, config.Conf.CveDict, config.Conf.LogOpts); err != nil { + return nil, xerrors.Errorf("Failed to detect Snap CVE: %w", err) + } + cpeURIs, owaspDCXMLPath := []string{}, "" cpes := []Cpe{} if len(r.Container.ContainerID) == 0 { @@ -367,6 +372,83 @@ func DetectPkgCves(r *models.ScanResult, ovalCnf config.GovalDictConf, gostCnf c return nil } +func DetectSnapCves(r *models.ScanResult, cnf config.GoCveDictConf, logOpts logging.LogOpts) error { + detects := make(map[string]string) + client, err := newGoCveDictClient(&cnf, logOpts) + if err != nil { + return xerrors.Errorf("Failed to newGoCveDictClient. err: %w", err) + } + defer func() { + if err := client.closeDB(); err != nil { + logging.Log.Errorf("Failed to close DB. err: %+v", err) + } + }() + + for _, snap := range r.Snaps { + results, err := client.driver.GetCveIDsByProduct(snap.Name) + if err != nil { + return xerrors.Errorf("Failed to detectCveByCpeURI. err: %w", err) + } + + vera, err := hver.NewVersion(snap.Version) + if err != nil { + continue + } + + for _, row := range results { + if row.VersionEndExcluding != "" { + verb, err := hver.NewVersion(row.VersionEndExcluding) + if err != nil { + continue + } + if vera.LessThan(verb) { + detects[row.CveID] = snap.Name + } + } else if row.VersionEndIncluding != "" { + verb, err := hver.NewVersion(row.VersionEndIncluding) + if err != nil { + continue + } + if vera.LessThanOrEqual(verb) { + detects[row.CveID] = snap.Name + } + } + } + } + + for cveId, snap := range detects { + v, ok := r.ScannedCves[cveId] + if ok { + if v.CveContents == nil { + v.CveContents = models.NewCveContents(models.CveContent{ + Type: models.Nvd, + CveID: cveId, + }) + } else { + v.CveContents[models.Nvd] = []models.CveContent{models.CveContent{ + Type: models.Nvd, + CveID: cveId, + }} + } + v.Confidences.AppendIfMissing(models.NvdVendorProductMatch) + } else { + v = models.VulnInfo{ + CveID: cveId, + CveContents: models.NewCveContents(models.CveContent{ + Type: models.Nvd, + CveID: cveId, + }), + Confidences: models.Confidences{models.NvdVendorProductMatch}, + } + } + + v.AffectedPackages = v.AffectedPackages.Store(models.PackageFixStatus{Name: snap}) + r.ScannedCves[cveId] = v + } + + return nil +} + // isPkgCvesDetactable checks whether CVEs is detactable with gost and oval from the result func isPkgCvesDetactable(r *models.ScanResult) bool { switch r.Family { diff --git a/models/packages.go b/models/packages.go index fa72f019a6..86292bc801 100644 --- a/models/packages.go +++ b/models/packages.go @@ -85,6 +85,7 @@ type Package struct { NewRelease string `json:"newRelease"` Arch string `json:"arch"` Repository string `json:"repository"` + Type string `json:"type,omitempty"` ModularityLabel string `json:"modularitylabel"` Changelog *Changelog `json:"changelog,omitempty"` AffectedProcs []AffectedProcess `json:",omitempty"` @@ -456,3 +457,11 @@ func IsKernelSourcePackage(family, name string) bool { return false } } + +type Snaps map[string]Snap + +type Snap struct { + Name string `json:"name"` + Version string `json:"version"` + Revision string `json:"revision"` +} diff --git a/models/scanresults.go b/models/scanresults.go index 508b992577..f56617a5dc 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -49,6 +49,7 @@ type ScanResult struct { RunningKernel Kernel `json:"runningKernel"` Packages Packages `json:"packages"` SrcPackages SrcPackages `json:",omitempty"` + Snaps Snaps `json:"snaps"` EnabledDnfModules []string `json:"enabledDnfModules,omitempty"` // for dnf modules WordPressPackages WordPressPackages `json:",omitempty"` GitHubManifests DependencyGraphManifests `json:"gitHubManifests,omitempty"` @@ -229,6 +230,11 @@ func (r ScanResult) FormatUpdatablePkgsSummary() string { nUpdatable) } +// FormatUpdatableSnapSummary returns a summary of updatable Snap packages +func (r ScanResult) FormatSnapSummary() string { + return fmt.Sprintf("Snap: %d packages", len(r.Snaps)) +} + // FormatExploitCveSummary returns a summary of exploit cve func (r ScanResult) FormatExploitCveSummary() string { nExploitCve := 0 diff --git a/reporter/util.go b/reporter/util.go index d9cfdaa93b..2526150c72 100644 --- a/reporter/util.go +++ b/reporter/util.go @@ -164,6 +164,9 @@ func formatScanSummary(rs ...models.ScanResult) string { fmt.Sprintf("%s%s", r.Family, r.Release), r.FormatUpdatablePkgsSummary(), } + if 0 < len(r.Snaps) { + cols = append(cols, r.FormatSnapSummary()) + } if 0 < len(r.WordPressPackages) { cols = append(cols, fmt.Sprintf("%d WordPress pkgs", len(r.WordPressPackages))) } diff --git a/scanner/base.go b/scanner/base.go index 8c07ec9717..525b570771 100644 --- a/scanner/base.go +++ b/scanner/base.go @@ -96,6 +96,9 @@ type osPackages struct { // installed source packages (Debian based only) SrcPackages models.SrcPackages + // Snaps + Snaps models.Snaps + // Detected Vulnerabilities Key: CVE-ID VulnInfos models.VulnInfos @@ -546,6 +549,7 @@ func (l *base) convertToModel() models.ScanResult { RunningKernel: l.Kernel, Packages: l.Packages, SrcPackages: l.SrcPackages, + Snaps: l.Snaps, WordPressPackages: l.WordPress, LibraryScanners: l.LibraryScanners, WindowsKB: l.windowsKB, @@ -1496,3 +1500,50 @@ func (l *base) pkgPs(getOwnerPkgs func([]string) ([]string, error)) error { } return nil } + +func (l *base) scanSnaps() error { + r := l.exec("snap version", noSudo) + if !r.isSuccess() { + return nil + } + + l.log.Info("Scanning Snap packages.") + + cmd := util.PrependProxyEnv("snap list") + r = l.exec(cmd, noSudo) + if !r.isSuccess() { + return xerrors.Errorf("Scanning snaps failed.") + } + + snaps, err := l.parseSnapList(r.Stdout) + if err != nil { + return err + } + + l.Snaps = snaps + + // Snaps update automatically, therefore we do not check for updates. + + return nil +} + +func (l *base) parseSnapList(commandOutput string) (models.Snaps, error) { + snaps := models.Snaps{} + scanner := bufio.NewScanner(strings.NewReader(commandOutput)) + + // Skip header line + scanner.Scan() + + for scanner.Scan() { + line := scanner.Text() + ss := strings.Fields(line) + + snaps[ss[0]] = models.Snap{ + Name: ss[0], + Version: ss[1], + Revision: ss[2], + } + } + + return snaps, nil +} diff --git a/scanner/base_test.go b/scanner/base_test.go index 4f7418fe89..732013a6a4 100644 --- a/scanner/base_test.go +++ b/scanner/base_test.go @@ -542,3 +542,43 @@ func Test_findPortScanSuccessOn(t *testing.T) { }) } } + +func TestParseSnapList(t *testing.T) { + var tests = struct { + in string + expected models.Snaps + }{ + `Name Version Rev Tracking Publisher Notes +bare 1.0 5 latest/stable canonical** base +core22 20240731 1564 latest/stable canonical** base +firefox 129.0.2-1 4793 latest/stable/… mozilla** - +firmware-updater 0+git.5007558 127 1/stable/… canonical** - +gnome-42-2204 0+git.510a601 176 latest/stable/… canonical** - +gtk-common-themes 0.1-81-g442e511 1535 latest/stable/… canonical** - +snap-store 0+git.e3dd562 1173 2/stable/… canonical** - +snapd 2.63 21759 latest/stable canonical** snapd +snapd-desktop-integration 0.9 178 latest/stable/… canonical** -`, + models.Snaps{ + "bare": models.Snap{Name: "bare", Version: "1.0", Revision: "5"}, + "core22": models.Snap{Name: "core22", Version: "20240731", Revision: "1564"}, + "firefox": models.Snap{Name: "firefox", Version: "129.0.2-1", Revision: "4793"}, + "firmware-updater": models.Snap{Name: "firmware-updater", Version: "0+git.5007558", Revision: "127"}, + "gnome-42-2204": models.Snap{Name: "gnome-42-2204", Version: "0+git.510a601", Revision: "176"}, + "gtk-common-themes": models.Snap{Name: "gtk-common-themes", Version: "0.1-81-g442e511", Revision: "1535"}, + "snap-store": models.Snap{Name: "snap-store", Version: "0+git.e3dd562", Revision: "1173"}, + "snapd": models.Snap{Name: "snapd", Version: "2.63", Revision: "21759"}, + "snapd-desktop-integration": models.Snap{Name: "snapd-desktop-integration", Version: "0.9", Revision: "178"}, + }, + } + l := base{} + actual, err := l.parseSnapList(tests.in) + if err != nil { + t.Errorf("Error occurred. in: %s, err: %+v", tests.in, err) + return + } + for _, e := range tests.expected { + if !reflect.DeepEqual(e, actual[e.Name]) { + t.Errorf("expected %v, actual %v", e, actual[e.Name]) + } + } +} diff --git a/scanner/scanner.go b/scanner/scanner.go index eb233a5624..cd3cd4dc21 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -62,6 +62,8 @@ type osTypeInterface interface { parseInstalledPackages(string) (models.Packages, models.SrcPackages, error) + scanSnaps() error + runningContainers() ([]config.Container, error) exitedContainers() ([]config.Container, error) allContainers() ([]config.Container, error) @@ -965,6 +967,9 @@ func (s Scanner) getScanResults(scannedAt time.Time) (results models.ScanResults if err = o.postScan(); err != nil { return err } + if err = o.scanSnaps(); err != nil { + return err + } } if o.getServerInfo().Module.IsScanPort() { if err = o.scanPorts(); err != nil { diff --git a/tui/tui.go b/tui/tui.go index 4407f5602c..0bfb526d62 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -737,6 +737,14 @@ func setChangelogLayout(g *gocui.Gui) error { lines = append(lines, fmt.Sprintf(" * PID: %s %s Port: %s", p.PID, p.Name, ports)) } } + + for _, snap := range currentScanResult.Snaps { + if snap.Name == affected.Name { + line := fmt.Sprintf("[snap] %s-%s", snap.Name, snap.Version) + lines = append(lines, line) + break + } + } } sort.Strings(vinfo.CpeURIs) for _, uri := range vinfo.CpeURIs {