* chore(deps): mod update * fix(scanner): do not attach tty because there is no need to enter ssh password * feat(windows): support Windows
1007 lines
29 KiB
Go
1007 lines
29 KiB
Go
package scanner
|
|
|
|
import (
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"os"
|
|
ex "os/exec"
|
|
"runtime"
|
|
"strings"
|
|
"time"
|
|
|
|
debver "github.com/knqyf263/go-deb-version"
|
|
"golang.org/x/exp/maps"
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/future-architect/vuls/cache"
|
|
"github.com/future-architect/vuls/config"
|
|
"github.com/future-architect/vuls/constant"
|
|
"github.com/future-architect/vuls/logging"
|
|
"github.com/future-architect/vuls/models"
|
|
"github.com/future-architect/vuls/util"
|
|
)
|
|
|
|
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")
|
|
errServerNameHeader = xerrors.New("X-Vuls-Server-Name header is required")
|
|
)
|
|
|
|
var servers, errServers []osTypeInterface
|
|
|
|
// Base Interface
|
|
type osTypeInterface interface {
|
|
setServerInfo(config.ServerInfo)
|
|
getServerInfo() config.ServerInfo
|
|
setDistro(string, string)
|
|
getDistro() config.Distro
|
|
detectPlatform()
|
|
detectIPS()
|
|
getPlatform() models.Platform
|
|
|
|
checkScanMode() error
|
|
checkDeps() error
|
|
checkIfSudoNoPasswd() error
|
|
|
|
preCure() error
|
|
postScan() error
|
|
scanWordPress() error
|
|
scanLibraries() error
|
|
scanPorts() 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)
|
|
|
|
setLogger(logging.Logger)
|
|
getErrs() []error
|
|
setErrs([]error)
|
|
}
|
|
|
|
// Scanner has functions for scan
|
|
type Scanner struct {
|
|
ResultsDir string
|
|
TimeoutSec int
|
|
ScanTimeoutSec int
|
|
CacheDBPath string
|
|
Debug bool
|
|
LogToFile bool
|
|
LogDir string
|
|
Quiet bool
|
|
DetectIPS bool
|
|
|
|
Targets map[string]config.ServerInfo
|
|
}
|
|
|
|
// Scan execute scan
|
|
func (s Scanner) Scan() error {
|
|
logging.Log.Info("Detecting Server/Container OS... ")
|
|
if err := s.initServers(); err != nil {
|
|
return xerrors.Errorf("Failed to init servers. err: %w", err)
|
|
}
|
|
|
|
logging.Log.Info("Checking Scan Modes... ")
|
|
if err := s.checkScanModes(); err != nil {
|
|
return xerrors.Errorf("Fix config.toml. err: %w", err)
|
|
}
|
|
|
|
logging.Log.Info("Detecting Platforms... ")
|
|
s.detectPlatform()
|
|
|
|
if s.DetectIPS {
|
|
logging.Log.Info("Detecting IPS identifiers... ")
|
|
s.detectIPS()
|
|
}
|
|
|
|
if err := s.execScan(); err != nil {
|
|
return xerrors.Errorf("Failed to scan. err: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Configtest checks if the server is scannable.
|
|
func (s Scanner) Configtest() error {
|
|
logging.Log.Info("Detecting Server/Container OS... ")
|
|
if err := s.initServers(); err != nil {
|
|
return xerrors.Errorf("Failed to init servers. err: %w", err)
|
|
}
|
|
|
|
logging.Log.Info("Checking Scan Modes...")
|
|
if err := s.checkScanModes(); err != nil {
|
|
return xerrors.Errorf("Fix config.toml. err: %w", err)
|
|
}
|
|
|
|
logging.Log.Info("Checking dependencies...")
|
|
s.checkDependencies()
|
|
|
|
logging.Log.Info("Checking sudo settings...")
|
|
s.checkIfSudoNoPasswd()
|
|
|
|
logging.Log.Info("It can be scanned with fast scan mode even if warn or err messages are displayed due to lack of dependent packages or sudo settings in fast-root or deep scan mode")
|
|
|
|
if len(servers) == 0 {
|
|
return xerrors.Errorf("No scannable servers")
|
|
}
|
|
|
|
logging.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 nil
|
|
}
|
|
|
|
// ViaHTTP scans servers by HTTP header and body
|
|
func ViaHTTP(header http.Header, body string, toLocalFile bool) (models.ScanResult, error) {
|
|
serverName := header.Get("X-Vuls-Server-Name")
|
|
if toLocalFile && serverName == "" {
|
|
return models.ScanResult{}, errServerNameHeader
|
|
}
|
|
|
|
family := header.Get("X-Vuls-OS-Family")
|
|
if family == "" {
|
|
return models.ScanResult{}, errOSFamilyHeader
|
|
}
|
|
|
|
switch family {
|
|
case constant.Windows:
|
|
osInfo, hotfixs, err := parseSystemInfo(toUTF8(body))
|
|
if err != nil {
|
|
return models.ScanResult{}, xerrors.Errorf("Failed to parse systeminfo.exe. err: %w", err)
|
|
}
|
|
|
|
release := header.Get("X-Vuls-OS-Release")
|
|
if release == "" {
|
|
release, err = detectOSName(osInfo)
|
|
if err != nil {
|
|
return models.ScanResult{}, xerrors.Errorf("Failed to detect os name. err: %w", err)
|
|
}
|
|
}
|
|
|
|
kernelVersion := header.Get("X-Vuls-Kernel-Version")
|
|
if kernelVersion == "" {
|
|
kernelVersion = formatKernelVersion(osInfo)
|
|
}
|
|
|
|
w := &windows{
|
|
base: base{
|
|
Distro: config.Distro{Family: family, Release: release},
|
|
osPackages: osPackages{
|
|
Kernel: models.Kernel{Version: kernelVersion},
|
|
},
|
|
log: logging.Log,
|
|
},
|
|
}
|
|
|
|
kbs, err := w.detectKBsFromKernelVersion()
|
|
if err != nil {
|
|
return models.ScanResult{}, xerrors.Errorf("Failed to detect KBs from kernel version. err: %w", err)
|
|
}
|
|
|
|
applied, unapplied := map[string]struct{}{}, map[string]struct{}{}
|
|
for _, kb := range hotfixs {
|
|
applied[kb] = struct{}{}
|
|
}
|
|
for _, kb := range kbs.Applied {
|
|
applied[kb] = struct{}{}
|
|
}
|
|
for _, kb := range kbs.Unapplied {
|
|
unapplied[kb] = struct{}{}
|
|
}
|
|
|
|
return models.ScanResult{
|
|
ServerName: serverName,
|
|
Family: family,
|
|
Release: release,
|
|
RunningKernel: models.Kernel{
|
|
Version: kernelVersion,
|
|
},
|
|
WindowsKB: &models.WindowsKB{Applied: maps.Keys(applied), Unapplied: maps.Keys(unapplied)},
|
|
ScannedCves: models.VulnInfos{},
|
|
}, nil
|
|
default:
|
|
release := header.Get("X-Vuls-OS-Release")
|
|
if release == "" {
|
|
return models.ScanResult{}, errOSReleaseHeader
|
|
}
|
|
|
|
kernelRelease := header.Get("X-Vuls-Kernel-Release")
|
|
if kernelRelease == "" {
|
|
logging.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 == constant.Debian {
|
|
if kernelVersion == "" {
|
|
logging.Log.Warn("X-Vuls-Kernel-Version is empty. skip kernel vulnerability detection.")
|
|
} else {
|
|
if _, err := debver.NewVersion(kernelVersion); err != nil {
|
|
logging.Log.Warnf("X-Vuls-Kernel-Version is invalid. skip kernel vulnerability detection. actual kernelVersion: %s, err: %s", kernelVersion, err)
|
|
kernelVersion = ""
|
|
}
|
|
}
|
|
}
|
|
|
|
distro := config.Distro{
|
|
Family: family,
|
|
Release: release,
|
|
}
|
|
|
|
kernel := models.Kernel{
|
|
Release: kernelRelease,
|
|
Version: kernelVersion,
|
|
}
|
|
installedPackages, srcPackages, err := ParseInstalledPkgs(distro, kernel, body)
|
|
if err != nil {
|
|
return models.ScanResult{}, err
|
|
}
|
|
|
|
return models.ScanResult{
|
|
ServerName: serverName,
|
|
Family: family,
|
|
Release: release,
|
|
RunningKernel: models.Kernel{
|
|
Release: kernelRelease,
|
|
Version: kernelVersion,
|
|
},
|
|
Packages: installedPackages,
|
|
SrcPackages: srcPackages,
|
|
ScannedCves: models.VulnInfos{},
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
// ParseInstalledPkgs parses installed pkgs line
|
|
func ParseInstalledPkgs(distro config.Distro, kernel models.Kernel, pkgList string) (models.Packages, models.SrcPackages, error) {
|
|
base := base{
|
|
Distro: distro,
|
|
osPackages: osPackages{
|
|
Kernel: kernel,
|
|
},
|
|
log: logging.Log,
|
|
}
|
|
|
|
var osType osTypeInterface
|
|
switch distro.Family {
|
|
case constant.Debian, constant.Ubuntu, constant.Raspbian:
|
|
osType = &debian{base: base}
|
|
case constant.RedHat:
|
|
osType = &rhel{redhatBase: redhatBase{base: base}}
|
|
case constant.CentOS:
|
|
osType = ¢os{redhatBase: redhatBase{base: base}}
|
|
case constant.Alma:
|
|
osType = &alma{redhatBase: redhatBase{base: base}}
|
|
case constant.Rocky:
|
|
osType = &rocky{redhatBase: redhatBase{base: base}}
|
|
case constant.Oracle:
|
|
osType = &oracle{redhatBase: redhatBase{base: base}}
|
|
case constant.Amazon:
|
|
osType = &amazon{redhatBase: redhatBase{base: base}}
|
|
case constant.Fedora:
|
|
osType = &fedora{redhatBase: redhatBase{base: base}}
|
|
case constant.OpenSUSE, constant.OpenSUSELeap, constant.SUSEEnterpriseServer, constant.SUSEEnterpriseDesktop:
|
|
osType = &suse{redhatBase: redhatBase{base: base}}
|
|
default:
|
|
return models.Packages{}, models.SrcPackages{}, xerrors.Errorf("Server mode for %s is not implemented yet", base.Distro.Family)
|
|
}
|
|
|
|
return osType.parseInstalledPackages(pkgList)
|
|
}
|
|
|
|
// initServers detect the kind of OS distribution of target servers
|
|
func (s Scanner) initServers() error {
|
|
hosts, errHosts := s.detectServerOSes()
|
|
if len(hosts) == 0 {
|
|
return xerrors.New("No scannable host OS")
|
|
}
|
|
|
|
// to generate random color for logging
|
|
rand.Seed(time.Now().UnixNano())
|
|
for _, srv := range hosts {
|
|
srv.setLogger(logging.NewCustomLogger(s.Debug, s.Quiet, s.LogToFile, s.LogDir, config.Colors[rand.Intn(len(config.Colors))], srv.getServerInfo().GetServerName()))
|
|
}
|
|
|
|
containers, errContainers := s.detectContainerOSes(hosts)
|
|
for _, srv := range containers {
|
|
srv.setLogger(logging.NewCustomLogger(s.Debug, s.Quiet, s.LogToFile, s.LogDir, config.Colors[rand.Intn(len(config.Colors))], srv.getServerInfo().GetServerName()))
|
|
}
|
|
|
|
// set to pkg global variable
|
|
for _, host := range hosts {
|
|
if !host.getServerInfo().ContainersOnly {
|
|
servers = append(servers, host)
|
|
}
|
|
}
|
|
servers = append(servers, containers...)
|
|
errServers = append(errHosts, errContainers...)
|
|
|
|
if len(servers) == 0 {
|
|
return xerrors.New("No scannable servers")
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s Scanner) detectServerOSes() (servers, errServers []osTypeInterface) {
|
|
logging.Log.Info("Detecting OS of servers... ")
|
|
osTypeChan := make(chan osTypeInterface, len(s.Targets))
|
|
defer close(osTypeChan)
|
|
for _, target := range s.Targets {
|
|
go func(srv config.ServerInfo) {
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
logging.Log.Debugf("Panic: %s on %s", p, srv.ServerName)
|
|
}
|
|
}()
|
|
if err := validateSSHConfig(&srv); err != nil {
|
|
checkOS := unknown{base{ServerInfo: srv}}
|
|
checkOS.setErrs([]error{err})
|
|
osTypeChan <- &checkOS
|
|
return
|
|
}
|
|
osTypeChan <- s.detectOS(srv)
|
|
}(target)
|
|
}
|
|
|
|
timeout := time.After(time.Duration(s.TimeoutSec) * time.Second)
|
|
for i := 0; i < len(s.Targets); i++ {
|
|
select {
|
|
case res := <-osTypeChan:
|
|
if 0 < len(res.getErrs()) {
|
|
errServers = append(errServers, res)
|
|
logging.Log.Errorf("(%d/%d) Failed: %s, err: %+v", i+1, len(s.Targets), res.getServerInfo().ServerName, res.getErrs())
|
|
} else {
|
|
servers = append(servers, res)
|
|
logging.Log.Infof("(%d/%d) Detected: %s: %s", i+1, len(s.Targets), res.getServerInfo().ServerName, res.getDistro())
|
|
}
|
|
case <-timeout:
|
|
msg := "Timed out while detecting servers"
|
|
logging.Log.Error(msg)
|
|
for servername, sInfo := range s.Targets {
|
|
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)
|
|
logging.Log.Errorf("(%d/%d) Timed out: %s", i+1, len(s.Targets), servername)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func validateSSHConfig(c *config.ServerInfo) error {
|
|
if isLocalExec(c.Port, c.Host) || c.Type == constant.ServerTypePseudo {
|
|
return nil
|
|
}
|
|
|
|
logging.Log.Debugf("Validating SSH Settings for Server:%s ...", c.GetServerName())
|
|
|
|
if runtime.GOOS == "windows" {
|
|
c.Distro.Family = constant.Windows
|
|
}
|
|
defer func(c *config.ServerInfo) {
|
|
c.Distro.Family = ""
|
|
}(c)
|
|
|
|
sshBinaryPath, err := lookpath(c.Distro.Family, "ssh")
|
|
if err != nil {
|
|
return xerrors.Errorf("Failed to lookup ssh binary path. err: %w", err)
|
|
}
|
|
|
|
sshConfigCmd := buildSSHConfigCmd(sshBinaryPath, c)
|
|
logging.Log.Debugf("Executing... %s", strings.Replace(sshConfigCmd, "\n", "", -1))
|
|
configResult := localExec(*c, sshConfigCmd, noSudo)
|
|
if !configResult.isSuccess() {
|
|
return xerrors.Errorf("Failed to print SSH configuration. err: %w", configResult.Error)
|
|
}
|
|
sshConfig := parseSSHConfiguration(configResult.Stdout)
|
|
c.User = sshConfig.user
|
|
logging.Log.Debugf("Setting SSH User:%s for Server:%s ...", sshConfig.user, c.GetServerName())
|
|
c.Port = sshConfig.port
|
|
logging.Log.Debugf("Setting SSH Port:%s for Server:%s ...", sshConfig.port, c.GetServerName())
|
|
if c.User == "" || c.Port == "" {
|
|
return xerrors.New("Failed to find User or Port setting. Please check the User or Port settings for SSH")
|
|
}
|
|
|
|
if sshConfig.strictHostKeyChecking == "false" {
|
|
return nil
|
|
}
|
|
if sshConfig.proxyCommand != "" || sshConfig.proxyJump != "" {
|
|
logging.Log.Debug("known_host check under Proxy is not yet implemented")
|
|
return nil
|
|
}
|
|
|
|
logging.Log.Debugf("Checking if the host's public key is in known_hosts...")
|
|
knownHostsPaths := []string{}
|
|
for _, knownHost := range append(sshConfig.userKnownHosts, sshConfig.globalKnownHosts...) {
|
|
if knownHost != "" && knownHost != "/dev/null" {
|
|
knownHostsPaths = append(knownHostsPaths, knownHost)
|
|
}
|
|
}
|
|
if len(knownHostsPaths) == 0 {
|
|
return xerrors.New("Failed to find any known_hosts to use. Please check the UserKnownHostsFile and GlobalKnownHostsFile settings for SSH")
|
|
}
|
|
|
|
sshKeyscanBinaryPath, err := lookpath(c.Distro.Family, "ssh-keyscan")
|
|
if err != nil {
|
|
return xerrors.Errorf("Failed to lookup ssh-keyscan binary path. err: %w", err)
|
|
}
|
|
sshScanCmd := strings.Join([]string{sshKeyscanBinaryPath, "-p", c.Port, sshConfig.hostname}, " ")
|
|
r := localExec(*c, sshScanCmd, noSudo)
|
|
if !r.isSuccess() {
|
|
return xerrors.Errorf("Failed to ssh-keyscan. cmd: %s, err: %w", sshScanCmd, r.Error)
|
|
}
|
|
serverKeys := parseSSHScan(r.Stdout)
|
|
|
|
sshKeygenBinaryPath, err := lookpath(c.Distro.Family, "ssh-keygen")
|
|
if err != nil {
|
|
return xerrors.Errorf("Failed to lookup ssh-keygen binary path. err: %w", err)
|
|
}
|
|
for _, knownHosts := range knownHostsPaths {
|
|
var hostname string
|
|
if sshConfig.hostKeyAlias != "" {
|
|
hostname = sshConfig.hostKeyAlias
|
|
} else {
|
|
if c.Port != "" && c.Port != "22" {
|
|
hostname = fmt.Sprintf("\"[%s]:%s\"", sshConfig.hostname, c.Port)
|
|
} else {
|
|
hostname = sshConfig.hostname
|
|
}
|
|
}
|
|
cmd := fmt.Sprintf("%s -F %s -f %s", sshKeygenBinaryPath, hostname, knownHosts)
|
|
logging.Log.Debugf("Executing... %s", strings.Replace(cmd, "\n", "", -1))
|
|
if r := localExec(*c, cmd, noSudo); r.isSuccess() {
|
|
keyType, clientKey, err := parseSSHKeygen(r.Stdout)
|
|
if err != nil {
|
|
return xerrors.Errorf("Failed to parse ssh-keygen result. stdout: %s, err: %w", r.Stdout, r.Error)
|
|
}
|
|
if serverKey, ok := serverKeys[keyType]; ok && serverKey == clientKey {
|
|
return nil
|
|
}
|
|
return xerrors.Errorf("Failed to find the server key that matches the key registered in the client. The server key may have been changed. Please exec `$ %s` and `$ %s` or `$ %s`",
|
|
fmt.Sprintf("%s -R %s -f %s", sshKeygenBinaryPath, hostname, knownHosts),
|
|
strings.Join(buildSSHBaseCmd(sshBinaryPath, c, nil), " "),
|
|
buildSSHKeyScanCmd(sshKeyscanBinaryPath, c.Port, knownHostsPaths[0], sshConfig))
|
|
}
|
|
}
|
|
return xerrors.Errorf("Failed to find the host in known_hosts. Please exec `$ %s` or `$ %s`",
|
|
strings.Join(buildSSHBaseCmd(sshBinaryPath, c, nil), " "),
|
|
buildSSHKeyScanCmd(sshKeyscanBinaryPath, c.Port, knownHostsPaths[0], sshConfig))
|
|
}
|
|
|
|
func lookpath(family, file string) (string, error) {
|
|
switch family {
|
|
case constant.Windows:
|
|
return fmt.Sprintf("%s.exe", strings.TrimPrefix(file, ".exe")), nil
|
|
default:
|
|
p, err := ex.LookPath(file)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
return p, nil
|
|
}
|
|
}
|
|
|
|
func buildSSHBaseCmd(sshBinaryPath string, c *config.ServerInfo, options []string) []string {
|
|
cmd := []string{sshBinaryPath}
|
|
if len(options) > 0 {
|
|
cmd = append(cmd, options...)
|
|
}
|
|
if c.SSHConfigPath != "" {
|
|
cmd = append(cmd, "-F", c.SSHConfigPath)
|
|
}
|
|
if c.KeyPath != "" {
|
|
cmd = append(cmd, "-i", c.KeyPath)
|
|
}
|
|
if c.Port != "" {
|
|
cmd = append(cmd, "-p", c.Port)
|
|
}
|
|
if c.User != "" {
|
|
cmd = append(cmd, "-l", c.User)
|
|
}
|
|
if len(c.JumpServer) > 0 {
|
|
cmd = append(cmd, "-J", strings.Join(c.JumpServer, ","))
|
|
}
|
|
cmd = append(cmd, c.Host)
|
|
return cmd
|
|
}
|
|
|
|
func buildSSHConfigCmd(sshBinaryPath string, c *config.ServerInfo) string {
|
|
return strings.Join(buildSSHBaseCmd(sshBinaryPath, c, []string{"-G"}), " ")
|
|
}
|
|
|
|
func buildSSHKeyScanCmd(sshKeyscanBinaryPath, port, knownHosts string, sshConfig sshConfiguration) string {
|
|
cmd := []string{sshKeyscanBinaryPath}
|
|
if sshConfig.hashKnownHosts == "yes" {
|
|
cmd = append(cmd, "-H")
|
|
}
|
|
if port != "" {
|
|
cmd = append(cmd, "-p", port)
|
|
}
|
|
return strings.Join(append(cmd, sshConfig.hostname, ">>", knownHosts), " ")
|
|
}
|
|
|
|
type sshConfiguration struct {
|
|
hostname string
|
|
hostKeyAlias string
|
|
hashKnownHosts string
|
|
user string
|
|
port string
|
|
strictHostKeyChecking string
|
|
globalKnownHosts []string
|
|
userKnownHosts []string
|
|
proxyCommand string
|
|
proxyJump string
|
|
}
|
|
|
|
func parseSSHConfiguration(stdout string) sshConfiguration {
|
|
sshConfig := sshConfiguration{}
|
|
for _, line := range strings.Split(stdout, "\n") {
|
|
line = strings.TrimSuffix(line, "\r")
|
|
switch {
|
|
case strings.HasPrefix(line, "user "):
|
|
sshConfig.user = strings.TrimPrefix(line, "user ")
|
|
case strings.HasPrefix(line, "hostname "):
|
|
sshConfig.hostname = strings.TrimPrefix(line, "hostname ")
|
|
case strings.HasPrefix(line, "hostkeyalias "):
|
|
sshConfig.hostKeyAlias = strings.TrimPrefix(line, "hostkeyalias ")
|
|
case strings.HasPrefix(line, "hashknownhosts "):
|
|
sshConfig.hashKnownHosts = strings.TrimPrefix(line, "hashknownhosts ")
|
|
case strings.HasPrefix(line, "port "):
|
|
sshConfig.port = strings.TrimPrefix(line, "port ")
|
|
case strings.HasPrefix(line, "stricthostkeychecking "):
|
|
sshConfig.strictHostKeyChecking = strings.TrimPrefix(line, "stricthostkeychecking ")
|
|
case strings.HasPrefix(line, "globalknownhostsfile "):
|
|
sshConfig.globalKnownHosts = strings.Split(strings.TrimPrefix(line, "globalknownhostsfile "), " ")
|
|
case strings.HasPrefix(line, "userknownhostsfile "):
|
|
sshConfig.userKnownHosts = strings.Split(strings.TrimPrefix(line, "userknownhostsfile "), " ")
|
|
case strings.HasPrefix(line, "proxycommand "):
|
|
sshConfig.proxyCommand = strings.TrimPrefix(line, "proxycommand ")
|
|
case strings.HasPrefix(line, "proxyjump "):
|
|
sshConfig.proxyJump = strings.TrimPrefix(line, "proxyjump ")
|
|
}
|
|
}
|
|
return sshConfig
|
|
}
|
|
|
|
func parseSSHScan(stdout string) map[string]string {
|
|
keys := map[string]string{}
|
|
for _, line := range strings.Split(stdout, "\n") {
|
|
line = strings.TrimSuffix(line, "\r")
|
|
if line == "" || strings.HasPrefix(line, "# ") {
|
|
continue
|
|
}
|
|
if ss := strings.Split(line, " "); len(ss) == 3 {
|
|
keys[ss[1]] = ss[2]
|
|
}
|
|
}
|
|
return keys
|
|
}
|
|
|
|
func parseSSHKeygen(stdout string) (string, string, error) {
|
|
for _, line := range strings.Split(stdout, "\n") {
|
|
line = strings.TrimSuffix(line, "\r")
|
|
if line == "" || strings.HasPrefix(line, "# ") {
|
|
continue
|
|
}
|
|
|
|
// HashKnownHosts yes
|
|
if strings.HasPrefix(line, "|1|") {
|
|
ss := strings.Split(line, "|")
|
|
if ss := strings.Split(ss[len(ss)-1], " "); len(ss) == 3 {
|
|
return ss[1], ss[2], nil
|
|
}
|
|
} else {
|
|
if ss := strings.Split(line, " "); len(ss) == 3 {
|
|
return ss[1], ss[2], nil
|
|
}
|
|
}
|
|
}
|
|
return "", "", xerrors.New("Failed to parse ssh-keygen result. err: public key not found")
|
|
}
|
|
|
|
func (s Scanner) detectContainerOSes(hosts []osTypeInterface) (actives, inactives []osTypeInterface) {
|
|
logging.Log.Info("Detecting OS of containers... ")
|
|
osTypesChan := make(chan []osTypeInterface, len(hosts))
|
|
defer close(osTypesChan)
|
|
for _, host := range hosts {
|
|
go func(h osTypeInterface) {
|
|
defer func() {
|
|
if p := recover(); p != nil {
|
|
logging.Log.Debugf("Panic: %s on %s",
|
|
p, h.getServerInfo().GetServerName())
|
|
}
|
|
}()
|
|
osTypesChan <- s.detectContainerOSesOnServer(h)
|
|
}(host)
|
|
}
|
|
|
|
timeout := time.After(time.Duration(s.TimeoutSec) * time.Second)
|
|
for i := 0; i < len(hosts); i++ {
|
|
select {
|
|
case res := <-osTypesChan:
|
|
for _, osi := range res {
|
|
sinfo := osi.getServerInfo()
|
|
if 0 < len(osi.getErrs()) {
|
|
inactives = append(inactives, osi)
|
|
logging.Log.Errorf("Failed: %s err: %+v", sinfo.ServerName, osi.getErrs())
|
|
continue
|
|
}
|
|
actives = append(actives, osi)
|
|
logging.Log.Infof("Detected: %s@%s: %s",
|
|
sinfo.Container.Name, sinfo.ServerName, osi.getDistro())
|
|
}
|
|
case <-timeout:
|
|
logging.Log.Error("Some containers timed out")
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
func (s Scanner) 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 := s.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 := s.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 (s Scanner) detectOS(c config.ServerInfo) osTypeInterface {
|
|
if itsMe, osType, _ := detectPseudo(c); itsMe {
|
|
return osType
|
|
}
|
|
|
|
if !isLocalExec(c.Port, c.Host) {
|
|
if err := testFirstSSHConnection(c); err != nil {
|
|
osType := &unknown{base{ServerInfo: c}}
|
|
osType.setErrs([]error{xerrors.Errorf("Failed to test first SSH Connection. err: %w", err)})
|
|
return osType
|
|
}
|
|
}
|
|
|
|
if itsMe, osType := detectWindows(c); itsMe {
|
|
logging.Log.Debugf("Windows. Host: %s:%s", c.Host, c.Port)
|
|
return osType
|
|
}
|
|
|
|
if itsMe, osType := detectDebian(c); itsMe {
|
|
logging.Log.Debugf("Debian based Linux. Host: %s:%s", c.Host, c.Port)
|
|
return osType
|
|
}
|
|
|
|
if itsMe, osType := detectRedhat(c); itsMe {
|
|
logging.Log.Debugf("Redhat based Linux. Host: %s:%s", c.Host, c.Port)
|
|
return osType
|
|
}
|
|
|
|
if itsMe, osType := detectSUSE(c); itsMe {
|
|
logging.Log.Debugf("SUSE Linux. Host: %s:%s", c.Host, c.Port)
|
|
return osType
|
|
}
|
|
|
|
if itsMe, osType := detectFreebsd(c); itsMe {
|
|
logging.Log.Debugf("FreeBSD. Host: %s:%s", c.Host, c.Port)
|
|
return osType
|
|
}
|
|
|
|
if itsMe, osType := detectAlpine(c); itsMe {
|
|
logging.Log.Debugf("Alpine. Host: %s:%s", c.Host, c.Port)
|
|
return osType
|
|
}
|
|
|
|
osType := &unknown{base{ServerInfo: c}}
|
|
osType.setErrs([]error{xerrors.New("Unknown OS Type")})
|
|
return osType
|
|
}
|
|
|
|
func testFirstSSHConnection(c config.ServerInfo) error {
|
|
for i := 3; i > 0; i-- {
|
|
rChan := make(chan execResult, 1)
|
|
go func() {
|
|
rChan <- exec(c, "exit", noSudo)
|
|
}()
|
|
select {
|
|
case r := <-rChan:
|
|
if r.ExitStatus == 255 {
|
|
return xerrors.Errorf("Unable to connect via SSH. Scan with -vvv option to print SSH debugging messages and check SSH settings.\n%s", r)
|
|
}
|
|
return nil
|
|
case <-time.After(time.Duration(3) * time.Second):
|
|
}
|
|
}
|
|
logging.Log.Warnf("First SSH Connection to Host: %s:%s timeout", c.Host, c.Port)
|
|
return nil
|
|
}
|
|
|
|
// checkScanModes checks scan mode
|
|
func (s Scanner) 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 (s Scanner) checkDependencies() {
|
|
parallelExec(func(o osTypeInterface) error {
|
|
return o.checkDeps()
|
|
}, s.TimeoutSec)
|
|
return
|
|
}
|
|
|
|
// checkIfSudoNoPasswd checks whether vuls can sudo with nopassword via SSH
|
|
func (s Scanner) checkIfSudoNoPasswd() {
|
|
parallelExec(func(o osTypeInterface) error {
|
|
return o.checkIfSudoNoPasswd()
|
|
}, s.TimeoutSec)
|
|
return
|
|
}
|
|
|
|
// detectPlatform detects the platform of each servers.
|
|
func (s Scanner) detectPlatform() {
|
|
parallelExec(func(o osTypeInterface) error {
|
|
o.detectPlatform()
|
|
// Logging only if platform can not be specified
|
|
return nil
|
|
}, s.TimeoutSec)
|
|
|
|
for i, s := range servers {
|
|
if s.getServerInfo().IsContainer() {
|
|
logging.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 {
|
|
logging.Log.Infof("(%d/%d) %s is running on %s",
|
|
i+1, len(servers),
|
|
s.getServerInfo().ServerName,
|
|
s.getPlatform().Name,
|
|
)
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// detectIPS detects the IPS of each servers.
|
|
func (s Scanner) detectIPS() {
|
|
parallelExec(func(o osTypeInterface) error {
|
|
o.detectIPS()
|
|
// Logging only if IPS can not be specified
|
|
return nil
|
|
}, s.TimeoutSec)
|
|
|
|
for i, s := range servers {
|
|
if !s.getServerInfo().IsContainer() {
|
|
logging.Log.Infof("(%d/%d) %s has %d IPS integration",
|
|
i+1, len(servers),
|
|
s.getServerInfo().ServerName,
|
|
len(s.getServerInfo().IPSIdentifiers),
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
// execScan scan
|
|
func (s Scanner) execScan() error {
|
|
if len(servers) == 0 {
|
|
return xerrors.New("No server defined. Check the configuration")
|
|
}
|
|
|
|
if err := s.setupChangelogCache(); err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if cache.DB != nil {
|
|
cache.DB.Close()
|
|
}
|
|
}()
|
|
|
|
scannedAt := time.Now()
|
|
dir, err := EnsureResultDir(s.ResultsDir, scannedAt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
results, err := s.getScanResults(scannedAt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
for i, r := range results {
|
|
if server, ok := s.Targets[r.ServerName]; ok {
|
|
results[i] = r.ClearFields(server.IgnoredJSONKeys)
|
|
}
|
|
}
|
|
|
|
return writeScanResults(dir, results)
|
|
}
|
|
|
|
func (s Scanner) setupChangelogCache() error {
|
|
needToSetupCache := false
|
|
for _, s := range servers {
|
|
switch s.getDistro().Family {
|
|
case constant.Raspbian:
|
|
needToSetupCache = true
|
|
break
|
|
case constant.Ubuntu, constant.Debian:
|
|
//TODO changelog cache for RedHat, Oracle, Amazon, CentOS is not implemented yet.
|
|
if s.getServerInfo().Mode.IsDeep() {
|
|
needToSetupCache = true
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if needToSetupCache {
|
|
if err := cache.SetupBolt(s.CacheDBPath, logging.Log); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// getScanResults returns ScanResults
|
|
func (s Scanner) getScanResults(scannedAt time.Time) (results models.ScanResults, err error) {
|
|
parallelExec(func(o osTypeInterface) (err error) {
|
|
if o.getServerInfo().Module.IsScanOSPkg() {
|
|
if err = o.preCure(); err != nil {
|
|
return err
|
|
}
|
|
if err = o.scanPackages(); err != nil {
|
|
return err
|
|
}
|
|
if err = o.postScan(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if o.getServerInfo().Module.IsScanPort() {
|
|
if err = o.scanPorts(); err != nil {
|
|
// continue scanning
|
|
logging.Log.Warnf("Failed to scan Ports: %+v", err)
|
|
}
|
|
}
|
|
if o.getServerInfo().Module.IsScanWordPress() {
|
|
if err = o.scanWordPress(); err != nil {
|
|
return xerrors.Errorf("Failed to scan WordPress: %w", err)
|
|
}
|
|
}
|
|
if o.getServerInfo().Module.IsScanLockFile() {
|
|
if err = o.scanLibraries(); err != nil {
|
|
return xerrors.Errorf("Failed to scan Library: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}, s.ScanTimeoutSec)
|
|
|
|
hostname, _ := os.Hostname()
|
|
ipv4s, ipv6s, err := util.IP()
|
|
if err != nil {
|
|
logging.Log.Warnf("Failed to get scannedIPs. err: %+v", err)
|
|
}
|
|
|
|
for _, s := range append(servers, errServers...) {
|
|
r := s.convertToModel()
|
|
r.CheckEOL()
|
|
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)
|
|
|
|
if 0 < len(r.Warnings) {
|
|
logging.Log.Warnf("Some warnings occurred during scanning on %s. Please fix the warnings to get a useful information. Execute configtest subcommand before scanning to know the cause of the warnings. warnings: %v",
|
|
r.ServerName, r.Warnings)
|
|
}
|
|
}
|
|
return results, nil
|
|
}
|