/* 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" "strings" "time" "github.com/Sirupsen/logrus" "github.com/future-architect/vuls/config" "github.com/future-architect/vuls/models" ) type base struct { ServerInfo config.ServerInfo Distro config.Distro Platform models.Platform osPackages log *logrus.Entry errs []error } func (l *base) exec(cmd string, sudo bool) execResult { return exec(l.ServerInfo, cmd, sudo, l.log) } func (l *base) setServerInfo(c config.ServerInfo) { l.ServerInfo = c } func (l base) getServerInfo() config.ServerInfo { return l.ServerInfo } func (l *base) setDistro(fam, rel string) { d := config.Distro{ Family: fam, Release: rel, } l.Distro = d s := l.getServerInfo() s.Distro = d l.setServerInfo(s) } func (l base) getDistro() config.Distro { return l.Distro } func (l *base) setPlatform(p models.Platform) { l.Platform = p } func (l base) getPlatform() models.Platform { return l.Platform } func (l base) allContainers() (containers []config.Container, err error) { switch l.ServerInfo.Containers.Type { case "", "docker": stdout, err := l.dockerPs("-a --format '{{.ID}} {{.Names}} {{.Image}}'") if err != nil { return containers, err } return l.parseDockerPs(stdout) case "lxd": stdout, err := l.lxdPs("-c n") if err != nil { return containers, err } return l.parseLxdPs(stdout) default: return containers, fmt.Errorf( "Not supported yet: %s", l.ServerInfo.Containers.Type) } } func (l *base) runningContainers() (containers []config.Container, err error) { switch l.ServerInfo.Containers.Type { case "", "docker": stdout, err := l.dockerPs("--format '{{.ID}} {{.Names}} {{.Image}}'") if err != nil { return containers, err } return l.parseDockerPs(stdout) case "lxd": stdout, err := l.lxdPs("volatile.last_state.power=RUNNING -c n") if err != nil { return containers, err } return l.parseLxdPs(stdout) default: return containers, fmt.Errorf( "Not supported yet: %s", l.ServerInfo.Containers.Type) } } func (l *base) exitedContainers() (containers []config.Container, err error) { switch l.ServerInfo.Containers.Type { case "", "docker": stdout, err := l.dockerPs("--filter 'status=exited' --format '{{.ID}} {{.Names}} {{.Image}}'") if err != nil { return containers, err } return l.parseDockerPs(stdout) case "lxd": stdout, err := l.lxdPs("volatile.last_state.power=STOPPED -c n") if err != nil { return containers, err } return l.parseLxdPs(stdout) default: return containers, fmt.Errorf( "Not supported yet: %s", l.ServerInfo.Containers.Type) } } func (l *base) dockerPs(option string) (string, error) { cmd := fmt.Sprintf("docker ps %s", option) r := l.exec(cmd, noSudo) if !r.isSuccess() { return "", fmt.Errorf("Failed to SSH: %s", r) } return r.Stdout, nil } func (l *base) lxdPs(option string) (string, error) { cmd := fmt.Sprintf("lxc list %s", option) r := l.exec(cmd, noSudo) if !r.isSuccess() { return "", fmt.Errorf("failed to SSH: %s", r) } return r.Stdout, nil } func (l *base) parseDockerPs(stdout string) (containers []config.Container, err error) { lines := strings.Split(stdout, "\n") for _, line := range lines { fields := strings.Fields(line) if len(fields) == 0 { break } if len(fields) != 3 { return containers, fmt.Errorf("Unknown format: %s", line) } containers = append(containers, config.Container{ ContainerID: fields[0], Name: fields[1], Image: fields[2], }) } return } func (l *base) parseLxdPs(stdout string) (containers []config.Container, err error) { lines := strings.Split(stdout, "\n") for i, line := range lines[3:] { if i%2 == 1 { continue } fields := strings.Fields(strings.Replace(line, "|", " ", -1)) if len(fields) == 0 { break } if len(fields) != 1 { return containers, fmt.Errorf("Unknown format: %s", line) } containers = append(containers, config.Container{ ContainerID: fields[0], Name: fields[0], }) } return } func (l *base) detectPlatform() { ok, instanceID, err := l.detectRunningOnAws() if err != nil { l.setPlatform(models.Platform{Name: "other"}) return } if ok { l.setPlatform(models.Platform{ Name: "aws", InstanceID: instanceID, }) return } //TODO Azure, GCP... l.setPlatform(models.Platform{Name: "other"}) return } func (l base) detectRunningOnAws() (ok bool, instanceID string, err error) { if r := l.exec("type curl", noSudo); r.isSuccess() { cmd := "curl --max-time 1 --retry 3 --noproxy 169.254.169.254 http://169.254.169.254/latest/meta-data/instance-id" r := l.exec(cmd, noSudo) if r.isSuccess() { id := strings.TrimSpace(r.Stdout) if !l.isAwsInstanceID(id) { return false, "", nil } return true, id, nil } switch r.ExitStatus { case 28, 7: // Not running on AWS // 7 Failed to connect to host. // 28 operation timeout. return false, "", nil } } if r := l.exec("type wget", noSudo); r.isSuccess() { cmd := "wget --tries=3 --timeout=1 --no-proxy -q -O - http://169.254.169.254/latest/meta-data/instance-id" r := l.exec(cmd, noSudo) if r.isSuccess() { id := strings.TrimSpace(r.Stdout) if !l.isAwsInstanceID(id) { return false, "", nil } return true, id, nil } switch r.ExitStatus { case 4, 8: // Not running on AWS // 4 Network failure // 8 Server issued an error response. return false, "", nil } } return false, "", fmt.Errorf( "Failed to curl or wget to AWS instance metadata on %s. container: %s", l.ServerInfo.ServerName, l.ServerInfo.Container.Name) } // http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/resource-ids.html var awsInstanceIDPattern = regexp.MustCompile(`^i-[0-9a-f]+$`) func (l base) isAwsInstanceID(str string) bool { return awsInstanceIDPattern.MatchString(str) } func (l *base) convertToModel() models.ScanResult { //TODO Remove // for _, p := range l.VulnInfos { // sort.Slice(p.Packages, func(i, j int) bool { // return p.Packages[i].Name < p.Packages[j].Name // }) // } // sort.Slice(l.VulnInfos, func(i, j int) bool { // return l.VulnInfos[i].CveID < l.VulnInfos[j].CveID // }) ctype := l.ServerInfo.Containers.Type if l.ServerInfo.Container.ContainerID != "" && ctype == "" { ctype = "docker" } container := models.Container{ ContainerID: l.ServerInfo.Container.ContainerID, Name: l.ServerInfo.Container.Name, Image: l.ServerInfo.Container.Image, Type: ctype, } errs := []string{} for _, e := range l.errs { errs = append(errs, fmt.Sprintf("%s", e)) } //TODO Remove // Avoid null slice being null in JSON // for cveID := range l.VulnInfos { // l.VulnInfos[i].NilToEmpty() // } return models.ScanResult{ ServerName: l.ServerInfo.ServerName, ScannedAt: time.Now(), Family: l.Distro.Family, Release: l.Distro.Release, Container: container, Platform: l.Platform, ScannedCves: l.VulnInfos, Packages: l.Packages, Optional: l.ServerInfo.Optional, Errors: errs, } } func (l *base) setErrs(errs []error) { l.errs = errs } func (l base) getErrs() []error { return l.errs }