Files
vuls/scan/serverapi.go
Tomoya Amachi abcea1a14d add Library Scan (with image scan) (#829)
* add static container image scan

* server has many staticContainers

* use go module

* for staticContainer

* fix typo

* fix setErrs error

* change name : StaticContainer -> Image

* add scan -images-only flag

* fix makefile

* fix makefile for go module

* use rpmcmd instead of rpm

* add scrutinizer.yml

* change scrutinizer.yml

* fix scrutinizer.yml

* fix scrutinizer.yml

* fix scrutinizer.yml

* fix scrutinizer.yml

* delete scrutinizer

* add report test

* add sourcePackages and Arch

* fix for sider

* fix staticContainer -> image

* init scan library

* add library scan for servers

* fix tui bug

* fix lint error

* divide WpPackageFixStats and LibraryPackageFixedIns

* fix error

* Delete libManager_test.go

* stop use alpine os if err occurred in container

* merge upstream/master

* Delete libManager.go

* update goval-dictionary

* fix go.mod

* update Readme

* add feature : auto detect lockfiles
2019-06-12 18:50:07 +09:00

793 lines
20 KiB
Go

/* Vuls - Vulnerability Scanner
Copyright (C) 2016 Future Corporation , 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 <http://www.gnu.org/licenses/>.
*/
package scan
import (
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"github.com/future-architect/vuls/cache"
"github.com/future-architect/vuls/config"
"github.com/future-architect/vuls/models"
"github.com/future-architect/vuls/report"
"github.com/future-architect/vuls/util"
"golang.org/x/xerrors"
)
const (
scannedViaRemote = "remote"
scannedViaLocal = "local"
scannedViaPseudo = "pseudo"
)
var (
errOSFamilyHeader = xerrors.New("X-Vuls-OS-Family header is required")
errOSReleaseHeader = xerrors.New("X-Vuls-OS-Release header is required")
errKernelVersionHeader = xerrors.New("X-Vuls-Kernel-Version header is required")
errServerNameHeader = xerrors.New("X-Vuls-Server-Name header is required")
)
var servers, errServers []osTypeInterface
// Base Interface of redhat, debian, freebsd
type osTypeInterface interface {
setServerInfo(config.ServerInfo)
getServerInfo() config.ServerInfo
setDistro(string, string)
getDistro() config.Distro
detectPlatform()
getPlatform() models.Platform
checkScanMode() error
checkDeps() error
checkIfSudoNoPasswd() error
preCure() error
postScan() error
scanWordPress() error
scanLibraries() error
scanPackages() error
convertToModel() models.ScanResult
parseInstalledPackages(string) (models.Packages, models.SrcPackages, error)
runningContainers() ([]config.Container, error)
exitedContainers() ([]config.Container, error)
allContainers() ([]config.Container, error)
getErrs() []error
setErrs([]error)
}
// osPackages is included by base struct
type osPackages struct {
// installed packages
Packages models.Packages
// installed source packages (Debian based only)
SrcPackages models.SrcPackages
// unsecure packages
VulnInfos models.VulnInfos
// kernel information
Kernel models.Kernel
}
// Retry as it may stall on the first SSH connection
// https://github.com/future-architect/vuls/pull/753
func detectDebianWithRetry(c config.ServerInfo) (itsMe bool, deb osTypeInterface, err error) {
type Response struct {
itsMe bool
deb osTypeInterface
err error
}
resChan := make(chan Response, 1)
go func(c config.ServerInfo) {
itsMe, osType, fatalErr := detectDebian(c)
resChan <- Response{itsMe, osType, fatalErr}
}(c)
timeout := time.After(time.Duration(3) * time.Second)
select {
case res := <-resChan:
return res.itsMe, res.deb, res.err
case <-timeout:
time.Sleep(100 * time.Millisecond)
return detectDebian(c)
}
}
func detectOS(c config.ServerInfo) (osType osTypeInterface) {
var itsMe bool
var fatalErr error
if itsMe, osType, _ = detectPseudo(c); itsMe {
util.Log.Debugf("Pseudo")
return
}
itsMe, osType, fatalErr = detectContainerImage(c)
if fatalErr != nil {
osType.setErrs(
[]error{xerrors.Errorf("Failed to detect OS: %w", fatalErr)},
)
return
}
if itsMe {
util.Log.Debugf("Container")
return
}
itsMe, osType, fatalErr = detectDebianWithRetry(c)
if fatalErr != nil {
osType.setErrs([]error{
xerrors.Errorf("Failed to detect OS: %w", fatalErr)})
return
}
if itsMe {
util.Log.Debugf("Debian like Linux. Host: %s:%s", c.Host, c.Port)
return
}
if itsMe, osType = detectRedhat(c); itsMe {
util.Log.Debugf("Redhat like Linux. Host: %s:%s", c.Host, c.Port)
return
}
if itsMe, osType = detectSUSE(c); itsMe {
util.Log.Debugf("SUSE Linux. Host: %s:%s", c.Host, c.Port)
return
}
if itsMe, osType = detectFreebsd(c); itsMe {
util.Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port)
return
}
if itsMe, osType = detectAlpine(c); itsMe {
util.Log.Debugf("Alpine. Host: %s:%s", c.Host, c.Port)
return
}
//TODO darwin https://github.com/mizzy/specinfra/blob/master/lib/specinfra/helper/detect_os/darwin.rb
osType.setErrs([]error{xerrors.New("Unknown OS Type")})
return
}
// PrintSSHableServerNames print SSH-able servernames
func PrintSSHableServerNames() bool {
if len(servers) == 0 {
util.Log.Error("No scannable servers")
return false
}
util.Log.Info("Scannable servers are below...")
for _, s := range servers {
if s.getServerInfo().IsContainer() {
fmt.Printf("%s@%s ",
s.getServerInfo().Container.Name,
s.getServerInfo().ServerName,
)
} else {
fmt.Printf("%s ", s.getServerInfo().ServerName)
}
}
fmt.Printf("\n")
return true
}
func needScans() (needBaseServer, scanContainer, scanImage bool) {
scanContainer = true
scanImage = true
if !config.Conf.ContainersOnly && !config.Conf.ImagesOnly {
needBaseServer = true
}
if config.Conf.ImagesOnly && !config.Conf.ContainersOnly {
scanContainer = false
}
if config.Conf.ContainersOnly && !config.Conf.ImagesOnly {
scanImage = false
}
return needBaseServer, scanContainer, scanImage
}
// InitServers detect the kind of OS distribution of target servers
func InitServers(timeoutSec int) error {
needBaseServers, scanContainer, scanImage := needScans()
// use global servers, errServers when scan containers and images
servers, errServers = detectServerOSes(timeoutSec)
if len(servers) == 0 {
return xerrors.New("No scannable base servers")
}
// scan additional servers
var actives, inactives []osTypeInterface
if scanImage {
oks, errs := detectImageOSes(timeoutSec)
actives = append(actives, oks...)
inactives = append(inactives, errs...)
}
if scanContainer {
oks, errs := detectContainerOSes(timeoutSec)
actives = append(actives, oks...)
inactives = append(inactives, errs...)
}
if needBaseServers {
servers = append(servers, actives...)
errServers = append(errServers, inactives...)
} else {
servers = actives
errServers = inactives
}
if len(servers) == 0 {
return xerrors.New("No scannable servers")
}
return nil
}
func detectServerOSes(timeoutSec int) (servers, errServers []osTypeInterface) {
util.Log.Info("Detecting OS of servers... ")
osTypeChan := make(chan osTypeInterface, len(config.Conf.Servers))
defer close(osTypeChan)
for _, s := range config.Conf.Servers {
go func(s config.ServerInfo) {
defer func() {
if p := recover(); p != nil {
util.Log.Debugf("Panic: %s on %s", p, s.ServerName)
}
}()
osTypeChan <- detectOS(s)
}(s)
}
timeout := time.After(time.Duration(timeoutSec) * time.Second)
for i := 0; i < len(config.Conf.Servers); i++ {
select {
case res := <-osTypeChan:
if 0 < len(res.getErrs()) {
errServers = append(errServers, res)
util.Log.Errorf("(%d/%d) Failed: %s, err: %+v",
i+1, len(config.Conf.Servers),
res.getServerInfo().ServerName,
res.getErrs())
} else {
servers = append(servers, res)
util.Log.Infof("(%d/%d) Detected: %s: %s",
i+1, len(config.Conf.Servers),
res.getServerInfo().ServerName,
res.getDistro())
}
case <-timeout:
msg := "Timed out while detecting servers"
util.Log.Error(msg)
for servername, sInfo := range config.Conf.Servers {
found := false
for _, o := range append(servers, errServers...) {
if servername == o.getServerInfo().ServerName {
found = true
break
}
}
if !found {
u := &unknown{}
u.setServerInfo(sInfo)
u.setErrs([]error{
xerrors.New("Timed out"),
})
errServers = append(errServers, u)
util.Log.Errorf("(%d/%d) Timed out: %s",
i+1, len(config.Conf.Servers),
servername)
i++
}
}
}
}
return
}
func detectContainerOSes(timeoutSec int) (actives, inactives []osTypeInterface) {
util.Log.Info("Detecting OS of containers... ")
osTypesChan := make(chan []osTypeInterface, len(servers))
defer close(osTypesChan)
for _, s := range servers {
go func(s osTypeInterface) {
defer func() {
if p := recover(); p != nil {
util.Log.Debugf("Panic: %s on %s",
p, s.getServerInfo().GetServerName())
}
}()
osTypesChan <- detectContainerOSesOnServer(s)
}(s)
}
timeout := time.After(time.Duration(timeoutSec) * time.Second)
for i := 0; i < len(servers); i++ {
select {
case res := <-osTypesChan:
for _, osi := range res {
sinfo := osi.getServerInfo()
if 0 < len(osi.getErrs()) {
inactives = append(inactives, osi)
util.Log.Errorf("Failed: %s err: %+v", sinfo.ServerName, osi.getErrs())
continue
}
actives = append(actives, osi)
util.Log.Infof("Detected: %s@%s: %s",
sinfo.Container.Name, sinfo.ServerName, osi.getDistro())
}
case <-timeout:
msg := "Timed out while detecting containers"
util.Log.Error(msg)
for servername, sInfo := range config.Conf.Servers {
found := false
for _, o := range append(actives, inactives...) {
if servername == o.getServerInfo().ServerName {
found = true
break
}
}
if !found {
u := &unknown{}
u.setServerInfo(sInfo)
u.setErrs([]error{
xerrors.New("Timed out"),
})
inactives = append(inactives)
util.Log.Errorf("Timed out: %s", servername)
}
}
}
}
return
}
func detectContainerOSesOnServer(containerHost osTypeInterface) (oses []osTypeInterface) {
containerHostInfo := containerHost.getServerInfo()
if len(containerHostInfo.ContainersIncluded) == 0 {
return
}
running, err := containerHost.runningContainers()
if err != nil {
containerHost.setErrs([]error{xerrors.Errorf(
"Failed to get running containers on %s. err: %w",
containerHost.getServerInfo().ServerName, err)})
return append(oses, containerHost)
}
if containerHostInfo.ContainersIncluded[0] == "${running}" {
for _, containerInfo := range running {
found := false
for _, ex := range containerHost.getServerInfo().ContainersExcluded {
if containerInfo.Name == ex || containerInfo.ContainerID == ex {
found = true
}
}
if found {
continue
}
copied := containerHostInfo
copied.SetContainer(config.Container{
ContainerID: containerInfo.ContainerID,
Name: containerInfo.Name,
Image: containerInfo.Image,
})
os := detectOS(copied)
oses = append(oses, os)
}
return oses
}
exitedContainers, err := containerHost.exitedContainers()
if err != nil {
containerHost.setErrs([]error{xerrors.Errorf(
"Failed to get exited containers on %s. err: %w",
containerHost.getServerInfo().ServerName, err)})
return append(oses, containerHost)
}
var exited, unknown []string
for _, container := range containerHostInfo.ContainersIncluded {
found := false
for _, c := range running {
if c.ContainerID == container || c.Name == container {
copied := containerHostInfo
copied.SetContainer(c)
os := detectOS(copied)
oses = append(oses, os)
found = true
break
}
}
if !found {
foundInExitedContainers := false
for _, c := range exitedContainers {
if c.ContainerID == container || c.Name == container {
exited = append(exited, container)
foundInExitedContainers = true
break
}
}
if !foundInExitedContainers {
unknown = append(unknown, container)
}
}
}
if 0 < len(exited) || 0 < len(unknown) {
containerHost.setErrs([]error{xerrors.Errorf(
"Some containers on %s are exited or unknown. exited: %s, unknown: %s",
containerHost.getServerInfo().ServerName, exited, unknown)})
return append(oses, containerHost)
}
return oses
}
func detectImageOSes(timeoutSec int) (actives, inactives []osTypeInterface) {
util.Log.Info("Detecting OS of static containers... ")
osTypesChan := make(chan []osTypeInterface, len(servers))
defer close(osTypesChan)
for _, s := range servers {
go func(s osTypeInterface) {
defer func() {
if p := recover(); p != nil {
util.Log.Debugf("Panic: %s on %s",
p, s.getServerInfo().GetServerName())
}
}()
osTypesChan <- detectImageOSesOnServer(s)
}(s)
}
timeout := time.After(time.Duration(timeoutSec) * time.Second)
for i := 0; i < len(servers); i++ {
select {
case res := <-osTypesChan:
for _, osi := range res {
sinfo := osi.getServerInfo()
if 0 < len(osi.getErrs()) {
inactives = append(inactives, osi)
util.Log.Errorf("Failed: %s err: %+v", sinfo.ServerName, osi.getErrs())
continue
}
actives = append(actives, osi)
util.Log.Infof("Detected: %s@%s: %s",
sinfo.Image.Name, sinfo.ServerName, osi.getDistro())
}
case <-timeout:
msg := "Timed out while detecting static containers"
util.Log.Error(msg)
for servername, sInfo := range config.Conf.Servers {
found := false
for _, o := range append(actives, inactives...) {
if servername == o.getServerInfo().ServerName {
found = true
break
}
}
if !found {
u := &unknown{}
u.setServerInfo(sInfo)
u.setErrs([]error{
xerrors.New("Timed out"),
})
inactives = append(inactives)
util.Log.Errorf("Timed out: %s", servername)
}
}
}
}
return
}
func detectImageOSesOnServer(containerHost osTypeInterface) (oses []osTypeInterface) {
containerHostInfo := containerHost.getServerInfo()
if len(containerHostInfo.Images) == 0 {
return
}
for idx, containerConf := range containerHostInfo.Images {
copied := containerHostInfo
// change servername for original
copied.ServerName = fmt.Sprintf("%s:%s@%s", idx, containerConf.Tag, containerHostInfo.ServerName)
copied.Image = containerConf
copied.Type = ""
os := detectOS(copied)
oses = append(oses, os)
}
return oses
}
// CheckScanModes checks scan mode
func CheckScanModes() error {
for _, s := range servers {
if err := s.checkScanMode(); err != nil {
return xerrors.Errorf("servers.%s.scanMode err: %w",
s.getServerInfo().GetServerName(), err)
}
}
return nil
}
// CheckDependencies checks dependencies are installed on target servers.
func CheckDependencies(timeoutSec int) {
parallelExec(func(o osTypeInterface) error {
return o.checkDeps()
}, timeoutSec)
return
}
// CheckIfSudoNoPasswd checks whether vuls can sudo with nopassword via SSH
func CheckIfSudoNoPasswd(timeoutSec int) {
parallelExec(func(o osTypeInterface) error {
return o.checkIfSudoNoPasswd()
}, timeoutSec)
return
}
// DetectPlatforms detects the platform of each servers.
func DetectPlatforms(timeoutSec int) {
detectPlatforms(timeoutSec)
for i, s := range servers {
if s.getServerInfo().IsContainer() {
util.Log.Infof("(%d/%d) %s on %s is running on %s",
i+1, len(servers),
s.getServerInfo().Container.Name,
s.getServerInfo().ServerName,
s.getPlatform().Name,
)
} else {
util.Log.Infof("(%d/%d) %s is running on %s",
i+1, len(servers),
s.getServerInfo().ServerName,
s.getPlatform().Name,
)
}
}
return
}
func detectPlatforms(timeoutSec int) {
parallelExec(func(o osTypeInterface) error {
o.detectPlatform()
// Logging only if platform can not be specified
return nil
}, timeoutSec)
return
}
// Scan scan
func Scan(timeoutSec int) error {
if len(servers) == 0 {
return xerrors.New("No server defined. Check the configuration")
}
if err := setupChangelogCache(); err != nil {
return err
}
defer func() {
if cache.DB != nil {
cache.DB.Close()
}
}()
util.Log.Info("Scanning vulnerable OS packages...")
scannedAt := time.Now()
dir, err := EnsureResultDir(scannedAt)
if err != nil {
return err
}
return scanVulns(dir, scannedAt, timeoutSec)
}
// ViaHTTP scans servers by HTTP header and body
func ViaHTTP(header http.Header, body string) (models.ScanResult, error) {
family := header.Get("X-Vuls-OS-Family")
if family == "" {
return models.ScanResult{}, errOSFamilyHeader
}
release := header.Get("X-Vuls-OS-Release")
if release == "" {
return models.ScanResult{}, errOSReleaseHeader
}
kernelRelease := header.Get("X-Vuls-Kernel-Release")
if kernelRelease == "" {
util.Log.Warn("If X-Vuls-Kernel-Release is not specified, there is a possibility of false detection")
}
kernelVersion := header.Get("X-Vuls-Kernel-Version")
if family == config.Debian && kernelVersion == "" {
return models.ScanResult{}, errKernelVersionHeader
}
serverName := header.Get("X-Vuls-Server-Name")
if config.Conf.ToLocalFile && serverName == "" {
return models.ScanResult{}, errServerNameHeader
}
distro := config.Distro{
Family: family,
Release: release,
}
kernel := models.Kernel{
Release: kernelRelease,
Version: kernelVersion,
}
base := base{
Distro: distro,
osPackages: osPackages{
Kernel: kernel,
},
log: util.Log,
}
var osType osTypeInterface
switch family {
case config.Debian, config.Ubuntu:
osType = &debian{base: base}
case config.RedHat:
osType = &rhel{
redhatBase: redhatBase{base: base},
}
case config.CentOS:
osType = &centos{
redhatBase: redhatBase{base: base},
}
default:
return models.ScanResult{}, xerrors.Errorf("Server mode for %s is not implemented yet", family)
}
installedPackages, srcPackages, err := osType.parseInstalledPackages(body)
if err != nil {
return models.ScanResult{}, err
}
result := models.ScanResult{
ServerName: serverName,
Family: family,
Release: release,
RunningKernel: models.Kernel{
Release: kernelRelease,
Version: kernelVersion,
},
Packages: installedPackages,
SrcPackages: srcPackages,
ScannedCves: models.VulnInfos{},
}
return result, nil
}
func setupChangelogCache() error {
needToSetupCache := false
for _, s := range servers {
switch s.getDistro().Family {
case config.Raspbian:
needToSetupCache = true
break
case config.Ubuntu, config.Debian:
//TODO changelopg cache for RedHat, Oracle, Amazon, CentOS is not implemented yet.
if s.getServerInfo().Mode.IsDeep() {
needToSetupCache = true
}
break
}
}
if needToSetupCache {
if err := cache.SetupBolt(config.Conf.CacheDBPath, util.Log); err != nil {
return err
}
}
return nil
}
func scanVulns(jsonDir string, scannedAt time.Time, timeoutSec int) error {
var results models.ScanResults
parallelExec(func(o osTypeInterface) (err error) {
if err = o.preCure(); err != nil {
return err
}
if err = o.scanPackages(); err != nil {
return err
}
if err = o.scanWordPress(); err != nil {
return xerrors.Errorf("Failed to scan WordPress: %w", err)
}
if err = o.scanLibraries(); err != nil {
return xerrors.Errorf("Failed to scan Library: %w", err)
}
return o.postScan()
}, timeoutSec)
hostname, _ := os.Hostname()
ipv4s, ipv6s, err := util.IP()
if err != nil {
util.Log.Errorf("Failed to fetch scannedIPs. err: %+v", err)
}
for _, s := range append(servers, errServers...) {
r := s.convertToModel()
r.ScannedAt = scannedAt
r.ScannedVersion = config.Version
r.ScannedRevision = config.Revision
r.ScannedBy = hostname
r.ScannedIPv4Addrs = ipv4s
r.ScannedIPv6Addrs = ipv6s
r.Config.Scan = config.Conf
results = append(results, r)
}
config.Conf.FormatJSON = true
ws := []report.ResultWriter{
report.LocalFileWriter{CurrentDir: jsonDir},
}
for _, w := range ws {
if err := w.Write(results...); err != nil {
return xerrors.Errorf("Failed to write summary report: %s", err)
}
}
report.StdoutWriter{}.WriteScanSummary(results...)
return nil
}
// EnsureResultDir ensures the directory for scan results
func EnsureResultDir(scannedAt time.Time) (currentDir string, err error) {
jsonDirName := scannedAt.Format(time.RFC3339)
resultsDir := config.Conf.ResultsDir
if len(resultsDir) == 0 {
wd, _ := os.Getwd()
resultsDir = filepath.Join(wd, "results")
}
jsonDir := filepath.Join(resultsDir, jsonDirName)
if err := os.MkdirAll(jsonDir, 0700); err != nil {
return "", xerrors.Errorf("Failed to create dir: %w", err)
}
symlinkPath := filepath.Join(resultsDir, "current")
if _, err := os.Lstat(symlinkPath); err == nil {
if err := os.Remove(symlinkPath); err != nil {
return "", xerrors.Errorf(
"Failed to remove symlink. path: %s, err: %w", symlinkPath, err)
}
}
if err := os.Symlink(jsonDir, symlinkPath); err != nil {
return "", xerrors.Errorf(
"Failed to create symlink: path: %s, err: %w", symlinkPath, err)
}
return jsonDir, nil
}