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:
		@@ -60,6 +60,7 @@ type base struct {
 | 
			
		||||
	osPackages
 | 
			
		||||
	LibraryScanners []models.LibraryScanner
 | 
			
		||||
	WordPress       models.WordPressPackages
 | 
			
		||||
	windowsKB       *models.WindowsKB
 | 
			
		||||
 | 
			
		||||
	log   logging.Logger
 | 
			
		||||
	errs  []error
 | 
			
		||||
@@ -506,6 +507,7 @@ func (l *base) convertToModel() models.ScanResult {
 | 
			
		||||
		EnabledDnfModules: l.EnabledDnfModules,
 | 
			
		||||
		WordPressPackages: l.WordPress,
 | 
			
		||||
		LibraryScanners:   l.LibraryScanners,
 | 
			
		||||
		WindowsKB:         l.windowsKB,
 | 
			
		||||
		Optional:          l.ServerInfo.Optional,
 | 
			
		||||
		Errors:            errs,
 | 
			
		||||
		Warnings:          warns,
 | 
			
		||||
 
 | 
			
		||||
@@ -42,16 +42,10 @@ func newDebian(c config.ServerInfo) *debian {
 | 
			
		||||
 | 
			
		||||
// Ubuntu, Debian, Raspbian
 | 
			
		||||
// https://github.com/serverspec/specinfra/blob/master/lib/specinfra/helper/detect_os/debian.rb
 | 
			
		||||
func detectDebian(c config.ServerInfo) (bool, osTypeInterface, error) {
 | 
			
		||||
func detectDebian(c config.ServerInfo) (bool, osTypeInterface) {
 | 
			
		||||
	if r := exec(c, "ls /etc/debian_version", noSudo); !r.isSuccess() {
 | 
			
		||||
		if r.Error != nil {
 | 
			
		||||
			return false, nil, nil
 | 
			
		||||
		}
 | 
			
		||||
		if r.ExitStatus == 255 {
 | 
			
		||||
			return false, &unknown{base{ServerInfo: c}}, xerrors.Errorf("Unable to connect via SSH. Scan with -vvv option to print SSH debugging messages and check SSH settings.\n%s", r)
 | 
			
		||||
		}
 | 
			
		||||
		logging.Log.Debugf("Not Debian like Linux. %s", r)
 | 
			
		||||
		return false, nil, nil
 | 
			
		||||
		return false, nil
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Raspbian
 | 
			
		||||
@@ -64,7 +58,7 @@ func detectDebian(c config.ServerInfo) (bool, osTypeInterface, error) {
 | 
			
		||||
		if len(result) > 2 && result[0] == constant.Raspbian {
 | 
			
		||||
			deb := newDebian(c)
 | 
			
		||||
			deb.setDistro(strings.ToLower(trim(result[0])), trim(result[2]))
 | 
			
		||||
			return true, deb, nil
 | 
			
		||||
			return true, deb
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
@@ -84,7 +78,7 @@ func detectDebian(c config.ServerInfo) (bool, osTypeInterface, error) {
 | 
			
		||||
			distro := strings.ToLower(trim(result[1]))
 | 
			
		||||
			deb.setDistro(distro, trim(result[2]))
 | 
			
		||||
		}
 | 
			
		||||
		return true, deb, nil
 | 
			
		||||
		return true, deb
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if r := exec(c, "cat /etc/lsb-release", noSudo); r.isSuccess() {
 | 
			
		||||
@@ -104,7 +98,7 @@ func detectDebian(c config.ServerInfo) (bool, osTypeInterface, error) {
 | 
			
		||||
			distro := strings.ToLower(trim(result[1]))
 | 
			
		||||
			deb.setDistro(distro, trim(result[2]))
 | 
			
		||||
		}
 | 
			
		||||
		return true, deb, nil
 | 
			
		||||
		return true, deb
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Debian
 | 
			
		||||
@@ -112,11 +106,11 @@ func detectDebian(c config.ServerInfo) (bool, osTypeInterface, error) {
 | 
			
		||||
	if r := exec(c, cmd, noSudo); r.isSuccess() {
 | 
			
		||||
		deb := newDebian(c)
 | 
			
		||||
		deb.setDistro(constant.Debian, trim(r.Stdout))
 | 
			
		||||
		return true, deb, nil
 | 
			
		||||
		return true, deb
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	logging.Log.Debugf("Not Debian like Linux: %s", c.ServerName)
 | 
			
		||||
	return false, nil, nil
 | 
			
		||||
	return false, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func trim(str string) string {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,17 +3,24 @@ package scanner
 | 
			
		||||
import (
 | 
			
		||||
	"bytes"
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io"
 | 
			
		||||
	ex "os/exec"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"runtime"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"syscall"
 | 
			
		||||
	"time"
 | 
			
		||||
 | 
			
		||||
	homedir "github.com/mitchellh/go-homedir"
 | 
			
		||||
	"github.com/saintfish/chardet"
 | 
			
		||||
	"golang.org/x/text/encoding/japanese"
 | 
			
		||||
	"golang.org/x/text/encoding/unicode"
 | 
			
		||||
	"golang.org/x/text/transform"
 | 
			
		||||
	"golang.org/x/xerrors"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/constant"
 | 
			
		||||
	"github.com/future-architect/vuls/logging"
 | 
			
		||||
	homedir "github.com/mitchellh/go-homedir"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
type execResult struct {
 | 
			
		||||
@@ -152,15 +159,14 @@ func localExec(c config.ServerInfo, cmdstr string, sudo bool) (result execResult
 | 
			
		||||
	cmdstr = decorateCmd(c, cmdstr, sudo)
 | 
			
		||||
	var cmd *ex.Cmd
 | 
			
		||||
	switch c.Distro.Family {
 | 
			
		||||
	// case conf.FreeBSD, conf.Alpine, conf.Debian:
 | 
			
		||||
	// cmd = ex.Command("/bin/sh", "-c", cmdstr)
 | 
			
		||||
	case constant.Windows:
 | 
			
		||||
		cmd = ex.Command("powershell.exe", "-NoProfile", "-NonInteractive", cmdstr)
 | 
			
		||||
	default:
 | 
			
		||||
		cmd = ex.Command("/bin/sh", "-c", cmdstr)
 | 
			
		||||
	}
 | 
			
		||||
	var stdoutBuf, stderrBuf bytes.Buffer
 | 
			
		||||
	cmd.Stdout = &stdoutBuf
 | 
			
		||||
	cmd.Stderr = &stderrBuf
 | 
			
		||||
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		result.Error = err
 | 
			
		||||
		if exitError, ok := err.(*ex.ExitError); ok {
 | 
			
		||||
@@ -172,42 +178,47 @@ func localExec(c config.ServerInfo, cmdstr string, sudo bool) (result execResult
 | 
			
		||||
	} else {
 | 
			
		||||
		result.ExitStatus = 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result.Stdout = stdoutBuf.String()
 | 
			
		||||
	result.Stderr = stderrBuf.String()
 | 
			
		||||
	result.Stdout = toUTF8(stdoutBuf.String())
 | 
			
		||||
	result.Stderr = toUTF8(stderrBuf.String())
 | 
			
		||||
	result.Cmd = strings.Replace(cmdstr, "\n", "", -1)
 | 
			
		||||
	return
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sshExecExternal(c config.ServerInfo, cmd string, sudo bool) (result execResult) {
 | 
			
		||||
func sshExecExternal(c config.ServerInfo, cmdstr string, sudo bool) (result execResult) {
 | 
			
		||||
	sshBinaryPath, err := ex.LookPath("ssh")
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return execResult{Error: err}
 | 
			
		||||
	}
 | 
			
		||||
	if runtime.GOOS == "windows" {
 | 
			
		||||
		sshBinaryPath = "ssh.exe"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	args := []string{"-tt"}
 | 
			
		||||
	var args []string
 | 
			
		||||
 | 
			
		||||
	if c.SSHConfigPath != "" {
 | 
			
		||||
		args = append(args, "-F", c.SSHConfigPath)
 | 
			
		||||
	} else {
 | 
			
		||||
		home, err := homedir.Dir()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			msg := fmt.Sprintf("Failed to get HOME directory: %s", err)
 | 
			
		||||
			result.Stderr = msg
 | 
			
		||||
			result.ExitStatus = 997
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		controlPath := filepath.Join(home, ".vuls", `controlmaster-%r-`+c.ServerName+`.%p`)
 | 
			
		||||
 | 
			
		||||
		args = append(args,
 | 
			
		||||
			"-o", "StrictHostKeyChecking=yes",
 | 
			
		||||
			"-o", "LogLevel=quiet",
 | 
			
		||||
			"-o", "ConnectionAttempts=3",
 | 
			
		||||
			"-o", "ConnectTimeout=10",
 | 
			
		||||
			"-o", "ControlMaster=auto",
 | 
			
		||||
			"-o", fmt.Sprintf("ControlPath=%s", controlPath),
 | 
			
		||||
			"-o", "Controlpersist=10m",
 | 
			
		||||
		)
 | 
			
		||||
		if runtime.GOOS != "windows" {
 | 
			
		||||
			home, err := homedir.Dir()
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				msg := fmt.Sprintf("Failed to get HOME directory: %s", err)
 | 
			
		||||
				result.Stderr = msg
 | 
			
		||||
				result.ExitStatus = 997
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
 | 
			
		||||
			controlPath := filepath.Join(home, ".vuls", `controlmaster-%r-`+c.ServerName+`.%p`)
 | 
			
		||||
			args = append(args,
 | 
			
		||||
				"-o", "ControlMaster=auto",
 | 
			
		||||
				"-o", fmt.Sprintf("ControlPath=%s", controlPath),
 | 
			
		||||
				"-o", "Controlpersist=10m")
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if config.Conf.Vvv {
 | 
			
		||||
@@ -228,16 +239,18 @@ func sshExecExternal(c config.ServerInfo, cmd string, sudo bool) (result execRes
 | 
			
		||||
	}
 | 
			
		||||
	args = append(args, c.Host)
 | 
			
		||||
 | 
			
		||||
	cmd = decorateCmd(c, cmd, sudo)
 | 
			
		||||
	cmd = fmt.Sprintf("stty cols 1000; %s", cmd)
 | 
			
		||||
 | 
			
		||||
	args = append(args, cmd)
 | 
			
		||||
	execCmd := ex.Command(sshBinaryPath, args...)
 | 
			
		||||
 | 
			
		||||
	cmdstr = decorateCmd(c, cmdstr, sudo)
 | 
			
		||||
	var cmd *ex.Cmd
 | 
			
		||||
	switch c.Distro.Family {
 | 
			
		||||
	case constant.Windows:
 | 
			
		||||
		cmd = ex.Command(sshBinaryPath, append(args, "powershell.exe", "-NoProfile", "-NonInteractive", fmt.Sprintf(`"%s`, cmdstr))...)
 | 
			
		||||
	default:
 | 
			
		||||
		cmd = ex.Command(sshBinaryPath, append(args, fmt.Sprintf("stty cols 1000; %s", cmdstr))...)
 | 
			
		||||
	}
 | 
			
		||||
	var stdoutBuf, stderrBuf bytes.Buffer
 | 
			
		||||
	execCmd.Stdout = &stdoutBuf
 | 
			
		||||
	execCmd.Stderr = &stderrBuf
 | 
			
		||||
	if err := execCmd.Run(); err != nil {
 | 
			
		||||
	cmd.Stdout = &stdoutBuf
 | 
			
		||||
	cmd.Stderr = &stderrBuf
 | 
			
		||||
	if err := cmd.Run(); err != nil {
 | 
			
		||||
		if e, ok := err.(*ex.ExitError); ok {
 | 
			
		||||
			if s, ok := e.Sys().(syscall.WaitStatus); ok {
 | 
			
		||||
				result.ExitStatus = s.ExitStatus()
 | 
			
		||||
@@ -250,9 +263,8 @@ func sshExecExternal(c config.ServerInfo, cmd string, sudo bool) (result execRes
 | 
			
		||||
	} else {
 | 
			
		||||
		result.ExitStatus = 0
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	result.Stdout = stdoutBuf.String()
 | 
			
		||||
	result.Stderr = stderrBuf.String()
 | 
			
		||||
	result.Stdout = toUTF8(stdoutBuf.String())
 | 
			
		||||
	result.Stderr = toUTF8(stderrBuf.String())
 | 
			
		||||
	result.Servername = c.ServerName
 | 
			
		||||
	result.Container = c.Container
 | 
			
		||||
	result.Host = c.Host
 | 
			
		||||
@@ -280,7 +292,7 @@ func dockerShell(family string) string {
 | 
			
		||||
 | 
			
		||||
func decorateCmd(c config.ServerInfo, cmd string, sudo bool) string {
 | 
			
		||||
	if sudo && c.User != "root" && !c.IsContainer() {
 | 
			
		||||
		cmd = fmt.Sprintf("sudo -S %s", cmd)
 | 
			
		||||
		cmd = fmt.Sprintf("sudo %s", cmd)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// If you are using pipe and you want to detect preprocessing errors, remove comment out
 | 
			
		||||
@@ -306,10 +318,40 @@ func decorateCmd(c config.ServerInfo, cmd string, sudo bool) string {
 | 
			
		||||
				c.Container.Name, dockerShell(c.Distro.Family), cmd)
 | 
			
		||||
			// LXC required root privilege
 | 
			
		||||
			if c.User != "root" {
 | 
			
		||||
				cmd = fmt.Sprintf("sudo -S %s", cmd)
 | 
			
		||||
				cmd = fmt.Sprintf("sudo %s", cmd)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	//  cmd = fmt.Sprintf("set -x; %s", cmd)
 | 
			
		||||
	return cmd
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func toUTF8(s string) string {
 | 
			
		||||
	d := chardet.NewTextDetector()
 | 
			
		||||
	res, err := d.DetectBest([]byte(s))
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return s
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var bs []byte
 | 
			
		||||
	switch res.Charset {
 | 
			
		||||
	case "UTF-8":
 | 
			
		||||
		bs, err = []byte(s), nil
 | 
			
		||||
	case "UTF-16LE":
 | 
			
		||||
		bs, err = io.ReadAll(transform.NewReader(strings.NewReader(s), unicode.UTF16(unicode.LittleEndian, unicode.UseBOM).NewDecoder()))
 | 
			
		||||
	case "UTF-16BE":
 | 
			
		||||
		bs, err = io.ReadAll(transform.NewReader(strings.NewReader(s), unicode.UTF16(unicode.BigEndian, unicode.UseBOM).NewDecoder()))
 | 
			
		||||
	case "Shift_JIS":
 | 
			
		||||
		bs, err = io.ReadAll(transform.NewReader(strings.NewReader(s), japanese.ShiftJIS.NewDecoder()))
 | 
			
		||||
	case "EUC-JP":
 | 
			
		||||
		bs, err = io.ReadAll(transform.NewReader(strings.NewReader(s), japanese.EUCJP.NewDecoder()))
 | 
			
		||||
	case "ISO-2022-JP":
 | 
			
		||||
		bs, err = io.ReadAll(transform.NewReader(strings.NewReader(s), japanese.ISO2022JP.NewDecoder()))
 | 
			
		||||
	default:
 | 
			
		||||
		bs, err = []byte(s), nil
 | 
			
		||||
	}
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return s
 | 
			
		||||
	}
 | 
			
		||||
	return string(bs)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -39,14 +39,14 @@ func TestDecorateCmd(t *testing.T) {
 | 
			
		||||
			conf:     config.ServerInfo{User: "non-root"},
 | 
			
		||||
			cmd:      "ls",
 | 
			
		||||
			sudo:     true,
 | 
			
		||||
			expected: "sudo -S ls",
 | 
			
		||||
			expected: "sudo ls",
 | 
			
		||||
		},
 | 
			
		||||
		// non-root sudo true
 | 
			
		||||
		{
 | 
			
		||||
			conf:     config.ServerInfo{User: "non-root"},
 | 
			
		||||
			cmd:      "ls | grep hoge",
 | 
			
		||||
			sudo:     true,
 | 
			
		||||
			expected: "sudo -S ls | grep hoge",
 | 
			
		||||
			expected: "sudo ls | grep hoge",
 | 
			
		||||
		},
 | 
			
		||||
		// -------------docker-------------
 | 
			
		||||
		// root sudo false docker
 | 
			
		||||
@@ -192,7 +192,7 @@ func TestDecorateCmd(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			cmd:      "ls",
 | 
			
		||||
			sudo:     false,
 | 
			
		||||
			expected: `sudo -S lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls'`,
 | 
			
		||||
			expected: `sudo lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls'`,
 | 
			
		||||
		},
 | 
			
		||||
		// non-root sudo true, lxc
 | 
			
		||||
		{
 | 
			
		||||
@@ -203,7 +203,7 @@ func TestDecorateCmd(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			cmd:      "ls",
 | 
			
		||||
			sudo:     true,
 | 
			
		||||
			expected: `sudo -S lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls'`,
 | 
			
		||||
			expected: `sudo lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls'`,
 | 
			
		||||
		},
 | 
			
		||||
		// non-root sudo true lxc
 | 
			
		||||
		{
 | 
			
		||||
@@ -214,7 +214,7 @@ func TestDecorateCmd(t *testing.T) {
 | 
			
		||||
			},
 | 
			
		||||
			cmd:      "ls | grep hoge",
 | 
			
		||||
			sudo:     true,
 | 
			
		||||
			expected: `sudo -S lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls | grep hoge'`,
 | 
			
		||||
			expected: `sudo lxc-attach -n def 2>/dev/null -- /bin/sh -c 'ls | grep hoge'`,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@ import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/constant"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
@@ -104,6 +106,74 @@ func TestViaHTTP(t *testing.T) {
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			header: map[string]string{
 | 
			
		||||
				"X-Vuls-OS-Family": "windows",
 | 
			
		||||
			},
 | 
			
		||||
			body: `
 | 
			
		||||
Host Name:                 DESKTOP
 | 
			
		||||
OS Name:                   Microsoft Windows 10 Pro
 | 
			
		||||
OS Version:                10.0.19044 N/A Build 19044
 | 
			
		||||
OS Manufacturer:           Microsoft Corporation
 | 
			
		||||
OS Configuration:          Member Workstation
 | 
			
		||||
OS Build Type:             Multiprocessor Free
 | 
			
		||||
Registered Owner:          Windows User
 | 
			
		||||
Registered Organization:
 | 
			
		||||
Product ID:                00000-00000-00000-AA000
 | 
			
		||||
Original Install Date:     2022/04/13, 12:25:41
 | 
			
		||||
System Boot Time:          2022/06/06, 16:43:45
 | 
			
		||||
System Manufacturer:       HP
 | 
			
		||||
System Model:              HP EliteBook 830 G7 Notebook PC
 | 
			
		||||
System Type:               x64-based PC
 | 
			
		||||
Processor(s):              1 Processor(s) Installed.
 | 
			
		||||
						   [01]: Intel64 Family 6 Model 142 Stepping 12 GenuineIntel ~1803 Mhz
 | 
			
		||||
BIOS Version:              HP S70 Ver. 01.05.00, 2021/04/26
 | 
			
		||||
Windows Directory:         C:\WINDOWS
 | 
			
		||||
System Directory:          C:\WINDOWS\system32
 | 
			
		||||
Boot Device:               \Device\HarddiskVolume2
 | 
			
		||||
System Locale:             en-us;English (United States)
 | 
			
		||||
Input Locale:              en-us;English (United States)
 | 
			
		||||
Time Zone:                 (UTC-08:00) Pacific Time (US & Canada)
 | 
			
		||||
Total Physical Memory:     15,709 MB
 | 
			
		||||
Available Physical Memory: 12,347 MB
 | 
			
		||||
Virtual Memory: Max Size:  18,141 MB
 | 
			
		||||
Virtual Memory: Available: 14,375 MB
 | 
			
		||||
Virtual Memory: In Use:    3,766 MB
 | 
			
		||||
Page File Location(s):     C:\pagefile.sys
 | 
			
		||||
Domain:                    WORKGROUP
 | 
			
		||||
Logon Server:              \\DESKTOP
 | 
			
		||||
Hotfix(s):                 7 Hotfix(s) Installed.
 | 
			
		||||
						   [01]: KB5012117
 | 
			
		||||
						   [02]: KB4562830
 | 
			
		||||
						   [03]: KB5003791
 | 
			
		||||
						   [04]: KB5007401
 | 
			
		||||
						   [05]: KB5012599
 | 
			
		||||
						   [06]: KB5011651
 | 
			
		||||
						   [07]: KB5005699
 | 
			
		||||
Network Card(s):           1 NIC(s) Installed.
 | 
			
		||||
						   [01]: Intel(R) Wi-Fi 6 AX201 160MHz
 | 
			
		||||
								 Connection Name: Wi-Fi
 | 
			
		||||
								 DHCP Enabled:    Yes
 | 
			
		||||
								 DHCP Server:     192.168.0.1
 | 
			
		||||
								 IP address(es)
 | 
			
		||||
								 [01]: 192.168.0.205
 | 
			
		||||
Hyper-V Requirements:      VM Monitor Mode Extensions: Yes
 | 
			
		||||
						   Virtualization Enabled In Firmware: Yes
 | 
			
		||||
						   Second Level Address Translation: Yes
 | 
			
		||||
						   Data Execution Prevention Available: Yes
 | 
			
		||||
`,
 | 
			
		||||
			expectedResult: models.ScanResult{
 | 
			
		||||
				Family:  "windows",
 | 
			
		||||
				Release: "Windows 10 Version 21H2 for x64-based Systems",
 | 
			
		||||
				RunningKernel: models.Kernel{
 | 
			
		||||
					Version: "10.0.19044",
 | 
			
		||||
				},
 | 
			
		||||
				WindowsKB: &models.WindowsKB{
 | 
			
		||||
					Applied:   []string{"5012117", "4562830", "5003791", "5007401", "5012599", "5011651", "5005699"},
 | 
			
		||||
					Unapplied: []string{},
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
@@ -144,6 +214,18 @@ func TestViaHTTP(t *testing.T) {
 | 
			
		||||
				t.Errorf("release: expected %s, actual %s", expectedPack.Release, pack.Release)
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if tt.expectedResult.WindowsKB != nil {
 | 
			
		||||
			slices.Sort(tt.expectedResult.WindowsKB.Applied)
 | 
			
		||||
			slices.Sort(tt.expectedResult.WindowsKB.Unapplied)
 | 
			
		||||
		}
 | 
			
		||||
		if result.WindowsKB != nil {
 | 
			
		||||
			slices.Sort(result.WindowsKB.Applied)
 | 
			
		||||
			slices.Sort(result.WindowsKB.Unapplied)
 | 
			
		||||
		}
 | 
			
		||||
		if !reflect.DeepEqual(tt.expectedResult.WindowsKB, result.WindowsKB) {
 | 
			
		||||
			t.Errorf("windows KB: expected %s, actual %s", tt.expectedResult.WindowsKB, result.WindowsKB)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -42,7 +42,7 @@ func isRunningKernel(pack models.Package, family string, kernel models.Kernel) (
 | 
			
		||||
 | 
			
		||||
// EnsureResultDir ensures the directory for scan results
 | 
			
		||||
func EnsureResultDir(resultsDir string, scannedAt time.Time) (currentDir string, err error) {
 | 
			
		||||
	jsonDirName := scannedAt.Format(time.RFC3339)
 | 
			
		||||
	jsonDirName := scannedAt.Format("2006-01-02T15-04-05-0700")
 | 
			
		||||
	if resultsDir == "" {
 | 
			
		||||
		wd, _ := os.Getwd()
 | 
			
		||||
		resultsDir = filepath.Join(wd, "results")
 | 
			
		||||
@@ -51,19 +51,6 @@ func EnsureResultDir(resultsDir string, scannedAt time.Time) (currentDir string,
 | 
			
		||||
	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
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4445
									
								
								scanner/windows.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4445
									
								
								scanner/windows.go
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										777
									
								
								scanner/windows_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										777
									
								
								scanner/windows_test.go
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,777 @@
 | 
			
		||||
package scanner
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"reflect"
 | 
			
		||||
	"testing"
 | 
			
		||||
 | 
			
		||||
	"github.com/future-architect/vuls/config"
 | 
			
		||||
	"github.com/future-architect/vuls/models"
 | 
			
		||||
	"golang.org/x/exp/slices"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func Test_parseSystemInfo(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    string
 | 
			
		||||
		osInfo  osInfo
 | 
			
		||||
		kbs     []string
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: `
 | 
			
		||||
Host Name:                 DESKTOP
 | 
			
		||||
OS Name:                   Microsoft Windows 10 Pro
 | 
			
		||||
OS Version:                10.0.19044 N/A Build 19044
 | 
			
		||||
OS Manufacturer:           Microsoft Corporation
 | 
			
		||||
OS Configuration:          Member Workstation
 | 
			
		||||
OS Build Type:             Multiprocessor Free
 | 
			
		||||
Registered Owner:          Windows User
 | 
			
		||||
Registered Organization:
 | 
			
		||||
Product ID:                00000-00000-00000-AA000
 | 
			
		||||
Original Install Date:     2022/04/13, 12:25:41
 | 
			
		||||
System Boot Time:          2022/06/06, 16:43:45
 | 
			
		||||
System Manufacturer:       HP
 | 
			
		||||
System Model:              HP EliteBook 830 G7 Notebook PC
 | 
			
		||||
System Type:               x64-based PC
 | 
			
		||||
Processor(s):              1 Processor(s) Installed.
 | 
			
		||||
						   [01]: Intel64 Family 6 Model 142 Stepping 12 GenuineIntel ~1803 Mhz
 | 
			
		||||
BIOS Version:              HP S70 Ver. 01.05.00, 2021/04/26
 | 
			
		||||
Windows Directory:         C:\WINDOWS
 | 
			
		||||
System Directory:          C:\WINDOWS\system32
 | 
			
		||||
Boot Device:               \Device\HarddiskVolume2
 | 
			
		||||
System Locale:             en-us;English (United States)
 | 
			
		||||
Input Locale:              en-us;English (United States)
 | 
			
		||||
Time Zone:                 (UTC-08:00) Pacific Time (US & Canada)
 | 
			
		||||
Total Physical Memory:     15,709 MB
 | 
			
		||||
Available Physical Memory: 12,347 MB
 | 
			
		||||
Virtual Memory: Max Size:  18,141 MB
 | 
			
		||||
Virtual Memory: Available: 14,375 MB
 | 
			
		||||
Virtual Memory: In Use:    3,766 MB
 | 
			
		||||
Page File Location(s):     C:\pagefile.sys
 | 
			
		||||
Domain:                    WORKGROUP
 | 
			
		||||
Logon Server:              \\DESKTOP
 | 
			
		||||
Hotfix(s):                 7 Hotfix(s) Installed.
 | 
			
		||||
						   [01]: KB5012117
 | 
			
		||||
						   [02]: KB4562830
 | 
			
		||||
						   [03]: KB5003791
 | 
			
		||||
						   [04]: KB5007401
 | 
			
		||||
						   [05]: KB5012599
 | 
			
		||||
						   [06]: KB5011651
 | 
			
		||||
						   [07]: KB5005699
 | 
			
		||||
Network Card(s):           1 NIC(s) Installed.
 | 
			
		||||
						   [01]: Intel(R) Wi-Fi 6 AX201 160MHz
 | 
			
		||||
								 Connection Name: Wi-Fi
 | 
			
		||||
								 DHCP Enabled:    Yes
 | 
			
		||||
								 DHCP Server:     192.168.0.1
 | 
			
		||||
								 IP address(es)
 | 
			
		||||
								 [01]: 192.168.0.205
 | 
			
		||||
Hyper-V Requirements:      VM Monitor Mode Extensions: Yes
 | 
			
		||||
						   Virtualization Enabled In Firmware: Yes
 | 
			
		||||
						   Second Level Address Translation: Yes
 | 
			
		||||
						   Data Execution Prevention Available: Yes
 | 
			
		||||
`,
 | 
			
		||||
			osInfo: osInfo{
 | 
			
		||||
				productName:      "Microsoft Windows 10 Pro",
 | 
			
		||||
				version:          "10.0",
 | 
			
		||||
				build:            "19044",
 | 
			
		||||
				revision:         "",
 | 
			
		||||
				edition:          "",
 | 
			
		||||
				servicePack:      "",
 | 
			
		||||
				arch:             "x64-based",
 | 
			
		||||
				installationType: "Client",
 | 
			
		||||
			},
 | 
			
		||||
			kbs: []string{"5012117", "4562830", "5003791", "5007401", "5012599", "5011651", "5005699"},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			osInfo, kbs, err := parseSystemInfo(tt.args)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("parseSystemInfo() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if osInfo != tt.osInfo {
 | 
			
		||||
				t.Errorf("parseSystemInfo() got = %v, want %v", osInfo, tt.osInfo)
 | 
			
		||||
			}
 | 
			
		||||
			if !reflect.DeepEqual(kbs, tt.kbs) {
 | 
			
		||||
				t.Errorf("parseSystemInfo() got = %v, want %v", kbs, tt.kbs)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_parseGetComputerInfo(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    string
 | 
			
		||||
		want    osInfo
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: `
 | 
			
		||||
WindowsProductName         : Windows 10 Pro
 | 
			
		||||
OsVersion                  : 10.0.19044
 | 
			
		||||
WindowsEditionId           : Professional
 | 
			
		||||
OsCSDVersion               :
 | 
			
		||||
CsSystemType               : x64-based PC
 | 
			
		||||
WindowsInstallationType    : Client
 | 
			
		||||
`,
 | 
			
		||||
			want: osInfo{
 | 
			
		||||
				productName:      "Windows 10 Pro",
 | 
			
		||||
				version:          "10.0",
 | 
			
		||||
				build:            "19044",
 | 
			
		||||
				revision:         "",
 | 
			
		||||
				edition:          "Professional",
 | 
			
		||||
				servicePack:      "",
 | 
			
		||||
				arch:             "x64-based",
 | 
			
		||||
				installationType: "Client",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			got, err := parseGetComputerInfo(tt.args)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("parseGetComputerInfo() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if got != tt.want {
 | 
			
		||||
				t.Errorf("parseGetComputerInfo() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_parseWmiObject(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    string
 | 
			
		||||
		want    osInfo
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: `
 | 
			
		||||
Caption            : Microsoft Windows 10 Pro
 | 
			
		||||
Version            : 10.0.19044
 | 
			
		||||
OperatingSystemSKU : 48
 | 
			
		||||
CSDVersion         :
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DomainRole : 1
 | 
			
		||||
SystemType : x64-based PC`,
 | 
			
		||||
			want: osInfo{
 | 
			
		||||
				productName:      "Microsoft Windows 10 Pro",
 | 
			
		||||
				version:          "10.0",
 | 
			
		||||
				build:            "19044",
 | 
			
		||||
				revision:         "",
 | 
			
		||||
				edition:          "Professional",
 | 
			
		||||
				servicePack:      "",
 | 
			
		||||
				arch:             "x64-based",
 | 
			
		||||
				installationType: "Client",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			got, err := parseWmiObject(tt.args)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("parseWmiObject() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if got != tt.want {
 | 
			
		||||
				t.Errorf("parseWmiObject() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_parseRegistry(t *testing.T) {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		stdout string
 | 
			
		||||
		arch   string
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		want    osInfo
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: args{
 | 
			
		||||
				stdout: `
 | 
			
		||||
ProductName               : Windows 10 Pro
 | 
			
		||||
CurrentVersion            : 6.3
 | 
			
		||||
CurrentMajorVersionNumber : 10
 | 
			
		||||
CurrentMinorVersionNumber : 0
 | 
			
		||||
CurrentBuildNumber        : 19044
 | 
			
		||||
UBR                       : 2364
 | 
			
		||||
EditionID                 : Professional
 | 
			
		||||
InstallationType          : Client`,
 | 
			
		||||
				arch: "AMD64",
 | 
			
		||||
			},
 | 
			
		||||
			want: osInfo{
 | 
			
		||||
				productName:      "Windows 10 Pro",
 | 
			
		||||
				version:          "10.0",
 | 
			
		||||
				build:            "19044",
 | 
			
		||||
				revision:         "2364",
 | 
			
		||||
				edition:          "Professional",
 | 
			
		||||
				servicePack:      "",
 | 
			
		||||
				arch:             "x64-based",
 | 
			
		||||
				installationType: "Client",
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			got, err := parseRegistry(tt.args.stdout, tt.args.arch)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("parseRegistry() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !reflect.DeepEqual(got, tt.want) {
 | 
			
		||||
				t.Errorf("parseRegistry() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_detectOSName(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    osInfo
 | 
			
		||||
		want    string
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "Windows 10 for x64-based Systems",
 | 
			
		||||
			args: osInfo{
 | 
			
		||||
				productName:      "Microsoft Windows 10 Pro",
 | 
			
		||||
				version:          "10.0",
 | 
			
		||||
				build:            "10585",
 | 
			
		||||
				revision:         "",
 | 
			
		||||
				edition:          "Professional",
 | 
			
		||||
				servicePack:      "",
 | 
			
		||||
				arch:             "x64-based",
 | 
			
		||||
				installationType: "Client",
 | 
			
		||||
			},
 | 
			
		||||
			want: "Windows 10 for x64-based Systems",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Windows 10 Version 21H2 for x64-based Systems",
 | 
			
		||||
			args: osInfo{
 | 
			
		||||
				productName:      "Microsoft Windows 10 Pro",
 | 
			
		||||
				version:          "10.0",
 | 
			
		||||
				build:            "19044",
 | 
			
		||||
				revision:         "",
 | 
			
		||||
				edition:          "Professional",
 | 
			
		||||
				servicePack:      "",
 | 
			
		||||
				arch:             "x64-based",
 | 
			
		||||
				installationType: "Client",
 | 
			
		||||
			},
 | 
			
		||||
			want: "Windows 10 Version 21H2 for x64-based Systems",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "Windows Server 2022",
 | 
			
		||||
			args: osInfo{
 | 
			
		||||
				productName:      "Windows Server",
 | 
			
		||||
				version:          "10.0",
 | 
			
		||||
				build:            "30000",
 | 
			
		||||
				revision:         "",
 | 
			
		||||
				edition:          "",
 | 
			
		||||
				servicePack:      "",
 | 
			
		||||
				arch:             "x64-based",
 | 
			
		||||
				installationType: "Server",
 | 
			
		||||
			},
 | 
			
		||||
			want: "Windows Server 2022",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "err",
 | 
			
		||||
			args: osInfo{
 | 
			
		||||
				productName:      "Microsoft Windows 10 Pro",
 | 
			
		||||
				version:          "10.0",
 | 
			
		||||
				build:            "build",
 | 
			
		||||
				revision:         "",
 | 
			
		||||
				edition:          "Professional",
 | 
			
		||||
				servicePack:      "",
 | 
			
		||||
				arch:             "x64-based",
 | 
			
		||||
				installationType: "Client",
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			got, err := detectOSName(tt.args)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("detectOSName() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if got != tt.want {
 | 
			
		||||
				t.Errorf("detectOSName() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_formatKernelVersion(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name string
 | 
			
		||||
		args osInfo
 | 
			
		||||
		want string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "major.minor.build.revision",
 | 
			
		||||
			args: osInfo{
 | 
			
		||||
				version:  "10.0",
 | 
			
		||||
				build:    "19045",
 | 
			
		||||
				revision: "2130",
 | 
			
		||||
			},
 | 
			
		||||
			want: "10.0.19045.2130",
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "major.minor.build",
 | 
			
		||||
			args: osInfo{
 | 
			
		||||
				version: "10.0",
 | 
			
		||||
				build:   "19045",
 | 
			
		||||
			},
 | 
			
		||||
			want: "10.0.19045",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			if got := formatKernelVersion(tt.args); got != tt.want {
 | 
			
		||||
				t.Errorf("formatKernelVersion() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_parseInstalledPackages(t *testing.T) {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		stdout string
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		want    models.Packages
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: args{
 | 
			
		||||
				stdout: `
 | 
			
		||||
Name         : Git
 | 
			
		||||
Version      : 2.35.1.2
 | 
			
		||||
ProviderName : Programs
 | 
			
		||||
 | 
			
		||||
Name         : Oracle Database 11g Express Edition
 | 
			
		||||
Version      : 11.2.0
 | 
			
		||||
ProviderName : msi
 | 
			
		||||
 | 
			
		||||
Name         : 2022-12 x64 ベース システム用 Windows 10 Version 21H2 の累積更新プログラム (KB5021233)
 | 
			
		||||
Version      :
 | 
			
		||||
ProviderName : msu
 | 
			
		||||
`,
 | 
			
		||||
			},
 | 
			
		||||
			want: models.Packages{
 | 
			
		||||
				"Git": {
 | 
			
		||||
					Name:    "Git",
 | 
			
		||||
					Version: "2.35.1.2",
 | 
			
		||||
				},
 | 
			
		||||
				"Oracle Database 11g Express Edition": {
 | 
			
		||||
					Name:    "Oracle Database 11g Express Edition",
 | 
			
		||||
					Version: "11.2.0",
 | 
			
		||||
				},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			o := &windows{}
 | 
			
		||||
			got, _, err := o.parseInstalledPackages(tt.args.stdout)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("windows.parseInstalledPackages() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !reflect.DeepEqual(got, tt.want) {
 | 
			
		||||
				t.Errorf("windows.parseInstalledPackages() got = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_parseGetHotfix(t *testing.T) {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		stdout string
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		want    []string
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: args{
 | 
			
		||||
				stdout: `
 | 
			
		||||
HotFixID : KB5020872
 | 
			
		||||
 | 
			
		||||
HotFixID : KB4562830
 | 
			
		||||
`,
 | 
			
		||||
			},
 | 
			
		||||
			want:    []string{"5020872", "4562830"},
 | 
			
		||||
			wantErr: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			o := &windows{}
 | 
			
		||||
			got, err := o.parseGetHotfix(tt.args.stdout)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("windows.parseGetHotfix() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !reflect.DeepEqual(got, tt.want) {
 | 
			
		||||
				t.Errorf("windows.parseGetHotfix() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_parseGetPackageMSU(t *testing.T) {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		stdout string
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		want    []string
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: args{
 | 
			
		||||
				stdout: `
 | 
			
		||||
Name         : Git
 | 
			
		||||
Version      : 2.35.1.2
 | 
			
		||||
ProviderName : Programs
 | 
			
		||||
 | 
			
		||||
Name         : Oracle Database 11g Express Edition
 | 
			
		||||
Version      : 11.2.0
 | 
			
		||||
ProviderName : msi
 | 
			
		||||
 | 
			
		||||
Name         : 2022-12 x64 ベース システム用 Windows 10 Version 21H2 の累積更新プログラム (KB5021233)
 | 
			
		||||
Version      :
 | 
			
		||||
ProviderName : msu
 | 
			
		||||
`,
 | 
			
		||||
			},
 | 
			
		||||
			want:    []string{"5021233"},
 | 
			
		||||
			wantErr: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			o := &windows{}
 | 
			
		||||
			got, err := o.parseGetPackageMSU(tt.args.stdout)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("windows.parseGetPackageMSU() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !reflect.DeepEqual(got, tt.want) {
 | 
			
		||||
				t.Errorf("windows.parseGetPackageMSU() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_parseWindowsUpdaterSearch(t *testing.T) {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		stdout string
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		want    []string
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: args{
 | 
			
		||||
				stdout: `5012170
 | 
			
		||||
5021233
 | 
			
		||||
5021088
 | 
			
		||||
`,
 | 
			
		||||
			},
 | 
			
		||||
			want:    []string{"5012170", "5021233", "5021088"},
 | 
			
		||||
			wantErr: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			o := &windows{}
 | 
			
		||||
			got, err := o.parseWindowsUpdaterSearch(tt.args.stdout)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("windows.parseWindowsUpdaterSearch() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !reflect.DeepEqual(got, tt.want) {
 | 
			
		||||
				t.Errorf("windows.parseWindowsUpdaterSearch() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_parseWindowsUpdateHistory(t *testing.T) {
 | 
			
		||||
	type args struct {
 | 
			
		||||
		stdout string
 | 
			
		||||
	}
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		args    args
 | 
			
		||||
		want    []string
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "happy",
 | 
			
		||||
			args: args{
 | 
			
		||||
				stdout: `
 | 
			
		||||
Title      : 2022-10 x64 ベース システム用 Windows 10 Version 21H2 の累積更新プログラム (KB5020435)
 | 
			
		||||
Operation  : 1
 | 
			
		||||
ResultCode : 2
 | 
			
		||||
 | 
			
		||||
Title      : 2022-10 x64 ベース システム用 Windows 10 Version 21H2 の累積更新プログラム (KB5020435)
 | 
			
		||||
Operation  : 2
 | 
			
		||||
ResultCode : 2
 | 
			
		||||
 | 
			
		||||
Title      : 2022-12 x64 (KB5021088) 向け Windows 10 Version 21H2 用 .NET Framework 3.5、4.8 および 4.8.1 の累積的な更新プログラム
 | 
			
		||||
Operation  : 1
 | 
			
		||||
ResultCode : 2
 | 
			
		||||
 | 
			
		||||
Title      : 2022-12 x64 ベース システム用 Windows 10 Version 21H2 の累積更新プログラム (KB5021233)
 | 
			
		||||
Operation  : 1
 | 
			
		||||
ResultCode : 2
 | 
			
		||||
`,
 | 
			
		||||
			},
 | 
			
		||||
			want:    []string{"5021088", "5021233"},
 | 
			
		||||
			wantErr: false,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			o := &windows{}
 | 
			
		||||
			got, err := o.parseWindowsUpdateHistory(tt.args.stdout)
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("windows.parseWindowsUpdateHistory() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			slices.Sort(got)
 | 
			
		||||
			if !reflect.DeepEqual(got, tt.want) {
 | 
			
		||||
				t.Errorf("windows.parseWindowsUpdateHistory() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_windows_detectKBsFromKernelVersion(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name    string
 | 
			
		||||
		base    base
 | 
			
		||||
		want    models.WindowsKB
 | 
			
		||||
		wantErr bool
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "10.0.19045.2129",
 | 
			
		||||
			base: base{
 | 
			
		||||
				Distro:     config.Distro{Release: "Windows 10 Version 22H2 for x64-based Systems"},
 | 
			
		||||
				osPackages: osPackages{Kernel: models.Kernel{Version: "10.0.19045.2129"}},
 | 
			
		||||
			},
 | 
			
		||||
			want: models.WindowsKB{
 | 
			
		||||
				Applied:   nil,
 | 
			
		||||
				Unapplied: []string{"5020953", "5019959", "5020030", "5021233", "5022282", "5019275", "5022834", "5022906"},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "10.0.19045.2130",
 | 
			
		||||
			base: base{
 | 
			
		||||
				Distro:     config.Distro{Release: "Windows 10 Version 22H2 for x64-based Systems"},
 | 
			
		||||
				osPackages: osPackages{Kernel: models.Kernel{Version: "10.0.19045.2130"}},
 | 
			
		||||
			},
 | 
			
		||||
			want: models.WindowsKB{
 | 
			
		||||
				Applied:   nil,
 | 
			
		||||
				Unapplied: []string{"5020953", "5019959", "5020030", "5021233", "5022282", "5019275", "5022834", "5022906"},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "10.0.22621.1105",
 | 
			
		||||
			base: base{
 | 
			
		||||
				Distro:     config.Distro{Release: "Windows 11 Version 22H2 for x64-based Systems"},
 | 
			
		||||
				osPackages: osPackages{Kernel: models.Kernel{Version: "10.0.22621.1105"}},
 | 
			
		||||
			},
 | 
			
		||||
			want: models.WindowsKB{
 | 
			
		||||
				Applied:   []string{"5019311", "5017389", "5018427", "5019509", "5018496", "5019980", "5020044", "5021255", "5022303"},
 | 
			
		||||
				Unapplied: []string{"5022360", "5022845"},
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "10.0.20348.1547",
 | 
			
		||||
			base: base{
 | 
			
		||||
				Distro:     config.Distro{Release: "Windows Server 2022"},
 | 
			
		||||
				osPackages: osPackages{Kernel: models.Kernel{Version: "10.0.20348.1547"}},
 | 
			
		||||
			},
 | 
			
		||||
			want: models.WindowsKB{
 | 
			
		||||
				Applied:   []string{"5005575", "5005619", "5006699", "5006745", "5007205", "5007254", "5008223", "5010197", "5009555", "5010796", "5009608", "5010354", "5010421", "5011497", "5011558", "5012604", "5012637", "5013944", "5015013", "5014021", "5014678", "5014665", "5015827", "5015879", "5016627", "5016693", "5017316", "5017381", "5018421", "5020436", "5018485", "5019081", "5021656", "5020032", "5021249", "5022553", "5022291", "5022842"},
 | 
			
		||||
				Unapplied: nil,
 | 
			
		||||
			},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "err",
 | 
			
		||||
			base: base{
 | 
			
		||||
				Distro:     config.Distro{Release: "Windows 10 Version 22H2 for x64-based Systems"},
 | 
			
		||||
				osPackages: osPackages{Kernel: models.Kernel{Version: "10.0"}},
 | 
			
		||||
			},
 | 
			
		||||
			wantErr: true,
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			o := &windows{
 | 
			
		||||
				base: tt.base,
 | 
			
		||||
			}
 | 
			
		||||
			got, err := o.detectKBsFromKernelVersion()
 | 
			
		||||
			if (err != nil) != tt.wantErr {
 | 
			
		||||
				t.Errorf("windows.detectKBsFromKernelVersion() error = %v, wantErr %v", err, tt.wantErr)
 | 
			
		||||
				return
 | 
			
		||||
			}
 | 
			
		||||
			if !reflect.DeepEqual(got, tt.want) {
 | 
			
		||||
				t.Errorf("windows.detectKBsFromKernelVersion() = %v, want %v", got, tt.want)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func Test_windows_parseIP(t *testing.T) {
 | 
			
		||||
	tests := []struct {
 | 
			
		||||
		name      string
 | 
			
		||||
		args      string
 | 
			
		||||
		ipv4Addrs []string
 | 
			
		||||
		ipv6Addrs []string
 | 
			
		||||
	}{
 | 
			
		||||
		{
 | 
			
		||||
			name: "en",
 | 
			
		||||
			args: `
 | 
			
		||||
 | 
			
		||||
Windows IP Configuration
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
Ethernet adapter イーサネット 4:
 | 
			
		||||
 | 
			
		||||
   Connection-specific DNS Suffix  . : vuls.local
 | 
			
		||||
   Link-local IPv6 Address . . . . . : fe80::19b6:ae27:d1fe:2041%33
 | 
			
		||||
   Link-local IPv6 Address . . . . . : fe80::7080:8828:5cc8:c0ba%33
 | 
			
		||||
   IPv4 Address. . . . . . . . . . . : 10.145.8.50
 | 
			
		||||
   Subnet Mask . . . . . . . . . . . : 255.255.0.0
 | 
			
		||||
   Default Gateway . . . . . . . . . : ::
 | 
			
		||||
 | 
			
		||||
Ethernet adapter イーサネット 2:
 | 
			
		||||
 | 
			
		||||
   Connection-specific DNS Suffix  . :
 | 
			
		||||
   Link-local IPv6 Address . . . . . : fe80::f49d:2c16:4270:759d%9
 | 
			
		||||
   IPv4 Address. . . . . . . . . . . : 192.168.56.1
 | 
			
		||||
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
 | 
			
		||||
   Default Gateway . . . . . . . . . :
 | 
			
		||||
 | 
			
		||||
Wireless LAN adapter ローカル エリア接続* 1:
 | 
			
		||||
 | 
			
		||||
   Media State . . . . . . . . . . . : Media disconnected
 | 
			
		||||
   Connection-specific DNS Suffix  . :
 | 
			
		||||
 | 
			
		||||
Wireless LAN adapter ローカル エリア接続* 2:
 | 
			
		||||
 | 
			
		||||
   Media State . . . . . . . . . . . : Media disconnected
 | 
			
		||||
   Connection-specific DNS Suffix  . :
 | 
			
		||||
 | 
			
		||||
Wireless LAN adapter Wi-Fi:
 | 
			
		||||
 | 
			
		||||
   Connection-specific DNS Suffix  . :
 | 
			
		||||
   IPv4 Address. . . . . . . . . . . : 192.168.0.205
 | 
			
		||||
   Subnet Mask . . . . . . . . . . . : 255.255.255.0
 | 
			
		||||
   Default Gateway . . . . . . . . . : 192.168.0.1
 | 
			
		||||
 | 
			
		||||
Ethernet adapter Bluetooth ネットワーク接続:
 | 
			
		||||
 | 
			
		||||
   Media State . . . . . . . . . . . : Media disconnected
 | 
			
		||||
   Connection-specific DNS Suffix  . :
 | 
			
		||||
`,
 | 
			
		||||
			ipv4Addrs: []string{"10.145.8.50", "192.168.56.1", "192.168.0.205"},
 | 
			
		||||
			ipv6Addrs: []string{"fe80::19b6:ae27:d1fe:2041", "fe80::7080:8828:5cc8:c0ba", "fe80::f49d:2c16:4270:759d"},
 | 
			
		||||
		},
 | 
			
		||||
		{
 | 
			
		||||
			name: "ja",
 | 
			
		||||
			args: `
 | 
			
		||||
 | 
			
		||||
Windows IP 構成
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
イーサネット アダプター イーサネット 4:
 | 
			
		||||
 | 
			
		||||
   接続固有の DNS サフィックス . . . . .: future.co.jp
 | 
			
		||||
   リンクローカル IPv6 アドレス. . . . .: fe80::19b6:ae27:d1fe:2041%33
 | 
			
		||||
   リンクローカル IPv6 アドレス. . . . .: fe80::7080:8828:5cc8:c0ba%33
 | 
			
		||||
   IPv4 アドレス . . . . . . . . . . . .: 10.145.8.50
 | 
			
		||||
   サブネット マスク . . . . . . . . . .: 255.255.0.0
 | 
			
		||||
   デフォルト ゲートウェイ . . . . . . .: ::
 | 
			
		||||
 | 
			
		||||
イーサネット アダプター イーサネット 2:
 | 
			
		||||
 | 
			
		||||
   接続固有の DNS サフィックス . . . . .:
 | 
			
		||||
   リンクローカル IPv6 アドレス. . . . .: fe80::f49d:2c16:4270:759d%9
 | 
			
		||||
   IPv4 アドレス . . . . . . . . . . . .: 192.168.56.1
 | 
			
		||||
   サブネット マスク . . . . . . . . . .: 255.255.255.0
 | 
			
		||||
   デフォルト ゲートウェイ . . . . . . .:
 | 
			
		||||
 | 
			
		||||
Wireless LAN adapter ローカル エリア接続* 1:
 | 
			
		||||
 | 
			
		||||
   メディアの状態. . . . . . . . . . . .: メディアは接続されていません
 | 
			
		||||
   接続固有の DNS サフィックス . . . . .:
 | 
			
		||||
 | 
			
		||||
Wireless LAN adapter ローカル エリア接続* 2:
 | 
			
		||||
 | 
			
		||||
   メディアの状態. . . . . . . . . . . .: メディアは接続されていません
 | 
			
		||||
   接続固有の DNS サフィックス . . . . .:
 | 
			
		||||
 | 
			
		||||
Wireless LAN adapter Wi-Fi:
 | 
			
		||||
 | 
			
		||||
   接続固有の DNS サフィックス . . . . .:
 | 
			
		||||
   IPv4 アドレス . . . . . . . . . . . .: 192.168.0.205
 | 
			
		||||
   サブネット マスク . . . . . . . . . .: 255.255.255.0
 | 
			
		||||
   デフォルト ゲートウェイ . . . . . . .: 192.168.0.1
 | 
			
		||||
 | 
			
		||||
イーサネット アダプター Bluetooth ネットワーク接続:
 | 
			
		||||
 | 
			
		||||
   メディアの状態. . . . . . . . . . . .: メディアは接続されていません
 | 
			
		||||
   接続固有の DNS サフィックス . . . . .:
 | 
			
		||||
`,
 | 
			
		||||
			ipv4Addrs: []string{"10.145.8.50", "192.168.56.1", "192.168.0.205"},
 | 
			
		||||
			ipv6Addrs: []string{"fe80::19b6:ae27:d1fe:2041", "fe80::7080:8828:5cc8:c0ba", "fe80::f49d:2c16:4270:759d"},
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	for _, tt := range tests {
 | 
			
		||||
		t.Run(tt.name, func(t *testing.T) {
 | 
			
		||||
			gotIPv4s, gotIPv6s := (&windows{}).parseIP(tt.args)
 | 
			
		||||
			if !reflect.DeepEqual(gotIPv4s, tt.ipv4Addrs) {
 | 
			
		||||
				t.Errorf("windows.parseIP() got = %v, want %v", gotIPv4s, tt.ipv4Addrs)
 | 
			
		||||
			}
 | 
			
		||||
			if !reflect.DeepEqual(gotIPv6s, tt.ipv6Addrs) {
 | 
			
		||||
				t.Errorf("windows.parseIP() got = %v, want %v", gotIPv6s, tt.ipv6Addrs)
 | 
			
		||||
			}
 | 
			
		||||
		})
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user