feat(windows): support Windows (#1581)
* chore(deps): mod update * fix(scanner): do not attach tty because there is no need to enter ssh password * feat(windows): support Windows
This commit is contained in:
@@ -6,10 +6,12 @@ import (
|
||||
"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"
|
||||
@@ -149,64 +151,122 @@ func (s Scanner) Configtest() error {
|
||||
|
||||
// ViaHTTP scans servers by HTTP header and body
|
||||
func ViaHTTP(header http.Header, body string, toLocalFile bool) (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 == "" {
|
||||
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 = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serverName := header.Get("X-Vuls-Server-Name")
|
||||
if toLocalFile && serverName == "" {
|
||||
return models.ScanResult{}, errServerNameHeader
|
||||
}
|
||||
|
||||
distro := config.Distro{
|
||||
Family: family,
|
||||
Release: release,
|
||||
family := header.Get("X-Vuls-OS-Family")
|
||||
if family == "" {
|
||||
return models.ScanResult{}, errOSFamilyHeader
|
||||
}
|
||||
|
||||
kernel := models.Kernel{
|
||||
Release: kernelRelease,
|
||||
Version: kernelVersion,
|
||||
}
|
||||
installedPackages, srcPackages, err := ParseInstalledPkgs(distro, kernel, body)
|
||||
if err != nil {
|
||||
return models.ScanResult{}, err
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
return models.ScanResult{
|
||||
ServerName: serverName,
|
||||
Family: family,
|
||||
Release: release,
|
||||
RunningKernel: models.Kernel{
|
||||
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,
|
||||
},
|
||||
Packages: installedPackages,
|
||||
SrcPackages: srcPackages,
|
||||
ScannedCves: models.VulnInfos{},
|
||||
}, nil
|
||||
}
|
||||
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
|
||||
@@ -342,7 +402,14 @@ func validateSSHConfig(c *config.ServerInfo) error {
|
||||
|
||||
logging.Log.Debugf("Validating SSH Settings for Server:%s ...", c.GetServerName())
|
||||
|
||||
sshBinaryPath, err := ex.LookPath("ssh")
|
||||
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)
|
||||
}
|
||||
@@ -381,7 +448,7 @@ func validateSSHConfig(c *config.ServerInfo) error {
|
||||
return xerrors.New("Failed to find any known_hosts to use. Please check the UserKnownHostsFile and GlobalKnownHostsFile settings for SSH")
|
||||
}
|
||||
|
||||
sshKeyscanBinaryPath, err := ex.LookPath("ssh-keyscan")
|
||||
sshKeyscanBinaryPath, err := lookpath(c.Distro.Family, "ssh-keyscan")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("Failed to lookup ssh-keyscan binary path. err: %w", err)
|
||||
}
|
||||
@@ -392,7 +459,7 @@ func validateSSHConfig(c *config.ServerInfo) error {
|
||||
}
|
||||
serverKeys := parseSSHScan(r.Stdout)
|
||||
|
||||
sshKeygenBinaryPath, err := ex.LookPath("ssh-keygen")
|
||||
sshKeygenBinaryPath, err := lookpath(c.Distro.Family, "ssh-keygen")
|
||||
if err != nil {
|
||||
return xerrors.Errorf("Failed to lookup ssh-keygen binary path. err: %w", err)
|
||||
}
|
||||
@@ -428,6 +495,19 @@ func validateSSHConfig(c *config.ServerInfo) error {
|
||||
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 {
|
||||
@@ -483,6 +563,7 @@ type sshConfiguration struct {
|
||||
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 ")
|
||||
@@ -512,6 +593,7 @@ func parseSSHConfiguration(stdout string) sshConfiguration {
|
||||
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
|
||||
}
|
||||
@@ -524,6 +606,7 @@ func parseSSHScan(stdout string) map[string]string {
|
||||
|
||||
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
|
||||
}
|
||||
@@ -669,10 +752,20 @@ func (s Scanner) detectOS(c config.ServerInfo) osTypeInterface {
|
||||
return osType
|
||||
}
|
||||
|
||||
if itsMe, osType, fatalErr := s.detectDebianWithRetry(c); fatalErr != nil {
|
||||
osType.setErrs([]error{xerrors.Errorf("Failed to detect OS: %w", fatalErr)})
|
||||
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
|
||||
} else if itsMe {
|
||||
}
|
||||
|
||||
if itsMe, osType := detectDebian(c); itsMe {
|
||||
logging.Log.Debugf("Debian based Linux. Host: %s:%s", c.Host, c.Port)
|
||||
return osType
|
||||
}
|
||||
@@ -702,28 +795,23 @@ func (s Scanner) detectOS(c config.ServerInfo) osTypeInterface {
|
||||
return osType
|
||||
}
|
||||
|
||||
// Retry as it may stall on the first SSH connection
|
||||
// https://github.com/future-architect/vuls/pull/753
|
||||
func (s Scanner) 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 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
|
||||
|
||||
Reference in New Issue
Block a user