/* 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/config" "github.com/future-architect/vuls/cveapi" "github.com/future-architect/vuls/models" "github.com/future-architect/vuls/util" ) // inherit OsTypeInterface type debian struct { linux } // NewDebian is constructor func newDebian(c config.ServerInfo) *debian { d := &debian{} d.log = util.NewCustomLogger(c) return d } // Ubuntu, Debian // https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/debian.rb func detectDebian(c config.ServerInfo) (itsMe bool, deb osTypeInterface) { deb = newDebian(c) // set sudo option flag c.SudoOpt = config.SudoOption{ExecBySudo: true} deb.setServerInfo(c) if r := sshExec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() { Log.Debugf("Not Debian like Linux. Host: %s:%s", c.Host, c.Port) return false, deb } if r := sshExec(c, "lsb_release -ir", noSudo); r.isSuccess() { // e.g. // root@fa3ec524be43:/# lsb_release -ir // Distributor ID: Ubuntu // Release: 14.04 re, _ := regexp.Compile( `(?s)^Distributor ID:\s*(.+?)\n*Release:\s*(.+?)$`) result := re.FindStringSubmatch(trim(r.Stdout)) if len(result) == 0 { deb.setDistributionInfo("debian/ubuntu", "unknown") Log.Warnf( "Unknown Debian/Ubuntu version. lsb_release -ir: %s, Host: %s:%s", r.Stdout, c.Host, c.Port) } else { distro := strings.ToLower(trim(result[1])) deb.setDistributionInfo(distro, trim(result[2])) } return true, deb } if r := sshExec(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.Compile( `(?s)^DISTRIB_ID=(.+?)\n*DISTRIB_RELEASE=(.+?)\n.*$`) result := re.FindStringSubmatch(trim(r.Stdout)) if len(result) == 0 { Log.Warnf( "Unknown Debian/Ubuntu. cat /etc/lsb-release: %s, Host: %s:%s", r.Stdout, c.Host, c.Port) deb.setDistributionInfo("debian/ubuntu", "unknown") } else { distro := strings.ToLower(trim(result[1])) deb.setDistributionInfo(distro, trim(result[2])) } return true, deb } // Debian cmd := "cat /etc/debian_version" if r := sshExec(c, cmd, noSudo); r.isSuccess() { deb.setDistributionInfo("debian", trim(r.Stdout)) return true, deb } Log.Debugf("Not Debian like Linux. Host: %s:%s", c.Host, c.Port) return false, deb } func trim(str string) string { return strings.TrimSpace(str) } func (o *debian) install() error { // apt-get update o.log.Infof("apt-get update...") cmd := util.PrependProxyEnv("apt-get update") if r := o.ssh(cmd, sudo); !r.isSuccess() { msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s", cmd, r.ExitStatus, r.Stdout, r.Stderr) o.log.Errorf(msg) return fmt.Errorf(msg) } if o.Family == "debian" { // install aptitude cmd = util.PrependProxyEnv("apt-get install --force-yes -y aptitude") if r := o.ssh(cmd, sudo); !r.isSuccess() { msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s", cmd, r.ExitStatus, r.Stdout, r.Stderr) o.log.Errorf(msg) return fmt.Errorf(msg) } o.log.Infof("Installed: aptitude") } // install unattended-upgrades if !config.Conf.UseUnattendedUpgrades { return nil } if r := o.ssh("type unattended-upgrade", noSudo); r.isSuccess() { o.log.Infof( "Ignored: unattended-upgrade already installed") return nil } cmd = util.PrependProxyEnv( "apt-get install --force-yes -y unattended-upgrades") if r := o.ssh(cmd, sudo); !r.isSuccess() { msg := fmt.Sprintf("Failed to %s. status: %d, stdout: %s, stderr: %s", cmd, r.ExitStatus, r.Stdout, r.Stderr) o.log.Errorf(msg) return fmt.Errorf(msg) } o.log.Infof("Installed: unattended-upgrades") return nil } func (o *debian) scanPackages() error { var err error var packs []models.PackageInfo if packs, err = o.scanInstalledPackages(); err != nil { o.log.Errorf("Failed to scan installed packages") return err } o.setPackages(packs) var unsecurePacks []CvePacksInfo if unsecurePacks, err = o.scanUnsecurePackages(packs); err != nil { o.log.Errorf("Failed to scan vulnerable packages") return err } o.setUnsecurePackages(unsecurePacks) return nil } func (o *debian) scanInstalledPackages() (packs []models.PackageInfo, err error) { r := o.ssh("dpkg-query -W", noSudo) if !r.isSuccess() { return packs, fmt.Errorf( "Failed to scan packages. status: %d, stdout:%s, stderr: %s", r.ExitStatus, r.Stdout, r.Stderr) } // 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, fmt.Errorf( "Debian: Failed to parse package line: %s", line) } packs = append(packs, models.PackageInfo{ Name: name, Version: version, }) } } return } func (o *debian) parseScannedPackagesLine(line string) (name, version string, err error) { re, _ := regexp.Compile(`^([^\t']+)\t(.+)$`) result := re.FindStringSubmatch(line) if len(result) == 3 { // remove :amd64, i386... name = regexp.MustCompile(":.+").ReplaceAllString(result[1], "") version = result[2] return } return "", "", fmt.Errorf("Unknown format: %s", line) } // unattended-upgrade command need to check security upgrades). func (o *debian) checkRequiredPackagesInstalled() error { if o.Family == "debian" { if r := o.ssh("test -f /usr/bin/aptitude", noSudo); !r.isSuccess() { msg := "aptitude is not installed" o.log.Errorf(msg) return fmt.Errorf(msg) } } if !config.Conf.UseUnattendedUpgrades { return nil } if r := o.ssh("type unattended-upgrade", noSudo); !r.isSuccess() { msg := "unattended-upgrade is not installed" o.log.Errorf(msg) return fmt.Errorf(msg) } return nil } //TODO return whether already expired. func (o *debian) scanUnsecurePackages(packs []models.PackageInfo) ([]CvePacksInfo, error) { // cmd := prependProxyEnv(conf.HTTPProxy, "apt-get update | cat; echo 1") cmd := util.PrependProxyEnv("apt-get update") if r := o.ssh(cmd, sudo); !r.isSuccess() { return nil, fmt.Errorf( "Failed to %s. status: %d, stdout: %s, stderr: %s", cmd, r.ExitStatus, r.Stdout, r.Stderr, ) } var upgradablePackNames []string var err error if config.Conf.UseUnattendedUpgrades { upgradablePackNames, err = o.GetUnsecurePackNamesUsingUnattendedUpgrades() if err != nil { return []CvePacksInfo{}, err } } else { upgradablePackNames, err = o.GetUpgradablePackNames() if err != nil { return []CvePacksInfo{}, err } } // Convert package name to PackageInfo struct var unsecurePacks []models.PackageInfo for _, name := range upgradablePackNames { for _, pack := range packs { if pack.Name == name { unsecurePacks = append(unsecurePacks, pack) break } } } unsecurePacks, err = o.fillCandidateVersion(unsecurePacks) if err != nil { return nil, fmt.Errorf("Failed to fill candidate versions. err: %s", err) } // Collect CVE information of upgradable packages cvePacksInfos, err := o.scanPackageCveInfos(unsecurePacks) if err != nil { return nil, fmt.Errorf("Failed to scan unsecure packages. err: %s", err) } return cvePacksInfos, nil } func (o *debian) fillCandidateVersion(packs []models.PackageInfo) ([]models.PackageInfo, error) { reqChan := make(chan models.PackageInfo, len(packs)) resChan := make(chan models.PackageInfo, len(packs)) errChan := make(chan error, len(packs)) defer close(resChan) defer close(errChan) defer close(reqChan) go func() { for _, pack := range packs { reqChan <- pack } }() timeout := time.After(5 * 60 * time.Second) concurrency := 5 tasks := util.GenWorkers(concurrency) for range packs { tasks <- func() { select { case pack := <-reqChan: func(p models.PackageInfo) { cmd := fmt.Sprintf("apt-cache policy %s", p.Name) r := o.ssh(cmd, sudo) if !r.isSuccess() { errChan <- fmt.Errorf( "Failed to %s. status: %d, stdout: %s, stderr: %s", cmd, r.ExitStatus, r.Stdout, r.Stderr) return } ver, err := o.parseAptCachePolicy(r.Stdout, p.Name) if err != nil { errChan <- fmt.Errorf("Failed to parse %s", err) } p.NewVersion = ver.Candidate resChan <- p }(pack) } } } errs := []error{} result := []models.PackageInfo{} for i := 0; i < len(packs); i++ { select { case pack := <-resChan: result = append(result, pack) o.log.Infof("(%d/%d) Upgradable: %s-%s -> %s", i+1, len(packs), pack.Name, pack.Version, pack.NewVersion) case err := <-errChan: errs = append(errs, err) case <-timeout: return nil, fmt.Errorf("Timeout fillCandidateVersion") } } if 0 < len(errs) { return nil, fmt.Errorf("%v", errs) } return result, nil } func (o *debian) GetUnsecurePackNamesUsingUnattendedUpgrades() (packNames []string, err error) { cmd := util.PrependProxyEnv("unattended-upgrades --dry-run -d 2>&1 ") release, err := strconv.ParseFloat(o.Release, 64) if err != nil { return packNames, fmt.Errorf( "OS Release Version is invalid, %s, %s", o.Family, o.Release) } switch { case release < 12: return packNames, fmt.Errorf( "Support expired. %s, %s", o.Family, o.Release) case 12 < release && release < 14: cmd += `| grep 'pkgs that look like they should be upgraded:' | sed -e 's/pkgs that look like they should be upgraded://g'` case 14 < release: cmd += `| grep 'Packages that will be upgraded:' | sed -e 's/Packages that will be upgraded://g'` default: return packNames, fmt.Errorf( "Not supported yet. %s, %s", o.Family, o.Release) } r := o.ssh(cmd, sudo) if r.isSuccess(0, 1) { packNames = strings.Split(strings.TrimSpace(r.Stdout), " ") return packNames, nil } return packNames, fmt.Errorf( "Failed to %s. status: %d, stdout: %s, stderr: %s", cmd, r.ExitStatus, r.Stdout, r.Stderr) } func (o *debian) GetUpgradablePackNames() (packNames []string, err error) { cmd := util.PrependProxyEnv("apt-get upgrade --dry-run") r := o.ssh(cmd, sudo) 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.Compile(`The following packages will be upgraded:`) stopRe, _ := regexp.Compile(`^(\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 } func (o *debian) scanPackageCveInfos(unsecurePacks []models.PackageInfo) (cvePacksList CvePacksList, err error) { // { CVE ID: [packageInfo] } cvePackages := make(map[string][]models.PackageInfo) type strarray []string resChan := make(chan struct { models.PackageInfo strarray }, len(unsecurePacks)) errChan := make(chan error, len(unsecurePacks)) reqChan := make(chan models.PackageInfo, len(unsecurePacks)) defer close(resChan) defer close(errChan) defer close(reqChan) go func() { for _, pack := range unsecurePacks { reqChan <- pack } }() timeout := time.After(30 * 60 * time.Second) concurrency := 10 tasks := util.GenWorkers(concurrency) for range unsecurePacks { tasks <- func() { select { case pack := <-reqChan: func(p models.PackageInfo) { if cveIDs, err := o.scanPackageCveIDs(p); err != nil { errChan <- err } else { resChan <- struct { models.PackageInfo strarray }{p, cveIDs} } }(pack) } } } errs := []error{} for i := 0; i < len(unsecurePacks); i++ { select { case pair := <-resChan: pack := pair.PackageInfo cveIDs := pair.strarray for _, cveID := range cveIDs { cvePackages[cveID] = appendPackIfMissing(cvePackages[cveID], pack) } o.log.Infof("(%d/%d) Scanned %s-%s : %s", i+1, len(unsecurePacks), pair.Name, pair.PackageInfo.Version, cveIDs) case err := <-errChan: errs = append(errs, err) case <-timeout: return nil, fmt.Errorf("Timeout scanPackageCveIDs") } } if 0 < len(errs) { return nil, fmt.Errorf("%v", errs) } var cveIDs []string for k := range cvePackages { cveIDs = append(cveIDs, k) } o.log.Debugf("%d Cves are found. cves: %v", len(cveIDs), cveIDs) o.log.Info("Fetching CVE details...") cveDetails, err := cveapi.CveClient.FetchCveDetails(cveIDs) if err != nil { return nil, err } o.log.Info("Done") for _, detail := range cveDetails { cvePacksList = append(cvePacksList, CvePacksInfo{ CveID: detail.CveID, CveDetail: detail, Packs: cvePackages[detail.CveID], // CvssScore: cinfo.CvssScore(conf.Lang), }) } return } func (o *debian) scanPackageCveIDs(pack models.PackageInfo) ([]string, error) { cmd := "" switch o.Family { case "ubuntu": cmd = fmt.Sprintf(`apt-get changelog %s | grep '\(urgency\|CVE\)'`, pack.Name) case "debian": cmd = fmt.Sprintf(`aptitude changelog %s | grep '\(urgency\|CVE\)'`, pack.Name) } cmd = util.PrependProxyEnv(cmd) r := o.ssh(cmd, noSudo) if !r.isSuccess() { o.log.Warnf( "Failed to %s. status: %d, stdout: %s, stderr: %s", cmd, r.ExitStatus, r.Stdout, r.Stderr) // Ignore this Error. return nil, nil } return o.getCveIDParsingChangelog(r.Stdout, pack.Name, pack.Version) } func (o *debian) getCveIDParsingChangelog(changelog string, packName string, versionOrLater string) (cveIDs []string, err error) { cveIDs, err = o.parseChangelog(changelog, packName, versionOrLater) if err == nil { return } ver := strings.Split(versionOrLater, "ubuntu")[0] cveIDs, err = o.parseChangelog(changelog, packName, ver) if err == nil { return } splittedByColon := strings.Split(versionOrLater, ":") if 1 < len(splittedByColon) { ver = splittedByColon[1] } cveIDs, err = o.parseChangelog(changelog, packName, ver) if err == nil { return } //TODO report as unable to parse changelog. o.log.Error(err) return []string{}, nil } // Collect CVE-IDs included in the changelog. // The version which specified in argument(versionOrLater) is excluded. func (o *debian) parseChangelog(changelog string, packName string, versionOrLater string) (cveIDs []string, err error) { cveRe, _ := regexp.Compile(`(CVE-\d{4}-\d{4})`) stopRe, _ := regexp.Compile(fmt.Sprintf(`\(%s\)`, regexp.QuoteMeta(versionOrLater))) stopLineFound := false lines := strings.Split(changelog, "\n") for _, line := range lines { if matche := stopRe.MatchString(line); matche { o.log.Debugf("Found the stop line. line: %s", line) stopLineFound = true break } else if matches := cveRe.FindAllString(line, -1); len(matches) > 0 { for _, m := range matches { cveIDs = util.AppendIfMissing(cveIDs, m) } } } if !stopLineFound { return []string{}, fmt.Errorf( "Failed to scan CVE IDs. The version is not in changelog. name: %s, version: %s", packName, versionOrLater, ) } return } 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) } func appendPackIfMissing(slice []models.PackageInfo, s models.PackageInfo) []models.PackageInfo { for _, ele := range slice { if ele.Name == s.Name && ele.Version == s.Version && ele.Release == s.Release { return slice } } return append(slice, s) }