/* Vuls - Vulnerability Scanner Copyright (C) 2016 Future Architect, Inc. Japan. This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . */ package scan import ( "fmt" "regexp" "strconv" "strings" "time" "github.com/future-architect/vuls/cache" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ) // inherit OsTypeInterface type debian struct { base } // NewDebian is constructor func newDebian(c config.ServerInfo) *debian { d := &debian{ base: base{ osPackages: osPackages{ Packages: models.Packages{}, VulnInfos: models.VulnInfos{}, }, }, } d.log = util.NewCustomLogger(c) d.setServerInfo(c) return d } // Ubuntu, Debian, Raspbian // https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/debian.rb func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err error) { deb = newDebian(c) if r := exec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() { if r.Error != nil { return false, deb, nil } if r.ExitStatus == 255 { return false, deb, fmt.Errorf("Unable to connect via SSH. Check SSH settings. If you have never SSH to the host to be scanned, SSH to the host before scanning in order to add a HostKey. %s", r) } util.Log.Debugf("Not Debian like Linux. %s", r) return false, deb, nil } // Raspbian // lsb_release in Raspbian Jessie returns 'Distributor ID: Raspbian'. // However, lsb_release in Raspbian Wheezy returns 'Distributor ID: Debian'. if r := exec(c, "cat /etc/issue", noSudo); r.isSuccess() { // e.g. // Raspbian GNU/Linux 7 \n \l result := strings.Fields(r.Stdout) if len(result) > 2 && result[0] == "Raspbian" { distro := strings.ToLower(trim(result[0])) deb.setDistro(distro, trim(result[2])) return true, deb, nil } } if r := exec(c, "lsb_release -ir", noSudo); r.isSuccess() { // e.g. // root@fa3ec524be43:/# lsb_release -ir // Distributor ID: Ubuntu // Release: 14.04 re := regexp.MustCompile(`(?s)^Distributor ID:\s*(.+?)\n*Release:\s*(.+?)$`) result := re.FindStringSubmatch(trim(r.Stdout)) if len(result) == 0 { deb.setDistro("debian/ubuntu", "unknown") util.Log.Warnf( "Unknown Debian/Ubuntu version. lsb_release -ir: %s", r) } else { distro := strings.ToLower(trim(result[1])) deb.setDistro(distro, trim(result[2])) } return true, deb, nil } if r := exec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() { // e.g. // DISTRIB_ID=Ubuntu // DISTRIB_RELEASE=14.04 // DISTRIB_CODENAME=trusty // DISTRIB_DESCRIPTION="Ubuntu 14.04.2 LTS" re := regexp.MustCompile(`(?s)^DISTRIB_ID=(.+?)\n*DISTRIB_RELEASE=(.+?)\n.*$`) result := re.FindStringSubmatch(trim(r.Stdout)) if len(result) == 0 { util.Log.Warnf( "Unknown Debian/Ubuntu. cat /etc/lsb-release: %s", r) deb.setDistro("debian/ubuntu", "unknown") } else { distro := strings.ToLower(trim(result[1])) deb.setDistro(distro, trim(result[2])) } return true, deb, nil } // Debian cmd := "cat /etc/debian_version" if r := exec(c, cmd, noSudo); r.isSuccess() { deb.setDistro("debian", trim(r.Stdout)) return true, deb, nil } util.Log.Debugf("Not Debian like Linux: %s", c.ServerName) return false, deb, nil } func trim(str string) string { return strings.TrimSpace(str) } func (o *debian) checkIfSudoNoPasswd() error { cmd := util.PrependProxyEnv("apt-get update") o.log.Infof("Checking... sudo %s", cmd) r := o.exec(cmd, sudo) if !r.isSuccess() { o.log.Errorf("sudo error on %s", r) return fmt.Errorf("Failed to sudo: %s", r) } o.log.Infof("Sudo... Pass") return nil } func (o *debian) checkDependencies() error { switch o.Distro.Family { case "ubuntu", "raspbian": return nil case "debian": // Debian needs aptitude to get changelogs. // Because unable to get changelogs via apt-get changelog on Debian. if r := o.exec("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { msg := fmt.Sprintf("aptitude is not installed: %s", r) o.log.Errorf(msg) return fmt.Errorf(msg) } o.log.Infof("Dependencies... Pass") return nil default: return fmt.Errorf("Not implemented yet: %s", o.Distro) } } func (o *debian) scanPackages() error { installed, upgradable, err := o.scanInstalledPackages() if err != nil { o.log.Errorf("Failed to scan installed packages") return err } o.setPackages(installed) if config.Conf.PackageListOnly { return nil } unsecure, err := o.scanUnsecurePackages(upgradable) if err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err } o.setVulnInfos(unsecure) return nil } func (o *debian) scanInstalledPackages() (models.Packages, models.Packages, error) { installed := models.Packages{} upgradable := models.Packages{} r := o.exec("dpkg-query -W", noSudo) if !r.isSuccess() { return nil, nil, fmt.Errorf("Failed to SSH: %s", r) } // e.g. // curl 7.19.7-40.el6_6.4 // openldap 2.4.39-8.el6 lines := strings.Split(r.Stdout, "\n") for _, line := range lines { if trimmed := strings.TrimSpace(line); len(trimmed) != 0 { name, version, err := o.parseScannedPackagesLine(trimmed) if err != nil { return nil, nil, fmt.Errorf( "Debian: Failed to parse package line: %s", line) } installed[name] = models.Package{ Name: name, Version: version, } } } upgradableNames, err := o.GetUpgradablePackNames() if err != nil { return nil, nil, err } for _, name := range upgradableNames { for _, pack := range installed { if pack.Name == name { upgradable[name] = pack break } } } // Fill the candidate versions of upgradable packages err = o.fillCandidateVersion(upgradable) if err != nil { return nil, nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err) } installed.MergeNewVersion(upgradable) return installed, upgradable, nil } var packageLinePattern = regexp.MustCompile(`^([^\t']+)\t(.+)$`) func (o *debian) parseScannedPackagesLine(line string) (name, version string, err error) { result := packageLinePattern.FindStringSubmatch(line) if len(result) == 3 { // remove :amd64, i386... name = result[1] if i := strings.IndexRune(name, ':'); i >= 0 { name = name[:i] } version = result[2] return } return "", "", fmt.Errorf("Unknown format: %s", line) } func (o *debian) aptGetUpdate() error { o.log.Infof("apt-get update...") cmd := util.PrependProxyEnv("apt-get update") if r := o.exec(cmd, sudo); !r.isSuccess() { return fmt.Errorf("Failed to SSH: %s", r) } return nil } func (o *debian) scanUnsecurePackages(upgradable models.Packages) (models.VulnInfos, error) { o.aptGetUpdate() // Setup changelog cache current := cache.Meta{ Name: o.getServerInfo().GetServerName(), Distro: o.getServerInfo().Distro, Packs: upgradable, } o.log.Debugf("Ensure changelog cache: %s", current.Name) meta, err := o.ensureChangelogCache(current) if err != nil { return nil, err } // Collect CVE information of upgradable packages vulnInfos, err := o.scanVulnInfos(upgradable, meta) if err != nil { return nil, fmt.Errorf("Failed to scan unsecure packages. err: %s", err) } return vulnInfos, nil } func (o *debian) ensureChangelogCache(current cache.Meta) (*cache.Meta, error) { // Search from cache cached, found, err := cache.DB.GetMeta(current.Name) if err != nil { return nil, fmt.Errorf( "Failed to get meta. Please remove cache.db and then try again. err: %s", err) } if !found { o.log.Debugf("Not found in meta: %s", current.Name) err = cache.DB.EnsureBuckets(current) if err != nil { return nil, fmt.Errorf("Failed to ensure buckets. err: %s", err) } return ¤t, nil } if current.Distro.Family != cached.Distro.Family || current.Distro.Release != cached.Distro.Release { o.log.Debugf("Need to refesh meta: %s", current.Name) err = cache.DB.EnsureBuckets(current) if err != nil { return nil, fmt.Errorf("Failed to ensure buckets. err: %s", err) } return ¤t, nil } o.log.Debugf("Reuse meta: %s", current.Name) if config.Conf.Debug { cache.DB.PrettyPrint(current) } return &cached, nil } func (o *debian) fillCandidateVersion(packages models.Packages) (err error) { names := []string{} for name := range packages { names = append(names, name) } cmd := fmt.Sprintf("LANGUAGE=en_US.UTF-8 apt-cache policy %s", strings.Join(names, " ")) r := o.exec(cmd, noSudo) if !r.isSuccess() { return fmt.Errorf("Failed to SSH: %s", r) } packChangelog := o.splitAptCachePolicy(r.Stdout) for k, v := range packChangelog { ver, err := o.parseAptCachePolicy(v, k) if err != nil { return fmt.Errorf("Failed to parse %s", err) } pack, ok := packages[k] if !ok { return fmt.Errorf("Not found: %s", k) } pack.NewVersion = ver.Candidate packages[k] = pack } return } func (o *debian) GetUpgradablePackNames() (packNames []string, err error) { cmd := util.PrependProxyEnv("LANGUAGE=en_US.UTF-8 apt-get upgrade --dry-run") r := o.exec(cmd, noSudo) if r.isSuccess(0, 1) { return o.parseAptGetUpgrade(r.Stdout) } return packNames, fmt.Errorf( "Failed to %s. status: %d, stdout: %s, stderr: %s", cmd, r.ExitStatus, r.Stdout, r.Stderr) } func (o *debian) parseAptGetUpgrade(stdout string) (upgradableNames []string, err error) { startRe := regexp.MustCompile(`The following packages will be upgraded:`) stopRe := regexp.MustCompile(`^(\d+) upgraded.*`) startLineFound, stopLineFound := false, false lines := strings.Split(stdout, "\n") for _, line := range lines { if !startLineFound { if matche := startRe.MatchString(line); matche { startLineFound = true } continue } result := stopRe.FindStringSubmatch(line) if len(result) == 2 { numUpgradablePacks, err := strconv.Atoi(result[1]) if err != nil { return nil, fmt.Errorf( "Failed to scan upgradable packages number. line: %s", line) } if numUpgradablePacks != len(upgradableNames) { return nil, fmt.Errorf( "Failed to scan upgradable packages, expected: %s, detected: %d", result[1], len(upgradableNames)) } stopLineFound = true o.log.Debugf("Found the stop line. line: %s", line) break } upgradableNames = append(upgradableNames, strings.Fields(line)...) } if !startLineFound { // no upgrades return } if !stopLineFound { // There are upgrades, but not found the stop line. return nil, fmt.Errorf("Failed to scan upgradable packages") } return } // DetectedCveID has CveID, Confidence and DetectionMethod fields // LenientMatching will be true if this vulnerability is not detected by accurate version matching. // see https://github.com/future-architect/vuls/pull/328 type DetectedCveID struct { CveID string Confidence models.Confidence } func (o *debian) scanVulnInfos(upgradablePacks models.Packages, meta *cache.Meta) (models.VulnInfos, error) { type response struct { pack *models.Package DetectedCveIDs []DetectedCveID } resChan := make(chan response, len(upgradablePacks)) errChan := make(chan error, len(upgradablePacks)) reqChan := make(chan models.Package, len(upgradablePacks)) defer close(resChan) defer close(errChan) defer close(reqChan) go func() { for _, pack := range upgradablePacks { reqChan <- pack } }() timeout := time.After(30 * 60 * time.Second) concurrency := 10 tasks := util.GenWorkers(concurrency) for range upgradablePacks { tasks <- func() { select { case pack := <-reqChan: func(p models.Package) { changelog := o.getChangelogCache(meta, p) if 0 < len(changelog) { cveIDs, pack := o.getCveIDsFromChangelog(changelog, p.Name, p.Version) resChan <- response{pack, cveIDs} return } // if the changelog is not in cache or failed to get from local cache, // get the changelog of the package via internet. // After that, store it in the cache. if cveIDs, pack, err := o.scanPackageCveIDs(p); err != nil { errChan <- err } else { resChan <- response{pack, cveIDs} } }(pack) } } } // { DetectedCveID{} : [package] } cvePackages := make(map[DetectedCveID][]string) errs := []error{} for i := 0; i < len(upgradablePacks); i++ { select { case response := <-resChan: o.Packages[response.pack.Name] = *response.pack cves := response.DetectedCveIDs for _, cve := range cves { packNames, ok := cvePackages[cve] if ok { packNames = append(packNames, response.pack.Name) } else { packNames = []string{response.pack.Name} } cvePackages[cve] = packNames } o.log.Infof("(%d/%d) Scanned %s: %s", i+1, len(upgradablePacks), response.pack.Name, cves) case err := <-errChan: errs = append(errs, err) case <-timeout: errs = append(errs, fmt.Errorf("Timeout scanPackageCveIDs")) } } if 0 < len(errs) { return nil, fmt.Errorf("%v", errs) } var cveIDs []DetectedCveID for k := range cvePackages { cveIDs = append(cveIDs, k) } o.log.Debugf("%d Cves are found. cves: %v", len(cveIDs), cveIDs) vinfos := models.VulnInfos{} for cveID, names := range cvePackages { vinfos[cveID.CveID] = models.VulnInfo{ CveID: cveID.CveID, Confidence: cveID.Confidence, PackageNames: names, } } // Update meta package information of changelog cache to the latest one. meta.Packs = upgradablePacks if err := cache.DB.RefreshMeta(*meta); err != nil { return nil, err } return vinfos, nil } func (o *debian) getChangelogCache(meta *cache.Meta, pack models.Package) string { cachedPack, found := meta.Packs[pack.Name] if !found { o.log.Debugf("Not found: %s", pack.Name) return "" } if cachedPack.NewVersion != pack.NewVersion { o.log.Debugf("Expired: %s, cache: %s, new: %s", pack.Name, cachedPack.NewVersion, pack.NewVersion) return "" } changelog, err := cache.DB.GetChangelog(meta.Name, pack.Name) if err != nil { o.log.Warnf("Failed to get changelog. bucket: %s, key:%s, err: %s", meta.Name, pack.Name, err) return "" } if len(changelog) == 0 { o.log.Debugf("Empty string: %s", pack.Name) return "" } o.log.Debugf("Hit: %s, %s, cache: %s, new: %s len: %d, %s...", meta.Name, pack.Name, cachedPack.NewVersion, pack.NewVersion, len(changelog), util.Truncate(changelog, 30)) return changelog } func (o *debian) scanPackageCveIDs(pack models.Package) ([]DetectedCveID, *models.Package, error) { cmd := "" switch o.Distro.Family { case "ubuntu", "raspbian": cmd = fmt.Sprintf(`PAGER=cat apt-get -q=2 changelog %s`, pack.Name) case "debian": cmd = fmt.Sprintf(`PAGER=cat aptitude -q=2 changelog %s`, pack.Name) } cmd = util.PrependProxyEnv(cmd) r := o.exec(cmd, noSudo) if !r.isSuccess() { o.log.Warnf("Failed to SSH: %s", r) // Ignore this Error. return nil, nil, nil } stdout := strings.Replace(r.Stdout, "\r", "", -1) cveIDs, clogFilledPack := o.getCveIDsFromChangelog(stdout, pack.Name, pack.Version) if clogFilledPack.Changelog.Method != models.FailedToGetChangelog { err := cache.DB.PutChangelog( o.getServerInfo().GetServerName(), pack.Name, pack.Changelog.Contents) if err != nil { return nil, nil, fmt.Errorf("Failed to put changelog into cache") } } // No error will be returned. Only logging. return cveIDs, clogFilledPack, nil } // Debian Version Numbers // https://readme.phys.ethz.ch/documentation/debian_version_numbers/ // TODO Changed to parse and compare versions func (o *debian) getCveIDsFromChangelog( changelog, name, ver string) ([]DetectedCveID, *models.Package) { if cveIDs, pack, err := o.parseChangelog( changelog, name, ver, models.ChangelogExactMatch); err == nil { return cveIDs, pack } var verAfterColon string splittedByColon := strings.Split(ver, ":") if 1 < len(splittedByColon) { verAfterColon = splittedByColon[1] if cveIDs, pack, err := o.parseChangelog( changelog, name, verAfterColon, models.ChangelogLenientMatch); err == nil { return cveIDs, pack } } delim := []string{"+", "~", "build"} switch o.Distro.Family { case "ubuntu": delim = append(delim, "ubuntu") case "debian": case "Raspbian": } for _, d := range delim { ss := strings.Split(ver, d) if 1 < len(ss) { if cveIDs, pack, err := o.parseChangelog( changelog, name, ss[0], models.ChangelogLenientMatch); err == nil { return cveIDs, pack } } ss = strings.Split(verAfterColon, d) if 1 < len(ss) { if cveIDs, pack, err := o.parseChangelog( changelog, name, ss[0], models.ChangelogLenientMatch); err == nil { return cveIDs, pack } } } // Only logging the error. o.log.Warnf("Failed to find the version in changelog: %s-%s", name, ver) o.log.Debugf("Changelog of : %s-%s", name, ver, changelog) // If the version is not in changelog, return entire changelog to put into cache pack := o.Packages[name] pack.Changelog = models.Changelog{ Contents: changelog, Method: models.FailedToFindVersionInChangelog, } return []DetectedCveID{}, &pack } var cveRe = regexp.MustCompile(`(CVE-\d{4}-\d{4,})`) // Collect CVE-IDs included in the changelog. // The version which specified in argument(versionOrLater) is excluded. func (o *debian) parseChangelog(changelog, name, ver string, confidence models.Confidence) ([]DetectedCveID, *models.Package, error) { buf, cveIDs := []string{}, []string{} stopRe := regexp.MustCompile(fmt.Sprintf(`\(%s\)`, regexp.QuoteMeta(ver))) stopLineFound := false lines := strings.Split(changelog, "\n") for _, line := range lines { buf = append(buf, line) if match := stopRe.MatchString(line); match { // o.log.Debugf("Found the stop line: %s", line) stopLineFound = true break } else if matches := cveRe.FindAllString(line, -1); 0 < len(matches) { for _, m := range matches { cveIDs = util.AppendIfMissing(cveIDs, m) } } } if !stopLineFound { pack := o.Packages[name] pack.Changelog = models.Changelog{ Contents: "", Method: models.FailedToFindVersionInChangelog, } return nil, &pack, fmt.Errorf( "Failed to scan CVE IDs. The version is not in changelog. name: %s, version: %s", name, ver) } clog := models.Changelog{ Contents: strings.Join(buf, "\n"), Method: string(confidence.DetectionMethod), } pack := o.Packages[name] pack.Changelog = clog cves := []DetectedCveID{} for _, id := range cveIDs { cves = append(cves, DetectedCveID{id, confidence}) } return cves, &pack, nil } func (o *debian) splitAptCachePolicy(stdout string) map[string]string { re := regexp.MustCompile(`(?m:^[^ \t]+:\r?\n)`) ii := re.FindAllStringIndex(stdout, -1) ri := []int{} for i := len(ii) - 1; 0 <= i; i-- { ri = append(ri, ii[i][0]) } splitted := []string{} lasti := len(stdout) for _, i := range ri { splitted = append(splitted, stdout[i:lasti]) lasti = i } packChangelog := map[string]string{} for _, r := range splitted { packName := r[:strings.Index(r, ":")] packChangelog[packName] = r } return packChangelog } type packCandidateVer struct { Name string Installed string Candidate string } // parseAptCachePolicy the stdout of parse pat-get cache policy func (o *debian) parseAptCachePolicy(stdout, name string) (packCandidateVer, error) { ver := packCandidateVer{Name: name} lines := strings.Split(stdout, "\n") for _, line := range lines { fields := strings.Fields(line) if len(fields) != 2 { continue } switch fields[0] { case "Installed:": ver.Installed = fields[1] case "Candidate:": ver.Candidate = fields[1] return ver, nil default: // nop } } return ver, fmt.Errorf("Unknown Format: %s", stdout) }