From d8495c0da681a3591264440e11f7d3b1b77e37ff Mon Sep 17 00:00:00 2001 From: MaineK00n Date: Wed, 11 Jan 2023 16:15:43 +0900 Subject: [PATCH] feat(windows): support Windows --- .github/workflows/goreleaser.yml | 20 +- .goreleaser.yml | 43 +- README.md | 4 +- config/config.go | 9 +- config/config_windows.go | 350 +++ config/os.go | 82 + config/os_test.go | 16 + config/syslogconf.go | 2 + config/tomlloader.go | 7 + config/windows.go | 27 + detector/detector.go | 4 +- detector/util.go | 28 +- go.mod | 9 +- go.sum | 10 +- gost/microsoft.go | 321 ++- gost/util.go | 9 + models/scanresults.go | 7 + models/vulninfos.go | 11 +- models/vulninfos_test.go | 100 + reporter/syslog.go | 2 + reporter/util.go | 40 +- scanner/base.go | 2 + scanner/debian.go | 20 +- scanner/executil.go | 106 +- scanner/scanner.go | 233 +- scanner/scanner_test.go | 82 + scanner/utils.go | 15 +- scanner/windows.go | 4408 ++++++++++++++++++++++++++++++ scanner/windows_test.go | 736 +++++ server/server.go | 7 +- subcmds/discover.go | 4 + subcmds/report.go | 3 +- subcmds/report_windows.go | 372 +++ tui/tui.go | 1 + 34 files changed, 6806 insertions(+), 284 deletions(-) create mode 100644 config/config_windows.go create mode 100644 config/windows.go create mode 100644 scanner/windows.go create mode 100644 scanner/windows_test.go create mode 100644 subcmds/report_windows.go diff --git a/.github/workflows/goreleaser.yml b/.github/workflows/goreleaser.yml index ea5ea805e3..fa91a7178c 100644 --- a/.github/workflows/goreleaser.yml +++ b/.github/workflows/goreleaser.yml @@ -12,9 +12,6 @@ jobs: - name: Checkout uses: actions/checkout@v3 - - - name: install package for cross compile - run: sudo apt update && sudo apt install -y gcc-aarch64-linux-gnu - name: Unshallow run: git fetch --prune --unshallow @@ -22,13 +19,16 @@ jobs: name: Set up Go uses: actions/setup-go@v3 with: - go-version: 1.18 + go-version-file: go.mod - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2 - with: - version: latest - args: release --rm-dist - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + docker run --rm \ + -e CGO_ENABLED=1 \ + -e GITHUB_TOKEN=${{ secrets.GITHUB_TOKEN }} \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v `pwd`:/go/src/github.com/future-architect/vuls \ + -w /go/src/github.com/future-architect/vuls \ + ghcr.io/goreleaser/goreleaser-cross:v1.20 \ + release --clean diff --git a/.goreleaser.yml b/.goreleaser.yml index 91bdf2d9d8..1d8a41dc45 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -6,7 +6,7 @@ release: owner: future-architect name: vuls builds: -- id: vuls-amd64 +- id: vuls-linux-amd64 goos: - linux goarch: @@ -21,7 +21,7 @@ builds: - -s -w -X github.com/future-architect/vuls/config.Version={{.Version}} -X github.com/future-architect/vuls/config.Revision={{.Commit}}-{{ .CommitDate }} binary: vuls -- id: vuls-arm64 +- id: vuls-linux-arm64 goos: - linux goarch: @@ -36,11 +36,42 @@ builds: - -s -w -X github.com/future-architect/vuls/config.Version={{.Version}} -X github.com/future-architect/vuls/config.Revision={{.Commit}}-{{ .CommitDate }} binary: vuls +- id: vuls-windows-amd64 + goos: + - windows + goarch: + - amd64 + env: + - CGO_ENABLED=1 + - CC=x86_64-w64-mingw32-gcc + main: ./cmd/vuls/main.go + flags: + - -a + ldflags: + - -s -w -X github.com/future-architect/vuls/config.Version={{.Version}} -X github.com/future-architect/vuls/config.Revision={{.Commit}}-{{ .CommitDate }} + binary: vuls + +- id: vuls-windows-arm64 + goos: + - windows + goarch: + - arm64 + env: + - CGO_ENABLED=1 + - CC=/llvm-mingw/bin/aarch64-w64-mingw32-gcc + main: ./cmd/vuls/main.go + flags: + - -a + ldflags: + - -s -w -X github.com/future-architect/vuls/config.Version={{.Version}} -X github.com/future-architect/vuls/config.Revision={{.Commit}}-{{ .CommitDate }} + binary: vuls + - id: vuls-scanner env: - CGO_ENABLED=0 goos: - linux + - windows goarch: - 386 - amd64 @@ -60,6 +91,7 @@ builds: - CGO_ENABLED=0 goos: - linux + - windows goarch: - 386 - amd64 @@ -75,6 +107,7 @@ builds: - CGO_ENABLED=0 goos: - linux + - windows goarch: - 386 - amd64 @@ -92,8 +125,10 @@ archives: - id: vuls name_template: '{{ .Binary }}_{{.Version}}_{{ .Os }}_{{ .Arch }}{{ if .Arm }}v{{ .Arm }}{{ end }}' builds: - - vuls-amd64 - - vuls-arm64 + - vuls-linux-amd64 + - vuls-linux-arm64 + - vuls-windows-amd64 + - vuls-windows-arm64 format: tar.gz files: - LICENSE diff --git a/README.md b/README.md index 37769d30ab..afca825692 100644 --- a/README.md +++ b/README.md @@ -48,10 +48,11 @@ Vuls is a tool created to solve the problems listed above. It has the following ### Scan for any vulnerabilities in Linux/FreeBSD Server -[Supports major Linux/FreeBSD](https://vuls.io/docs/en/supported-os.html) +[Supports major Linux/FreeBSD/Windows](https://vuls.io/docs/en/supported-os.html) - Alpine, Amazon Linux, CentOS, AlmaLinux, Rocky Linux, Debian, Oracle Linux, Raspbian, RHEL, openSUSE, openSUSE Leap, SUSE Enterprise Linux, Fedora, and Ubuntu - FreeBSD +- Windows - Cloud, on-premise, Running Docker Container ### High-quality scan @@ -72,6 +73,7 @@ Vuls is a tool created to solve the problems listed above. It has the following - [Red Hat Security Advisories](https://access.redhat.com/security/security-updates/) - [Debian Security Bug Tracker](https://security-tracker.debian.org/tracker/) - [Ubuntu CVE Tracker](https://people.canonical.com/~ubuntu-security/cve/) + - [Microsoft CVRF](https://api.msrc.microsoft.com/cvrf/v2.0/swagger/index) - Commands(yum, zypper, pkg-audit) - RHSA / ALAS / ELSA / FreeBSD-SA diff --git a/config/config.go b/config/config.go index 13667f546c..5c18b0b046 100644 --- a/config/config.go +++ b/config/config.go @@ -1,3 +1,5 @@ +//go:build !windows + package config import ( @@ -7,9 +9,10 @@ import ( "strings" "github.com/asaskevich/govalidator" + "golang.org/x/xerrors" + "github.com/future-architect/vuls/constant" "github.com/future-architect/vuls/logging" - "golang.org/x/xerrors" ) // Version of Vuls @@ -117,6 +120,9 @@ func (c Config) ValidateOnScan() bool { if es := server.PortScan.Validate(); 0 < len(es) { errs = append(errs, es...) } + if es := server.Windows.Validate(); 0 < len(es) { + errs = append(errs, es...) + } } for _, err := range errs { @@ -245,6 +251,7 @@ type ServerInfo struct { IgnoredJSONKeys []string `toml:"ignoredJSONKeys,omitempty" json:"ignoredJSONKeys,omitempty"` WordPress *WordPressConf `toml:"wordpress,omitempty" json:"wordpress,omitempty"` PortScan *PortScanConf `toml:"portscan,omitempty" json:"portscan,omitempty"` + Windows *WindowsConf `toml:"windows,omitempty" json:"windows,omitempty"` IPv4Addrs []string `toml:"-" json:"ipv4Addrs,omitempty"` IPv6Addrs []string `toml:"-" json:"ipv6Addrs,omitempty"` diff --git a/config/config_windows.go b/config/config_windows.go new file mode 100644 index 0000000000..adce777957 --- /dev/null +++ b/config/config_windows.go @@ -0,0 +1,350 @@ +//go:build windows + +package config + +import ( + "fmt" + "os" + "strconv" + "strings" + + "github.com/asaskevich/govalidator" + "golang.org/x/xerrors" + + "github.com/future-architect/vuls/constant" + "github.com/future-architect/vuls/logging" +) + +// Version of Vuls +var Version = "`make build` or `make install` will show the version" + +// Revision of Git +var Revision string + +// Conf has Configuration +var Conf Config + +// Config is struct of Configuration +type Config struct { + logging.LogOpts + + // scan, report + HTTPProxy string `valid:"url" json:"httpProxy,omitempty"` + ResultsDir string `json:"resultsDir,omitempty"` + Pipe bool `json:"pipe,omitempty"` + + Default ServerInfo `json:"default,omitempty"` + Servers map[string]ServerInfo `json:"servers,omitempty"` + + ScanOpts + + // report + CveDict GoCveDictConf `json:"cveDict,omitempty"` + OvalDict GovalDictConf `json:"ovalDict,omitempty"` + Gost GostConf `json:"gost,omitempty"` + Exploit ExploitConf `json:"exploit,omitempty"` + Metasploit MetasploitConf `json:"metasploit,omitempty"` + KEVuln KEVulnConf `json:"kevuln,omitempty"` + Cti CtiConf `json:"cti,omitempty"` + + Slack SlackConf `json:"-"` + EMail SMTPConf `json:"-"` + HTTP HTTPConf `json:"-"` + AWS AWSConf `json:"-"` + Azure AzureConf `json:"-"` + ChatWork ChatWorkConf `json:"-"` + GoogleChat GoogleChatConf `json:"-"` + Telegram TelegramConf `json:"-"` + WpScan WpScanConf `json:"-"` + Saas SaasConf `json:"-"` + + ReportOpts +} + +// ReportConf is an interface to Validate Report Config +type ReportConf interface { + Validate() []error +} + +// ScanOpts is options for scan +type ScanOpts struct { + Vvv bool `json:"vvv,omitempty"` +} + +// ReportOpts is options for report +type ReportOpts struct { + CvssScoreOver float64 `json:"cvssScoreOver,omitempty"` + ConfidenceScoreOver int `json:"confidenceScoreOver,omitempty"` + TrivyCacheDBDir string `json:"trivyCacheDBDir,omitempty"` + NoProgress bool `json:"noProgress,omitempty"` + RefreshCve bool `json:"refreshCve,omitempty"` + IgnoreUnfixed bool `json:"ignoreUnfixed,omitempty"` + IgnoreUnscoredCves bool `json:"ignoreUnscoredCves,omitempty"` + DiffPlus bool `json:"diffPlus,omitempty"` + DiffMinus bool `json:"diffMinus,omitempty"` + Diff bool `json:"diff,omitempty"` + Lang string `json:"lang,omitempty"` +} + +// ValidateOnConfigtest validates +func (c Config) ValidateOnConfigtest() bool { + errs := c.checkSSHKeyExist() + if _, err := govalidator.ValidateStruct(c); err != nil { + errs = append(errs, err) + } + for _, err := range errs { + logging.Log.Error(err) + } + return len(errs) == 0 +} + +// ValidateOnScan validates configuration +func (c Config) ValidateOnScan() bool { + errs := c.checkSSHKeyExist() + if len(c.ResultsDir) != 0 { + if ok, _ := govalidator.IsFilePath(c.ResultsDir); !ok { + errs = append(errs, xerrors.Errorf( + "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) + } + } + + if _, err := govalidator.ValidateStruct(c); err != nil { + errs = append(errs, err) + } + + for _, server := range c.Servers { + if !server.Module.IsScanPort() { + continue + } + if es := server.PortScan.Validate(); 0 < len(es) { + errs = append(errs, es...) + } + if es := server.Windows.Validate(); 0 < len(es) { + errs = append(errs, es...) + } + } + + for _, err := range errs { + logging.Log.Error(err) + } + return len(errs) == 0 +} + +func (c Config) checkSSHKeyExist() (errs []error) { + for serverName, v := range c.Servers { + if v.Type == constant.ServerTypePseudo { + continue + } + if v.KeyPath != "" { + if _, err := os.Stat(v.KeyPath); err != nil { + errs = append(errs, xerrors.Errorf( + "%s is invalid. keypath: %s not exists", serverName, v.KeyPath)) + } + } + } + return errs +} + +// ValidateOnReport validates configuration +func (c *Config) ValidateOnReport() bool { + errs := []error{} + + if len(c.ResultsDir) != 0 { + if ok, _ := govalidator.IsFilePath(c.ResultsDir); !ok { + errs = append(errs, xerrors.Errorf( + "JSON base directory must be a *Absolute* file path. -results-dir: %s", c.ResultsDir)) + } + } + + _, err := govalidator.ValidateStruct(c) + if err != nil { + errs = append(errs, err) + } + + for _, rc := range []ReportConf{ + &c.EMail, + &c.Slack, + &c.ChatWork, + &c.GoogleChat, + &c.Telegram, + &c.HTTP, + &c.AWS, + &c.Azure, + } { + if es := rc.Validate(); 0 < len(es) { + errs = append(errs, es...) + } + } + + for _, cnf := range []VulnDictInterface{ + &Conf.CveDict, + &Conf.OvalDict, + &Conf.Gost, + &Conf.Exploit, + &Conf.Metasploit, + &Conf.KEVuln, + &Conf.Cti, + } { + if err := cnf.Validate(); err != nil { + errs = append(errs, xerrors.Errorf("Failed to validate %s: %+v", cnf.GetName(), err)) + } + if err := cnf.CheckHTTPHealth(); err != nil { + errs = append(errs, xerrors.Errorf("Run %s as server mode before reporting: %+v", cnf.GetName(), err)) + } + } + + for _, err := range errs { + logging.Log.Error(err) + } + + return len(errs) == 0 +} + +// ValidateOnSaaS validates configuration +func (c Config) ValidateOnSaaS() bool { + saaserrs := c.Saas.Validate() + for _, err := range saaserrs { + logging.Log.Error("Failed to validate SaaS conf: %+w", err) + } + return len(saaserrs) == 0 +} + +// WpScanConf is wpscan.com config +type WpScanConf struct { + Token string `toml:"token,omitempty" json:"-"` + DetectInactive bool `toml:"detectInactive,omitempty" json:"detectInactive,omitempty"` +} + +// ServerInfo has SSH Info, additional CPE packages to scan. +type ServerInfo struct { + BaseName string `toml:"-" json:"-"` + ServerName string `toml:"-" json:"serverName,omitempty"` + User string `toml:"user,omitempty" json:"user,omitempty"` + Host string `toml:"host,omitempty" json:"host,omitempty"` + IgnoreIPAddresses []string `toml:"ignoreIPAddresses,omitempty" json:"ignoreIPAddresses,omitempty"` + JumpServer []string `toml:"jumpServer,omitempty" json:"jumpServer,omitempty"` + Port string `toml:"port,omitempty" json:"port,omitempty"` + SSHConfigPath string `toml:"sshConfigPath,omitempty" json:"sshConfigPath,omitempty"` + KeyPath string `toml:"keyPath,omitempty" json:"keyPath,omitempty"` + CpeNames []string `toml:"cpeNames,omitempty" json:"cpeNames,omitempty"` + ScanMode []string `toml:"scanMode,omitempty" json:"scanMode,omitempty"` + ScanModules []string `toml:"scanModules,omitempty" json:"scanModules,omitempty"` + OwaspDCXMLPath string `toml:"owaspDCXMLPath,omitempty" json:"owaspDCXMLPath,omitempty"` + ContainersOnly bool `toml:"containersOnly,omitempty" json:"containersOnly,omitempty"` + ContainersIncluded []string `toml:"containersIncluded,omitempty" json:"containersIncluded,omitempty"` + ContainersExcluded []string `toml:"containersExcluded,omitempty" json:"containersExcluded,omitempty"` + ContainerType string `toml:"containerType,omitempty" json:"containerType,omitempty"` + Containers map[string]ContainerSetting `toml:"containers,omitempty" json:"containers,omitempty"` + IgnoreCves []string `toml:"ignoreCves,omitempty" json:"ignoreCves,omitempty"` + IgnorePkgsRegexp []string `toml:"ignorePkgsRegexp,omitempty" json:"ignorePkgsRegexp,omitempty"` + GitHubRepos map[string]GitHubConf `toml:"githubs" json:"githubs,omitempty"` // key: owner/repo + UUIDs map[string]string `toml:"uuids,omitempty" json:"uuids,omitempty"` + Memo string `toml:"memo,omitempty" json:"memo,omitempty"` + Enablerepo []string `toml:"enablerepo,omitempty" json:"enablerepo,omitempty"` // For CentOS, Alma, Rocky, RHEL, Amazon + Optional map[string]interface{} `toml:"optional,omitempty" json:"optional,omitempty"` // Optional key-value set that will be outputted to JSON + Lockfiles []string `toml:"lockfiles,omitempty" json:"lockfiles,omitempty"` // ie) path/to/package-lock.json + FindLock bool `toml:"findLock,omitempty" json:"findLock,omitempty"` + FindLockDirs []string `toml:"findLockDirs,omitempty" json:"findLockDirs,omitempty"` + Type string `toml:"type,omitempty" json:"type,omitempty"` // "pseudo" or "" + IgnoredJSONKeys []string `toml:"ignoredJSONKeys,omitempty" json:"ignoredJSONKeys,omitempty"` + WordPress *WordPressConf `toml:"wordpress,omitempty" json:"wordpress,omitempty"` + PortScan *PortScanConf `toml:"portscan,omitempty" json:"portscan,omitempty"` + Windows *WindowsConf `toml:"windows,omitempty" json:"windows,omitempty"` + + IPv4Addrs []string `toml:"-" json:"ipv4Addrs,omitempty"` + IPv6Addrs []string `toml:"-" json:"ipv6Addrs,omitempty"` + IPSIdentifiers map[string]string `toml:"-" json:"ipsIdentifiers,omitempty"` + + // internal use + LogMsgAnsiColor string `toml:"-" json:"-"` // DebugLog Color + Container Container `toml:"-" json:"-"` + Distro Distro `toml:"-" json:"-"` + Mode ScanMode `toml:"-" json:"-"` + Module ScanModule `toml:"-" json:"-"` +} + +// ContainerSetting is used for loading container setting in config.toml +type ContainerSetting struct { + Cpes []string `json:"cpes,omitempty"` + OwaspDCXMLPath string `json:"owaspDCXMLPath,omitempty"` + IgnorePkgsRegexp []string `json:"ignorePkgsRegexp,omitempty"` + IgnoreCves []string `json:"ignoreCves,omitempty"` +} + +// WordPressConf used for WordPress Scanning +type WordPressConf struct { + OSUser string `toml:"osUser,omitempty" json:"osUser,omitempty"` + DocRoot string `toml:"docRoot,omitempty" json:"docRoot,omitempty"` + CmdPath string `toml:"cmdPath,omitempty" json:"cmdPath,omitempty"` +} + +// IsZero return whether this struct is not specified in config.toml +func (cnf WordPressConf) IsZero() bool { + return cnf.OSUser == "" && cnf.DocRoot == "" && cnf.CmdPath == "" +} + +// GitHubConf is used for GitHub Security Alerts +type GitHubConf struct { + Token string `json:"-"` + IgnoreGitHubDismissed bool `json:"ignoreGitHubDismissed,omitempty"` +} + +// GetServerName returns ServerName if this serverInfo is about host. +// If this serverInfo is about a container, returns containerID@ServerName +func (s ServerInfo) GetServerName() string { + if len(s.Container.ContainerID) == 0 { + return s.ServerName + } + return fmt.Sprintf("%s@%s", s.Container.Name, s.ServerName) +} + +// Distro has distribution info +type Distro struct { + Family string + Release string +} + +func (l Distro) String() string { + return fmt.Sprintf("%s %s", l.Family, l.Release) +} + +// MajorVersion returns Major version +func (l Distro) MajorVersion() (int, error) { + switch l.Family { + case constant.Amazon: + return strconv.Atoi(getAmazonLinuxVersion(l.Release)) + case constant.CentOS: + if 0 < len(l.Release) { + return strconv.Atoi(strings.Split(strings.TrimPrefix(l.Release, "stream"), ".")[0]) + } + case constant.OpenSUSE: + if l.Release != "" { + if l.Release == "tumbleweed" { + return 0, nil + } + return strconv.Atoi(strings.Split(l.Release, ".")[0]) + } + default: + if 0 < len(l.Release) { + return strconv.Atoi(strings.Split(l.Release, ".")[0]) + } + } + return 0, xerrors.New("Release is empty") +} + +// IsContainer returns whether this ServerInfo is about container +func (s ServerInfo) IsContainer() bool { + return 0 < len(s.Container.ContainerID) +} + +// SetContainer set container +func (s *ServerInfo) SetContainer(d Container) { + s.Container = d +} + +// Container has Container information. +type Container struct { + ContainerID string + Name string + Image string +} diff --git a/config/os.go b/config/os.go index ba958d1302..59cdd5b113 100644 --- a/config/os.go +++ b/config/os.go @@ -311,6 +311,88 @@ func GetEOL(family, release string) (eol EOL, found bool) { "36": {StandardSupportUntil: time.Date(2023, 5, 16, 23, 59, 59, 0, time.UTC)}, "37": {StandardSupportUntil: time.Date(2023, 12, 15, 23, 59, 59, 0, time.UTC)}, }[major(release)] + case constant.Windows: + // https://learn.microsoft.com/ja-jp/lifecycle/products/?products=windows + + lhs, rhs, _ := strings.Cut(strings.TrimSuffix(release, "(Server Core installation)"), "for") + switch strings.TrimSpace(lhs) { + case "Windows 7": + eol, found = EOL{StandardSupportUntil: time.Date(2013, 4, 9, 23, 59, 59, 0, time.UTC)}, true + if strings.Contains(rhs, "Service Pack 1") { + eol, found = EOL{StandardSupportUntil: time.Date(2020, 1, 14, 23, 59, 59, 0, time.UTC)}, true + } + case "Windows 8": + eol, found = EOL{StandardSupportUntil: time.Date(2016, 1, 12, 23, 59, 59, 0, time.UTC)}, true + case "Windows 8.1": + eol, found = EOL{StandardSupportUntil: time.Date(2023, 1, 10, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10": + eol, found = EOL{StandardSupportUntil: time.Date(2017, 5, 9, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 1511": + eol, found = EOL{StandardSupportUntil: time.Date(2017, 10, 10, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 1607": + eol, found = EOL{StandardSupportUntil: time.Date(2018, 4, 10, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 1703": + eol, found = EOL{StandardSupportUntil: time.Date(2018, 10, 9, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 1709": + eol, found = EOL{StandardSupportUntil: time.Date(2019, 4, 9, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 1803": + eol, found = EOL{StandardSupportUntil: time.Date(2019, 11, 12, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 1809": + eol, found = EOL{StandardSupportUntil: time.Date(2020, 11, 10, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 1903": + eol, found = EOL{StandardSupportUntil: time.Date(2020, 12, 8, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 1909": + eol, found = EOL{StandardSupportUntil: time.Date(2021, 5, 11, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 2004": + eol, found = EOL{StandardSupportUntil: time.Date(2021, 12, 14, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 20H2": + eol, found = EOL{StandardSupportUntil: time.Date(2022, 5, 10, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 21H1": + eol, found = EOL{StandardSupportUntil: time.Date(2022, 12, 13, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 21H2": + eol, found = EOL{StandardSupportUntil: time.Date(2023, 6, 13, 23, 59, 59, 0, time.UTC)}, true + case "Windows 10 Version 22H2": + eol, found = EOL{StandardSupportUntil: time.Date(2024, 5, 14, 23, 59, 59, 0, time.UTC)}, true + case "Windows 11 Version 21H2": + eol, found = EOL{StandardSupportUntil: time.Date(2024, 10, 8, 23, 59, 59, 0, time.UTC)}, true + case "Windows 11 Version 22H2": + eol, found = EOL{StandardSupportUntil: time.Date(2025, 10, 14, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server 2008": + eol, found = EOL{StandardSupportUntil: time.Date(2011, 7, 12, 23, 59, 59, 0, time.UTC)}, true + if strings.Contains(rhs, "Service Pack 2") { + eol, found = EOL{StandardSupportUntil: time.Date(2020, 1, 14, 23, 59, 59, 0, time.UTC)}, true + } + case "Windows Server 2008 R2": + eol, found = EOL{StandardSupportUntil: time.Date(2013, 4, 9, 23, 59, 59, 0, time.UTC)}, true + if strings.Contains(rhs, "Service Pack 1") { + eol, found = EOL{StandardSupportUntil: time.Date(2020, 1, 14, 23, 59, 59, 0, time.UTC)}, true + } + case "Windows Server 2012": + eol, found = EOL{StandardSupportUntil: time.Date(2023, 10, 10, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server 2012 R2": + eol, found = EOL{StandardSupportUntil: time.Date(2023, 10, 10, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server 2016": + eol, found = EOL{StandardSupportUntil: time.Date(2027, 1, 12, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server, Version 1709": + eol, found = EOL{StandardSupportUntil: time.Date(2019, 4, 9, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server, Version 1803": + eol, found = EOL{StandardSupportUntil: time.Date(2019, 11, 12, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server, Version 1809": + eol, found = EOL{StandardSupportUntil: time.Date(2020, 11, 10, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server 2019": + eol, found = EOL{StandardSupportUntil: time.Date(2029, 1, 9, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server, Version 1903": + eol, found = EOL{StandardSupportUntil: time.Date(2020, 12, 8, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server, Version 1909": + eol, found = EOL{StandardSupportUntil: time.Date(2021, 5, 11, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server, Version 2004": + eol, found = EOL{StandardSupportUntil: time.Date(2021, 12, 14, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server, Version 20H2": + eol, found = EOL{StandardSupportUntil: time.Date(2022, 8, 9, 23, 59, 59, 0, time.UTC)}, true + case "Windows Server 2022": + eol, found = EOL{StandardSupportUntil: time.Date(2031, 10, 14, 23, 59, 59, 0, time.UTC)}, true + default: + } } return } diff --git a/config/os_test.go b/config/os_test.go index 1fbdfd7af0..7e81b010ea 100644 --- a/config/os_test.go +++ b/config/os_test.go @@ -607,6 +607,22 @@ func TestEOL_IsStandardSupportEnded(t *testing.T) { extEnded: false, found: false, }, + { + name: "Windows 10 EOL", + fields: fields{family: Windows, release: "Windows 10 for x64-based Systems"}, + now: time.Date(2022, 12, 8, 23, 59, 59, 0, time.UTC), + stdEnded: true, + extEnded: true, + found: true, + }, + { + name: "Windows 10 Version 22H2 supported", + fields: fields{family: Windows, release: "Windows 10 Version 22H2 for x64-based Systems"}, + now: time.Date(2022, 12, 8, 23, 59, 59, 0, time.UTC), + stdEnded: false, + extEnded: false, + found: true, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/config/syslogconf.go b/config/syslogconf.go index d68fe70f86..33cfdcbf68 100644 --- a/config/syslogconf.go +++ b/config/syslogconf.go @@ -1,3 +1,5 @@ +//go:build !windows + package config import ( diff --git a/config/tomlloader.go b/config/tomlloader.go index 63948363ed..f9f704a769 100644 --- a/config/tomlloader.go +++ b/config/tomlloader.go @@ -294,6 +294,13 @@ func setDefaultIfEmpty(server *ServerInfo) error { } } + if server.Windows == nil { + server.Windows = Conf.Default.Windows + if server.Windows == nil { + server.Windows = &WindowsConf{} + } + } + if len(server.IgnoredJSONKeys) == 0 { server.IgnoredJSONKeys = Conf.Default.IgnoredJSONKeys } diff --git a/config/windows.go b/config/windows.go new file mode 100644 index 0000000000..1121477b10 --- /dev/null +++ b/config/windows.go @@ -0,0 +1,27 @@ +package config + +import ( + "os" + + "golang.org/x/xerrors" +) + +// WindowsConf used for Windows Update Setting +type WindowsConf struct { + ServerSelection int `toml:"serverSelection,omitempty" json:"serverSelection,omitempty"` + CabPath string `toml:"cabPath,omitempty" json:"cabPath,omitempty"` +} + +// Validate validates configuration +func (c *WindowsConf) Validate() []error { + switch c.ServerSelection { + case 0, 1, 2: + case 3: + if _, err := os.Stat(c.CabPath); err != nil { + return []error{xerrors.Errorf("%s does not exist. err: %w", c.CabPath, err)} + } + default: + return []error{xerrors.Errorf("ServerSelection: %d does not support . Reference: https://learn.microsoft.com/en-us/openspecs/windows_protocols/ms-uamg/07e2bfa4-6795-4189-b007-cc50b476181a", c.ServerSelection)} + } + return nil +} diff --git a/detector/detector.go b/detector/detector.go index 59437f874b..c8880eaa1d 100644 --- a/detector/detector.go +++ b/detector/detector.go @@ -473,7 +473,7 @@ func detectPkgsCvesWithGost(cnf config.GostConf, r *models.ScanResult, logOpts l nCVEs, err := client.DetectCVEs(r, true) if err != nil { switch r.Family { - case constant.Debian, constant.Ubuntu: + case constant.Debian, constant.Ubuntu, constant.Windows: return xerrors.Errorf("Failed to detect CVEs with gost: %w", err) default: return xerrors.Errorf("Failed to detect unfixed CVEs with gost: %w", err) @@ -481,7 +481,7 @@ func detectPkgsCvesWithGost(cnf config.GostConf, r *models.ScanResult, logOpts l } switch r.Family { - case constant.Debian, constant.Ubuntu: + case constant.Debian, constant.Ubuntu, constant.Windows: logging.Log.Infof("%s: %d CVEs are detected with gost", r.FormatServerName(), nCVEs) default: logging.Log.Infof("%s: %d unfixed CVEs are detected with gost", r.FormatServerName(), nCVEs) diff --git a/detector/util.go b/detector/util.go index a89357435a..a119b1e550 100644 --- a/detector/util.go +++ b/detector/util.go @@ -6,11 +6,9 @@ package detector import ( "encoding/json" "fmt" - "io/fs" "os" "path/filepath" "reflect" - "regexp" "sort" "time" @@ -221,25 +219,23 @@ func isCveInfoUpdated(cveID string, previous, current models.ScanResult) bool { return false } -// jsonDirPattern is file name pattern of JSON directory -// 2016-11-16T10:43:28+09:00 -// 2016-11-16T10:43:28Z -var jsonDirPattern = regexp.MustCompile( - `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$`) - // ListValidJSONDirs returns valid json directory as array // Returned array is sorted so that recent directories are at the head func ListValidJSONDirs(resultsDir string) (dirs []string, err error) { - var dirInfo []fs.DirEntry - if dirInfo, err = os.ReadDir(resultsDir); err != nil { - err = xerrors.Errorf("Failed to read %s: %w", - config.Conf.ResultsDir, err) - return + dirInfo, err := os.ReadDir(resultsDir) + if err != nil { + return nil, xerrors.Errorf("Failed to read %s: %w", config.Conf.ResultsDir, err) } for _, d := range dirInfo { - if d.IsDir() && jsonDirPattern.MatchString(d.Name()) { - jsonDir := filepath.Join(resultsDir, d.Name()) - dirs = append(dirs, jsonDir) + if !d.IsDir() { + continue + } + + for _, layout := range []string{"2006-01-02T15:04:05Z", "2006-01-02T15:04:05-07:00", "2006-01-02T15-04-05-0700"} { + if _, err := time.Parse(layout, d.Name()); err == nil { + dirs = append(dirs, filepath.Join(resultsDir, d.Name())) + break + } } } sort.Slice(dirs, func(i, j int) bool { diff --git a/go.mod b/go.mod index 77e7249dc8..24ef1b8d8d 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/future-architect/vuls -go 1.18 +go 1.20 require ( github.com/Azure/azure-sdk-for-go v66.0.0+incompatible @@ -36,6 +36,7 @@ require ( github.com/package-url/packageurl-go v0.1.1-0.20220203205134-d70459300c8a github.com/parnurzeal/gorequest v0.2.16 github.com/rifflock/lfshook v0.0.0-20180920164130-b9218ef580f5 + github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d github.com/sirupsen/logrus v1.9.0 github.com/spf13/cobra v1.6.1 github.com/vulsio/go-cti v0.0.2 @@ -43,12 +44,13 @@ require ( github.com/vulsio/go-exploitdb v0.4.4 github.com/vulsio/go-kev v0.1.1 github.com/vulsio/go-msfdb v0.2.1 - github.com/vulsio/gost v0.4.2-0.20230203045609-dcfab39a9ff4 + github.com/vulsio/gost v0.4.2 github.com/vulsio/goval-dictionary v0.8.2 go.etcd.io/bbolt v1.3.6 golang.org/x/exp v0.0.0-20230213192124-5e25df0256eb golang.org/x/oauth2 v0.1.0 golang.org/x/sync v0.1.0 + golang.org/x/text v0.7.0 golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 ) @@ -142,7 +144,7 @@ require ( github.com/sergi/go-diff v1.3.1 // indirect github.com/smartystreets/assertions v1.13.0 // indirect github.com/spdx/tools-golang v0.3.0 // indirect - github.com/spf13/afero v1.9.3 // indirect + github.com/spf13/afero v1.9.4 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -162,7 +164,6 @@ require ( golang.org/x/net v0.7.0 // indirect golang.org/x/sys v0.5.0 // indirect golang.org/x/term v0.5.0 // indirect - golang.org/x/text v0.7.0 // indirect golang.org/x/time v0.3.0 // indirect golang.org/x/tools v0.6.0 // indirect google.golang.org/api v0.107.0 // indirect diff --git a/go.sum b/go.sum index e151d6c9c1..46d2693d68 100644 --- a/go.sum +++ b/go.sum @@ -539,6 +539,8 @@ github.com/rubenv/sql-migrate v1.1.2 h1:9M6oj4e//owVVHYrFISmY9LBRw6gzkCNmD9MV36t github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d h1:hrujxIzL1woJ7AwssoOcM/tq5JjjG2yYOc8odClEiXA= +github.com/saintfish/chardet v0.0.0-20230101081208-5e3ef4b5456d/go.mod h1:uugorj2VCxiV1x+LzaIdVa9b4S4qGAcH6cbhh4qVxOU= github.com/samber/lo v1.33.0 h1:2aKucr+rQV6gHpY3bpeZu69uYoQOzVhGT3J22Op6Cjk= github.com/samber/lo v1.33.0/go.mod h1:HLeWcJRRyLKp3+/XBJvOrerCQn9mhdKMHyd7IRlgeQ8= github.com/secure-systems-lab/go-securesystemslib v0.4.0 h1:b23VGrQhTA8cN2CbBw7/FulN9fTtqYUdS5+Oxzt+DUE= @@ -559,8 +561,8 @@ github.com/spdx/gordf v0.0.0-20201111095634-7098f93598fb/go.mod h1:uKWaldnbMnjsS github.com/spdx/tools-golang v0.3.0 h1:rtm+DHk3aAt74Fh0Wgucb4pCxjXV8SqHCPEb2iBd30k= github.com/spdx/tools-golang v0.3.0/go.mod h1:RO4Y3IFROJnz+43JKm1YOrbtgQNljW4gAPpA/sY2eqo= github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk= -github.com/spf13/afero v1.9.3 h1:41FoI0fD7OR7mGcKE/aOiLkGreyf8ifIOQmJANWogMk= -github.com/spf13/afero v1.9.3/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= +github.com/spf13/afero v1.9.4 h1:Sd43wM1IWz/s1aVXdOBkjJvuP8UdyqioeE4AmM0QsBs= +github.com/spf13/afero v1.9.4/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/spf13/cobra v1.6.1 h1:o94oiPyS4KD1mPy2fmcYYHHfCxLqYjJOhGsCHFZtEzA= @@ -604,8 +606,8 @@ github.com/vulsio/go-kev v0.1.1 h1:Xi0FjUj2czQpnurfbXxSrJFbaePolbTrM+gfYxsvj2o= github.com/vulsio/go-kev v0.1.1/go.mod h1:3CiN3/Ojlodj9ACt2SAhAk5L36m27czTKDfSEf8U8Qg= github.com/vulsio/go-msfdb v0.2.1 h1:s3Czz+WdgtaXjHRy+1fUzSdEjZGXie354IvT+9syAY0= github.com/vulsio/go-msfdb v0.2.1/go.mod h1:8A7AyeSqZtFxfd5bljiB1/z2hvkFPe3/jpRtV/mqGbo= -github.com/vulsio/gost v0.4.2-0.20230203045609-dcfab39a9ff4 h1:aitlGPmn5WPb9aR6MFsikt+/EaxJtMNttaeayXsDxs0= -github.com/vulsio/gost v0.4.2-0.20230203045609-dcfab39a9ff4/go.mod h1:6xRvzXkpm8nJ/jMmL/TJZvabfVZyy2aB1nr4wtmJ1KI= +github.com/vulsio/gost v0.4.2 h1:WtjSeTkvvmJdhn6Dv2Ew934MC4dGmojjC6cu7Q9sHhA= +github.com/vulsio/gost v0.4.2/go.mod h1:PxCHzwylur7/EiP7Jo6UPRYkipi76EhA015FOTjKol0= github.com/vulsio/goval-dictionary v0.8.2 h1:6aI10z/RFZjADzP4fvf7I1zGqbY3EfAsF0I1VOh/ep0= github.com/vulsio/goval-dictionary v0.8.2/go.mod h1:yRO+Xuce12lSQiV6gdMb86uc8V5Vncgzc6U84WvB/5k= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= diff --git a/gost/microsoft.go b/gost/microsoft.go index 831040b8b8..ffd52838a4 100644 --- a/gost/microsoft.go +++ b/gost/microsoft.go @@ -4,17 +4,23 @@ package gost import ( + "encoding/json" "fmt" - "regexp" + "net/http" "strconv" "strings" + "time" + "github.com/cenkalti/backoff" + "github.com/hashicorp/go-version" + "github.com/parnurzeal/gorequest" "golang.org/x/exp/maps" "golang.org/x/exp/slices" "golang.org/x/xerrors" "github.com/future-architect/vuls/logging" "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/util" gostmodels "github.com/vulsio/gost/models" ) @@ -23,123 +29,256 @@ type Microsoft struct { Base } -var kbIDPattern = regexp.MustCompile(`KB(\d{6,7})`) - // DetectCVEs fills cve information that has in Gost func (ms Microsoft) DetectCVEs(r *models.ScanResult, _ bool) (nCVEs int, err error) { - if ms.driver == nil { - return 0, nil + var applied, unapplied []string + if r.WindowsKB != nil { + applied = r.WindowsKB.Applied + unapplied = r.WindowsKB.Unapplied } + if ms.driver == nil { + u, err := util.URLPathJoin(ms.baseURL, "microsoft", "kbs") + if err != nil { + return 0, xerrors.Errorf("Failed to join URLPath. err: %w", err) + } + + content := map[string]interface{}{"applied": applied, "unapplied": unapplied} + var body []byte + var errs []error + var resp *http.Response + f := func() error { + resp, body, errs = gorequest.New().Timeout(10 * time.Second).Post(u).SendStruct(content).Type("json").EndBytes() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + return xerrors.Errorf("HTTP POST error. url: %s, resp: %v, err: %+v", u, resp, errs) + } + return nil + } + notify := func(err error, t time.Duration) { + logging.Log.Warnf("Failed to HTTP POST. retrying in %s seconds. err: %+v", t, err) + } + if err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify); err != nil { + return 0, xerrors.Errorf("HTTP Error: %w", err) + } - var osName string - osName, ok := r.Optional["OSName"].(string) - if !ok { - logging.Log.Warnf("This Windows has wrong type option(OSName). UUID: %s", r.ServerUUID) + var r struct { + Applied []string `json:"applied"` + Unapplied []string `json:"unapplied"` + } + if err := json.Unmarshal(body, &r); err != nil { + return 0, xerrors.Errorf("Failed to Unmarshal. body: %s, err: %w", body, err) + } + applied = r.Applied + unapplied = r.Unapplied + } else { + applied, unapplied, err = ms.driver.GetExpandKB(applied, unapplied) + if err != nil { + return 0, xerrors.Errorf("Failed to detect CVEs. err: %w", err) + } } var products []string - if _, ok := r.Optional["InstalledProducts"]; ok { - switch ps := r.Optional["InstalledProducts"].(type) { - case []interface{}: - for _, p := range ps { - pname, ok := p.(string) - if !ok { - logging.Log.Warnf("skip products: %v", p) - continue - } - products = append(products, pname) - } - case []string: - for _, p := range ps { - products = append(products, p) + if ms.driver == nil { + u, err := util.URLPathJoin(ms.baseURL, "microsoft", "products") + if err != nil { + return 0, xerrors.Errorf("Failed to join URLPath. err: %w", err) + } + + content := map[string]interface{}{"release": r.Release, "kbs": append(applied, unapplied...)} + var body []byte + var errs []error + var resp *http.Response + f := func() error { + resp, body, errs = gorequest.New().Timeout(10 * time.Second).Post(u).SendStruct(content).Type("json").EndBytes() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + return xerrors.Errorf("HTTP POST error. url: %s, resp: %v, err: %+v", u, resp, errs) } - case nil: - logging.Log.Warnf("This Windows has no option(InstalledProducts). UUID: %s", r.ServerUUID) + return nil + } + notify := func(err error, t time.Duration) { + logging.Log.Warnf("Failed to HTTP POST. retrying in %s seconds. err: %+v", t, err) + } + if err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify); err != nil { + return 0, xerrors.Errorf("HTTP Error: %w", err) + } + + if err := json.Unmarshal(body, &products); err != nil { + return 0, xerrors.Errorf("Failed to Unmarshal. body: %s, err: %w", body, err) + } + } else { + ps, err := ms.driver.GetRelatedProducts(r.Release, append(applied, unapplied...)) + if err != nil { + return 0, xerrors.Errorf("Failed to detect CVEs. err: %w", err) } + products = ps } - applied, unapplied := map[string]struct{}{}, map[string]struct{}{} - if _, ok := r.Optional["KBID"]; ok { - switch kbIDs := r.Optional["KBID"].(type) { - case []interface{}: - for _, kbID := range kbIDs { - s, ok := kbID.(string) - if !ok { - logging.Log.Warnf("skip KBID: %v", kbID) + m := map[string]struct{}{} + for _, p := range products { + m[p] = struct{}{} + } + for _, n := range []string{"Microsoft Edge (Chromium-based)", fmt.Sprintf("Microsoft Edge on %s", r.Release), fmt.Sprintf("Microsoft Edge (Chromium-based) in IE Mode on %s", r.Release), fmt.Sprintf("Microsoft Edge (EdgeHTML-based) on %s", r.Release)} { + delete(m, n) + } + filtered := []string{r.Release} + for _, p := range r.Packages { + switch p.Name { + case "Microsoft Edge": + if ss := strings.Split(p.Version, "."); len(ss) > 0 { + v, err := strconv.ParseInt(ss[0], 10, 8) + if err != nil { continue } - unapplied[strings.TrimPrefix(s, "KB")] = struct{}{} - } - case []string: - for _, kbID := range kbIDs { - unapplied[strings.TrimPrefix(kbID, "KB")] = struct{}{} + if v > 44 { + filtered = append(filtered, "Microsoft Edge (Chromium-based)", fmt.Sprintf("Microsoft Edge on %s", r.Release), fmt.Sprintf("Microsoft Edge (Chromium-based) in IE Mode on %s", r.Release)) + } else { + filtered = append(filtered, fmt.Sprintf("Microsoft Edge on %s", r.Release), fmt.Sprintf("Microsoft Edge (EdgeHTML-based) on %s", r.Release)) + } } - case nil: - logging.Log.Warnf("This Windows has no option(KBID). UUID: %s", r.ServerUUID) + default: } + } + filtered = unique(append(filtered, maps.Keys(m)...)) - for _, pkg := range r.Packages { - matches := kbIDPattern.FindAllStringSubmatch(pkg.Name, -1) - for _, match := range matches { - applied[match[1]] = struct{}{} + var cves map[string]gostmodels.MicrosoftCVE + if ms.driver == nil { + u, err := util.URLPathJoin(ms.baseURL, "microsoft", "filtered-cves") + if err != nil { + return 0, xerrors.Errorf("Failed to join URLPath. err: %w", err) + } + + content := map[string]interface{}{"products": filtered, "kbs": append(applied, unapplied...)} + var body []byte + var errs []error + var resp *http.Response + f := func() error { + resp, body, errs = gorequest.New().Timeout(10 * time.Second).Post(u).SendStruct(content).Type("json").EndBytes() + if 0 < len(errs) || resp == nil || resp.StatusCode != 200 { + return xerrors.Errorf("HTTP POST error. url: %s, resp: %v, err: %+v", u, resp, errs) } + return nil + } + notify := func(err error, t time.Duration) { + logging.Log.Warnf("Failed to HTTP POST. retrying in %s seconds. err: %+v", t, err) + } + if err := backoff.RetryNotify(f, backoff.NewExponentialBackOff(), notify); err != nil { + return 0, xerrors.Errorf("HTTP Error: %w", err) + } + + if err := json.Unmarshal(body, &cves); err != nil { + return 0, xerrors.Errorf("Failed to Unmarshal. body: %s, err: %w", body, err) } } else { - switch kbIDs := r.Optional["AppliedKBID"].(type) { - case []interface{}: - for _, kbID := range kbIDs { - s, ok := kbID.(string) - if !ok { - logging.Log.Warnf("skip KBID: %v", kbID) - continue - } - applied[strings.TrimPrefix(s, "KB")] = struct{}{} - } - case []string: - for _, kbID := range kbIDs { - applied[strings.TrimPrefix(kbID, "KB")] = struct{}{} - } - case nil: - logging.Log.Warnf("This Windows has no option(AppliedKBID). UUID: %s", r.ServerUUID) + cves, err = ms.driver.GetFilteredCvesMicrosoft(filtered, append(applied, unapplied...)) + if err != nil { + return 0, xerrors.Errorf("Failed to detect CVEs. err: %w", err) } + } - switch kbIDs := r.Optional["UnappliedKBID"].(type) { - case []interface{}: - for _, kbID := range kbIDs { - s, ok := kbID.(string) - if !ok { - logging.Log.Warnf("skip KBID: %v", kbID) - continue + for cveID, cve := range cves { + var ps []gostmodels.MicrosoftProduct + for _, p := range cve.Products { + if len(p.KBs) == 0 { + ps = append(ps, p) + continue + } + + var kbs []gostmodels.MicrosoftKB + for _, kb := range p.KBs { + if _, err := strconv.Atoi(kb.Article); err != nil { + switch { + case strings.HasPrefix(p.Name, "Microsoft Edge"): + p, ok := r.Packages["Microsoft Edge"] + if !ok { + break + } + + if kb.FixedBuild == "" { + kbs = append(kbs, kb) + break + } + + vera, err := version.NewVersion(p.Version) + if err != nil { + kbs = append(kbs, kb) + break + } + verb, err := version.NewVersion(kb.FixedBuild) + if err != nil { + kbs = append(kbs, kb) + break + } + if vera.LessThan(verb) { + kbs = append(kbs, kb) + } + } + } else { + if slices.Contains(applied, kb.Article) { + kbs = []gostmodels.MicrosoftKB{} + break + } + if slices.Contains(unapplied, kb.Article) { + kbs = append(kbs, kb) + } } - unapplied[strings.TrimPrefix(s, "KB")] = struct{}{} } - case []string: - for _, kbID := range kbIDs { - unapplied[strings.TrimPrefix(kbID, "KB")] = struct{}{} + if len(kbs) > 0 { + p.KBs = kbs + ps = append(ps, p) } - case nil: - logging.Log.Warnf("This Windows has no option(UnappliedKBID). UUID: %s", r.ServerUUID) } - } - - logging.Log.Debugf(`GetCvesByMicrosoftKBID query body {"osName": %s, "installedProducts": %q, "applied": %q, "unapplied: %q"}`, osName, products, maps.Keys(applied), maps.Keys(unapplied)) - cves, err := ms.driver.GetCvesByMicrosoftKBID(osName, products, maps.Keys(applied), maps.Keys(unapplied)) - if err != nil { - return 0, xerrors.Errorf("Failed to detect CVEs. err: %w", err) - } + cve.Products = ps + if len(cve.Products) == 0 { + continue + } + nCVEs++ - for cveID, cve := range cves { cveCont, mitigations := ms.ConvertToModel(&cve) uniqKB := map[string]struct{}{} + var stats models.PackageFixStatuses for _, p := range cve.Products { for _, kb := range p.KBs { - if _, err := strconv.Atoi(kb.Article); err == nil { - uniqKB[fmt.Sprintf("KB%s", kb.Article)] = struct{}{} + if _, err := strconv.Atoi(kb.Article); err != nil { + switch { + case strings.HasPrefix(p.Name, "Microsoft Edge"): + s := models.PackageFixStatus{ + Name: "Microsoft Edge", + FixState: "fixed", + FixedIn: kb.FixedBuild, + } + if kb.FixedBuild == "" { + s.FixState = "unknown" + } + stats = append(stats, s) + default: + stats = append(stats, models.PackageFixStatus{ + Name: p.Name, + FixState: "unknown", + FixedIn: kb.FixedBuild, + }) + } } else { - uniqKB[kb.Article] = struct{}{} + uniqKB[fmt.Sprintf("KB%s", kb.Article)] = struct{}{} + } + } + } + if len(uniqKB) == 0 && len(stats) == 0 { + for _, p := range cve.Products { + switch { + case strings.HasPrefix(p.Name, "Microsoft Edge"): + stats = append(stats, models.PackageFixStatus{ + Name: "Microsoft Edge", + FixState: "unknown", + }) + default: + stats = append(stats, models.PackageFixStatus{ + Name: p.Name, + FixState: "unknown", + }) } + } } + advisories := []models.DistroAdvisory{} for kb := range uniqKB { advisories = append(advisories, models.DistroAdvisory{ @@ -149,14 +288,16 @@ func (ms Microsoft) DetectCVEs(r *models.ScanResult, _ bool) (nCVEs int, err err } r.ScannedCves[cveID] = models.VulnInfo{ - CveID: cveID, - Confidences: models.Confidences{models.WindowsUpdateSearch}, - DistroAdvisories: advisories, - CveContents: models.NewCveContents(*cveCont), - Mitigations: mitigations, + CveID: cveID, + Confidences: models.Confidences{models.WindowsUpdateSearch}, + DistroAdvisories: advisories, + CveContents: models.NewCveContents(*cveCont), + Mitigations: mitigations, + AffectedPackages: stats, + WindowsKBFixedIns: maps.Keys(uniqKB), } } - return len(cves), nil + return nCVEs, nil } // ConvertToModel converts gost model to vuls model diff --git a/gost/util.go b/gost/util.go index 243703a06b..2e60c1fb3f 100644 --- a/gost/util.go +++ b/gost/util.go @@ -10,6 +10,7 @@ import ( "github.com/cenkalti/backoff" "github.com/parnurzeal/gorequest" + "golang.org/x/exp/maps" "golang.org/x/xerrors" "github.com/future-architect/vuls/logging" @@ -189,3 +190,11 @@ func httpGet(url string, req request, resChan chan<- response, errChan chan<- er func major(osVer string) (majorVersion string) { return strings.Split(osVer, ".")[0] } + +func unique[T comparable](s []T) []T { + m := map[T]struct{}{} + for _, v := range s { + m[v] = struct{}{} + } + return maps.Keys(m) +} diff --git a/models/scanresults.go b/models/scanresults.go index 0d6b124e69..508b992577 100644 --- a/models/scanresults.go +++ b/models/scanresults.go @@ -53,6 +53,7 @@ type ScanResult struct { WordPressPackages WordPressPackages `json:",omitempty"` GitHubManifests DependencyGraphManifests `json:"gitHubManifests,omitempty"` LibraryScanners LibraryScanners `json:"libraries,omitempty"` + WindowsKB *WindowsKB `json:"windowsKB,omitempty"` CweDict CweDict `json:"cweDict,omitempty"` Optional map[string]interface{} `json:",omitempty"` Config struct { @@ -83,6 +84,12 @@ type Kernel struct { RebootRequired bool `json:"rebootRequired"` } +// WindowsKB has applied and unapplied KBs +type WindowsKB struct { + Applied []string `json:"applied,omitempty"` + Unapplied []string `json:"unapplied,omitempty"` +} + // FilterInactiveWordPressLibs is filter function. func (r *ScanResult) FilterInactiveWordPressLibs(detectInactive bool) { if detectInactive { diff --git a/models/vulninfos.go b/models/vulninfos.go index b7087561ad..6d8cad4ff3 100644 --- a/models/vulninfos.go +++ b/models/vulninfos.go @@ -267,6 +267,7 @@ type VulnInfo struct { GitHubSecurityAlerts GitHubSecurityAlerts `json:"gitHubSecurityAlerts,omitempty"` WpPackageFixStats WpPackageFixStats `json:"wpPackageFixStats,omitempty"` LibraryFixedIns LibraryFixedIns `json:"libraryFixedIns,omitempty"` + WindowsKBFixedIns []string `json:"windowsKBFixedIns,omitempty"` VulnType string `json:"vulnType,omitempty"` DiffStatus DiffStatus `json:"diffStatus,omitempty"` } @@ -531,7 +532,7 @@ func (v VulnInfo) Cvss2Scores() (values []CveContentCvss) { // Cvss3Scores returns CVSS V3 Score func (v VulnInfo) Cvss3Scores() (values []CveContentCvss) { - order := []CveContentType{RedHatAPI, RedHat, SUSE, Nvd, Jvn} + order := []CveContentType{RedHatAPI, RedHat, SUSE, Microsoft, Nvd, Jvn} for _, ctype := range order { if conts, found := v.CveContents[ctype]; found { for _, cont := range conts { @@ -661,6 +662,7 @@ func (v VulnInfo) PatchStatus(packs Packages) string { if len(v.CpeURIs) != 0 { return "" } + for _, p := range v.AffectedPackages { if p.NotFixedYet { return "unfixed" @@ -680,6 +682,13 @@ func (v VulnInfo) PatchStatus(packs Packages) string { } } } + + for _, c := range v.Confidences { + if c == WindowsUpdateSearch && len(v.WindowsKBFixedIns) == 0 { + return "unfixed" + } + } + return "fixed" } diff --git a/models/vulninfos_test.go b/models/vulninfos_test.go index edca0dbda3..3830fc3ecd 100644 --- a/models/vulninfos_test.go +++ b/models/vulninfos_test.go @@ -1717,3 +1717,103 @@ func TestVulnInfos_FilterByConfidenceOver(t *testing.T) { }) } } + +func TestVulnInfo_PatchStatus(t *testing.T) { + type fields struct { + Confidences Confidences + AffectedPackages PackageFixStatuses + CpeURIs []string + WindowsKBFixedIns []string + } + type args struct { + packs Packages + } + tests := []struct { + name string + fields fields + args args + want string + }{ + { + name: "cpe", + fields: fields{ + CpeURIs: []string{"cpe:/a:microsoft:internet_explorer:10"}, + }, + want: "", + }, + { + name: "package unfixed", + fields: fields{ + AffectedPackages: PackageFixStatuses{ + { + Name: "bash", + NotFixedYet: true, + }, + }, + }, + want: "unfixed", + }, + { + name: "package unknown", + fields: fields{ + AffectedPackages: PackageFixStatuses{ + { + Name: "bash", + }, + }, + }, + args: args{ + packs: Packages{"bash": { + Name: "bash", + }}, + }, + want: "unknown", + }, + { + name: "package fixed", + fields: fields{ + AffectedPackages: PackageFixStatuses{ + { + Name: "bash", + }, + }, + }, + args: args{ + packs: Packages{"bash": { + Name: "bash", + Version: "4.3-9.1", + NewVersion: "5.0-4", + }}, + }, + want: "fixed", + }, + { + name: "windows unfixed", + fields: fields{ + Confidences: Confidences{WindowsUpdateSearch}, + }, + want: "unfixed", + }, + { + name: "windows fixed", + fields: fields{ + Confidences: Confidences{WindowsUpdateSearch}, + WindowsKBFixedIns: []string{"000000"}, + }, + want: "fixed", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + v := VulnInfo{ + Confidences: tt.fields.Confidences, + AffectedPackages: tt.fields.AffectedPackages, + CpeURIs: tt.fields.CpeURIs, + WindowsKBFixedIns: tt.fields.WindowsKBFixedIns, + } + if got := v.PatchStatus(tt.args.packs); got != tt.want { + t.Errorf("VulnInfo.PatchStatus() = %v, want %v", got, tt.want) + } + }) + } +} diff --git a/reporter/syslog.go b/reporter/syslog.go index 8b6e8f476f..03e9d2a5e3 100644 --- a/reporter/syslog.go +++ b/reporter/syslog.go @@ -1,3 +1,5 @@ +//go:build !windows + package reporter import ( diff --git a/reporter/util.go b/reporter/util.go index f08491c2ce..c14a7c246e 100644 --- a/reporter/util.go +++ b/reporter/util.go @@ -10,7 +10,6 @@ import ( "os" "path/filepath" "reflect" - "regexp" "sort" "strings" "time" @@ -81,24 +80,23 @@ func loadOneServerScanResult(jsonFile string) (*models.ScanResult, error) { return result, nil } -// jsonDirPattern is file name pattern of JSON directory -// 2016-11-16T10:43:28+09:00 -// 2016-11-16T10:43:28Z -var jsonDirPattern = regexp.MustCompile( - `^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:Z|[+-]\d{2}:\d{2})$`) - // ListValidJSONDirs returns valid json directory as array // Returned array is sorted so that recent directories are at the head func ListValidJSONDirs(resultsDir string) (dirs []string, err error) { - var dirInfo []fs.DirEntry - if dirInfo, err = os.ReadDir(resultsDir); err != nil { - err = xerrors.Errorf("Failed to read %s: %w", resultsDir, err) - return + dirInfo, err := os.ReadDir(resultsDir) + if err != nil { + return nil, xerrors.Errorf("Failed to read %s: %w", resultsDir, err) } for _, d := range dirInfo { - if d.IsDir() && jsonDirPattern.MatchString(d.Name()) { - jsonDir := filepath.Join(resultsDir, d.Name()) - dirs = append(dirs, jsonDir) + if !d.IsDir() { + continue + } + + for _, layout := range []string{"2006-01-02T15:04:05Z", "2006-01-02T15:04:05-07:00", "2006-01-02T15-04-05-0700"} { + if _, err := time.Parse(layout, d.Name()); err == nil { + dirs = append(dirs, filepath.Join(resultsDir, d.Name())) + break + } } } sort.Slice(dirs, func(i, j int) bool { @@ -258,9 +256,13 @@ No CVE-IDs are found in updatable packages. // v2max := vinfo.MaxCvss2Score().Value.Score // v3max := vinfo.MaxCvss3Score().Value.Score - packnames := strings.Join(vinfo.AffectedPackages.Names(), ", ") - // packname := vinfo.AffectedPackages.FormatTuiSummary() - // packname += strings.Join(vinfo.CpeURIs, ", ") + pkgNames := vinfo.AffectedPackages.Names() + pkgNames = append(pkgNames, vinfo.CpeURIs...) + pkgNames = append(pkgNames, vinfo.GitHubSecurityAlerts.Names()...) + pkgNames = append(pkgNames, vinfo.WpPackageFixStats.Names()...) + pkgNames = append(pkgNames, vinfo.LibraryFixedIns.Names()...) + pkgNames = append(pkgNames, vinfo.WindowsKBFixedIns...) + packnames := strings.Join(pkgNames, ", ") exploits := "" if 0 < len(vinfo.Exploits) || 0 < len(vinfo.Metasploits) { @@ -431,6 +433,10 @@ No CVE-IDs are found in updatable packages. } } + if len(vuln.WindowsKBFixedIns) > 0 { + data = append(data, []string{"Windows KB", fmt.Sprintf("FixedIn: %s", strings.Join(vuln.WindowsKBFixedIns, ", "))}) + } + for _, confidence := range vuln.Confidences { data = append(data, []string{"Confidence", confidence.String()}) } diff --git a/scanner/base.go b/scanner/base.go index d2feaf25e6..b75b786600 100644 --- a/scanner/base.go +++ b/scanner/base.go @@ -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, diff --git a/scanner/debian.go b/scanner/debian.go index ef293b05ea..a4cbbc08c1 100644 --- a/scanner/debian.go +++ b/scanner/debian.go @@ -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 { diff --git a/scanner/executil.go b/scanner/executil.go index 0ce8bb24da..c732d29187 100644 --- a/scanner/executil.go +++ b/scanner/executil.go @@ -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" + } 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 @@ -313,3 +325,33 @@ func decorateCmd(c config.ServerInfo, cmd string, sudo bool) string { // 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) +} diff --git a/scanner/scanner.go b/scanner/scanner.go index 97ab532713..d255a5ba0f 100644 --- a/scanner/scanner.go +++ b/scanner/scanner.go @@ -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,127 @@ 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) { + 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 } - release := header.Get("X-Vuls-OS-Release") - if release == "" { - return models.ScanResult{}, errOSReleaseHeader - } + switch family { + case constant.Windows: + osInfo, hotfixs, err := parseSystemInfo(body) + if err != nil { + return models.ScanResult{}, xerrors.Errorf("Failed to parse systeminfo.exe. err: %w", err) + } - 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") - } + 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 family == constant.Debian { + kernelVersion := header.Get("X-Vuls-Kernel-Version") 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 = "" - } + kernelVersion = formatKernelVersion(osInfo) } - } - serverName := header.Get("X-Vuls-Server-Name") - if toLocalFile && serverName == "" { - return models.ScanResult{}, errServerNameHeader - } + w := &windows{ + base: base{ + Distro: config.Distro{Family: family, Release: release}, + osPackages: osPackages{ + Kernel: models.Kernel{Version: kernelVersion}, + }, + log: logging.Log, + }, + } + v, err := w.detectKernelVersion(hotfixs) + if err != nil { + return models.ScanResult{}, xerrors.Errorf("Failed to detect kernel version. err: %w", err) + } + w.Kernel = models.Kernel{Version: v} - distro := config.Distro{ - Family: family, - Release: release, - } + kbs, err := w.detectKBsFromKernelVersion() + if err != nil { + return models.ScanResult{}, xerrors.Errorf("Failed to detect KBs from kernel version. err: %w", err) + } - kernel := models.Kernel{ - Release: kernelRelease, - Version: kernelVersion, - } - installedPackages, srcPackages, err := ParseInstalledPkgs(distro, kernel, body) - if err != nil { - return models.ScanResult{}, 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: v, + }, + 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, + } - return models.ScanResult{ - ServerName: serverName, - Family: family, - Release: release, - RunningKernel: models.Kernel{ + 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 +407,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 +453,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 +464,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 +500,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 +568,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 +598,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 +611,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 +757,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 +800,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 diff --git a/scanner/scanner_test.go b/scanner/scanner_test.go index f0bb9e5af0..36e56b17b7 100644 --- a/scanner/scanner_test.go +++ b/scanner/scanner_test.go @@ -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.1645", + }, + WindowsKB: &models.WindowsKB{ + Applied: []string{"5009543", "5011487", "5007401", "5011651", "5008212", "5012117", "4562830", "5005699", "5011543", "5012599", "5007253", "5010793", "5010415", "5003791", "5009596", "5010342"}, + Unapplied: []string{"5021233", "5019275", "5015020", "5014023", "5014666", "5017380", "5020435", "5020030", "5011831", "5014699", "5017308", "5018482", "5022834", "5016139", "5016688", "5018410", "5022282", "5013942", "5015807", "5015878", "5016616", "5020953", "5019959", "5022906"}, + }, + }, + }, } 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) + } } } diff --git a/scanner/utils.go b/scanner/utils.go index 05fea0723c..43328280ed 100644 --- a/scanner/utils.go +++ b/scanner/utils.go @@ -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 } diff --git a/scanner/windows.go b/scanner/windows.go new file mode 100644 index 0000000000..4a73003772 --- /dev/null +++ b/scanner/windows.go @@ -0,0 +1,4408 @@ +package scanner + +import ( + "bufio" + "fmt" + "regexp" + "strconv" + "strings" + + "golang.org/x/exp/maps" + "golang.org/x/exp/slices" + "golang.org/x/xerrors" + + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/constant" + "github.com/future-architect/vuls/logging" + "github.com/future-architect/vuls/models" +) + +// inherit OsTypeInterface +type windows struct { + base +} + +type osInfo struct { + productName string + version string + build string + revision string + edition string + servicePack string + arch string + installationType string +} + +func newWindows(c config.ServerInfo) *windows { + d := &windows{ + base: base{ + osPackages: osPackages{ + Packages: models.Packages{}, + VulnInfos: models.VulnInfos{}, + }, + }, + } + d.log = logging.NewNormalLogger() + d.setServerInfo(c) + return d +} + +func detectWindows(c config.ServerInfo) (bool, osTypeInterface) { + tmp := c + tmp.Distro.Family = constant.Windows + + if r, r2 := exec(tmp, `$CurrentVersion = (Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\Software\Microsoft\Windows NT\CurrentVersion"); Format-List -InputObject $CurrentVersion -Property ProductName, CurrentVersion, CurrentMajorVersionNumber, CurrentMinorVersionNumber, CurrentBuildNumber, UBR, CSDVersion, EditionID, InstallationType`, noSudo), exec(tmp, `(Get-ItemProperty -Path "Registry::HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session Manager\Environment").PROCESSOR_ARCHITECTURE`, noSudo); (r.isSuccess() && r.Stdout != "") && (r2.isSuccess() && r2.Stdout != "") { + w := newWindows(c) + osInfo, err := parseRegistry(r.Stdout, strings.TrimSpace(r2.Stdout)) + if err != nil { + w.setErrs([]error{xerrors.Errorf("Failed to parse Registry. err: %w", err)}) + return true, w + } + + release, err := detectOSName(osInfo) + if err != nil { + w.setErrs([]error{xerrors.Errorf("Failed to detect os name. err: %w", err)}) + return true, w + } + w.setDistro(constant.Windows, release) + w.Kernel = models.Kernel{Version: formatKernelVersion(osInfo)} + return true, w + } + + if r := exec(tmp, "Get-ComputerInfo -Property WindowsProductName, OsVersion, WindowsEditionId, OsCSDVersion, CsSystemType, WindowsInstallationType", noSudo); r.isSuccess() && r.Stdout != "" { + w := newWindows(c) + osInfo, err := parseGetComputerInfo(r.Stdout) + if err != nil { + w.setErrs([]error{xerrors.Errorf("Failed to parse Get-ComputerInfo. err: %w", err)}) + return true, w + } + + release, err := detectOSName(osInfo) + if err != nil { + w.setErrs([]error{xerrors.Errorf("Failed to detect os name. err: %w", err)}) + return true, w + } + w.setDistro(constant.Windows, release) + w.Kernel = models.Kernel{Version: formatKernelVersion(osInfo)} + return true, w + } + + if r := exec(tmp, "$WmiOS = (Get-WmiObject Win32_OperatingSystem); Format-List -InputObject $WmiOS -Property Caption, Version, OperatingSystemSKU, CSDVersion; $WmiCS = (Get-WmiObject Win32_ComputerSystem); Format-List -InputObject $WmiCS -Property SystemType, DomainRole", noSudo); r.isSuccess() && r.Stdout != "" { + w := newWindows(c) + osInfo, err := parseWmiObject(r.Stdout) + if err != nil { + w.setErrs([]error{xerrors.Errorf("Failed to parse Get-WmiObject. err: %w", err)}) + return true, w + } + + release, err := detectOSName(osInfo) + if err != nil { + w.setErrs([]error{xerrors.Errorf("Failed to detect os name. err: %w", err)}) + return true, w + } + w.setDistro(constant.Windows, release) + w.Kernel = models.Kernel{Version: formatKernelVersion(osInfo)} + return true, w + } + + if r := exec(tmp, "systeminfo.exe", noSudo); r.isSuccess() && r.Stdout != "" { + w := newWindows(c) + osInfo, _, err := parseSystemInfo(r.Stdout) + if err != nil { + w.setErrs([]error{xerrors.Errorf("Failed to parse systeminfo.exe. err: %w", err)}) + return true, w + } + + release, err := detectOSName(osInfo) + if err != nil { + w.setErrs([]error{xerrors.Errorf("Failed to detect os name. err: %w", err)}) + return true, w + } + w.setDistro(constant.Windows, release) + w.Kernel = models.Kernel{Version: formatKernelVersion(osInfo)} + return true, w + } + + return false, nil +} + +func parseSystemInfo(stdout string) (osInfo, []string, error) { + var ( + o osInfo + kbs []string + ) + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + + switch { + case strings.HasPrefix(line, "OS 名:"): + line = strings.NewReplacer("OS 名:", "OS Name:").Replace(line) + case strings.HasPrefix(line, "OS バージョン:"): + line = strings.NewReplacer("OS バージョン:", "OS Version:", "ビルド", "Build").Replace(line) + case strings.HasPrefix(line, "システムの種類:"): + line = strings.NewReplacer("システムの種類:", "System Type:").Replace(line) + case strings.HasPrefix(line, "OS 構成:"): + line = strings.NewReplacer("OS 構成:", "OS Configuration:", "サーバー", "Server", "ワークステーション", "Workstation").Replace(line) + case strings.HasPrefix(line, "ホットフィックス:"): + line = strings.NewReplacer("ホットフィックス:", "Hotfix(s):", "ホットフィックスがインストールされています。", "Hotfix(s) Installed.").Replace(line) + default: + } + + switch { + case strings.HasPrefix(line, "OS Name:"): + o.productName = strings.TrimSpace(strings.TrimPrefix(line, "OS Name:")) + case strings.HasPrefix(line, "OS Version:"): + s := strings.TrimSpace(strings.TrimPrefix(line, "OS Version:")) + lhs, build, _ := strings.Cut(s, " Build ") + vb, sp, _ := strings.Cut(lhs, " ") + o.version = strings.TrimSuffix(vb, fmt.Sprintf(".%s", build)) + o.build = build + if sp != "N/A" { + o.servicePack = sp + } + case strings.HasPrefix(line, "System Type:"): + o.arch = strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "System Type:"), "PC")) + case strings.HasPrefix(line, "OS Configuration:"): + switch { + case strings.Contains(line, "Server"): + o.installationType = "Server" + case strings.Contains(line, "Workstation"): + o.installationType = "Client" + default: + return osInfo{}, nil, xerrors.Errorf("Failed to detect installation type. line: %s", line) + } + case strings.HasPrefix(line, "Hotfix(s):"): + nKB, err := strconv.Atoi(strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(line, "Hotfix(s):"), "Hotfix(s) Installed."))) + if err != nil { + return osInfo{}, nil, xerrors.Errorf("Failed to detect number of installed hotfix from %s", line) + } + for i := 0; i < nKB; i++ { + scanner.Scan() + line := scanner.Text() + _, rhs, found := strings.Cut(line, ":") + if !found { + continue + } + s := strings.TrimSpace(rhs) + if strings.HasPrefix(s, "KB") { + kbs = append(kbs, strings.TrimPrefix(s, "KB")) + } + } + default: + } + } + return o, kbs, nil +} + +func parseGetComputerInfo(stdout string) (osInfo, error) { + var o osInfo + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + + switch { + case strings.HasPrefix(line, "WindowsProductName"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect ProductName. expected: "WindowsProductName : ", line: "%s"`, line) + } + o.productName = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "OsVersion"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect OsVersion. expected: "OsVersion : ", line: "%s"`, line) + } + ss := strings.Split(strings.TrimSpace(rhs), ".") + o.version = strings.Join(ss[0:len(ss)-1], ".") + o.build = ss[len(ss)-1] + case strings.HasPrefix(line, "WindowsEditionId"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect WindowsEditionId. expected: "WindowsEditionId : ", line: "%s"`, line) + } + o.edition = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "OsCSDVersion"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect OsCSDVersion. expected: "OsCSDVersion : ", line: "%s"`, line) + } + o.servicePack = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "CsSystemType"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect CsSystemType. expected: "CsSystemType : ", line: "%s"`, line) + } + o.arch = strings.TrimSpace(strings.TrimSuffix(rhs, "PC")) + case strings.HasPrefix(line, "WindowsInstallationType"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect WindowsInstallationType. expected: "WindowsInstallationType : ", line: "%s"`, line) + } + o.installationType = strings.TrimSpace(rhs) + default: + } + } + return o, nil +} + +func parseWmiObject(stdout string) (osInfo, error) { + var o osInfo + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + + switch { + case strings.HasPrefix(line, "Caption"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect Caption. expected: "Caption : ", line: "%s"`, line) + } + o.productName = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "Version"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect Version. expected: "Version : ", line: "%s"`, line) + } + ss := strings.Split(strings.TrimSpace(rhs), ".") + o.version = strings.Join(ss[0:len(ss)-1], ".") + o.build = ss[len(ss)-1] + case strings.HasPrefix(line, "OperatingSystemSKU"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect OperatingSystemSKU. expected: "OperatingSystemSKU : ", line: "%s"`, line) + } + switch n := strings.TrimSpace(rhs); n { + case "0": + o.edition = "Undefined" + case "1": + o.edition = "Ultimate" + o.installationType = "Client" + case "2": + o.edition = "Home Basic" + o.installationType = "Client" + case "3": + o.edition = "Home Premium" + o.installationType = "Client" + case "4": + o.edition = "Enterprise" + o.installationType = "Client" + case "6": + o.edition = "Business" + o.installationType = "Client" + case "7": + o.edition = "Windows Server Standard Edition (Desktop Experience installation)" + o.installationType = "Server" + case "8": + o.edition = "Windows Server Datacenter Edition (Desktop Experience installation)" + o.installationType = "Server" + case "9": + o.edition = "Small Business Server" + o.installationType = "Server" + case "10": + o.edition = "Enterprise Server" + o.installationType = "Server" + case "11": + o.edition = "Starter" + case "12": + o.edition = "Datacenter Server Core" + o.installationType = "Server Core" + case "13": + o.edition = "Standard Server Core" + o.installationType = "Server Core" + case "14": + o.edition = "Enterprise Server Core" + o.installationType = "Server Core" + case "17": + o.edition = "Web Server" + o.installationType = "Server" + case "19": + o.edition = "Home Server" + o.installationType = "Server" + case "20": + o.edition = "Storage Express Server" + o.installationType = "Server" + case "21": + o.edition = "Windows Storage Server Standard" + o.installationType = "Server" + case "22": + o.edition = "Windows Storage Server Workgroup" + o.installationType = "Server" + case "23": + o.edition = "Storage Enterprise Server" + o.installationType = "Server" + case "24": + o.edition = "Server For Small Business" + o.installationType = "Server" + case "25": + o.edition = "Small Business Server Premium" + o.installationType = "Server" + case "27": + o.edition = "Enterprise" + o.installationType = "Client" + case "28": + o.edition = "Ultimate" + o.installationType = "Client" + case "29": + o.edition = "Windows Server Web Server Edition (Server Core installation)" + o.installationType = "Server Core" + case "36": + o.edition = "Windows Server Standard Edition without Hyper-V" + o.installationType = "Server" + case "37": + o.edition = "Windows Server Datacenter Edition without Hyper-V (full installation)" + o.installationType = "Server" + case "38": + o.edition = "Windows Server Enterprise Edition without Hyper-V (full installation)" + o.installationType = "Server" + case "39": + o.edition = "Windows Server Datacenter Edition without Hyper-V (Server Core installation)" + o.installationType = "Server Core" + case "40": + o.edition = "Windows Server Standard Edition without Hyper-V (Server Core installation)" + o.installationType = "Server Core" + case "41": + o.edition = "Windows Server Enterprise Edition without Hyper-V (Server Core installation)" + o.installationType = "Server Core" + case "42": + o.edition = "Microsoft Hyper-V Server" + o.installationType = "Server" + case "43": + o.edition = "Storage Server Express Edition (Server Core installation)" + o.installationType = "Server Core" + case "44": + o.edition = "Storage Server Standard Edition (Server Core installation)" + o.installationType = "Server Core" + case "45": + o.edition = "Storage Server Workgroup Edition (Server Core installation)" + o.installationType = "Server Core" + case "46": + o.edition = "Storage Server Enterprise Edition (Server Core installation)" + o.installationType = "Server Core" + case "48": + o.edition = "Professional" + o.installationType = "Client" + case "50": + o.edition = "Windows Server Essentials (Desktop Experience installation)" + o.installationType = "Server" + case "63": + o.edition = "Small Business Server Premium (Server Core installation)" + o.installationType = "Server Core" + case "64": + o.edition = "Windows Compute Cluster Server without Hyper-V" + o.installationType = "Server" + case "97": + o.edition = "Windows RT" + o.installationType = "Client" + case "101": + o.edition = "Home" + o.installationType = "Client" + case "103": + o.edition = "Media Center" + o.installationType = "Client" + case "104": + o.edition = "Mobile" + o.installationType = "Client" + case "123": + o.edition = "Windows IoT (Internet of Things) Core" + o.installationType = "Client" + case "143": + o.edition = "Windows Server Datacenter Edition (Nano Server installation)" + o.installationType = "Server" + case "144": + o.edition = "Windows Server Standard Edition (Nano Server installation)" + o.installationType = "Server" + case "147": + o.edition = "Windows Server Datacenter Edition (Server Core installation)" + o.installationType = "Server Core" + case "148": + o.edition = "Windows Server Standard Edition (Server Core installation)" + o.installationType = "Server Core" + case "175": + o.edition = "Windows Enterprise for Virtual Desktops (Azure Virtual Desktop)" + o.installationType = "Client" + default: + } + + case strings.HasPrefix(line, "CSDVersion"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect CSDVersion. expected: "CSDVersion : ", line: "%s"`, line) + } + o.servicePack = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "SystemType"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect SystemType. expected: "SystemType : ", line: "%s"`, line) + } + o.arch = strings.TrimSpace(strings.TrimSuffix(rhs, "PC")) + case strings.HasPrefix(line, "DomainRole"): + if o.installationType != "" { + break + } + + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect DomainRole. expected: "DomainRole : ", line: "%s"`, line) + } + switch domainRole := strings.TrimSpace(rhs); domainRole { // https://learn.microsoft.com/en-us/windows/win32/api/dsrole/ne-dsrole-dsrole_machine_role + case "0", "1": + o.installationType = "Client" + case "2", "3": + o.installationType = "Server" + case "4", "5": + o.installationType = "Controller" + default: + return osInfo{}, xerrors.Errorf("Failed to detect Installation Type from DomainRole. err: %s is invalid DomainRole", domainRole) + } + default: + } + } + return o, nil +} + +func parseRegistry(stdout, arch string) (osInfo, error) { + var ( + o osInfo + major string + minor string + ) + + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + + switch { + case strings.HasPrefix(line, "ProductName"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect ProductName. expected: "ProductName : ", line: "%s"`, line) + } + o.productName = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "CurrentVersion"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect CurrentVersion. expected: "CurrentVersion : ", line: "%s"`, line) + } + o.version = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "CurrentMajorVersionNumber"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect CurrentMajorVersionNumber. expected: "CurrentMajorVersionNumber : ", line: "%s"`, line) + } + major = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "CurrentMinorVersionNumber"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect CurrentMinorVersionNumber. expected: "CurrentMinorVersionNumber : ", line: "%s"`, line) + } + minor = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "CurrentBuildNumber"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect CurrentBuildNumber. expected: "CurrentBuildNumber : ", line: "%s"`, line) + } + o.build = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "UBR"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect UBR. expected: "UBR : ", line: "%s"`, line) + } + o.revision = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "EditionID"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect EditionID. expected: "EditionID : ", line: "%s"`, line) + } + o.edition = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "CSDVersion"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect CSDVersion. expected: "CSDVersion : ", line: "%s"`, line) + } + o.servicePack = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "InstallationType"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return osInfo{}, xerrors.Errorf(`Failed to detect InstallationType. expected: "InstallationType : ", line: "%s"`, line) + } + o.installationType = strings.TrimSpace(rhs) + default: + } + } + if major != "" && minor != "" { + o.version = fmt.Sprintf("%s.%s", major, minor) + } + + formatted, err := formatArch(arch) + if err != nil { + return osInfo{}, xerrors.Errorf("Failed to format arch. arch: %s, err: %w", arch, err) + } + o.arch = formatted + + return o, nil +} + +func detectOSName(osInfo osInfo) (string, error) { + osName, err := detectOSNameFromOSInfo(osInfo) + if err != nil { + return "", xerrors.Errorf("Failed to detect OS Name from OSInfo: %+v, err: %w", osInfo, err) + } + return osName, nil +} + +func detectOSNameFromOSInfo(osInfo osInfo) (string, error) { + switch osInfo.version { + case "5.0": + switch osInfo.installationType { + case "Client": + if osInfo.servicePack != "" { + return fmt.Sprintf("Microsoft Windows 2000 %s", osInfo.servicePack), nil + } + return "Microsoft Windows 2000", nil + case "Server": + if osInfo.servicePack != "" { + return fmt.Sprintf("Microsoft Windows 2000 Server %s", osInfo.servicePack), nil + } + return "Microsoft Windows 2000 Server", nil + } + case "5.1": + switch osInfo.installationType { + case "Client": + var n string + switch osInfo.edition { + case "Professional": + n = "Microsoft Windows XP Professional" + case "Media Center": + n = "Microsoft Windows XP Media Center Edition 2005" + case "Tablet PC": + n = "Microsoft Windows XP Tablet PC Edition 2005" + default: + n = "Microsoft Windows XP" + } + switch osInfo.arch { + case "x64-based": + n = fmt.Sprintf("%s x64 Edition", n) + } + if osInfo.servicePack != "" { + return fmt.Sprintf("%s %s", n, osInfo.servicePack), nil + } + return n, nil + } + case "5.2": + switch osInfo.installationType { + case "Client": + var n string + switch osInfo.edition { + case "Professional": + n = "Microsoft Windows XP Professional" + case "Media Center": + n = "Microsoft Windows XP Media Center Edition 2005" + case "Tablet PC": + n = "Microsoft Windows XP Tablet PC Edition 2005" + default: + n = "Microsoft Windows XP" + } + switch osInfo.arch { + case "x64-based": + n = fmt.Sprintf("%s x64 Edition", n) + } + if osInfo.servicePack != "" { + return fmt.Sprintf("%s %s", n, osInfo.servicePack), nil + } + return n, nil + case "Server": + n := "Microsoft Windows Server 2003" + if strings.Contains(osInfo.productName, "R2") { + n = "Microsoft Windows Server 2003 R2" + } + switch osInfo.arch { + case "x64-based": + n = fmt.Sprintf("%s x64 Edition", n) + case "Itanium-based": + if osInfo.edition == "Enterprise" { + n = fmt.Sprintf("%s, Enterprise Edition for Itanium-based Systems", n) + } else { + n = fmt.Sprintf("%s for Itanium-based Systems", n) + } + } + if osInfo.servicePack != "" { + return fmt.Sprintf("%s %s", n, osInfo.servicePack), nil + } + return n, nil + } + case "6.0": + switch osInfo.installationType { + case "Client": + var n string + switch osInfo.arch { + case "x64-based": + n = "Windows Vista x64 Editions" + default: + n = "Windows Vista" + } + if osInfo.servicePack != "" { + return fmt.Sprintf("%s %s", n, osInfo.servicePack), nil + } + return n, nil + case "Server": + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + if osInfo.servicePack != "" { + return fmt.Sprintf("Windows Server 2008 for %s Systems %s", arch, osInfo.servicePack), nil + } + return fmt.Sprintf("Windows Server 2008 for %s Systems", arch), nil + case "Server Core": + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + if osInfo.servicePack != "" { + return fmt.Sprintf("Windows Server 2008 for %s Systems %s (Server Core installation)", arch, osInfo.servicePack), nil + } + return fmt.Sprintf("Windows Server 2008 for %s Systems (Server Core installation)", arch), nil + } + case "6.1": + switch osInfo.installationType { + case "Client": + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + if osInfo.servicePack != "" { + return fmt.Sprintf("Windows 7 for %s Systems %s", arch, osInfo.servicePack), nil + } + return fmt.Sprintf("Windows 7 for %s Systems", arch), nil + case "Server": + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + if osInfo.servicePack != "" { + return fmt.Sprintf("Windows Server 2008 R2 for %s Systems %s", arch, osInfo.servicePack), nil + } + return fmt.Sprintf("Windows Server 2008 R2 for %s Systems", arch), nil + case "Server Core": + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + if osInfo.servicePack != "" { + return fmt.Sprintf("Windows Server 2008 R2 for %s Systems %s (Server Core installation)", arch, osInfo.servicePack), nil + } + return fmt.Sprintf("Windows Server 2008 R2 for %s Systems (Server Core installation)", arch), nil + } + case "6.2": + switch osInfo.installationType { + case "Client": + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + return fmt.Sprintf("Windows 8 for %s Systems", arch), nil + case "Server": + return "Windows Server 2012", nil + case "Server Core": + return "Windows Server 2012 (Server Core installation)", nil + } + case "6.3": + switch osInfo.installationType { + case "Client": + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + return fmt.Sprintf("Windows 8.1 for %s Systems", arch), nil + case "Server": + return "Windows Server 2012 R2", nil + case "Server Core": + return "Windows Server 2012 R2 (Server Core installation)", nil + } + case "10.0": + switch osInfo.installationType { + case "Client": + if strings.Contains(osInfo.productName, "Windows 11") { + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + name, err := formatNamebyBuild("11", osInfo.build) + if err != nil { + return "", err + } + return fmt.Sprintf("%s for %s Systems", name, arch), nil + } + + arch, err := formatArch(osInfo.arch) + if err != nil { + return "", err + } + name, err := formatNamebyBuild("10", osInfo.build) + if err != nil { + return "", err + } + return fmt.Sprintf("%s for %s Systems", name, arch), nil + case "Server": + return formatNamebyBuild("Server", osInfo.build) + case "Server Core": + name, err := formatNamebyBuild("Server", osInfo.build) + if err != nil { + return "", err + } + return fmt.Sprintf("%s (Server Core installation)", name), nil + } + } + return "", xerrors.New("OS Name not found") +} + +func formatArch(arch string) (string, error) { + switch arch { + case "AMD64", "x64-based": + return "x64-based", nil + case "ARM64", "ARM64-based": + return "ARM64-based", nil + case "IA64", "Itanium-based": + return "Itanium-based", nil + case "x86", "X86-based": + return "32-bit", nil + default: + return "", xerrors.New("CPU Architecture not found") + } +} + +type buildNumber struct { + build string + name string +} + +var ( + winBuilds = map[string][]buildNumber{ + "10": { + { + build: "10240", + name: "Windows 10", // not "Windows 10 Version 1507" + }, + { + build: "10586", + name: "Windows 10 Version 1511", + }, + { + build: "14393", + name: "Windows 10 Version 1607", + }, + { + build: "15063", + name: "Windows 10 Version 1703", + }, + { + build: "16299", + name: "Windows 10 Version 1709", + }, + { + build: "17134", + name: "Windows 10 Version 1803", + }, + { + build: "17763", + name: "Windows 10 Version 1809", + }, + { + build: "18362", + name: "Windows 10 Version 1903", + }, + { + build: "18363", + name: "Windows 10 Version 1909", + }, + { + build: "19041", + name: "Windows 10 Version 2004", + }, + { + build: "19042", + name: "Windows 10 Version 20H2", + }, + { + build: "19043", + name: "Windows 10 Version 21H1", + }, + { + build: "19044", + name: "Windows 10 Version 21H2", + }, + { + build: "19045", + name: "Windows 10 Version 22H2", + }, + // It seems that there are cases where the Product Name is Windows 10 even though it is Windows 11 + // ref: https://docs.microsoft.com/en-us/answers/questions/586548/in-the-official-version-of-windows-11-why-the-key.html + { + build: "22000", + name: "Windows 11 Version 21H2", + }, + { + build: "22621", + name: "Windows 11 Version 22H2", + }, + }, + "11": { + { + build: "22000", + name: "Windows 11 Version 21H2", + }, + { + build: "22621", + name: "Windows 11 Version 22H2", + }, + }, + "Server": { + { + build: "14393", + name: "Windows Server 2016", + }, + { + build: "16299", + name: "Windows Server, Version 1709", + }, + { + build: "17134", + name: "Windows Server, Version 1803", + }, + { + build: "17763", + name: "Windows Server, Version 1809", + }, + { + build: "17763", + name: "Windows Server 2019", + }, + { + build: "18362", + name: "Windows Server, Version 1903", + }, + { + build: "18363", + name: "Windows Server, Version 1909", + }, + { + build: "19041", + name: "Windows Server, Version 2004", + }, + { + build: "19042", + name: "Windows Server, Version 20H2", + }, + { + build: "20348", + name: "Windows Server 2022", + }, + }, + } +) + +func formatNamebyBuild(osType string, mybuild string) (string, error) { + builds, ok := winBuilds[osType] + if !ok { + return "", xerrors.New("OS Type not found") + } + + nMybuild, err := strconv.Atoi(mybuild) + if err != nil { + return "", xerrors.Errorf("Failed to parse build number. err: %w", err) + } + + v := builds[0].name + for _, b := range builds { + nBuild, err := strconv.Atoi(b.build) + if err != nil { + return "", xerrors.Errorf("Failed to parse build number. err: %w", err) + } + if nMybuild < nBuild { + break + } + v = b.name + } + return v, nil +} + +func formatKernelVersion(osInfo osInfo) string { + v := fmt.Sprintf("%s.%s", osInfo.version, osInfo.build) + if osInfo.revision != "" { + v = fmt.Sprintf("%s.%s", v, osInfo.revision) + } + return v +} + +func (o *windows) checkScanMode() error { + return nil +} + +func (o *windows) checkIfSudoNoPasswd() error { + return nil +} + +func (o *windows) checkDeps() error { + return nil +} + +func (o *windows) preCure() error { + return nil +} + +func (o *windows) postScan() error { + return nil +} + +func (o *windows) scanPackages() error { + if r := o.exec("$Packages = (Get-Package); Format-List -InputObject $Packages -Property Name, Version, ProviderName", noSudo); r.isSuccess() { + installed, _, err := o.parseInstalledPackages(r.Stdout) + if err != nil { + return xerrors.Errorf("Failed to parse installed packages. err: %w", err) + } + o.Packages = installed + } + + kbs, err := o.scanKBs() + if err != nil { + return xerrors.Errorf("Failed to scan KB. err: %w", err) + } + o.windowsKB = kbs + + return nil +} + +func (o *windows) parseInstalledPackages(stdout string) (models.Packages, models.SrcPackages, error) { + installed := models.Packages{} + + var name, version string + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + switch { + case line == "": + name, version = "", "" + case strings.HasPrefix(line, "Name"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return nil, nil, xerrors.Errorf(`Failed to detect PackageName. expected: "Name : ", line: "%s"`, line) + } + name = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "Version"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return nil, nil, xerrors.Errorf(`Failed to detect Version. expected: "Version : ", line: "%s"`, line) + } + version = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "ProviderName"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return nil, nil, xerrors.Errorf(`Failed to detect ProviderName. expected: "ProviderName : ", line: "%s"`, line) + } + + switch strings.TrimSpace(rhs) { + case "msu": + default: + if name != "" { + installed[name] = models.Package{Name: name, Version: version} + } + } + default: + } + } + + return installed, nil, nil +} + +func (o *windows) scanKBs() (*models.WindowsKB, error) { + applied, unapplied := map[string]struct{}{}, map[string]struct{}{} + if r := o.exec("$Hotfix = (Get-Hotfix); Format-List -InputObject $Hotfix -Property HotFixID", noSudo); r.isSuccess() { + kbs, err := o.parseGetHotfix(r.Stdout) + if err != nil { + return nil, xerrors.Errorf("Failed to parse Get-Hotifx. err: %w", err) + } + for _, kb := range kbs { + applied[kb] = struct{}{} + } + } + + if r := o.exec("$Packages = (Get-Package -ProviderName msu); Format-List -InputObject $Packages -Property Name", noSudo); r.isSuccess() { + kbs, err := o.parseGetPackageMSU(r.Stdout) + if err != nil { + return nil, xerrors.Errorf("Failed to parse Get-Package. err: %w", err) + } + for _, kb := range kbs { + applied[kb] = struct{}{} + } + } + + var searcher string + switch c := o.getServerInfo().Windows; c.ServerSelection { + case 3: // https://learn.microsoft.com/en-us/windows/win32/wua_sdk/using-wua-to-scan-for-updates-offline + searcher = fmt.Sprintf("$UpdateSession = (New-Object -ComObject Microsoft.Update.Session); $UpdateServiceManager = (New-Object -ComObject Microsoft.Update.ServiceManager); $UpdateService = $UpdateServiceManager.AddScanPackageService(\"Offline Sync Service\", \"%s\", 1); $UpdateSearcher = $UpdateSession.CreateUpdateSearcher(); $UpdateSearcher.ServerSelection = %d; $UpdateSearcher.ServiceID = $UpdateService.ServiceID;", c.CabPath, c.ServerSelection) + default: + searcher = fmt.Sprintf("$UpdateSession = (New-Object -ComObject Microsoft.Update.Session); $UpdateSearcher = $UpdateSession.CreateUpdateSearcher(); $UpdateSearcher.ServerSelection = %d;", c.ServerSelection) + } + + if r := o.exec(fmt.Sprintf(`%s $UpdateSearcher.search("IsInstalled = 1 and RebootRequired = 0 and Type='Software'").Updates | ForEach-Object -MemberName KBArticleIDs`, searcher), noSudo); r.isSuccess() { + kbs, err := o.parseWindowsUpdaterSearch(r.Stdout) + if err != nil { + return nil, xerrors.Errorf("Failed to parse Windows Update Search. err: %w", err) + } + for _, kb := range kbs { + applied[kb] = struct{}{} + } + } + if r := o.exec(fmt.Sprintf(`%s $UpdateSearcher.search("IsInstalled = 0 and Type='Software'").Updates | ForEach-Object -MemberName KBArticleIDs`, searcher), noSudo); r.isSuccess() { + kbs, err := o.parseWindowsUpdaterSearch(r.Stdout) + if err != nil { + return nil, xerrors.Errorf("Failed to parse Windows Update Search. err: %w", err) + } + for _, kb := range kbs { + unapplied[kb] = struct{}{} + } + } + if r := o.exec(fmt.Sprintf(`%s $UpdateSearcher.search("IsInstalled = 1 and RebootRequired = 1 and Type='Software'").Updates | ForEach-Object -MemberName KBArticleIDs`, searcher), noSudo); r.isSuccess() { + kbs, err := o.parseWindowsUpdaterSearch(r.Stdout) + if err != nil { + return nil, xerrors.Errorf("Failed to parse Windows Update Search. err: %w", err) + } + for _, kb := range kbs { + unapplied[kb] = struct{}{} + } + } + + if r := o.exec("$UpdateSearcher = (New-Object -ComObject Microsoft.Update.Session).CreateUpdateSearcher(); $HistoryCount = $UpdateSearcher.GetTotalHistoryCount(); $UpdateSearcher.QueryHistory(0, $HistoryCount) | Sort-Object -Property Date | Format-List -Property Title, Operation, ResultCode", noSudo); r.isSuccess() { + kbs, err := o.parseWindowsUpdateHistory(r.Stdout) + if err != nil { + return nil, xerrors.Errorf("Failed to parse Windows Update History. err: %w", err) + } + for _, kb := range kbs { + applied[kb] = struct{}{} + } + } + + v, err := o.detectKernelVersion(maps.Keys(applied)) + if err != nil { + return nil, xerrors.Errorf("Failed to detect kernel version. err: %w", err) + } + o.Kernel = models.Kernel{Version: v} + + kbs, err := o.detectKBsFromKernelVersion() + if err != nil { + return nil, xerrors.Errorf("Failed to detect KBs from kernel version. err: %w", err) + } + for _, kb := range kbs.Applied { + applied[kb] = struct{}{} + } + for _, kb := range kbs.Unapplied { + unapplied[kb] = struct{}{} + } + + return &models.WindowsKB{Applied: maps.Keys(applied), Unapplied: maps.Keys(unapplied)}, nil +} + +func (o *windows) parseGetHotfix(stdout string) ([]string, error) { + var kbs []string + + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + switch { + case strings.HasPrefix(line, "HotFixID"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return nil, xerrors.Errorf(`Failed to detect HotFixID. expected: "HotFixID : ", line: "%s"`, line) + } + kbs = append(kbs, strings.TrimPrefix(strings.TrimSpace(rhs), "KB")) + default: + } + } + + return kbs, nil +} + +func (o *windows) parseGetPackageMSU(stdout string) ([]string, error) { + var kbs []string + + kbIDPattern := regexp.MustCompile(`KB(\d{6,7})`) + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + switch { + case strings.HasPrefix(line, "Name"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return nil, xerrors.Errorf(`Failed to detect PackageName. expected: "Name : ", line: "%s"`, line) + } + + for _, m := range kbIDPattern.FindAllStringSubmatch(strings.TrimSpace(rhs), -1) { + kbs = append(kbs, m[1]) + } + default: + } + } + + return kbs, nil +} + +func (o *windows) parseWindowsUpdaterSearch(stdout string) ([]string, error) { + var kbs []string + + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + if line := scanner.Text(); line != "" { + kbs = append(kbs, line) + } + } + + return kbs, nil +} + +func (o *windows) parseWindowsUpdateHistory(stdout string) ([]string, error) { + kbs := map[string]struct{}{} + + kbIDPattern := regexp.MustCompile(`KB(\d{6,7})`) + var title, operation string + scanner := bufio.NewScanner(strings.NewReader(stdout)) + for scanner.Scan() { + line := scanner.Text() + switch { + case line == "": + title, operation = "", "" + case strings.HasPrefix(line, "Title"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return nil, xerrors.Errorf(`Failed to detect Title. expected: "Title : ", line: "%s"`, line) + } + title = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "Operation"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return nil, xerrors.Errorf(`Failed to detect Operation. expected: "Operation : <Operation>", line: "%s"`, line) + } + operation = strings.TrimSpace(rhs) + case strings.HasPrefix(line, "ResultCode"): + _, rhs, found := strings.Cut(line, ":") + if !found { + return nil, xerrors.Errorf(`Failed to detect ResultCode. expected: "ResultCode : <ResultCode>", line: "%s"`, line) + } + + // https://learn.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-operationresultcode + if strings.TrimSpace(rhs) == "2" { + for _, m := range kbIDPattern.FindAllStringSubmatch(title, -1) { + // https://learn.microsoft.com/en-us/windows/win32/api/wuapi/ne-wuapi-updateoperation + switch operation { + case "1": + kbs[m[1]] = struct{}{} + case "2": + delete(kbs, m[1]) + default: + } + } + } + default: + } + } + + return maps.Keys(kbs), nil +} + +type windowsRelease struct { + revision string + kb string +} + +type updateProgram struct { + rollup []windowsRelease + securityOnly []string +} + +var windowsReleases = map[string]map[string]map[string]updateProgram{ + "Client": { + "7": { + // https://support.microsoft.com/en-us/topic/windows-7-sp1-and-windows-server-2008-r2-sp1-update-history-720c2590-fd58-26ba-16cc-6d8f3b547599 + "SP1": { + rollup: []windowsRelease{ + {revision: "", kb: "3172605"}, + {revision: "", kb: "3179573"}, + {revision: "", kb: "3185278"}, + {revision: "", kb: "3185330"}, + {revision: "", kb: "3192403"}, + {revision: "", kb: "3197868"}, + {revision: "", kb: "3197869"}, + {revision: "", kb: "3207752"}, + {revision: "", kb: "3212646"}, + {revision: "", kb: "4012215"}, + {revision: "", kb: "4012218"}, + {revision: "", kb: "4015549"}, + {revision: "", kb: "4015552"}, + {revision: "", kb: "4019264"}, + {revision: "", kb: "4019265"}, + {revision: "", kb: "4022719"}, + {revision: "", kb: "4022168"}, + {revision: "", kb: "4025341"}, + {revision: "", kb: "4025340"}, + {revision: "", kb: "4034664"}, + {revision: "", kb: "4034670"}, + {revision: "", kb: "4038777"}, + {revision: "", kb: "4038803"}, + {revision: "", kb: "4041681"}, + {revision: "", kb: "4041686"}, + {revision: "", kb: "4048957"}, + {revision: "", kb: "4051034"}, + {revision: "", kb: "4054518"}, + {revision: "", kb: "4056894"}, + {revision: "", kb: "4057400"}, + {revision: "", kb: "4074598"}, + {revision: "", kb: "4075211"}, + {revision: "", kb: "4088875"}, + {revision: "", kb: "4088881"}, + {revision: "", kb: "4093118"}, + {revision: "", kb: "4093113"}, + {revision: "", kb: "4103718"}, + {revision: "", kb: "4103713"}, + {revision: "", kb: "4284826"}, + {revision: "", kb: "4284842"}, + {revision: "", kb: "4338818"}, + {revision: "", kb: "4338821"}, + {revision: "", kb: "4343900"}, + {revision: "", kb: "4343894"}, + {revision: "", kb: "4457144"}, + {revision: "", kb: "4457139"}, + {revision: "", kb: "4462923"}, + {revision: "", kb: "4462927"}, + {revision: "", kb: "4467107"}, + {revision: "", kb: "4467108"}, + {revision: "", kb: "4471318"}, + {revision: "", kb: "4480970"}, + {revision: "", kb: "4480955"}, + {revision: "", kb: "4486563"}, + {revision: "", kb: "4486565"}, + {revision: "", kb: "4489878"}, + {revision: "", kb: "4489892"}, + {revision: "", kb: "4493472"}, + {revision: "", kb: "4493453"}, + {revision: "", kb: "4499164"}, + {revision: "", kb: "4499178"}, + {revision: "", kb: "4503292"}, + {revision: "", kb: "4503277"}, + {revision: "", kb: "4507449"}, + {revision: "", kb: "4507437"}, + {revision: "", kb: "4512506"}, + {revision: "", kb: "4512514"}, + {revision: "", kb: "4516065"}, + {revision: "", kb: "4516048"}, + {revision: "", kb: "4524157"}, + {revision: "", kb: "4519976"}, + {revision: "", kb: "4519972"}, + {revision: "", kb: "4525235"}, + {revision: "", kb: "4525251"}, + {revision: "", kb: "4530734"}, + {revision: "", kb: "4534310"}, + {revision: "", kb: "4539601"}, + {revision: "", kb: "4537820"}, + {revision: "", kb: "4540688"}, + {revision: "", kb: "4550964"}, + {revision: "", kb: "4556836"}, + {revision: "", kb: "4561643"}, + {revision: "", kb: "4565524"}, + {revision: "", kb: "4571729"}, + {revision: "", kb: "4577051"}, + {revision: "", kb: "4580345"}, + {revision: "", kb: "4586827"}, + {revision: "", kb: "4592471"}, + {revision: "", kb: "4598279"}, + {revision: "", kb: "4601347"}, + {revision: "", kb: "5000841"}, + {revision: "", kb: "5001335"}, + {revision: "", kb: "5003233"}, + {revision: "", kb: "5003667"}, + {revision: "", kb: "5004953"}, + {revision: "", kb: "5004289"}, + {revision: "", kb: "5005088"}, + {revision: "", kb: "5005633"}, + {revision: "", kb: "5006743"}, + {revision: "", kb: "5007236"}, + {revision: "", kb: "5008244"}, + {revision: "", kb: "5009610"}, + {revision: "", kb: "5010404"}, + {revision: "", kb: "5011552"}, + {revision: "", kb: "5012626"}, + {revision: "", kb: "5014012"}, + {revision: "", kb: "5014748"}, + {revision: "", kb: "5015861"}, + {revision: "", kb: "5016676"}, + {revision: "", kb: "5017361"}, + {revision: "", kb: "5018454"}, + {revision: "", kb: "5020000"}, + {revision: "", kb: "5021291"}, + {revision: "", kb: "5022338"}, + {revision: "", kb: "5022872"}, + }, + securityOnly: []string{ + "3192391", + "3197867", + "3205394", + "3212642", + "4012212", + "4015546", + "4019263", + "4022722", + "4025337", + "4034679", + "4038779", + "4041678", + "4048960", + "4054521", + "4056897", + "4074587", + "4088878", + "4093108", + "4103712", + "4284867", + "4338823", + "4343899", + "4457145", + "4462915", + "4467106", + "4471328", + "4480960", + "4486564", + "4489885", + "4493448", + "4499175", + "4503269", + "4507456", + "4512486", + "4516033", + "4520003", + "4525233", + "4530692", + "4534314", + "4537813", + "4541500", + "4550965", + "4556843", + "4561669", + "4565539", + "4571719", + "4577053", + "4580387", + "4586805", + "4592503", + "4598289", + "4601363", + "5000851", + "5001392", + "5003228", + "5003694", + "5004951", + "5004307", + "5005089", + "5005615", + "5006728", + "5007233", + "5008282", + "5009621", + "5010422", + "5011529", + "5012649", + "5013999", + "5014742", + "5015862", + "5016679", + "5017373", + "5018479", + "5020013", + "5021288", + "5022339", + "5022874", + }, + }, + }, + "8.1": { + // https://support.microsoft.com/en-us/topic/windows-8-1-and-windows-server-2012-r2-update-history-47d81dd2-6804-b6ae-4112-20089467c7a6 + "": { + rollup: []windowsRelease{ + {revision: "", kb: "3172614"}, + {revision: "", kb: "3179574"}, + {revision: "", kb: "3185279"}, + {revision: "", kb: "3185331"}, + {revision: "", kb: "3192404"}, + {revision: "", kb: "3197874"}, + {revision: "", kb: "3197875"}, + {revision: "", kb: "3205401"}, + {revision: "", kb: "4012216"}, + {revision: "", kb: "4012219"}, + {revision: "", kb: "4015550"}, + {revision: "", kb: "4015553"}, + {revision: "", kb: "4019215"}, + {revision: "", kb: "4019217"}, + {revision: "", kb: "4022726"}, + {revision: "", kb: "4022720"}, + {revision: "", kb: "4025336"}, + {revision: "", kb: "4025335"}, + {revision: "", kb: "4034681"}, + {revision: "", kb: "4034663"}, + {revision: "", kb: "4038792"}, + {revision: "", kb: "4038774"}, + {revision: "", kb: "4041693"}, + {revision: "", kb: "4041685"}, + {revision: "", kb: "4048958"}, + {revision: "", kb: "4050946"}, + {revision: "", kb: "4054519"}, + {revision: "", kb: "4056895"}, + {revision: "", kb: "4057401"}, + {revision: "", kb: "4074594"}, + {revision: "", kb: "4075212"}, + {revision: "", kb: "4088876"}, + {revision: "", kb: "4088882"}, + {revision: "", kb: "4093114"}, + {revision: "", kb: "4093121"}, + {revision: "", kb: "4103725"}, + {revision: "", kb: "4103724"}, + {revision: "", kb: "4284815"}, + {revision: "", kb: "4284863"}, + {revision: "", kb: "4338815"}, + {revision: "", kb: "4338831"}, + {revision: "", kb: "4343898"}, + {revision: "", kb: "4343891"}, + {revision: "", kb: "4457129"}, + {revision: "", kb: "4457133"}, + {revision: "", kb: "4462926"}, + {revision: "", kb: "4462921"}, + {revision: "", kb: "4467697"}, + {revision: "", kb: "4467695"}, + {revision: "", kb: "4471320"}, + {revision: "", kb: "4480963"}, + {revision: "", kb: "4480969"}, + {revision: "", kb: "4487000"}, + {revision: "", kb: "4487016"}, + {revision: "", kb: "4489881"}, + {revision: "", kb: "4489893"}, + {revision: "", kb: "4493446"}, + {revision: "", kb: "4493443"}, + {revision: "", kb: "4499151"}, + {revision: "", kb: "4499182"}, + {revision: "", kb: "4503276"}, + {revision: "", kb: "4503283"}, + {revision: "", kb: "4507448"}, + {revision: "", kb: "4507463"}, + {revision: "", kb: "4512488"}, + {revision: "", kb: "4512478"}, + {revision: "", kb: "4516067"}, + {revision: "", kb: "4516041"}, + {revision: "", kb: "4524156"}, + {revision: "", kb: "4520005"}, + {revision: "", kb: "4520012"}, + {revision: "", kb: "4525243"}, + {revision: "", kb: "4525252"}, + {revision: "", kb: "4530702"}, + {revision: "", kb: "4534297"}, + {revision: "", kb: "4534324"}, + {revision: "", kb: "4537821"}, + {revision: "", kb: "4537819"}, + {revision: "", kb: "4541509"}, + {revision: "", kb: "4541334"}, + {revision: "", kb: "4550961"}, + {revision: "", kb: "4550958"}, + {revision: "", kb: "4556846"}, + {revision: "", kb: "4561666"}, + {revision: "", kb: "4565541"}, + {revision: "", kb: "4571703"}, + {revision: "", kb: "4577066"}, + {revision: "", kb: "4580347"}, + {revision: "", kb: "4586845"}, + {revision: "", kb: "4592484"}, + {revision: "", kb: "4598285"}, + {revision: "", kb: "4601384"}, + {revision: "", kb: "5000848"}, + {revision: "", kb: "5001382"}, + {revision: "", kb: "5003209"}, + {revision: "", kb: "5003671"}, + {revision: "", kb: "5004954"}, + {revision: "", kb: "5004298"}, + {revision: "", kb: "5005076"}, + {revision: "", kb: "5005613"}, + {revision: "", kb: "5006714"}, + {revision: "", kb: "5007247"}, + {revision: "", kb: "5008263"}, + {revision: "", kb: "5009624"}, + {revision: "", kb: "5010419"}, + {revision: "", kb: "5011564"}, + {revision: "", kb: "5012670"}, + {revision: "", kb: "5014011"}, + {revision: "", kb: "5014738"}, + {revision: "", kb: "5015874"}, + {revision: "", kb: "5016681"}, + {revision: "", kb: "5017367"}, + {revision: "", kb: "5018474"}, + {revision: "", kb: "5020023"}, + {revision: "", kb: "5021294"}, + {revision: "", kb: "5022352"}, + {revision: "", kb: "5022899"}, + }, + securityOnly: []string{ + "3192392", + "3197873", + "3205400", + "4012213", + "4015547", + "4019213", + "4022717", + "4025333", + "4034672", + "4038793", + "4041687", + "4048961", + "4054522", + "4056898", + "4074597", + "4088879", + "4093115", + "4103715", + "4284878", + "4338824", + "4343888", + "4457143", + "4462941", + "4467703", + "4471322", + "4480964", + "4487028", + "4489883", + "4493467", + "4499165", + "4503290", + "4507457", + "4512489", + "4516064", + "4519990", + "4525250", + "4530730", + "4534309", + "4537803", + "4541505", + "4550970", + "4556853", + "4561673", + "4565540", + "4571723", + "4577071", + "4580358", + "4586823", + "4592495", + "4598275", + "4601349", + "5000853", + "5001393", + "5003220", + "5003681", + "5004958", + "5004285", + "5005106", + "5005627", + "5006729", + "5007255", + "5008285", + "5009595", + "5010395", + "5011560", + "5012639", + "5014001", + "5014746", + "5015877", + "5016683", + "5017365", + "5018476", + "5020010", + "5021296", + "5022346", + "5022894", + }, + }, + }, + "10": { + // https://learn.microsoft.com/en-us/windows/release-health/release-information + // https://support.microsoft.com/en-us/topic/windows-10-update-history-93345c32-4ae1-6d1c-f885-6c0b718adf3b + "10240": { + rollup: []windowsRelease{ + {revision: "16405", kb: "3074683"}, + {revision: "16413", kb: "3081424"}, + {revision: "16430", kb: "3081436"}, + {revision: "16433", kb: "3081438"}, + {revision: "16445", kb: "3081444"}, + {revision: "16463", kb: "3081448"}, + {revision: "16487", kb: "3081455"}, + {revision: "16520", kb: "3093266"}, + {revision: "16549", kb: "3097617"}, + {revision: "16566", kb: "3105210"}, + {revision: "16590", kb: "3105213"}, + {revision: "16601", kb: "3116869"}, + {revision: "16644", kb: "3124266"}, + {revision: "16683", kb: "3135174"}, + {revision: "16725", kb: "3140745"}, + {revision: "16769", kb: "3147461"}, + {revision: "16771", kb: "3147461"}, + {revision: "16854", kb: "3156387"}, + {revision: "16942", kb: "3163017"}, + {revision: "17024", kb: "3163912"}, + {revision: "17071", kb: "3176492"}, + {revision: "17113", kb: "3185611"}, + {revision: "17113", kb: "3193821"}, + {revision: "17146", kb: "3192440"}, + {revision: "17190", kb: "3198585"}, + {revision: "17202", kb: "3205383"}, + {revision: "17236", kb: "3210720"}, + {revision: "17319", kb: "4012606"}, + {revision: "17320", kb: "4016637"}, + {revision: "17354", kb: "4015221"}, + {revision: "17394", kb: "4019474"}, + {revision: "17443", kb: "4022727"}, + {revision: "17446", kb: "4032695"}, + {revision: "17488", kb: "4025338"}, + {revision: "17533", kb: "4034668"}, + {revision: "17609", kb: "4038781"}, + {revision: "17643", kb: "4042895"}, + {revision: "17673", kb: "4048956"}, + {revision: "17709", kb: "4053581"}, + {revision: "17738", kb: "4056893"}, + {revision: "17741", kb: "4075199"}, + {revision: "17741", kb: "4077735"}, + {revision: "17770", kb: "4074596"}, + {revision: "17797", kb: "4088786"}, + {revision: "17831", kb: "4093111"}, + {revision: "17861", kb: "4103716"}, + {revision: "17889", kb: "4284860"}, + {revision: "17914", kb: "4338829"}, + {revision: "17918", kb: "4345455"}, + {revision: "17946", kb: "4343892"}, + {revision: "17976", kb: "4457132"}, + {revision: "18005", kb: "4462922"}, + {revision: "18036", kb: "4467680"}, + {revision: "18063", kb: "4471323"}, + {revision: "18064", kb: "4483228"}, + {revision: "18094", kb: "4480962"}, + {revision: "18132", kb: "4487018"}, + {revision: "18135", kb: "4491101"}, + {revision: "18158", kb: "4489872"}, + {revision: "18186", kb: "4493475"}, + {revision: "18187", kb: "4498375"}, + {revision: "18215", kb: "4499154"}, + {revision: "18218", kb: "4505051"}, + {revision: "18244", kb: "4503291"}, + {revision: "18275", kb: "4507458"}, + {revision: "18305", kb: "4512497"}, + {revision: "18308", kb: "4517276"}, + {revision: "18333", kb: "4516070"}, + {revision: "18334", kb: "4522009"}, + {revision: "18335", kb: "4524153"}, + {revision: "18368", kb: "4520011"}, + {revision: "18395", kb: "4525232"}, + {revision: "18427", kb: "4530681"}, + {revision: "18453", kb: "4534306"}, + {revision: "18486", kb: "4537776"}, + {revision: "18519", kb: "4540693"}, + {revision: "18545", kb: "4550930"}, + {revision: "18575", kb: "4556826"}, + {revision: "18608", kb: "4561649"}, + {revision: "18609", kb: "4567518"}, + {revision: "18638", kb: "4565513"}, + {revision: "18666", kb: "4571692"}, + {revision: "18696", kb: "4577049"}, + {revision: "18725", kb: "4580327"}, + {revision: "18756", kb: "4586787"}, + {revision: "18782", kb: "4592464"}, + {revision: "18818", kb: "4598231"}, + {revision: "18841", kb: "4601331"}, + {revision: "18842", kb: "4601331"}, + {revision: "18874", kb: "5000807"}, + {revision: "18875", kb: "5001631"}, + {revision: "18906", kb: "5001340"}, + {revision: "18932", kb: "5003172"}, + {revision: "18967", kb: "5003687"}, + {revision: "18969", kb: "5004950"}, + {revision: "19003", kb: "5004249"}, + {revision: "19022", kb: "5005040"}, + {revision: "19060", kb: "5005569"}, + {revision: "19086", kb: "5006675"}, + {revision: "19119", kb: "5007207"}, + {revision: "19145", kb: "5008230"}, + {revision: "19177", kb: "5009585"}, + {revision: "19179", kb: "5010789"}, + {revision: "19204", kb: "5010358"}, + {revision: "19235", kb: "5011491"}, + {revision: "19265", kb: "5012653"}, + {revision: "19297", kb: "5013963"}, + {revision: "19325", kb: "5014710"}, + {revision: "19360", kb: "5015832"}, + {revision: "19387", kb: "5016639"}, + {revision: "19444", kb: "5017327"}, + {revision: "19507", kb: "5018425"}, + {revision: "19509", kb: "5020440"}, + {revision: "19567", kb: "5019970"}, + {revision: "19624", kb: "5021243"}, + {revision: "19685", kb: "5022297"}, + {revision: "19747", kb: "5022858"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-2ad7900f-882c-1dfc-f9d7-82b7ca162010 + "10586": { + rollup: []windowsRelease{ + {revision: "3", kb: "3105211"}, + {revision: "11", kb: "3118754"}, + {revision: "14", kb: "3120677"}, + {revision: "17", kb: "3116908"}, + {revision: "29", kb: "3116900"}, + {revision: "36", kb: "3124200"}, + {revision: "63", kb: "3124263"}, + {revision: "71", kb: "3124262"}, + {revision: "104", kb: "3135173"}, + {revision: "122", kb: "3140743"}, + {revision: "164", kb: "3140768"}, + {revision: "218", kb: "3147458"}, + {revision: "318", kb: "3156421"}, + {revision: "420", kb: "3163018"}, + {revision: "494", kb: "3172985"}, + {revision: "545", kb: "3176493"}, + {revision: "589", kb: "3185614"}, + {revision: "633", kb: "3192441"}, + {revision: "679", kb: "3198586"}, + {revision: "682", kb: "3198586"}, + {revision: "713", kb: "3205386"}, + {revision: "753", kb: "3210721"}, + {revision: "839", kb: "4013198"}, + {revision: "842", kb: "4016636"}, + {revision: "873", kb: "4015219"}, + {revision: "916", kb: "4019473"}, + {revision: "962", kb: "4022714"}, + {revision: "965", kb: "4032693"}, + {revision: "1007", kb: "4025344"}, + {revision: "1045", kb: "4034660"}, + {revision: "1106", kb: "4038783"}, + {revision: "1176", kb: "4041689"}, + {revision: "1177", kb: "4052232"}, + {revision: "1232", kb: "4048952"}, + {revision: "1295", kb: "4053578"}, + {revision: "1356", kb: "4056888"}, + {revision: "1358", kb: "4075200"}, + {revision: "1417", kb: "4074591"}, + {revision: "1478", kb: "4088779"}, + {revision: "1540", kb: "4093109"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-and-windows-server-2016-update-history-4acfbc84-a290-1b54-536a-1c0430e9f3fd + "14393": { + rollup: []windowsRelease{ + {revision: "10", kb: "3176929"}, + {revision: "51", kb: "3176495"}, + {revision: "82", kb: "3176934"}, + {revision: "105", kb: "3176938"}, + {revision: "187", kb: "3189866"}, + {revision: "187", kb: "3193494"}, + {revision: "189", kb: "3193494"}, + {revision: "222", kb: "3194496"}, + {revision: "321", kb: "3194798"}, + {revision: "351", kb: "3197954"}, + {revision: "447", kb: "3200970"}, + {revision: "448", kb: "3200970"}, + {revision: "479", kb: "3201845"}, + {revision: "571", kb: "3206632"}, + {revision: "576", kb: "3206632"}, + {revision: "693", kb: "3213986"}, + {revision: "729", kb: "4010672"}, + {revision: "953", kb: "4013429"}, + {revision: "969", kb: "4015438"}, + {revision: "970", kb: "4016635"}, + {revision: "1066", kb: "4015217"}, + {revision: "1083", kb: "4015217"}, + {revision: "1198", kb: "4019472"}, + {revision: "1230", kb: "4023680"}, + {revision: "1358", kb: "4022715"}, + {revision: "1378", kb: "4022723"}, + {revision: "1480", kb: "4025339"}, + {revision: "1532", kb: "4025334"}, + {revision: "1537", kb: "4038220"}, + {revision: "1593", kb: "4034658"}, + {revision: "1613", kb: "4034661"}, + {revision: "1670", kb: "4039396"}, + {revision: "1715", kb: "4038782"}, + {revision: "1737", kb: "4038801"}, + {revision: "1770", kb: "4041691"}, + {revision: "1794", kb: "4041688"}, + {revision: "1797", kb: "4052231"}, + {revision: "1884", kb: "4048953"}, + {revision: "1914", kb: "4051033"}, + {revision: "1944", kb: "4053579"}, + {revision: "2007", kb: "4056890"}, + {revision: "2034", kb: "4057142"}, + {revision: "2035", kb: "4057142"}, + {revision: "2068", kb: "4074590"}, + {revision: "2097", kb: "4077525"}, + {revision: "2125", kb: "4088787"}, + {revision: "2126", kb: "4088787"}, + {revision: "2155", kb: "4088889"}, + {revision: "2156", kb: "4096309"}, + {revision: "2189", kb: "4093119"}, + {revision: "2214", kb: "4093120"}, + {revision: "2248", kb: "4103723"}, + {revision: "2273", kb: "4103720"}, + {revision: "2312", kb: "4284880"}, + {revision: "2339", kb: "4284833"}, + {revision: "2363", kb: "4338814"}, + {revision: "2368", kb: "4345418"}, + {revision: "2395", kb: "4338822"}, + {revision: "2396", kb: "4346877"}, + {revision: "2430", kb: "4343887"}, + {revision: "2457", kb: "4343884"}, + {revision: "2485", kb: "4457131"}, + {revision: "2515", kb: "4457127"}, + {revision: "2551", kb: "4462917"}, + {revision: "2580", kb: "4462928"}, + {revision: "2608", kb: "4467691"}, + {revision: "2639", kb: "4467684"}, + {revision: "2641", kb: "4478877"}, + {revision: "2665", kb: "4471321"}, + {revision: "2670", kb: "4483229"}, + {revision: "2724", kb: "4480961"}, + {revision: "2759", kb: "4480977"}, + {revision: "2791", kb: "4487026"}, + {revision: "2828", kb: "4487006"}, + {revision: "2848", kb: "4489882"}, + {revision: "2879", kb: "4489889"}, + {revision: "2906", kb: "4493470"}, + {revision: "2908", kb: "4499418"}, + {revision: "2941", kb: "4493473"}, + {revision: "2969", kb: "4494440"}, + {revision: "2972", kb: "4505052"}, + {revision: "2999", kb: "4499177"}, + {revision: "3025", kb: "4503267"}, + {revision: "3053", kb: "4503294"}, + {revision: "3056", kb: "4509475"}, + {revision: "3085", kb: "4507460"}, + {revision: "3115", kb: "4507459"}, + {revision: "3144", kb: "4512517"}, + {revision: "3181", kb: "4512495"}, + {revision: "3204", kb: "4516044"}, + {revision: "3206", kb: "4522010"}, + {revision: "3242", kb: "4516061"}, + {revision: "3243", kb: "4524152"}, + {revision: "3274", kb: "4519998"}, + {revision: "3300", kb: "4519979"}, + {revision: "3326", kb: "4525236"}, + {revision: "3384", kb: "4530689"}, + {revision: "3443", kb: "4534271"}, + {revision: "3474", kb: "4534307"}, + {revision: "3504", kb: "4537764"}, + {revision: "3542", kb: "4537806"}, + {revision: "3564", kb: "4540670"}, + {revision: "3595", kb: "4541329"}, + {revision: "3630", kb: "4550929"}, + {revision: "3659", kb: "4550947"}, + {revision: "3686", kb: "4556813"}, + {revision: "3750", kb: "4561616"}, + {revision: "3755", kb: "4567517"}, + {revision: "3808", kb: "4565511"}, + {revision: "3866", kb: "4571694"}, + {revision: "3930", kb: "4577015"}, + {revision: "3986", kb: "4580346"}, + {revision: "4046", kb: "4586830"}, + {revision: "4048", kb: "4594441"}, + {revision: "4104", kb: "4593226"}, + {revision: "4169", kb: "4598243"}, + {revision: "4225", kb: "4601318"}, + {revision: "4283", kb: "5000803"}, + {revision: "4288", kb: "5001633"}, + {revision: "4350", kb: "5001347"}, + {revision: "4402", kb: "5003197"}, + {revision: "4467", kb: "5003638"}, + {revision: "4470", kb: "5004948"}, + {revision: "4530", kb: "5004238"}, + {revision: "4532", kb: "5005393"}, + {revision: "4583", kb: "5005043"}, + {revision: "4651", kb: "5005573"}, + {revision: "4704", kb: "5006669"}, + {revision: "4770", kb: "5007192"}, + {revision: "4771", kb: "5008601"}, + {revision: "4825", kb: "5008207"}, + {revision: "4827", kb: "5010195"}, + {revision: "4886", kb: "5009546"}, + {revision: "4889", kb: "5010790"}, + {revision: "4946", kb: "5010359"}, + {revision: "5006", kb: "5011495"}, + {revision: "5066", kb: "5012596"}, + {revision: "5125", kb: "5013952"}, + {revision: "5127", kb: "5015019"}, + {revision: "5192", kb: "5014702"}, + {revision: "5246", kb: "5015808"}, + {revision: "5291", kb: "5016622"}, + {revision: "5356", kb: "5017305"}, + {revision: "5427", kb: "5018411"}, + {revision: "5429", kb: "5020439"}, + {revision: "5501", kb: "5019964"}, + {revision: "5502", kb: "5021654"}, + {revision: "5582", kb: "5021235"}, + {revision: "5648", kb: "5022289"}, + {revision: "5717", kb: "5022838"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-83aa43c0-82e0-92d8-1580-10642c9ed612 + "15063": { + rollup: []windowsRelease{ + {revision: "13", kb: "4016251"}, + {revision: "138", kb: "4015583"}, + {revision: "250", kb: "4016240"}, + {revision: "296", kb: "4016871"}, + {revision: "297", kb: "4016871"}, + {revision: "332", kb: "4020102"}, + {revision: "413", kb: "4022725"}, + {revision: "414", kb: "4022725"}, + {revision: "447", kb: "4022716"}, + {revision: "483", kb: "4025342"}, + {revision: "502", kb: "4032188"}, + {revision: "540", kb: "4034674"}, + {revision: "608", kb: "4038788"}, + {revision: "632", kb: "4040724"}, + {revision: "674", kb: "4041676"}, + {revision: "675", kb: "4049370"}, + {revision: "726", kb: "4048954"}, + {revision: "728", kb: "4048954"}, + {revision: "729", kb: "4055254"}, + {revision: "786", kb: "4053580"}, + {revision: "850", kb: "4056891"}, + {revision: "877", kb: "4057144"}, + {revision: "909", kb: "4074592"}, + {revision: "936", kb: "4077528"}, + {revision: "936", kb: "4092077"}, + {revision: "966", kb: "4088782"}, + {revision: "968", kb: "4088782"}, + {revision: "994", kb: "4088891"}, + {revision: "1029", kb: "4093107"}, + {revision: "1058", kb: "4093117"}, + {revision: "1088", kb: "4103731"}, + {revision: "1112", kb: "4103722"}, + {revision: "1155", kb: "4284874"}, + {revision: "1182", kb: "4284830"}, + {revision: "1206", kb: "4338826"}, + {revision: "1209", kb: "4345419"}, + {revision: "1235", kb: "4338827"}, + {revision: "1266", kb: "4343885"}, + {revision: "1292", kb: "4343889"}, + {revision: "1324", kb: "4457138"}, + {revision: "1356", kb: "4457141"}, + {revision: "1358", kb: "4457141"}, + {revision: "1387", kb: "4462937"}, + {revision: "1418", kb: "4462939"}, + {revision: "1446", kb: "4467696"}, + {revision: "1478", kb: "4467699"}, + {revision: "1506", kb: "4471327"}, + {revision: "1508", kb: "4483230"}, + {revision: "1563", kb: "4480973"}, + {revision: "1596", kb: "4480959"}, + {revision: "1631", kb: "4487020"}, + {revision: "1659", kb: "4487011"}, + {revision: "1689", kb: "4489871"}, + {revision: "1716", kb: "4489888"}, + {revision: "1746", kb: "4493474"}, + {revision: "1784", kb: "4493436"}, + {revision: "1785", kb: "4502112"}, + {revision: "1805", kb: "4499181"}, + {revision: "1808", kb: "4505055"}, + {revision: "1839", kb: "4499162"}, + {revision: "1868", kb: "4503279"}, + {revision: "1897", kb: "4503289"}, + {revision: "1898", kb: "4509476"}, + {revision: "1928", kb: "4507450"}, + {revision: "1955", kb: "4507467"}, + {revision: "1988", kb: "4512507"}, + {revision: "2021", kb: "4512474"}, + {revision: "2045", kb: "4516068"}, + {revision: "2046", kb: "4522011"}, + {revision: "2078", kb: "4516059"}, + {revision: "2079", kb: "4524151"}, + {revision: "2108", kb: "4520010"}, + {revision: "2172", kb: "4525245"}, + {revision: "2224", kb: "4530711"}, + {revision: "2254", kb: "4534296"}, + {revision: "2284", kb: "4537765"}, + {revision: "2313", kb: "4540705"}, + {revision: "2346", kb: "4550939"}, + {revision: "2375", kb: "4556804"}, + {revision: "2409", kb: "4561605"}, + {revision: "2411", kb: "4567516"}, + {revision: "2439", kb: "4565499"}, + {revision: "2467", kb: "4571689"}, + {revision: "2500", kb: "4577021"}, + {revision: "2525", kb: "4580370"}, + {revision: "2554", kb: "4586782"}, + {revision: "2584", kb: "4592473"}, + {revision: "2614", kb: "4599208"}, + {revision: "2642", kb: "4601330"}, + {revision: "2679", kb: "5000812"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-and-windows-server-update-history-8e779ac1-e840-d3b8-524e-91037bf7645a + "16299": { + rollup: []windowsRelease{ + {revision: "19", kb: "4043961"}, + {revision: "64", kb: "4048955"}, + {revision: "98", kb: "4051963"}, + {revision: "125", kb: "4054517"}, + {revision: "192", kb: "4056892"}, + {revision: "194", kb: "4073290"}, + {revision: "201", kb: "4073291"}, + {revision: "214", kb: "4058258"}, + {revision: "248", kb: "4074588"}, + {revision: "251", kb: "4090913"}, + {revision: "309", kb: "4088776"}, + {revision: "334", kb: "4089848"}, + {revision: "371", kb: "4093112"}, + {revision: "402", kb: "4093105"}, + {revision: "431", kb: "4103727"}, + {revision: "461", kb: "4103714"}, + {revision: "492", kb: "4284819"}, + {revision: "522", kb: "4284822"}, + {revision: "547", kb: "4338825"}, + {revision: "551", kb: "4345420"}, + {revision: "579", kb: "4338817"}, + {revision: "611", kb: "4343897"}, + {revision: "637", kb: "4343893"}, + {revision: "665", kb: "4457142"}, + {revision: "666", kb: "4464217"}, + {revision: "699", kb: "4457136"}, + {revision: "726", kb: "4462918"}, + {revision: "755", kb: "4462932"}, + {revision: "785", kb: "4467686"}, + {revision: "820", kb: "4467681"}, + {revision: "846", kb: "4471329"}, + {revision: "847", kb: "4483232"}, + {revision: "904", kb: "4480978"}, + {revision: "936", kb: "4480967"}, + {revision: "967", kb: "4486996"}, + {revision: "1004", kb: "4487021"}, + {revision: "1029", kb: "4489886"}, + {revision: "1059", kb: "4489890"}, + {revision: "1087", kb: "4493441"}, + {revision: "1127", kb: "4493440"}, + {revision: "1146", kb: "4499179"}, + {revision: "1150", kb: "4505062"}, + {revision: "1182", kb: "4499147"}, + {revision: "1217", kb: "4503284"}, + {revision: "1237", kb: "4503281"}, + {revision: "1239", kb: "4509477"}, + {revision: "1268", kb: "4507455"}, + {revision: "1296", kb: "4507465"}, + {revision: "1331", kb: "4512516"}, + {revision: "1365", kb: "4512494"}, + {revision: "1387", kb: "4516066"}, + {revision: "1392", kb: "4522012"}, + {revision: "1420", kb: "4516071"}, + {revision: "1421", kb: "4524150"}, + {revision: "1451", kb: "4520004"}, + {revision: "1481", kb: "4520006"}, + {revision: "1508", kb: "4525241"}, + {revision: "1565", kb: "4530714"}, + {revision: "1625", kb: "4534276"}, + {revision: "1654", kb: "4534318"}, + {revision: "1686", kb: "4537789"}, + {revision: "1717", kb: "4537816"}, + {revision: "1747", kb: "4540681"}, + {revision: "1775", kb: "4541330"}, + {revision: "1776", kb: "4554342"}, + {revision: "1806", kb: "4550927"}, + {revision: "1868", kb: "4556812"}, + {revision: "1932", kb: "4561602"}, + {revision: "1937", kb: "4567515"}, + {revision: "1992", kb: "4565508"}, + {revision: "2045", kb: "4571741"}, + {revision: "2107", kb: "4577041"}, + {revision: "2166", kb: "4580328"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-0d8c2da6-3dba-66e4-2ef2-059192bf7869 + "17134": { + rollup: []windowsRelease{ + {revision: "48", kb: "4103721"}, + {revision: "81", kb: "4100403"}, + {revision: "83", kb: "4338548"}, + {revision: "112", kb: "4284835"}, + {revision: "137", kb: "4284848"}, + {revision: "165", kb: "4338819"}, + {revision: "167", kb: "4345421"}, + {revision: "191", kb: "4340917"}, + {revision: "228", kb: "4343909"}, + {revision: "254", kb: "4346783"}, + {revision: "285", kb: "4457128"}, + {revision: "286", kb: "4464218"}, + {revision: "320", kb: "4458469"}, + {revision: "345", kb: "4462919"}, + {revision: "376", kb: "4462933"}, + {revision: "407", kb: "4467702"}, + {revision: "441", kb: "4467682"}, + {revision: "471", kb: "4471324"}, + {revision: "472", kb: "4483234"}, + {revision: "523", kb: "4480966"}, + {revision: "556", kb: "4480976"}, + {revision: "590", kb: "4487017"}, + {revision: "619", kb: "4487029"}, + {revision: "648", kb: "4489868"}, + {revision: "677", kb: "4489894"}, + {revision: "706", kb: "4493464"}, + {revision: "753", kb: "4493437"}, + {revision: "765", kb: "4499167"}, + {revision: "766", kb: "4505064"}, + {revision: "799", kb: "4499183"}, + {revision: "829", kb: "4503286"}, + {revision: "858", kb: "4503288"}, + {revision: "860", kb: "4509478"}, + {revision: "885", kb: "4507435"}, + {revision: "915", kb: "4507466"}, + {revision: "950", kb: "4512501"}, + {revision: "984", kb: "4512509"}, + {revision: "1006", kb: "4516058"}, + {revision: "1009", kb: "4522014"}, + {revision: "1039", kb: "4516045"}, + {revision: "1040", kb: "4524149"}, + {revision: "1069", kb: "4520008"}, + {revision: "1099", kb: "4519978"}, + {revision: "1130", kb: "4525237"}, + {revision: "1184", kb: "4530717"}, + {revision: "1246", kb: "4534293"}, + {revision: "1276", kb: "4534308"}, + {revision: "1304", kb: "4537762"}, + {revision: "1345", kb: "4537795"}, + {revision: "1365", kb: "4540689"}, + {revision: "1399", kb: "4541333"}, + {revision: "1401", kb: "4554349"}, + {revision: "1425", kb: "4550922"}, + {revision: "1456", kb: "4550944"}, + {revision: "1488", kb: "4556807"}, + {revision: "1550", kb: "4561621"}, + {revision: "1553", kb: "4567514"}, + {revision: "1610", kb: "4565489"}, + {revision: "1667", kb: "4571709"}, + {revision: "1726", kb: "4577032"}, + {revision: "1792", kb: "4580330"}, + {revision: "1845", kb: "4586785"}, + {revision: "1902", kb: "4592446"}, + {revision: "1967", kb: "4598245"}, + {revision: "2026", kb: "4601354"}, + {revision: "2087", kb: "5000809"}, + {revision: "2088", kb: "5001565"}, + {revision: "2090", kb: "5001634"}, + {revision: "2145", kb: "5001339"}, + {revision: "2208", kb: "5003174"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-and-windows-server-2019-update-history-725fc2e1-4443-6831-a5ca-51ff5cbcb059 + "17763": { + rollup: []windowsRelease{ + {revision: "1", kb: ""}, + {revision: "55", kb: "4464330"}, + {revision: "107", kb: "4464455"}, + {revision: "134", kb: "4467708"}, + {revision: "168", kb: "4469342"}, + {revision: "194", kb: "4471332"}, + {revision: "195", kb: "4483235"}, + {revision: "253", kb: "4480116"}, + {revision: "292", kb: "4476976"}, + {revision: "316", kb: "4487044"}, + {revision: "348", kb: "4482887"}, + {revision: "379", kb: "4489899"}, + {revision: "402", kb: "4490481"}, + {revision: "404", kb: "4490481"}, + {revision: "437", kb: "4493509"}, + {revision: "439", kb: "4501835"}, + {revision: "475", kb: "4495667"}, + {revision: "503", kb: "4494441"}, + {revision: "504", kb: "4505056"}, + {revision: "529", kb: "4497934"}, + {revision: "557", kb: "4503327"}, + {revision: "592", kb: "4501371"}, + {revision: "593", kb: "4509479"}, + {revision: "615", kb: "4507469"}, + {revision: "652", kb: "4505658"}, + {revision: "678", kb: "4511553"}, + {revision: "720", kb: "4512534"}, + {revision: "737", kb: "4512578"}, + {revision: "740", kb: "4522015"}, + {revision: "774", kb: "4516077"}, + {revision: "775", kb: "4524148"}, + {revision: "805", kb: "4519338"}, + {revision: "832", kb: "4520062"}, + {revision: "864", kb: "4523205"}, + {revision: "914", kb: "4530715"}, + {revision: "973", kb: "4534273"}, + {revision: "1012", kb: "4534321"}, + {revision: "1039", kb: "4532691"}, + {revision: "1075", kb: "4537818"}, + {revision: "1098", kb: "4538461"}, + {revision: "1131", kb: "4541331"}, + {revision: "1132", kb: "4554354"}, + {revision: "1158", kb: "4549949"}, + {revision: "1192", kb: "4550969"}, + {revision: "1217", kb: "4551853"}, + {revision: "1282", kb: "4561608"}, + {revision: "1294", kb: "4567513"}, + {revision: "1339", kb: "4558998"}, + {revision: "1369", kb: "4559003"}, + {revision: "1397", kb: "4565349"}, + {revision: "1432", kb: "4571748"}, + {revision: "1457", kb: "4570333"}, + {revision: "1490", kb: "4577069"}, + {revision: "1518", kb: "4577668"}, + {revision: "1554", kb: "4580390"}, + {revision: "1577", kb: "4586793"}, + {revision: "1579", kb: "4594442"}, + {revision: "1613", kb: "4586839"}, + {revision: "1637", kb: "4592440"}, + {revision: "1697", kb: "4598230"}, + {revision: "1728", kb: "4598296"}, + {revision: "1757", kb: "4601345"}, + {revision: "1790", kb: "4601383"}, + {revision: "1817", kb: "5000822"}, + {revision: "1821", kb: "5001568"}, + {revision: "1823", kb: "5001638"}, + {revision: "1852", kb: "5000854"}, + {revision: "1879", kb: "5001342"}, + {revision: "1911", kb: "5001384"}, + {revision: "1935", kb: "5003171"}, + {revision: "1971", kb: "5003217"}, + {revision: "1999", kb: "5003646"}, + {revision: "2028", kb: "5003703"}, + {revision: "2029", kb: "5004947"}, + {revision: "2061", kb: "5004244"}, + {revision: "2090", kb: "5004308"}, + {revision: "2091", kb: "5005394"}, + {revision: "2114", kb: "5005030"}, + {revision: "2145", kb: "5005102"}, + {revision: "2183", kb: "5005568"}, + {revision: "2210", kb: "5005625"}, + {revision: "2213", kb: "5005625"}, + {revision: "2237", kb: "5006672"}, + {revision: "2268", kb: "5006744"}, + {revision: "2300", kb: "5007206"}, + {revision: "2305", kb: "5008602"}, + {revision: "2330", kb: "5007266"}, + {revision: "2366", kb: "5008218"}, + {revision: "2369", kb: "5010196"}, + {revision: "2452", kb: "5009557"}, + {revision: "2458", kb: "5010791"}, + {revision: "2510", kb: "5009616"}, + {revision: "2565", kb: "5010351"}, + {revision: "2628", kb: "5010427"}, + {revision: "2686", kb: "5011503"}, + {revision: "2746", kb: "5011551"}, + {revision: "2803", kb: "5012647"}, + {revision: "2867", kb: "5012636"}, + {revision: "2928", kb: "5013941"}, + {revision: "2931", kb: "5015018"}, + {revision: "2989", kb: "5014022"}, + {revision: "3046", kb: "5014692"}, + {revision: "3113", kb: "5014669"}, + {revision: "3165", kb: "5015811"}, + {revision: "3232", kb: "5015880"}, + {revision: "3287", kb: "5016623"}, + {revision: "3346", kb: "5016690"}, + {revision: "3406", kb: "5017315"}, + {revision: "3469", kb: "5017379"}, + {revision: "3532", kb: "5018419"}, + {revision: "3534", kb: "5020438"}, + {revision: "3650", kb: "5019966"}, + {revision: "3653", kb: "5021655"}, + {revision: "3770", kb: "5021237"}, + {revision: "3772", kb: "5022554"}, + {revision: "3887", kb: "5022286"}, + {revision: "4010", kb: "5022840"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-e6058e7c-4116-38f1-b984-4fcacfba5e5d + "18362": { + rollup: []windowsRelease{ + {revision: "116", kb: "4505057"}, + {revision: "145", kb: "4497935"}, + {revision: "175", kb: "4503293"}, + {revision: "207", kb: "4501375"}, + {revision: "239", kb: "4507453"}, + {revision: "267", kb: "4505903"}, + {revision: "295", kb: "4512508"}, + {revision: "329", kb: "4512941"}, + {revision: "356", kb: "4515384"}, + {revision: "357", kb: "4522016"}, + {revision: "387", kb: "4517211"}, + {revision: "388", kb: "4524147"}, + {revision: "418", kb: "4517389"}, + {revision: "449", kb: "4522355"}, + {revision: "476", kb: "4524570"}, + {revision: "535", kb: "4530684"}, + {revision: "592", kb: "4528760"}, + {revision: "628", kb: "4532695"}, + {revision: "657", kb: "4532693"}, + {revision: "693", kb: "4535996"}, + {revision: "719", kb: "4540673"}, + {revision: "720", kb: "4551762"}, + {revision: "752", kb: "4541335"}, + {revision: "753", kb: "4554364"}, + {revision: "778", kb: "4549951"}, + {revision: "815", kb: "4550945"}, + {revision: "836", kb: "4556799"}, + {revision: "900", kb: "4560960"}, + {revision: "904", kb: "4567512"}, + {revision: "959", kb: "4565483"}, + {revision: "997", kb: "4559004"}, + {revision: "1016", kb: "4565351"}, + {revision: "1049", kb: "4566116"}, + {revision: "1082", kb: "4574727"}, + {revision: "1110", kb: "4577062"}, + {revision: "1139", kb: "4577671"}, + {revision: "1171", kb: "4580386"}, + {revision: "1198", kb: "4586786"}, + {revision: "1199", kb: "4594443"}, + {revision: "1237", kb: "4586819"}, + {revision: "1256", kb: "4592449"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-53c270dc-954f-41f7-7ced-488578904dfe + "18363": { + rollup: []windowsRelease{ + {revision: "476", kb: "4524570"}, + {revision: "535", kb: "4530684"}, + {revision: "592", kb: "4528760"}, + {revision: "628", kb: "4532695"}, + {revision: "657", kb: "4532693"}, + {revision: "693", kb: "4535996"}, + {revision: "719", kb: "4540673"}, + {revision: "720", kb: "4551762"}, + {revision: "752", kb: "4541335"}, + {revision: "753", kb: "4554364"}, + {revision: "778", kb: "4549951"}, + {revision: "815", kb: "4550945"}, + {revision: "836", kb: "4556799"}, + {revision: "900", kb: "4560960"}, + {revision: "904", kb: "4567512"}, + {revision: "959", kb: "4565483"}, + {revision: "997", kb: "4559004"}, + {revision: "1016", kb: "4565351"}, + {revision: "1049", kb: "4566116"}, + {revision: "1082", kb: "4574727"}, + {revision: "1110", kb: "4577062"}, + {revision: "1139", kb: "4577671"}, + {revision: "1171", kb: "4580386"}, + {revision: "1198", kb: "4586786"}, + {revision: "1199", kb: "4594443"}, + {revision: "1237", kb: "4586819"}, + {revision: "1256", kb: "4592449"}, + {revision: "1316", kb: "4598229"}, + {revision: "1350", kb: "4598298"}, + {revision: "1377", kb: "4601315"}, + {revision: "1379", kb: "5001028"}, + {revision: "1411", kb: "4601380"}, + {revision: "1440", kb: "5000808"}, + {revision: "1441", kb: "5001566"}, + {revision: "1443", kb: "5001648"}, + {revision: "1474", kb: "5000850"}, + {revision: "1500", kb: "5001337"}, + {revision: "1533", kb: "5001396"}, + {revision: "1556", kb: "5003169"}, + {revision: "1593", kb: "5003212"}, + {revision: "1621", kb: "5003635"}, + {revision: "1645", kb: "5003698"}, + {revision: "1646", kb: "5004946"}, + {revision: "1679", kb: "5004245"}, + {revision: "1714", kb: "5004293"}, + {revision: "1734", kb: "5005031"}, + {revision: "1766", kb: "5005103"}, + {revision: "1801", kb: "5005566"}, + {revision: "1830", kb: "5005624"}, + {revision: "1832", kb: "5005624"}, + {revision: "1854", kb: "5006667"}, + {revision: "1916", kb: "5007189"}, + {revision: "1977", kb: "5008206"}, + {revision: "2037", kb: "5009545"}, + {revision: "2039", kb: "5010792"}, + {revision: "2094", kb: "5010345"}, + {revision: "2158", kb: "5011485"}, + {revision: "2212", kb: "5012591"}, + {revision: "2274", kb: "5013945"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-24ea91f4-36e7-d8fd-0ddb-d79d9d0cdbda + "19041": { + rollup: []windowsRelease{ + {revision: "264", kb: ""}, + {revision: "329", kb: "4557957"}, + {revision: "331", kb: "4567523"}, + {revision: "388", kb: "4565503"}, + {revision: "423", kb: "4568831"}, + {revision: "450", kb: "4566782"}, + {revision: "488", kb: "4571744"}, + {revision: "508", kb: "4571756"}, + {revision: "546", kb: "4577063"}, + {revision: "572", kb: "4579311"}, + {revision: "610", kb: "4580364"}, + {revision: "630", kb: "4586781"}, + {revision: "631", kb: "4594440"}, + {revision: "662", kb: "4586853"}, + {revision: "685", kb: "4592438"}, + {revision: "746", kb: "4598242"}, + {revision: "789", kb: "4598291"}, + {revision: "804", kb: "4601319"}, + {revision: "844", kb: "4601382"}, + {revision: "867", kb: "5000802"}, + {revision: "868", kb: "5001567"}, + {revision: "870", kb: "5001649"}, + {revision: "906", kb: "5000842"}, + {revision: "928", kb: "5001330"}, + {revision: "964", kb: "5001391"}, + {revision: "985", kb: "5003173"}, + {revision: "1023", kb: "5003214"}, + {revision: "1052", kb: "5003637"}, + {revision: "1055", kb: "5004476"}, + {revision: "1081", kb: "5003690"}, + {revision: "1082", kb: "5004760"}, + {revision: "1083", kb: "5004945"}, + {revision: "1110", kb: "5004237"}, + {revision: "1151", kb: "5004296"}, + {revision: "1165", kb: "5005033"}, + {revision: "1202", kb: "5005101"}, + {revision: "1237", kb: "5005565"}, + {revision: "1266", kb: "5005611"}, + {revision: "1288", kb: "5006670"}, + {revision: "1320", kb: "5006738"}, + {revision: "1348", kb: "5007186"}, + {revision: "1387", kb: "5007253"}, + {revision: "1415", kb: "5008212"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-7dd3071a-3906-fa2c-c342-f7f86728a6e3 + "19042": { + rollup: []windowsRelease{ + {revision: "572", kb: ""}, + {revision: "610", kb: "4580364"}, + {revision: "630", kb: "4586781"}, + {revision: "631", kb: "4594440"}, + {revision: "662", kb: "4586853"}, + {revision: "685", kb: "4592438"}, + {revision: "746", kb: "4598242"}, + {revision: "789", kb: "4598291"}, + {revision: "804", kb: "4601319"}, + {revision: "844", kb: "4601382"}, + {revision: "867", kb: "5000802"}, + {revision: "868", kb: "5001567"}, + {revision: "870", kb: "5001649"}, + {revision: "906", kb: "5000842"}, + {revision: "928", kb: "5001330"}, + {revision: "964", kb: "5001391"}, + {revision: "985", kb: "5003173"}, + {revision: "1023", kb: "5003214"}, + {revision: "1052", kb: "5003637"}, + {revision: "1055", kb: "5004476"}, + {revision: "1081", kb: "5003690"}, + {revision: "1082", kb: "5004760"}, + {revision: "1083", kb: "5004945"}, + {revision: "1110", kb: "5004237"}, + {revision: "1151", kb: "5004296"}, + {revision: "1165", kb: "5005033"}, + {revision: "1202", kb: "5005101"}, + {revision: "1237", kb: "5005565"}, + {revision: "1266", kb: "5005611"}, + {revision: "1288", kb: "5006670"}, + {revision: "1320", kb: "5006738"}, + {revision: "1348", kb: "5007186"}, + {revision: "1387", kb: "5007253"}, + {revision: "1415", kb: "5008212"}, + {revision: "1466", kb: "5009543"}, + {revision: "1469", kb: "5010793"}, + {revision: "1503", kb: "5009596"}, + {revision: "1526", kb: "5010342"}, + {revision: "1566", kb: "5010415"}, + {revision: "1586", kb: "5011487"}, + {revision: "1620", kb: "5011543"}, + {revision: "1645", kb: "5012599"}, + {revision: "1682", kb: "5011831"}, + {revision: "1706", kb: "5013942"}, + {revision: "1708", kb: "5015020"}, + {revision: "1741", kb: "5014023"}, + {revision: "1766", kb: "5014699"}, + {revision: "1767", kb: "5016139"}, + {revision: "1806", kb: "5014666"}, + {revision: "1826", kb: "5015807"}, + {revision: "1865", kb: "5015878"}, + {revision: "1889", kb: "5016616"}, + {revision: "1949", kb: "5016688"}, + {revision: "2006", kb: "5017308"}, + {revision: "2075", kb: "5017380"}, + {revision: "2130", kb: "5018410"}, + {revision: "2132", kb: "5020435"}, + {revision: "2193", kb: "5018482"}, + {revision: "2194", kb: "5020953"}, + {revision: "2251", kb: "5019959"}, + {revision: "2311", kb: "5020030"}, + {revision: "2364", kb: "5021233"}, + {revision: "2486", kb: "5022282"}, + {revision: "2546", kb: "5019275"}, + {revision: "2604", kb: "5022834"}, + {revision: "2673", kb: "5022906"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-1b6aac92-bf01-42b5-b158-f80c6d93eb11 + "19043": { + rollup: []windowsRelease{ + {revision: "985", kb: "5003173"}, + {revision: "1023", kb: "5003214"}, + {revision: "1052", kb: "5003637"}, + {revision: "1055", kb: "5004476"}, + {revision: "1081", kb: "5003690"}, + {revision: "1082", kb: "5004760"}, + {revision: "1083", kb: "5004945"}, + {revision: "1110", kb: "5004237"}, + {revision: "1151", kb: "5004296"}, + {revision: "1165", kb: "5005033"}, + {revision: "1202", kb: "5005101"}, + {revision: "1237", kb: "5005565"}, + {revision: "1266", kb: "5005611"}, + {revision: "1288", kb: "5006670"}, + {revision: "1320", kb: "5006738"}, + {revision: "1348", kb: "5007186"}, + {revision: "1387", kb: "5007253"}, + {revision: "1415", kb: "5008212"}, + {revision: "1466", kb: "5009543"}, + {revision: "1469", kb: "5010793"}, + {revision: "1503", kb: "5009596"}, + {revision: "1526", kb: "5010342"}, + {revision: "1566", kb: "5010415"}, + {revision: "1586", kb: "5011487"}, + {revision: "1620", kb: "5011543"}, + {revision: "1645", kb: "5012599"}, + {revision: "1682", kb: "5011831"}, + {revision: "1706", kb: "5013942"}, + {revision: "1708", kb: "5015020"}, + {revision: "1741", kb: "5014023"}, + {revision: "1766", kb: "5014699"}, + {revision: "1767", kb: "5016139"}, + {revision: "1806", kb: "5014666"}, + {revision: "1826", kb: "5015807"}, + {revision: "1865", kb: "5015878"}, + {revision: "1889", kb: "5016616"}, + {revision: "1949", kb: "5016688"}, + {revision: "2006", kb: "5017308"}, + {revision: "2075", kb: "5017380"}, + {revision: "2130", kb: "5018410"}, + {revision: "2132", kb: "5020435"}, + {revision: "2193", kb: "5018482"}, + {revision: "2194", kb: "5020953"}, + {revision: "2251", kb: "5019959"}, + {revision: "2311", kb: "5020030"}, + {revision: "2364", kb: "5021233"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-857b8ccb-71e4-49e5-b3f6-7073197d98fb + "19044": { + rollup: []windowsRelease{ + {revision: "1288", kb: ""}, + {revision: "1387", kb: "5007253"}, + {revision: "1415", kb: "5008212"}, + {revision: "1466", kb: "5009543"}, + {revision: "1469", kb: "5010793"}, + {revision: "1503", kb: "5009596"}, + {revision: "1526", kb: "5010342"}, + {revision: "1566", kb: "5010415"}, + {revision: "1586", kb: "5011487"}, + {revision: "1620", kb: "5011543"}, + {revision: "1645", kb: "5012599"}, + {revision: "1682", kb: "5011831"}, + {revision: "1706", kb: "5013942"}, + {revision: "1708", kb: "5015020"}, + {revision: "1741", kb: "5014023"}, + {revision: "1766", kb: "5014699"}, + {revision: "1767", kb: "5016139"}, + {revision: "1806", kb: "5014666"}, + {revision: "1826", kb: "5015807"}, + {revision: "1865", kb: "5015878"}, + {revision: "1889", kb: "5016616"}, + {revision: "1949", kb: "5016688"}, + {revision: "2006", kb: "5017308"}, + {revision: "2075", kb: "5017380"}, + {revision: "2130", kb: "5018410"}, + {revision: "2132", kb: "5020435"}, + {revision: "2193", kb: "5018482"}, + {revision: "2194", kb: "5020953"}, + {revision: "2251", kb: "5019959"}, + {revision: "2311", kb: "5020030"}, + {revision: "2364", kb: "5021233"}, + {revision: "2486", kb: "5022282"}, + {revision: "2546", kb: "5019275"}, + {revision: "2604", kb: "5022834"}, + {revision: "2673", kb: "5022906"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-10-update-history-8127c2c6-6edf-4fdf-8b9f-0f7be1ef3562 + "19045": { + rollup: []windowsRelease{ + {revision: "2130", kb: ""}, + {revision: "2194", kb: "5020953"}, + {revision: "2251", kb: "5019959"}, + {revision: "2311", kb: "5020030"}, + {revision: "2364", kb: "5021233"}, + {revision: "2486", kb: "5022282"}, + {revision: "2546", kb: "5019275"}, + {revision: "2604", kb: "5022834"}, + {revision: "2673", kb: "5022906"}, + }, + }, + }, + "11": { + // https://learn.microsoft.com/en-us/windows/release-health/windows11-release-information + // https://support.microsoft.com/en-us/topic/windows-11-version-21h2-update-history-a19cd327-b57f-44b9-84e0-26ced7109ba9/ + "22000": { + rollup: []windowsRelease{ + {revision: "194", kb: ""}, + {revision: "258", kb: "5006674"}, + {revision: "282", kb: "5006746"}, + {revision: "318", kb: "5007215"}, + {revision: "348", kb: "5007262"}, + {revision: "376", kb: "5008215"}, + {revision: "434", kb: "5009566"}, + {revision: "438", kb: "5010795"}, + {revision: "469", kb: "5008353"}, + {revision: "493", kb: "5010386"}, + {revision: "527", kb: "5010414"}, + {revision: "556", kb: "5011493"}, + {revision: "593", kb: "5011563"}, + {revision: "613", kb: "5012592"}, + {revision: "652", kb: "5012643"}, + {revision: "675", kb: "5013943"}, + {revision: "708", kb: "5014019"}, + {revision: "739", kb: "5014697"}, + {revision: "740", kb: "5016138"}, + {revision: "778", kb: "5014668"}, + {revision: "795", kb: "5015814"}, + {revision: "832", kb: "5015882"}, + {revision: "856", kb: "5016629"}, + {revision: "918", kb: "5016691"}, + {revision: "978", kb: "5017328"}, + {revision: "1042", kb: "5017383"}, + {revision: "1098", kb: "5018418"}, + {revision: "1100", kb: "5020387"}, + {revision: "1165", kb: "5018483"}, + {revision: "1219", kb: "5019961"}, + {revision: "1281", kb: "5019157"}, + {revision: "1335", kb: "5021234"}, + {revision: "1455", kb: "5022287"}, + {revision: "1516", kb: "5019274"}, + {revision: "1574", kb: "5022836"}, + {revision: "1641", kb: "5022905"}, + }, + }, + // https://support.microsoft.com/en-us/topic/windows-11-version-22h2-update-history-ec4229c3-9c5f-4e75-9d6d-9025ab70fcce + "22621": { + rollup: []windowsRelease{ + {revision: "521", kb: ""}, + {revision: "525", kb: "5019311"}, + {revision: "608", kb: "5017389"}, + {revision: "674", kb: "5018427"}, + {revision: "675", kb: "5019509"}, + {revision: "755", kb: "5018496"}, + {revision: "819", kb: "5019980"}, + {revision: "900", kb: "5020044"}, + {revision: "963", kb: "5021255"}, + {revision: "1105", kb: "5022303"}, + {revision: "1194", kb: "5022360"}, + {revision: "1265", kb: "5022845"}, + }, + }, + }, + }, + "Server": { + "2008": { + // https://support.microsoft.com/en-us/topic/windows-server-2008-sp2-update-history-9197740a-7430-f69f-19ff-4998a4e8b25b + "SP2": { + rollup: []windowsRelease{ + {revision: "", kb: "4458010"}, + {revision: "", kb: "4458315"}, + {revision: "", kb: "4463097"}, + {revision: "", kb: "4463105"}, + {revision: "", kb: "4467706"}, + {revision: "", kb: "4467687"}, + {revision: "", kb: "4471325"}, + {revision: "", kb: "4480968"}, + {revision: "", kb: "4480974"}, + {revision: "", kb: "4487023"}, + {revision: "", kb: "4487022"}, + {revision: "", kb: "4489880"}, + {revision: "", kb: "4489887"}, + {revision: "", kb: "4493471"}, + {revision: "", kb: "4493460"}, + {revision: "", kb: "4499149"}, + {revision: "", kb: "4499184"}, + {revision: "", kb: "4503273"}, + {revision: "", kb: "4503271"}, + {revision: "", kb: "4507452"}, + {revision: "", kb: "4507451"}, + {revision: "", kb: "4512476"}, + {revision: "", kb: "4512499"}, + {revision: "", kb: "4516026"}, + {revision: "", kb: "4516030"}, + {revision: "", kb: "4520002"}, + {revision: "", kb: "4520015"}, + {revision: "", kb: "4525234"}, + {revision: "", kb: "4525244"}, + {revision: "", kb: "4530695"}, + {revision: "", kb: "4534303"}, + {revision: "", kb: "4537810"}, + {revision: "", kb: "4541506"}, + {revision: "", kb: "4550951"}, + {revision: "", kb: "4556860"}, + {revision: "", kb: "4561670"}, + {revision: "", kb: "4565536"}, + {revision: "", kb: "4571730"}, + {revision: "", kb: "4577064"}, + {revision: "", kb: "4580378"}, + {revision: "", kb: "4586807"}, + {revision: "", kb: "4592498"}, + {revision: "", kb: "4598288"}, + {revision: "", kb: "4601360"}, + {revision: "", kb: "5000844"}, + {revision: "", kb: "5001389"}, + {revision: "", kb: "5003210"}, + {revision: "", kb: "5003661"}, + {revision: "", kb: "5004955"}, + {revision: "", kb: "5004305"}, + {revision: "", kb: "5005090"}, + {revision: "", kb: "5005606"}, + {revision: "", kb: "5006736"}, + {revision: "", kb: "5007263"}, + {revision: "", kb: "5008274"}, + {revision: "", kb: "5009627"}, + {revision: "", kb: "5010384"}, + {revision: "", kb: "5011534"}, + {revision: "", kb: "5012658"}, + {revision: "", kb: "5014010"}, + {revision: "", kb: "5014752"}, + {revision: "", kb: "5015866"}, + {revision: "", kb: "5016669"}, + {revision: "", kb: "5017358"}, + {revision: "", kb: "5018450"}, + {revision: "", kb: "5020019"}, + {revision: "", kb: "5021289"}, + {revision: "", kb: "5022340"}, + {revision: "", kb: "5022890"}, + }, + securityOnly: []string{ + "4457984", + "4463104", + "4467700", + "4471319", + "4480957", + "4487019", + "4489876", + "4493458", + "4499180", + "4503287", + "4507461", + "4512491", + "4516051", + "4520009", + "4525239", + "4530719", + "4534312", + "4537822", + "4541504", + "4550957", + "4556854", + "4561645", + "4565529", + "4571746", + "4577070", + "4580385", + "4586817", + "4592504", + "4598287", + "4601366", + "5000856", + "5001332", + "5003225", + "5003695", + "5004959", + "5004299", + "5005095", + "5005618", + "5006715", + "5007246", + "5008271", + "5009601", + "5010403", + "5011525", + "5012632", + "5014006", + "5014743", + "5015870", + "5016686", + "5017371", + "5018446", + "5020005", + "5021293", + "5022353", + "5022893", + }, + }, + }, + "2008 R2": { + // https://support.microsoft.com/en-us/topic/windows-7-sp1-and-windows-server-2008-r2-sp1-update-history-720c2590-fd58-26ba-16cc-6d8f3b547599 + "SP1": { + rollup: []windowsRelease{ + {revision: "", kb: "3172605"}, + {revision: "", kb: "3179573"}, + {revision: "", kb: "3185278"}, + {revision: "", kb: "3185330"}, + {revision: "", kb: "3192403"}, + {revision: "", kb: "3197868"}, + {revision: "", kb: "3197869"}, + {revision: "", kb: "3207752"}, + {revision: "", kb: "3212646"}, + {revision: "", kb: "4012215"}, + {revision: "", kb: "4012218"}, + {revision: "", kb: "4015549"}, + {revision: "", kb: "4015552"}, + {revision: "", kb: "4019264"}, + {revision: "", kb: "4019265"}, + {revision: "", kb: "4022719"}, + {revision: "", kb: "4022168"}, + {revision: "", kb: "4025341"}, + {revision: "", kb: "4025340"}, + {revision: "", kb: "4034664"}, + {revision: "", kb: "4034670"}, + {revision: "", kb: "4038777"}, + {revision: "", kb: "4038803"}, + {revision: "", kb: "4041681"}, + {revision: "", kb: "4041686"}, + {revision: "", kb: "4048957"}, + {revision: "", kb: "4051034"}, + {revision: "", kb: "4054518"}, + {revision: "", kb: "4056894"}, + {revision: "", kb: "4057400"}, + {revision: "", kb: "4074598"}, + {revision: "", kb: "4075211"}, + {revision: "", kb: "4088875"}, + {revision: "", kb: "4088881"}, + {revision: "", kb: "4093118"}, + {revision: "", kb: "4093113"}, + {revision: "", kb: "4103718"}, + {revision: "", kb: "4103713"}, + {revision: "", kb: "4284826"}, + {revision: "", kb: "4284842"}, + {revision: "", kb: "4338818"}, + {revision: "", kb: "4338821"}, + {revision: "", kb: "4343900"}, + {revision: "", kb: "4343894"}, + {revision: "", kb: "4457144"}, + {revision: "", kb: "4457139"}, + {revision: "", kb: "4462923"}, + {revision: "", kb: "4462927"}, + {revision: "", kb: "4467107"}, + {revision: "", kb: "4467108"}, + {revision: "", kb: "4471318"}, + {revision: "", kb: "4480970"}, + {revision: "", kb: "4480955"}, + {revision: "", kb: "4486563"}, + {revision: "", kb: "4486565"}, + {revision: "", kb: "4489878"}, + {revision: "", kb: "4489892"}, + {revision: "", kb: "4493472"}, + {revision: "", kb: "4493453"}, + {revision: "", kb: "4499164"}, + {revision: "", kb: "4499178"}, + {revision: "", kb: "4503292"}, + {revision: "", kb: "4503277"}, + {revision: "", kb: "4507449"}, + {revision: "", kb: "4507437"}, + {revision: "", kb: "4512506"}, + {revision: "", kb: "4512514"}, + {revision: "", kb: "4516065"}, + {revision: "", kb: "4516048"}, + {revision: "", kb: "4524157"}, + {revision: "", kb: "4519976"}, + {revision: "", kb: "4519972"}, + {revision: "", kb: "4525235"}, + {revision: "", kb: "4525251"}, + {revision: "", kb: "4530734"}, + {revision: "", kb: "4534310"}, + {revision: "", kb: "4539601"}, + {revision: "", kb: "4537820"}, + {revision: "", kb: "4540688"}, + {revision: "", kb: "4550964"}, + {revision: "", kb: "4556836"}, + {revision: "", kb: "4561643"}, + {revision: "", kb: "4565524"}, + {revision: "", kb: "4571729"}, + {revision: "", kb: "4577051"}, + {revision: "", kb: "4580345"}, + {revision: "", kb: "4586827"}, + {revision: "", kb: "4592471"}, + {revision: "", kb: "4598279"}, + {revision: "", kb: "4601347"}, + {revision: "", kb: "5000841"}, + {revision: "", kb: "5001335"}, + {revision: "", kb: "5003233"}, + {revision: "", kb: "5003667"}, + {revision: "", kb: "5004953"}, + {revision: "", kb: "5004289"}, + {revision: "", kb: "5005088"}, + {revision: "", kb: "5005633"}, + {revision: "", kb: "5006743"}, + {revision: "", kb: "5007236"}, + {revision: "", kb: "5008244"}, + {revision: "", kb: "5009610"}, + {revision: "", kb: "5010404"}, + {revision: "", kb: "5011552"}, + {revision: "", kb: "5012626"}, + {revision: "", kb: "5014012"}, + {revision: "", kb: "5014748"}, + {revision: "", kb: "5015861"}, + {revision: "", kb: "5016676"}, + {revision: "", kb: "5017361"}, + {revision: "", kb: "5018454"}, + {revision: "", kb: "5020000"}, + {revision: "", kb: "5021291"}, + {revision: "", kb: "5022338"}, + {revision: "", kb: "5022872"}, + }, + securityOnly: []string{ + "3192391", + "3197867", + "3205394", + "3212642", + "4012212", + "4015546", + "4019263", + "4022722", + "4025337", + "4034679", + "4038779", + "4041678", + "4048960", + "4054521", + "4056897", + "4074587", + "4088878", + "4093108", + "4103712", + "4284867", + "4338823", + "4343899", + "4457145", + "4462915", + "4467106", + "4471328", + "4480960", + "4486564", + "4489885", + "4493448", + "4499175", + "4503269", + "4507456", + "4512486", + "4516033", + "4520003", + "4525233", + "4530692", + "4534314", + "4537813", + "4541500", + "4550965", + "4556843", + "4561669", + "4565539", + "4571719", + "4577053", + "4580387", + "4586805", + "4592503", + "4598289", + "4601363", + "5000851", + "5001392", + "5003228", + "5003694", + "5004951", + "5004307", + "5005089", + "5005615", + "5006728", + "5007233", + "5008282", + "5009621", + "5010422", + "5011529", + "5012649", + "5013999", + "5014742", + "5015862", + "5016679", + "5017373", + "5018479", + "5020013", + "5021288", + "5022339", + "5022874", + }, + }, + }, + "2012": { + // https://support.microsoft.com/en-us/topic/windows-server-2012-update-history-abfb9afd-2ebf-1c19-4224-ad86f8741edd + "": { + rollup: []windowsRelease{ + {revision: "", kb: "3172615"}, + {revision: "", kb: "3179575"}, + {revision: "", kb: "3185280"}, + {revision: "", kb: "3185332"}, + {revision: "", kb: "3192406"}, + {revision: "", kb: "3197877"}, + {revision: "", kb: "3197878"}, + {revision: "", kb: "3205409"}, + {revision: "", kb: "4012217"}, + {revision: "", kb: "4012220"}, + {revision: "", kb: "4015551"}, + {revision: "", kb: "4015554"}, + {revision: "", kb: "4019216"}, + {revision: "", kb: "4019218"}, + {revision: "", kb: "4022724"}, + {revision: "", kb: "4022721"}, + {revision: "", kb: "4025331"}, + {revision: "", kb: "4025332"}, + {revision: "", kb: "4034665"}, + {revision: "", kb: "4034659"}, + {revision: "", kb: "4038799"}, + {revision: "", kb: "4038797"}, + {revision: "", kb: "4041690"}, + {revision: "", kb: "4041692"}, + {revision: "", kb: "4048959"}, + {revision: "", kb: "4050945"}, + {revision: "", kb: "4054520"}, + {revision: "", kb: "4056896"}, + {revision: "", kb: "4057402"}, + {revision: "", kb: "4074593"}, + {revision: "", kb: "4075213"}, + {revision: "", kb: "4088877"}, + {revision: "", kb: "4088883"}, + {revision: "", kb: "4093123"}, + {revision: "", kb: "4093116"}, + {revision: "", kb: "4103730"}, + {revision: "", kb: "4103719"}, + {revision: "", kb: "4284855"}, + {revision: "", kb: "4284852"}, + {revision: "", kb: "4338830"}, + {revision: "", kb: "4338816"}, + {revision: "", kb: "4343901"}, + {revision: "", kb: "4343895"}, + {revision: "", kb: "4457135"}, + {revision: "", kb: "4457134"}, + {revision: "", kb: "4462929"}, + {revision: "", kb: "4462925"}, + {revision: "", kb: "4467701"}, + {revision: "", kb: "4467683"}, + {revision: "", kb: "4471330"}, + {revision: "", kb: "4480975"}, + {revision: "", kb: "4480971"}, + {revision: "", kb: "4487025"}, + {revision: "", kb: "4487024"}, + {revision: "", kb: "4489891"}, + {revision: "", kb: "4489920"}, + {revision: "", kb: "4493451"}, + {revision: "", kb: "4493462"}, + {revision: "", kb: "4499171"}, + {revision: "", kb: "4499145"}, + {revision: "", kb: "4503285"}, + {revision: "", kb: "4503295"}, + {revision: "", kb: "4507462"}, + {revision: "", kb: "4507447"}, + {revision: "", kb: "4512518"}, + {revision: "", kb: "4512512"}, + {revision: "", kb: "4516055"}, + {revision: "", kb: "4516069"}, + {revision: "", kb: "4524154"}, + {revision: "", kb: "4520007"}, + {revision: "", kb: "4520013"}, + {revision: "", kb: "4525246"}, + {revision: "", kb: "4525242"}, + {revision: "", kb: "4530691"}, + {revision: "", kb: "4534283"}, + {revision: "", kb: "4534320"}, + {revision: "", kb: "4537814"}, + {revision: "", kb: "4537807"}, + {revision: "", kb: "4541510"}, + {revision: "", kb: "4541332"}, + {revision: "", kb: "4550917"}, + {revision: "", kb: "4550960"}, + {revision: "", kb: "4556840"}, + {revision: "", kb: "4561612"}, + {revision: "", kb: "4565537"}, + {revision: "", kb: "4571736"}, + {revision: "", kb: "4577038"}, + {revision: "", kb: "4580382"}, + {revision: "", kb: "4586834"}, + {revision: "", kb: "4592468"}, + {revision: "", kb: "4598278"}, + {revision: "", kb: "4601348"}, + {revision: "", kb: "5000847"}, + {revision: "", kb: "5001387"}, + {revision: "", kb: "5003208"}, + {revision: "", kb: "5003697"}, + {revision: "", kb: "5004956"}, + {revision: "", kb: "5004294"}, + {revision: "", kb: "5005099"}, + {revision: "", kb: "5005623"}, + {revision: "", kb: "5006739"}, + {revision: "", kb: "5007260"}, + {revision: "", kb: "5008277"}, + {revision: "", kb: "5009586"}, + {revision: "", kb: "5010392"}, + {revision: "", kb: "5011535"}, + {revision: "", kb: "5012650"}, + {revision: "", kb: "5014017"}, + {revision: "", kb: "5014747"}, + {revision: "", kb: "5015863"}, + {revision: "", kb: "5016672"}, + {revision: "", kb: "5017370"}, + {revision: "", kb: "5018457"}, + {revision: "", kb: "5020009"}, + {revision: "", kb: "5021285"}, + {revision: "", kb: "5022348"}, + {revision: "", kb: "5022903"}, + }, + securityOnly: []string{ + "3192393", + "3197876", + "3205408", + "4012214", + "4015548", + "4019214", + "4022718", + "4025343", + "4034666", + "4038786", + "4041679", + "4048962", + "4054523", + "4056899", + "4074589", + "4088880", + "4093122", + "4103726", + "4284846", + "4338820", + "4343896", + "4457140", + "4462931", + "4467678", + "4471326", + "4480972", + "4486993", + "4489884", + "4493450", + "4499158", + "4503263", + "4507464", + "4512482", + "4516062", + "4519985", + "4525253", + "4530698", + "4534288", + "4537794", + "4540694", + "4550971", + "4556852", + "4561674", + "4565535", + "4571702", + "4577048", + "4580353", + "4586808", + "4592497", + "4598297", + "4601357", + "5000840", + "5001383", + "5003203", + "5003696", + "5004960", + "5004302", + "5005094", + "5005607", + "5006732", + "5007245", + "5008255", + "5009619", + "5010412", + "5011527", + "5012666", + "5014018", + "5014741", + "5015875", + "5016684", + "5017377", + "5018478", + "5020003", + "5021303", + "5022343", + "5022895", + }, + }, + }, + "2012 R2": { + // https://support.microsoft.com/en-us/topic/windows-8-1-and-windows-server-2012-r2-update-history-47d81dd2-6804-b6ae-4112-20089467c7a6 + "": { + rollup: []windowsRelease{ + {revision: "", kb: "3172614"}, + {revision: "", kb: "3179574"}, + {revision: "", kb: "3185279"}, + {revision: "", kb: "3185331"}, + {revision: "", kb: "3192404"}, + {revision: "", kb: "3197874"}, + {revision: "", kb: "3197875"}, + {revision: "", kb: "3205401"}, + {revision: "", kb: "4012216"}, + {revision: "", kb: "4012219"}, + {revision: "", kb: "4015550"}, + {revision: "", kb: "4015553"}, + {revision: "", kb: "4019215"}, + {revision: "", kb: "4019217"}, + {revision: "", kb: "4022726"}, + {revision: "", kb: "4022720"}, + {revision: "", kb: "4025336"}, + {revision: "", kb: "4025335"}, + {revision: "", kb: "4034681"}, + {revision: "", kb: "4034663"}, + {revision: "", kb: "4038792"}, + {revision: "", kb: "4038774"}, + {revision: "", kb: "4041693"}, + {revision: "", kb: "4041685"}, + {revision: "", kb: "4048958"}, + {revision: "", kb: "4050946"}, + {revision: "", kb: "4054519"}, + {revision: "", kb: "4056895"}, + {revision: "", kb: "4057401"}, + {revision: "", kb: "4074594"}, + {revision: "", kb: "4075212"}, + {revision: "", kb: "4088876"}, + {revision: "", kb: "4088882"}, + {revision: "", kb: "4093114"}, + {revision: "", kb: "4093121"}, + {revision: "", kb: "4103725"}, + {revision: "", kb: "4103724"}, + {revision: "", kb: "4284815"}, + {revision: "", kb: "4284863"}, + {revision: "", kb: "4338815"}, + {revision: "", kb: "4338831"}, + {revision: "", kb: "4343898"}, + {revision: "", kb: "4343891"}, + {revision: "", kb: "4457129"}, + {revision: "", kb: "4457133"}, + {revision: "", kb: "4462926"}, + {revision: "", kb: "4462921"}, + {revision: "", kb: "4467697"}, + {revision: "", kb: "4467695"}, + {revision: "", kb: "4471320"}, + {revision: "", kb: "4480963"}, + {revision: "", kb: "4480969"}, + {revision: "", kb: "4487000"}, + {revision: "", kb: "4487016"}, + {revision: "", kb: "4489881"}, + {revision: "", kb: "4489893"}, + {revision: "", kb: "4493446"}, + {revision: "", kb: "4493443"}, + {revision: "", kb: "4499151"}, + {revision: "", kb: "4499182"}, + {revision: "", kb: "4503276"}, + {revision: "", kb: "4503283"}, + {revision: "", kb: "4507448"}, + {revision: "", kb: "4507463"}, + {revision: "", kb: "4512488"}, + {revision: "", kb: "4512478"}, + {revision: "", kb: "4516067"}, + {revision: "", kb: "4516041"}, + {revision: "", kb: "4524156"}, + {revision: "", kb: "4520005"}, + {revision: "", kb: "4520012"}, + {revision: "", kb: "4525243"}, + {revision: "", kb: "4525252"}, + {revision: "", kb: "4530702"}, + {revision: "", kb: "4534297"}, + {revision: "", kb: "4534324"}, + {revision: "", kb: "4537821"}, + {revision: "", kb: "4537819"}, + {revision: "", kb: "4541509"}, + {revision: "", kb: "4541334"}, + {revision: "", kb: "4550961"}, + {revision: "", kb: "4550958"}, + {revision: "", kb: "4556846"}, + {revision: "", kb: "4561666"}, + {revision: "", kb: "4565541"}, + {revision: "", kb: "4571703"}, + {revision: "", kb: "4577066"}, + {revision: "", kb: "4580347"}, + {revision: "", kb: "4586845"}, + {revision: "", kb: "4592484"}, + {revision: "", kb: "4598285"}, + {revision: "", kb: "4601384"}, + {revision: "", kb: "5000848"}, + {revision: "", kb: "5001382"}, + {revision: "", kb: "5003209"}, + {revision: "", kb: "5003671"}, + {revision: "", kb: "5004954"}, + {revision: "", kb: "5004298"}, + {revision: "", kb: "5005076"}, + {revision: "", kb: "5005613"}, + {revision: "", kb: "5006714"}, + {revision: "", kb: "5007247"}, + {revision: "", kb: "5008263"}, + {revision: "", kb: "5009624"}, + {revision: "", kb: "5010419"}, + {revision: "", kb: "5011564"}, + {revision: "", kb: "5012670"}, + {revision: "", kb: "5014011"}, + {revision: "", kb: "5014738"}, + {revision: "", kb: "5015874"}, + {revision: "", kb: "5016681"}, + {revision: "", kb: "5017367"}, + {revision: "", kb: "5018474"}, + {revision: "", kb: "5020023"}, + {revision: "", kb: "5021294"}, + {revision: "", kb: "5022352"}, + {revision: "", kb: "5022899"}, + }, + securityOnly: []string{ + "3192392", + "3197873", + "3205400", + "4012213", + "4015547", + "4019213", + "4022717", + "4025333", + "4034672", + "4038793", + "4041687", + "4048961", + "4054522", + "4056898", + "4074597", + "4088879", + "4093115", + "4103715", + "4284878", + "4338824", + "4343888", + "4457143", + "4462941", + "4467703", + "4471322", + "4480964", + "4487028", + "4489883", + "4493467", + "4499165", + "4503290", + "4507457", + "4512489", + "4516064", + "4519990", + "4525250", + "4530730", + "4534309", + "4537803", + "4541505", + "4550970", + "4556853", + "4561673", + "4565540", + "4571723", + "4577071", + "4580358", + "4586823", + "4592495", + "4598275", + "4601349", + "5000853", + "5001393", + "5003220", + "5003681", + "5004958", + "5004285", + "5005106", + "5005627", + "5006729", + "5007255", + "5008285", + "5009595", + "5010395", + "5011560", + "5012639", + "5014001", + "5014746", + "5015877", + "5016683", + "5017365", + "5018476", + "5020010", + "5021296", + "5022346", + "5022894", + }, + }, + }, + "2016": { + // https://support.microsoft.com/en-us/topic/windows-10-and-windows-server-2016-update-history-4acfbc84-a290-1b54-536a-1c0430e9f3fd + "14393": { + rollup: []windowsRelease{ + {revision: "10", kb: "3176929"}, + {revision: "51", kb: "3176495"}, + {revision: "82", kb: "3176934"}, + {revision: "105", kb: "3176938"}, + {revision: "187", kb: "3189866"}, + {revision: "187", kb: "3193494"}, + {revision: "189", kb: "3193494"}, + {revision: "222", kb: "3194496"}, + {revision: "321", kb: "3194798"}, + {revision: "351", kb: "3197954"}, + {revision: "447", kb: "3200970"}, + {revision: "448", kb: "3200970"}, + {revision: "479", kb: "3201845"}, + {revision: "571", kb: "3206632"}, + {revision: "576", kb: "3206632"}, + {revision: "693", kb: "3213986"}, + {revision: "729", kb: "4010672"}, + {revision: "953", kb: "4013429"}, + {revision: "969", kb: "4015438"}, + {revision: "970", kb: "4016635"}, + {revision: "1066", kb: "4015217"}, + {revision: "1083", kb: "4015217"}, + {revision: "1198", kb: "4019472"}, + {revision: "1230", kb: "4023680"}, + {revision: "1358", kb: "4022715"}, + {revision: "1378", kb: "4022723"}, + {revision: "1480", kb: "4025339"}, + {revision: "1532", kb: "4025334"}, + {revision: "1537", kb: "4038220"}, + {revision: "1593", kb: "4034658"}, + {revision: "1613", kb: "4034661"}, + {revision: "1670", kb: "4039396"}, + {revision: "1715", kb: "4038782"}, + {revision: "1737", kb: "4038801"}, + {revision: "1770", kb: "4041691"}, + {revision: "1794", kb: "4041688"}, + {revision: "1797", kb: "4052231"}, + {revision: "1884", kb: "4048953"}, + {revision: "1914", kb: "4051033"}, + {revision: "1944", kb: "4053579"}, + {revision: "2007", kb: "4056890"}, + {revision: "2034", kb: "4057142"}, + {revision: "2035", kb: "4057142"}, + {revision: "2068", kb: "4074590"}, + {revision: "2097", kb: "4077525"}, + {revision: "2125", kb: "4088787"}, + {revision: "2126", kb: "4088787"}, + {revision: "2155", kb: "4088889"}, + {revision: "2156", kb: "4096309"}, + {revision: "2189", kb: "4093119"}, + {revision: "2214", kb: "4093120"}, + {revision: "2248", kb: "4103723"}, + {revision: "2273", kb: "4103720"}, + {revision: "2312", kb: "4284880"}, + {revision: "2339", kb: "4284833"}, + {revision: "2363", kb: "4338814"}, + {revision: "2368", kb: "4345418"}, + {revision: "2395", kb: "4338822"}, + {revision: "2396", kb: "4346877"}, + {revision: "2430", kb: "4343887"}, + {revision: "2457", kb: "4343884"}, + {revision: "2485", kb: "4457131"}, + {revision: "2515", kb: "4457127"}, + {revision: "2551", kb: "4462917"}, + {revision: "2580", kb: "4462928"}, + {revision: "2608", kb: "4467691"}, + {revision: "2639", kb: "4467684"}, + {revision: "2641", kb: "4478877"}, + {revision: "2665", kb: "4471321"}, + {revision: "2670", kb: "4483229"}, + {revision: "2724", kb: "4480961"}, + {revision: "2759", kb: "4480977"}, + {revision: "2791", kb: "4487026"}, + {revision: "2828", kb: "4487006"}, + {revision: "2848", kb: "4489882"}, + {revision: "2879", kb: "4489889"}, + {revision: "2906", kb: "4493470"}, + {revision: "2908", kb: "4499418"}, + {revision: "2941", kb: "4493473"}, + {revision: "2969", kb: "4494440"}, + {revision: "2972", kb: "4505052"}, + {revision: "2999", kb: "4499177"}, + {revision: "3025", kb: "4503267"}, + {revision: "3053", kb: "4503294"}, + {revision: "3056", kb: "4509475"}, + {revision: "3085", kb: "4507460"}, + {revision: "3115", kb: "4507459"}, + {revision: "3144", kb: "4512517"}, + {revision: "3181", kb: "4512495"}, + {revision: "3204", kb: "4516044"}, + {revision: "3206", kb: "4522010"}, + {revision: "3242", kb: "4516061"}, + {revision: "3243", kb: "4524152"}, + {revision: "3274", kb: "4519998"}, + {revision: "3300", kb: "4519979"}, + {revision: "3326", kb: "4525236"}, + {revision: "3384", kb: "4530689"}, + {revision: "3443", kb: "4534271"}, + {revision: "3474", kb: "4534307"}, + {revision: "3504", kb: "4537764"}, + {revision: "3542", kb: "4537806"}, + {revision: "3564", kb: "4540670"}, + {revision: "3595", kb: "4541329"}, + {revision: "3630", kb: "4550929"}, + {revision: "3659", kb: "4550947"}, + {revision: "3686", kb: "4556813"}, + {revision: "3750", kb: "4561616"}, + {revision: "3755", kb: "4567517"}, + {revision: "3808", kb: "4565511"}, + {revision: "3866", kb: "4571694"}, + {revision: "3930", kb: "4577015"}, + {revision: "3986", kb: "4580346"}, + {revision: "4046", kb: "4586830"}, + {revision: "4048", kb: "4594441"}, + {revision: "4104", kb: "4593226"}, + {revision: "4169", kb: "4598243"}, + {revision: "4225", kb: "4601318"}, + {revision: "4283", kb: "5000803"}, + {revision: "4288", kb: "5001633"}, + {revision: "4350", kb: "5001347"}, + {revision: "4402", kb: "5003197"}, + {revision: "4467", kb: "5003638"}, + {revision: "4470", kb: "5004948"}, + {revision: "4530", kb: "5004238"}, + {revision: "4532", kb: "5005393"}, + {revision: "4583", kb: "5005043"}, + {revision: "4651", kb: "5005573"}, + {revision: "4704", kb: "5006669"}, + {revision: "4770", kb: "5007192"}, + {revision: "4771", kb: "5008601"}, + {revision: "4825", kb: "5008207"}, + {revision: "4827", kb: "5010195"}, + {revision: "4886", kb: "5009546"}, + {revision: "4889", kb: "5010790"}, + {revision: "4946", kb: "5010359"}, + {revision: "5006", kb: "5011495"}, + {revision: "5066", kb: "5012596"}, + {revision: "5125", kb: "5013952"}, + {revision: "5127", kb: "5015019"}, + {revision: "5192", kb: "5014702"}, + {revision: "5246", kb: "5015808"}, + {revision: "5291", kb: "5016622"}, + {revision: "5356", kb: "5017305"}, + {revision: "5427", kb: "5018411"}, + {revision: "5429", kb: "5020439"}, + {revision: "5501", kb: "5019964"}, + {revision: "5502", kb: "5021654"}, + {revision: "5582", kb: "5021235"}, + {revision: "5648", kb: "5022289"}, + {revision: "5717", kb: "5022838"}, + }, + }, + }, + "Version 1709": { + // https://support.microsoft.com/en-us/topic/windows-10-update-history-8127c2c6-6edf-4fdf-8b9f-0f7be1ef3562 + "16299": { + rollup: []windowsRelease{ + {revision: "19", kb: "4043961"}, + {revision: "64", kb: "4048955"}, + {revision: "98", kb: "4051963"}, + {revision: "125", kb: "4054517"}, + {revision: "192", kb: "4056892"}, + {revision: "194", kb: "4073290"}, + {revision: "201", kb: "4073291"}, + {revision: "214", kb: "4058258"}, + {revision: "248", kb: "4074588"}, + {revision: "251", kb: "4090913"}, + {revision: "309", kb: "4088776"}, + {revision: "334", kb: "4089848"}, + {revision: "371", kb: "4093112"}, + {revision: "402", kb: "4093105"}, + {revision: "431", kb: "4103727"}, + {revision: "461", kb: "4103714"}, + {revision: "492", kb: "4284819"}, + {revision: "522", kb: "4284822"}, + {revision: "547", kb: "4338825"}, + {revision: "551", kb: "4345420"}, + {revision: "579", kb: "4338817"}, + {revision: "611", kb: "4343897"}, + {revision: "637", kb: "4343893"}, + {revision: "665", kb: "4457142"}, + {revision: "666", kb: "4464217"}, + {revision: "699", kb: "4457136"}, + {revision: "726", kb: "4462918"}, + {revision: "755", kb: "4462932"}, + {revision: "785", kb: "4467686"}, + {revision: "820", kb: "4467681"}, + {revision: "846", kb: "4471329"}, + {revision: "847", kb: "4483232"}, + {revision: "904", kb: "4480978"}, + {revision: "936", kb: "4480967"}, + {revision: "967", kb: "4486996"}, + {revision: "1004", kb: "4487021"}, + {revision: "1029", kb: "4489886"}, + {revision: "1059", kb: "4489890"}, + {revision: "1087", kb: "4493441"}, + {revision: "1127", kb: "4493440"}, + {revision: "1146", kb: "4499179"}, + {revision: "1150", kb: "4505062"}, + {revision: "1182", kb: "4499147"}, + {revision: "1217", kb: "4503284"}, + {revision: "1237", kb: "4503281"}, + {revision: "1239", kb: "4509477"}, + {revision: "1268", kb: "4507455"}, + {revision: "1296", kb: "4507465"}, + {revision: "1331", kb: "4512516"}, + {revision: "1365", kb: "4512494"}, + {revision: "1387", kb: "4516066"}, + {revision: "1392", kb: "4522012"}, + {revision: "1420", kb: "4516071"}, + {revision: "1421", kb: "4524150"}, + {revision: "1451", kb: "4520004"}, + {revision: "1481", kb: "4520006"}, + {revision: "1508", kb: "4525241"}, + {revision: "1565", kb: "4530714"}, + {revision: "1625", kb: "4534276"}, + {revision: "1654", kb: "4534318"}, + {revision: "1686", kb: "4537789"}, + {revision: "1717", kb: "4537816"}, + {revision: "1747", kb: "4540681"}, + {revision: "1775", kb: "4541330"}, + {revision: "1776", kb: "4554342"}, + {revision: "1806", kb: "4550927"}, + {revision: "1868", kb: "4556812"}, + {revision: "1932", kb: "4561602"}, + {revision: "1937", kb: "4567515"}, + {revision: "1992", kb: "4565508"}, + {revision: "2045", kb: "4571741"}, + {revision: "2107", kb: "4577041"}, + {revision: "2166", kb: "4580328"}, + }, + }, + }, + "Version 1803": { + "17134": { + rollup: []windowsRelease{}, + }, + }, + "Version 1809": { + // https://support.microsoft.com/en-us/topic/windows-10-and-windows-server-2019-update-history-725fc2e1-4443-6831-a5ca-51ff5cbcb059 + "17763": { + rollup: []windowsRelease{ + {revision: "1", kb: ""}, + {revision: "55", kb: "4464330"}, + {revision: "107", kb: "4464455"}, + {revision: "134", kb: "4467708"}, + {revision: "168", kb: "4469342"}, + {revision: "194", kb: "4471332"}, + {revision: "195", kb: "4483235"}, + {revision: "253", kb: "4480116"}, + {revision: "292", kb: "4476976"}, + {revision: "316", kb: "4487044"}, + {revision: "348", kb: "4482887"}, + {revision: "379", kb: "4489899"}, + {revision: "402", kb: "4490481"}, + {revision: "404", kb: "4490481"}, + {revision: "437", kb: "4493509"}, + {revision: "439", kb: "4501835"}, + {revision: "475", kb: "4495667"}, + {revision: "503", kb: "4494441"}, + {revision: "504", kb: "4505056"}, + {revision: "529", kb: "4497934"}, + {revision: "557", kb: "4503327"}, + {revision: "592", kb: "4501371"}, + {revision: "593", kb: "4509479"}, + {revision: "615", kb: "4507469"}, + {revision: "652", kb: "4505658"}, + {revision: "678", kb: "4511553"}, + {revision: "720", kb: "4512534"}, + {revision: "737", kb: "4512578"}, + {revision: "740", kb: "4522015"}, + {revision: "774", kb: "4516077"}, + {revision: "775", kb: "4524148"}, + {revision: "805", kb: "4519338"}, + {revision: "832", kb: "4520062"}, + {revision: "864", kb: "4523205"}, + {revision: "914", kb: "4530715"}, + {revision: "973", kb: "4534273"}, + {revision: "1012", kb: "4534321"}, + {revision: "1039", kb: "4532691"}, + {revision: "1075", kb: "4537818"}, + {revision: "1098", kb: "4538461"}, + {revision: "1131", kb: "4541331"}, + {revision: "1132", kb: "4554354"}, + {revision: "1158", kb: "4549949"}, + {revision: "1192", kb: "4550969"}, + {revision: "1217", kb: "4551853"}, + {revision: "1282", kb: "4561608"}, + {revision: "1294", kb: "4567513"}, + {revision: "1339", kb: "4558998"}, + {revision: "1369", kb: "4559003"}, + {revision: "1397", kb: "4565349"}, + {revision: "1432", kb: "4571748"}, + {revision: "1457", kb: "4570333"}, + {revision: "1490", kb: "4577069"}, + {revision: "1518", kb: "4577668"}, + {revision: "1554", kb: "4580390"}, + {revision: "1577", kb: "4586793"}, + {revision: "1579", kb: "4594442"}, + {revision: "1613", kb: "4586839"}, + {revision: "1637", kb: "4592440"}, + {revision: "1697", kb: "4598230"}, + {revision: "1728", kb: "4598296"}, + {revision: "1757", kb: "4601345"}, + {revision: "1790", kb: "4601383"}, + {revision: "1817", kb: "5000822"}, + {revision: "1821", kb: "5001568"}, + {revision: "1823", kb: "5001638"}, + {revision: "1852", kb: "5000854"}, + {revision: "1879", kb: "5001342"}, + {revision: "1911", kb: "5001384"}, + {revision: "1935", kb: "5003171"}, + {revision: "1971", kb: "5003217"}, + {revision: "1999", kb: "5003646"}, + {revision: "2028", kb: "5003703"}, + {revision: "2029", kb: "5004947"}, + {revision: "2061", kb: "5004244"}, + {revision: "2090", kb: "5004308"}, + {revision: "2091", kb: "5005394"}, + {revision: "2114", kb: "5005030"}, + {revision: "2145", kb: "5005102"}, + {revision: "2183", kb: "5005568"}, + {revision: "2210", kb: "5005625"}, + {revision: "2213", kb: "5005625"}, + {revision: "2237", kb: "5006672"}, + {revision: "2268", kb: "5006744"}, + {revision: "2300", kb: "5007206"}, + {revision: "2305", kb: "5008602"}, + {revision: "2330", kb: "5007266"}, + {revision: "2366", kb: "5008218"}, + {revision: "2369", kb: "5010196"}, + {revision: "2452", kb: "5009557"}, + {revision: "2458", kb: "5010791"}, + {revision: "2510", kb: "5009616"}, + {revision: "2565", kb: "5010351"}, + {revision: "2628", kb: "5010427"}, + {revision: "2686", kb: "5011503"}, + {revision: "2746", kb: "5011551"}, + {revision: "2803", kb: "5012647"}, + {revision: "2867", kb: "5012636"}, + {revision: "2928", kb: "5013941"}, + {revision: "2931", kb: "5015018"}, + {revision: "2989", kb: "5014022"}, + {revision: "3046", kb: "5014692"}, + {revision: "3113", kb: "5014669"}, + {revision: "3165", kb: "5015811"}, + {revision: "3232", kb: "5015880"}, + {revision: "3287", kb: "5016623"}, + {revision: "3346", kb: "5016690"}, + {revision: "3406", kb: "5017315"}, + {revision: "3469", kb: "5017379"}, + {revision: "3532", kb: "5018419"}, + {revision: "3534", kb: "5020438"}, + {revision: "3650", kb: "5019966"}, + {revision: "3653", kb: "5021655"}, + {revision: "3770", kb: "5021237"}, + {revision: "3772", kb: "5022554"}, + {revision: "3887", kb: "5022286"}, + {revision: "4010", kb: "5022840"}, + }, + }, + }, + "2019": { + // https://support.microsoft.com/en-us/topic/windows-10-and-windows-server-2019-update-history-725fc2e1-4443-6831-a5ca-51ff5cbcb059 + "17763": { + rollup: []windowsRelease{ + {revision: "1", kb: ""}, + {revision: "55", kb: "4464330"}, + {revision: "107", kb: "4464455"}, + {revision: "134", kb: "4467708"}, + {revision: "168", kb: "4469342"}, + {revision: "194", kb: "4471332"}, + {revision: "195", kb: "4483235"}, + {revision: "253", kb: "4480116"}, + {revision: "292", kb: "4476976"}, + {revision: "316", kb: "4487044"}, + {revision: "348", kb: "4482887"}, + {revision: "379", kb: "4489899"}, + {revision: "402", kb: "4490481"}, + {revision: "404", kb: "4490481"}, + {revision: "437", kb: "4493509"}, + {revision: "439", kb: "4501835"}, + {revision: "475", kb: "4495667"}, + {revision: "503", kb: "4494441"}, + {revision: "504", kb: "4505056"}, + {revision: "529", kb: "4497934"}, + {revision: "557", kb: "4503327"}, + {revision: "592", kb: "4501371"}, + {revision: "593", kb: "4509479"}, + {revision: "615", kb: "4507469"}, + {revision: "652", kb: "4505658"}, + {revision: "678", kb: "4511553"}, + {revision: "720", kb: "4512534"}, + {revision: "737", kb: "4512578"}, + {revision: "740", kb: "4522015"}, + {revision: "774", kb: "4516077"}, + {revision: "775", kb: "4524148"}, + {revision: "805", kb: "4519338"}, + {revision: "832", kb: "4520062"}, + {revision: "864", kb: "4523205"}, + {revision: "914", kb: "4530715"}, + {revision: "973", kb: "4534273"}, + {revision: "1012", kb: "4534321"}, + {revision: "1039", kb: "4532691"}, + {revision: "1075", kb: "4537818"}, + {revision: "1098", kb: "4538461"}, + {revision: "1131", kb: "4541331"}, + {revision: "1132", kb: "4554354"}, + {revision: "1158", kb: "4549949"}, + {revision: "1192", kb: "4550969"}, + {revision: "1217", kb: "4551853"}, + {revision: "1282", kb: "4561608"}, + {revision: "1294", kb: "4567513"}, + {revision: "1339", kb: "4558998"}, + {revision: "1369", kb: "4559003"}, + {revision: "1397", kb: "4565349"}, + {revision: "1432", kb: "4571748"}, + {revision: "1457", kb: "4570333"}, + {revision: "1490", kb: "4577069"}, + {revision: "1518", kb: "4577668"}, + {revision: "1554", kb: "4580390"}, + {revision: "1577", kb: "4586793"}, + {revision: "1579", kb: "4594442"}, + {revision: "1613", kb: "4586839"}, + {revision: "1637", kb: "4592440"}, + {revision: "1697", kb: "4598230"}, + {revision: "1728", kb: "4598296"}, + {revision: "1757", kb: "4601345"}, + {revision: "1790", kb: "4601383"}, + {revision: "1817", kb: "5000822"}, + {revision: "1821", kb: "5001568"}, + {revision: "1823", kb: "5001638"}, + {revision: "1852", kb: "5000854"}, + {revision: "1879", kb: "5001342"}, + {revision: "1911", kb: "5001384"}, + {revision: "1935", kb: "5003171"}, + {revision: "1971", kb: "5003217"}, + {revision: "1999", kb: "5003646"}, + {revision: "2028", kb: "5003703"}, + {revision: "2029", kb: "5004947"}, + {revision: "2061", kb: "5004244"}, + {revision: "2090", kb: "5004308"}, + {revision: "2091", kb: "5005394"}, + {revision: "2114", kb: "5005030"}, + {revision: "2145", kb: "5005102"}, + {revision: "2183", kb: "5005568"}, + {revision: "2210", kb: "5005625"}, + {revision: "2213", kb: "5005625"}, + {revision: "2237", kb: "5006672"}, + {revision: "2268", kb: "5006744"}, + {revision: "2300", kb: "5007206"}, + {revision: "2305", kb: "5008602"}, + {revision: "2330", kb: "5007266"}, + {revision: "2366", kb: "5008218"}, + {revision: "2369", kb: "5010196"}, + {revision: "2452", kb: "5009557"}, + {revision: "2458", kb: "5010791"}, + {revision: "2510", kb: "5009616"}, + {revision: "2565", kb: "5010351"}, + {revision: "2628", kb: "5010427"}, + {revision: "2686", kb: "5011503"}, + {revision: "2746", kb: "5011551"}, + {revision: "2803", kb: "5012647"}, + {revision: "2867", kb: "5012636"}, + {revision: "2928", kb: "5013941"}, + {revision: "2931", kb: "5015018"}, + {revision: "2989", kb: "5014022"}, + {revision: "3046", kb: "5014692"}, + {revision: "3113", kb: "5014669"}, + {revision: "3165", kb: "5015811"}, + {revision: "3232", kb: "5015880"}, + {revision: "3287", kb: "5016623"}, + {revision: "3346", kb: "5016690"}, + {revision: "3406", kb: "5017315"}, + {revision: "3469", kb: "5017379"}, + {revision: "3532", kb: "5018419"}, + {revision: "3534", kb: "5020438"}, + {revision: "3650", kb: "5019966"}, + {revision: "3653", kb: "5021655"}, + {revision: "3770", kb: "5021237"}, + {revision: "3772", kb: "5022554"}, + {revision: "3887", kb: "5022286"}, + {revision: "4010", kb: "5022840"}, + }, + }, + }, + "Version 1903": { + // https://support.microsoft.com/en-us/topic/windows-10-update-history-e6058e7c-4116-38f1-b984-4fcacfba5e5d + "18362": { + rollup: []windowsRelease{ + {revision: "116", kb: "4505057"}, + {revision: "145", kb: "4497935"}, + {revision: "175", kb: "4503293"}, + {revision: "207", kb: "4501375"}, + {revision: "239", kb: "4507453"}, + {revision: "267", kb: "4505903"}, + {revision: "295", kb: "4512508"}, + {revision: "329", kb: "4512941"}, + {revision: "356", kb: "4515384"}, + {revision: "357", kb: "4522016"}, + {revision: "387", kb: "4517211"}, + {revision: "388", kb: "4524147"}, + {revision: "418", kb: "4517389"}, + {revision: "449", kb: "4522355"}, + {revision: "476", kb: "4524570"}, + {revision: "535", kb: "4530684"}, + {revision: "592", kb: "4528760"}, + {revision: "628", kb: "4532695"}, + {revision: "657", kb: "4532693"}, + {revision: "693", kb: "4535996"}, + {revision: "719", kb: "4540673"}, + {revision: "720", kb: "4551762"}, + {revision: "752", kb: "4541335"}, + {revision: "753", kb: "4554364"}, + {revision: "778", kb: "4549951"}, + {revision: "815", kb: "4550945"}, + {revision: "836", kb: "4556799"}, + {revision: "900", kb: "4560960"}, + {revision: "904", kb: "4567512"}, + {revision: "959", kb: "4565483"}, + {revision: "997", kb: "4559004"}, + {revision: "1016", kb: "4565351"}, + {revision: "1049", kb: "4566116"}, + {revision: "1082", kb: "4574727"}, + {revision: "1110", kb: "4577062"}, + {revision: "1139", kb: "4577671"}, + {revision: "1171", kb: "4580386"}, + {revision: "1198", kb: "4586786"}, + {revision: "1199", kb: "4594443"}, + {revision: "1237", kb: "4586819"}, + {revision: "1256", kb: "4592449"}, + }, + }, + }, + "Version 1909": { + // https://support.microsoft.com/en-us/topic/windows-10-update-history-53c270dc-954f-41f7-7ced-488578904dfe + "18363": { + rollup: []windowsRelease{ + {revision: "476", kb: "4524570"}, + {revision: "535", kb: "4530684"}, + {revision: "592", kb: "4528760"}, + {revision: "628", kb: "4532695"}, + {revision: "657", kb: "4532693"}, + {revision: "693", kb: "4535996"}, + {revision: "719", kb: "4540673"}, + {revision: "720", kb: "4551762"}, + {revision: "752", kb: "4541335"}, + {revision: "753", kb: "4554364"}, + {revision: "778", kb: "4549951"}, + {revision: "815", kb: "4550945"}, + {revision: "836", kb: "4556799"}, + {revision: "900", kb: "4560960"}, + {revision: "904", kb: "4567512"}, + {revision: "959", kb: "4565483"}, + {revision: "997", kb: "4559004"}, + {revision: "1016", kb: "4565351"}, + {revision: "1049", kb: "4566116"}, + {revision: "1082", kb: "4574727"}, + {revision: "1110", kb: "4577062"}, + {revision: "1139", kb: "4577671"}, + {revision: "1171", kb: "4580386"}, + {revision: "1198", kb: "4586786"}, + {revision: "1199", kb: "4594443"}, + {revision: "1237", kb: "4586819"}, + {revision: "1256", kb: "4592449"}, + {revision: "1316", kb: "4598229"}, + {revision: "1350", kb: "4598298"}, + {revision: "1377", kb: "4601315"}, + {revision: "1379", kb: "5001028"}, + {revision: "1411", kb: "4601380"}, + {revision: "1440", kb: "5000808"}, + {revision: "1441", kb: "5001566"}, + {revision: "1443", kb: "5001648"}, + {revision: "1474", kb: "5000850"}, + {revision: "1500", kb: "5001337"}, + {revision: "1533", kb: "5001396"}, + {revision: "1556", kb: "5003169"}, + {revision: "1593", kb: "5003212"}, + {revision: "1621", kb: "5003635"}, + {revision: "1645", kb: "5003698"}, + {revision: "1646", kb: "5004946"}, + {revision: "1679", kb: "5004245"}, + {revision: "1714", kb: "5004293"}, + {revision: "1734", kb: "5005031"}, + {revision: "1766", kb: "5005103"}, + {revision: "1801", kb: "5005566"}, + {revision: "1830", kb: "5005624"}, + {revision: "1832", kb: "5005624"}, + {revision: "1854", kb: "5006667"}, + {revision: "1916", kb: "5007189"}, + {revision: "1977", kb: "5008206"}, + {revision: "2037", kb: "5009545"}, + {revision: "2039", kb: "5010792"}, + {revision: "2094", kb: "5010345"}, + {revision: "2158", kb: "5011485"}, + {revision: "2212", kb: "5012591"}, + {revision: "2274", kb: "5013945"}, + }, + }, + }, + "Version 2004": { + // https://support.microsoft.com/en-us/topic/windows-10-update-history-24ea91f4-36e7-d8fd-0ddb-d79d9d0cdbda + "19041": { + rollup: []windowsRelease{ + {revision: "264", kb: ""}, + {revision: "329", kb: "4557957"}, + {revision: "331", kb: "4567523"}, + {revision: "388", kb: "4565503"}, + {revision: "423", kb: "4568831"}, + {revision: "450", kb: "4566782"}, + {revision: "488", kb: "4571744"}, + {revision: "508", kb: "4571756"}, + {revision: "546", kb: "4577063"}, + {revision: "572", kb: "4579311"}, + {revision: "610", kb: "4580364"}, + {revision: "630", kb: "4586781"}, + {revision: "631", kb: "4594440"}, + {revision: "662", kb: "4586853"}, + {revision: "685", kb: "4592438"}, + {revision: "746", kb: "4598242"}, + {revision: "789", kb: "4598291"}, + {revision: "804", kb: "4601319"}, + {revision: "844", kb: "4601382"}, + {revision: "867", kb: "5000802"}, + {revision: "868", kb: "5001567"}, + {revision: "870", kb: "5001649"}, + {revision: "906", kb: "5000842"}, + {revision: "928", kb: "5001330"}, + {revision: "964", kb: "5001391"}, + {revision: "985", kb: "5003173"}, + {revision: "1023", kb: "5003214"}, + {revision: "1052", kb: "5003637"}, + {revision: "1055", kb: "5004476"}, + {revision: "1081", kb: "5003690"}, + {revision: "1082", kb: "5004760"}, + {revision: "1083", kb: "5004945"}, + {revision: "1110", kb: "5004237"}, + {revision: "1151", kb: "5004296"}, + {revision: "1165", kb: "5005033"}, + {revision: "1202", kb: "5005101"}, + {revision: "1237", kb: "5005565"}, + {revision: "1266", kb: "5005611"}, + {revision: "1288", kb: "5006670"}, + {revision: "1320", kb: "5006738"}, + {revision: "1348", kb: "5007186"}, + {revision: "1387", kb: "5007253"}, + {revision: "1415", kb: "5008212"}, + }, + }, + }, + "Version 20H2": { + // https://support.microsoft.com/en-us/topic/windows-10-update-history-7dd3071a-3906-fa2c-c342-f7f86728a6e3 + "19042": { + rollup: []windowsRelease{ + {revision: "572", kb: ""}, + {revision: "610", kb: "4580364"}, + {revision: "630", kb: "4586781"}, + {revision: "631", kb: "4594440"}, + {revision: "662", kb: "4586853"}, + {revision: "685", kb: "4592438"}, + {revision: "746", kb: "4598242"}, + {revision: "789", kb: "4598291"}, + {revision: "804", kb: "4601319"}, + {revision: "844", kb: "4601382"}, + {revision: "867", kb: "5000802"}, + {revision: "868", kb: "5001567"}, + {revision: "870", kb: "5001649"}, + {revision: "906", kb: "5000842"}, + {revision: "928", kb: "5001330"}, + {revision: "964", kb: "5001391"}, + {revision: "985", kb: "5003173"}, + {revision: "1023", kb: "5003214"}, + {revision: "1052", kb: "5003637"}, + {revision: "1055", kb: "5004476"}, + {revision: "1081", kb: "5003690"}, + {revision: "1082", kb: "5004760"}, + {revision: "1083", kb: "5004945"}, + {revision: "1110", kb: "5004237"}, + {revision: "1151", kb: "5004296"}, + {revision: "1165", kb: "5005033"}, + {revision: "1202", kb: "5005101"}, + {revision: "1237", kb: "5005565"}, + {revision: "1266", kb: "5005611"}, + {revision: "1288", kb: "5006670"}, + {revision: "1320", kb: "5006738"}, + {revision: "1348", kb: "5007186"}, + {revision: "1387", kb: "5007253"}, + {revision: "1415", kb: "5008212"}, + {revision: "1466", kb: "5009543"}, + {revision: "1469", kb: "5010793"}, + {revision: "1503", kb: "5009596"}, + {revision: "1526", kb: "5010342"}, + {revision: "1566", kb: "5010415"}, + {revision: "1586", kb: "5011487"}, + {revision: "1620", kb: "5011543"}, + {revision: "1645", kb: "5012599"}, + {revision: "1682", kb: "5011831"}, + {revision: "1706", kb: "5013942"}, + {revision: "1708", kb: "5015020"}, + {revision: "1741", kb: "5014023"}, + {revision: "1766", kb: "5014699"}, + {revision: "1767", kb: "5016139"}, + {revision: "1806", kb: "5014666"}, + {revision: "1826", kb: "5015807"}, + {revision: "1865", kb: "5015878"}, + {revision: "1889", kb: "5016616"}, + {revision: "1949", kb: "5016688"}, + {revision: "2006", kb: "5017308"}, + {revision: "2075", kb: "5017380"}, + {revision: "2130", kb: "5018410"}, + {revision: "2132", kb: "5020435"}, + {revision: "2193", kb: "5018482"}, + {revision: "2194", kb: "5020953"}, + {revision: "2251", kb: "5019959"}, + {revision: "2311", kb: "5020030"}, + {revision: "2364", kb: "5021233"}, + {revision: "2486", kb: "5022282"}, + {revision: "2546", kb: "5019275"}, + {revision: "2604", kb: "5022834"}, + {revision: "2673", kb: "5022906"}, + }, + }, + }, + "2022": { + // https://support.microsoft.com/en-us/topic/windows-server-2022-update-history-e1caa597-00c5-4ab9-9f3e-8212fe80b2ee + "20348": { + rollup: []windowsRelease{ + {revision: "230", kb: "5005575"}, + {revision: "261", kb: "5005619"}, + {revision: "288", kb: "5006699"}, + {revision: "320", kb: "5006745"}, + {revision: "350", kb: "5007205"}, + {revision: "380", kb: "5007254"}, + {revision: "405", kb: "5008223"}, + {revision: "407", kb: "5010197"}, + {revision: "469", kb: "5009555"}, + {revision: "473", kb: "5010796"}, + {revision: "502", kb: "5009608"}, + {revision: "524", kb: "5010354"}, + {revision: "558", kb: "5010421"}, + {revision: "587", kb: "5011497"}, + {revision: "617", kb: "5011558"}, + {revision: "643", kb: "5012604"}, + {revision: "681", kb: "5012637"}, + {revision: "707", kb: "5013944"}, + {revision: "709", kb: "5015013"}, + {revision: "740", kb: "5014021"}, + {revision: "768", kb: "5014678"}, + {revision: "803", kb: "5014665"}, + {revision: "825", kb: "5015827"}, + {revision: "859", kb: "5015879"}, + {revision: "887", kb: "5016627"}, + {revision: "946", kb: "5016693"}, + {revision: "1006", kb: "5017316"}, + {revision: "1070", kb: "5017381"}, + {revision: "1129", kb: "5018421"}, + {revision: "1131", kb: "5020436"}, + {revision: "1194", kb: "5018485"}, + {revision: "1249", kb: "5019081"}, + {revision: "1251", kb: "5021656"}, + {revision: "1311", kb: "5020032"}, + {revision: "1366", kb: "5021249"}, + {revision: "1368", kb: "5022553"}, + {revision: "1487", kb: "5022291"}, + {revision: "1547", kb: "5022842"}, + }, + }, + }, + }, +} + +func (o *windows) detectKernelVersion(applied []string) (string, error) { + switch ss := strings.Split(o.Kernel.Version, "."); len(ss) { + case 3: + switch { + case strings.HasPrefix(o.getDistro().Release, "Windows 10"), strings.HasPrefix(o.getDistro().Release, "Windows 11"): + osver := strings.Fields(o.getDistro().Release)[1] + + verReleases, ok := windowsReleases["Client"][osver] + if !ok { + return o.Kernel.Version, nil + } + + rels, ok := verReleases[ss[2]] + if !ok { + return o.Kernel.Version, nil + } + + var revs []string + for _, r := range rels.rollup { + if slices.Contains(applied, r.kb) { + revs = append(revs, r.revision) + } + } + if len(revs) == 0 { + return o.Kernel.Version, nil + } + + slices.SortFunc(revs, func(i, j string) bool { + ni, _ := strconv.Atoi(i) + nj, _ := strconv.Atoi(j) + return ni < nj + }) + + return fmt.Sprintf("%s.%s", o.Kernel.Version, revs[len(revs)-1]), nil + case strings.HasPrefix(o.getDistro().Release, "Windows Server 2016"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 1709"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 1809"), strings.HasPrefix(o.getDistro().Release, "Windows Server 2019"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 1903"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 1909"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 2004"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 20H2"), strings.HasPrefix(o.getDistro().Release, "Windows Server 2022"): + osver := strings.TrimSpace(strings.NewReplacer("Windows Server", "", ",", "", "(Server Core installation)", "").Replace(o.getDistro().Release)) + + verReleases, ok := windowsReleases["Server"][osver] + if !ok { + return o.Kernel.Version, nil + } + + rels, ok := verReleases[ss[2]] + if !ok { + return o.Kernel.Version, nil + } + + var revs []string + for _, r := range rels.rollup { + if slices.Contains(applied, r.kb) { + revs = append(revs, r.revision) + } + } + if len(revs) == 0 { + return o.Kernel.Version, nil + } + + slices.SortFunc(revs, func(i, j string) bool { + ni, _ := strconv.Atoi(i) + nj, _ := strconv.Atoi(j) + return ni < nj + }) + + return fmt.Sprintf("%s.%s", o.Kernel.Version, revs[len(revs)-1]), nil + default: + return o.Kernel.Version, nil + } + case 4: + return o.Kernel.Version, nil + default: + return "", xerrors.Errorf("unexpected kernel version. expected: <major version>.<minor version>.<build>(.<revision>), actual: %s", o.Kernel.Version) + } +} + +func (o *windows) detectKBsFromKernelVersion() (models.WindowsKB, error) { + switch ss := strings.Split(o.Kernel.Version, "."); len(ss) { + case 3: + return models.WindowsKB{}, nil + case 4: + switch { + case strings.HasPrefix(o.getDistro().Release, "Windows 10 "), strings.HasPrefix(o.getDistro().Release, "Windows 11 "): + osver := strings.Split(o.getDistro().Release, " ")[1] + + verReleases, ok := windowsReleases["Client"][osver] + if !ok { + return models.WindowsKB{}, nil + } + + rels, ok := verReleases[ss[2]] + if !ok { + return models.WindowsKB{}, nil + } + + nMyRevision, err := strconv.Atoi(ss[3]) + if err != nil { + return models.WindowsKB{}, xerrors.Errorf("Failed to parse revision number. err: %w", err) + } + + var index int + for i, r := range rels.rollup { + nRevision, err := strconv.Atoi(r.revision) + if err != nil { + return models.WindowsKB{}, xerrors.Errorf("Failed to parse revision number. err: %w", err) + } + if nMyRevision < nRevision { + break + } + index = i + } + + var kbs models.WindowsKB + for _, r := range rels.rollup[:index+1] { + if r.kb != "" { + kbs.Applied = append(kbs.Applied, r.kb) + } + } + for _, r := range rels.rollup[index+1:] { + if r.kb != "" { + kbs.Unapplied = append(kbs.Unapplied, r.kb) + } + } + + return kbs, nil + case strings.HasPrefix(o.getDistro().Release, "Windows Server 2016"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 1709"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 1809"), strings.HasPrefix(o.getDistro().Release, "Windows Server 2019"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 1903"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 1909"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 2004"), strings.HasPrefix(o.getDistro().Release, "Windows Server, Version 20H2"), strings.HasPrefix(o.getDistro().Release, "Windows Server 2022"): + osver := strings.TrimSpace(strings.NewReplacer("Windows Server", "", ",", "", "(Server Core installation)", "").Replace(o.getDistro().Release)) + + verReleases, ok := windowsReleases["Server"][osver] + if !ok { + return models.WindowsKB{}, nil + } + + rels, ok := verReleases[ss[2]] + if !ok { + return models.WindowsKB{}, nil + } + + nMyRevision, err := strconv.Atoi(ss[3]) + if err != nil { + return models.WindowsKB{}, xerrors.Errorf("Failed to parse revision number. err: %w", err) + } + + var index int + for i, r := range rels.rollup { + nRevision, err := strconv.Atoi(r.revision) + if err != nil { + return models.WindowsKB{}, xerrors.Errorf("Failed to parse revision number. err: %w", err) + } + if nMyRevision < nRevision { + break + } + index = i + } + + var kbs models.WindowsKB + for _, r := range rels.rollup[:index+1] { + if r.kb != "" { + kbs.Applied = append(kbs.Applied, r.kb) + } + } + for _, r := range rels.rollup[index+1:] { + if r.kb != "" { + kbs.Unapplied = append(kbs.Unapplied, r.kb) + } + } + + return kbs, nil + default: + return models.WindowsKB{}, nil + } + default: + return models.WindowsKB{}, xerrors.Errorf("unexpected kernel version. expected: <major version>.<minor version>.<build>(.<revision>), actual: %s", o.Kernel.Version) + } +} + +func (o *windows) detectPlatform() { + o.setPlatform(models.Platform{Name: "other"}) +} diff --git a/scanner/windows_test.go b/scanner/windows_test.go new file mode 100644 index 0000000000..4acbc1c9a1 --- /dev/null +++ b/scanner/windows_test.go @@ -0,0 +1,736 @@ +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_detectKernelVersion(t *testing.T) { + tests := []struct { + name string + base base + args []string + want string + wantErr bool + }{ + { + name: "major.minor.build, applied on 10", + base: base{ + Distro: config.Distro{Release: "Windows 10 Version 22H2 for x64-based Systems"}, + osPackages: osPackages{Kernel: models.Kernel{Version: "10.0.19045"}}, + }, + args: []string{"5020030", "5019275"}, + want: "10.0.19045.2546", + }, + { + name: "major.minor.build, zero applied on 10", + base: base{ + Distro: config.Distro{Release: "Windows 10 Version 22H2 for x64-based Systems"}, + osPackages: osPackages{Kernel: models.Kernel{Version: "10.0.19045"}}, + }, + args: []string{}, + want: "10.0.19045", + }, + { + name: "major.minor.build.revision", + 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: "10.0.19045.2130", + }, + { + name: "major.minor.build, applied on 11", + base: base{ + Distro: config.Distro{Release: "Windows 11 Version 22H2 for x64-based Systems"}, + osPackages: osPackages{Kernel: models.Kernel{Version: "10.0.22621"}}, + }, + args: []string{"5017389", "5022303"}, + want: "10.0.22621.1105", + }, + { + name: "major.minor.build, applied on server 2022", + base: base{ + Distro: config.Distro{Release: "Windows Server 2022"}, + osPackages: osPackages{Kernel: models.Kernel{Version: "10.0.20348"}}, + }, + args: []string{"5022842"}, + want: "10.0.20348.1547", + }, + { + name: "major.minor", + 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.detectKernelVersion(tt.args) + if (err != nil) != tt.wantErr { + t.Errorf("windows.detectKernelVersion() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("windows.detectKernelVersion() = %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) + } + }) + } +} diff --git a/server/server.go b/server/server.go index 7078d32acb..87545b681a 100644 --- a/server/server.go +++ b/server/server.go @@ -39,13 +39,14 @@ func (h VulsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { return } - if mediatype == "application/json" { + switch mediatype { + case "application/json": if err = json.NewDecoder(req.Body).Decode(&r); err != nil { logging.Log.Error(err) http.Error(w, "Invalid JSON", http.StatusBadRequest) return } - } else if mediatype == "text/plain" { + case "text/plain": buf := new(bytes.Buffer) if _, err := io.Copy(buf, req.Body); err != nil { http.Error(w, err.Error(), http.StatusBadRequest) @@ -56,7 +57,7 @@ func (h VulsHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { http.Error(w, err.Error(), http.StatusBadRequest) return } - } else { + default: logging.Log.Error(mediatype) http.Error(w, fmt.Sprintf("Invalid Content-Type: %s", contentType), http.StatusUnsupportedMediaType) return diff --git a/subcmds/discover.go b/subcmds/discover.go index 06cc6d4c37..63da18f50f 100644 --- a/subcmds/discover.go +++ b/subcmds/discover.go @@ -247,6 +247,10 @@ host = "{{$ip}}" #scanTechniques = ["sS"] #sourcePort = "65535" +#[servers.{{index $names $i}}.windows] +#serverSelection = 3 +#cabPath = "/path/to/wsusscn2.cab" + #[servers.{{index $names $i}}.optional] #key = "value1" diff --git a/subcmds/report.go b/subcmds/report.go index cc6d7976dd..bdf3c01a54 100644 --- a/subcmds/report.go +++ b/subcmds/report.go @@ -1,5 +1,4 @@ -//go:build !scanner -// +build !scanner +//go:build !scanner && !windows package subcmds diff --git a/subcmds/report_windows.go b/subcmds/report_windows.go new file mode 100644 index 0000000000..2addfdfad5 --- /dev/null +++ b/subcmds/report_windows.go @@ -0,0 +1,372 @@ +//go:build !scanner && windows + +package subcmds + +import ( + "context" + "flag" + "os" + "path/filepath" + + "github.com/aquasecurity/trivy/pkg/utils" + "github.com/google/subcommands" + "github.com/k0kubun/pp" + + "github.com/future-architect/vuls/config" + "github.com/future-architect/vuls/detector" + "github.com/future-architect/vuls/logging" + "github.com/future-architect/vuls/models" + "github.com/future-architect/vuls/reporter" +) + +// ReportCmd is subcommand for reporting +type ReportCmd struct { + configPath string + + formatJSON bool + formatOneEMail bool + formatCsv bool + formatFullText bool + formatOneLineText bool + formatList bool + formatCycloneDXJSON bool + formatCycloneDXXML bool + gzip bool + + toSlack bool + toChatWork bool + toGoogleChat bool + toTelegram bool + toEmail bool + toLocalFile bool + toS3 bool + toAzureBlob bool + toHTTP bool +} + +// Name return subcommand name +func (*ReportCmd) Name() string { return "report" } + +// Synopsis return synopsis +func (*ReportCmd) Synopsis() string { return "Reporting" } + +// Usage return usage +func (*ReportCmd) Usage() string { + return `report: + report + [-lang=en|ja] + [-config=/path/to/config.toml] + [-results-dir=/path/to/results] + [-log-to-file] + [-log-dir=/path/to/log] + [-refresh-cve] + [-cvss-over=7] + [-confidence-over=80] + [-diff] + [-diff-minus] + [-diff-plus] + [-ignore-unscored-cves] + [-ignore-unfixed] + [-to-email] + [-to-http] + [-to-slack] + [-to-chatwork] + [-to-googlechat] + [-to-telegram] + [-to-localfile] + [-to-s3] + [-to-azure-blob] + [-format-json] + [-format-one-email] + [-format-one-line-text] + [-format-list] + [-format-full-text] + [-format-csv] + [-format-cyclonedx-json] + [-format-cyclonedx-xml] + [-gzip] + [-http-proxy=http://192.168.0.1:8080] + [-debug] + [-debug-sql] + [-quiet] + [-no-progress] + [-pipe] + [-http="http://vuls-report-server"] + [-trivy-cachedb-dir=/path/to/dir] + + [RFC3339 datetime format under results dir] +` +} + +// SetFlags set flag +func (p *ReportCmd) SetFlags(f *flag.FlagSet) { + f.StringVar(&config.Conf.Lang, "lang", "en", "[en|ja]") + f.BoolVar(&config.Conf.Debug, "debug", false, "debug mode") + f.BoolVar(&config.Conf.DebugSQL, "debug-sql", false, "SQL debug mode") + f.BoolVar(&config.Conf.Quiet, "quiet", false, "Quiet mode. No output on stdout") + f.BoolVar(&config.Conf.NoProgress, "no-progress", false, "Suppress progress bar") + + wd, _ := os.Getwd() + defaultConfPath := filepath.Join(wd, "config.toml") + f.StringVar(&p.configPath, "config", defaultConfPath, "/path/to/toml") + + defaultResultsDir := filepath.Join(wd, "results") + f.StringVar(&config.Conf.ResultsDir, "results-dir", defaultResultsDir, "/path/to/results") + + defaultLogDir := logging.GetDefaultLogDir() + f.StringVar(&config.Conf.LogDir, "log-dir", defaultLogDir, "/path/to/log") + f.BoolVar(&config.Conf.LogToFile, "log-to-file", false, "Output log to file") + + f.BoolVar(&config.Conf.RefreshCve, "refresh-cve", false, + "Refresh CVE information in JSON file under results dir") + + f.Float64Var(&config.Conf.CvssScoreOver, "cvss-over", 0, + "-cvss-over=6.5 means reporting CVSS Score 6.5 and over (default: 0 (means report all))") + + f.IntVar(&config.Conf.ConfidenceScoreOver, "confidence-over", 80, + "-confidence-over=40 means reporting Confidence Score 40 and over (default: 80)") + + f.BoolVar(&config.Conf.DiffMinus, "diff-minus", false, + "Minus Difference between previous result and current result") + + f.BoolVar(&config.Conf.DiffPlus, "diff-plus", false, + "Plus Difference between previous result and current result") + + f.BoolVar(&config.Conf.Diff, "diff", false, + "Plus & Minus Difference between previous result and current result") + + f.BoolVar(&config.Conf.IgnoreUnscoredCves, "ignore-unscored-cves", false, + "Don't report the unscored CVEs") + + f.BoolVar(&config.Conf.IgnoreUnfixed, "ignore-unfixed", false, + "Don't report the unfixed CVEs") + + f.StringVar( + &config.Conf.HTTPProxy, "http-proxy", "", + "http://proxy-url:port (default: empty)") + + f.BoolVar(&p.formatJSON, "format-json", false, "JSON format") + f.BoolVar(&p.formatCsv, "format-csv", false, "CSV format") + f.BoolVar(&p.formatOneEMail, "format-one-email", false, + "Send all the host report via only one EMail (Specify with -to-email)") + f.BoolVar(&p.formatOneLineText, "format-one-line-text", false, + "One line summary in plain text") + f.BoolVar(&p.formatList, "format-list", false, "Display as list format") + f.BoolVar(&p.formatFullText, "format-full-text", false, + "Detail report in plain text") + f.BoolVar(&p.formatCycloneDXJSON, "format-cyclonedx-json", false, "CycloneDX JSON format") + f.BoolVar(&p.formatCycloneDXXML, "format-cyclonedx-xml", false, "CycloneDX XML format") + + f.BoolVar(&p.toSlack, "to-slack", false, "Send report via Slack") + f.BoolVar(&p.toChatWork, "to-chatwork", false, "Send report via chatwork") + f.BoolVar(&p.toGoogleChat, "to-googlechat", false, "Send report via Google Chat") + f.BoolVar(&p.toTelegram, "to-telegram", false, "Send report via Telegram") + f.BoolVar(&p.toEmail, "to-email", false, "Send report via Email") + f.BoolVar(&p.toLocalFile, "to-localfile", false, "Write report to localfile") + f.BoolVar(&p.toS3, "to-s3", false, "Write report to S3 (bucket/yyyyMMdd_HHmm/servername.json/txt)") + f.BoolVar(&p.toHTTP, "to-http", false, "Send report via HTTP POST") + f.BoolVar(&p.toAzureBlob, "to-azure-blob", false, + "Write report to Azure Storage blob (container/yyyyMMdd_HHmm/servername.json/txt)") + + f.BoolVar(&p.gzip, "gzip", false, "gzip compression") + f.BoolVar(&config.Conf.Pipe, "pipe", false, "Use args passed via PIPE") + + f.StringVar(&config.Conf.TrivyCacheDBDir, "trivy-cachedb-dir", + utils.DefaultCacheDir(), "/path/to/dir") +} + +// Execute execute +func (p *ReportCmd) Execute(_ context.Context, f *flag.FlagSet, _ ...interface{}) subcommands.ExitStatus { + logging.Log = logging.NewCustomLogger(config.Conf.Debug, config.Conf.Quiet, config.Conf.LogToFile, config.Conf.LogDir, "", "") + logging.Log.Infof("vuls-%s-%s", config.Version, config.Revision) + + if p.configPath == "" { + for _, cnf := range []config.VulnDictInterface{ + &config.Conf.CveDict, + &config.Conf.OvalDict, + &config.Conf.Gost, + &config.Conf.Exploit, + &config.Conf.Metasploit, + &config.Conf.KEVuln, + } { + cnf.Init() + } + } else { + if err := config.Load(p.configPath); err != nil { + logging.Log.Errorf("Error loading %s. err: %+v", p.configPath, err) + return subcommands.ExitUsageError + } + } + + config.Conf.Slack.Enabled = p.toSlack + config.Conf.ChatWork.Enabled = p.toChatWork + config.Conf.GoogleChat.Enabled = p.toGoogleChat + config.Conf.Telegram.Enabled = p.toTelegram + config.Conf.EMail.Enabled = p.toEmail + config.Conf.AWS.Enabled = p.toS3 + config.Conf.Azure.Enabled = p.toAzureBlob + config.Conf.HTTP.Enabled = p.toHTTP + + if config.Conf.Diff { + config.Conf.DiffPlus, config.Conf.DiffMinus = true, true + } + + var dir string + var err error + if config.Conf.DiffPlus || config.Conf.DiffMinus { + dir, err = reporter.JSONDir(config.Conf.ResultsDir, []string{}) + } else { + dir, err = reporter.JSONDir(config.Conf.ResultsDir, f.Args()) + } + if err != nil { + logging.Log.Errorf("Failed to read from JSON: %+v", err) + return subcommands.ExitFailure + } + + logging.Log.Info("Validating config...") + if !config.Conf.ValidateOnReport() { + return subcommands.ExitUsageError + } + + if !(p.formatJSON || p.formatOneLineText || + p.formatList || p.formatFullText || p.formatCsv || + p.formatCycloneDXJSON || p.formatCycloneDXXML) { + p.formatList = true + } + + var loaded models.ScanResults + if loaded, err = reporter.LoadScanResults(dir); err != nil { + logging.Log.Error(err) + return subcommands.ExitFailure + } + logging.Log.Infof("Loaded: %s", dir) + + var res models.ScanResults + hasError := false + for _, r := range loaded { + if len(r.Errors) == 0 { + res = append(res, r) + } else { + logging.Log.Errorf("Ignored since errors occurred during scanning: %s, err: %v", + r.ServerName, r.Errors) + hasError = true + } + } + + if len(res) == 0 { + return subcommands.ExitFailure + } + + for _, r := range res { + logging.Log.Debugf("%s: %s", + r.ServerInfo(), pp.Sprintf("%s", config.Conf.Servers[r.ServerName])) + } + + if res, err = detector.Detect(res, dir); err != nil { + logging.Log.Errorf("%+v", err) + return subcommands.ExitFailure + } + + // report + reports := []reporter.ResultWriter{ + reporter.StdoutWriter{ + FormatFullText: p.formatFullText, + FormatOneLineText: p.formatOneLineText, + FormatList: p.formatList, + }, + } + + if p.toSlack { + reports = append(reports, reporter.SlackWriter{ + FormatOneLineText: p.formatOneLineText, + Cnf: config.Conf.Slack, + Proxy: config.Conf.HTTPProxy, + }) + } + + if p.toChatWork { + reports = append(reports, reporter.ChatWorkWriter{Cnf: config.Conf.ChatWork, Proxy: config.Conf.HTTPProxy}) + } + + if p.toGoogleChat { + reports = append(reports, reporter.GoogleChatWriter{Cnf: config.Conf.GoogleChat, Proxy: config.Conf.HTTPProxy}) + } + + if p.toTelegram { + reports = append(reports, reporter.TelegramWriter{Cnf: config.Conf.Telegram}) + } + + if p.toEmail { + reports = append(reports, reporter.EMailWriter{ + FormatOneEMail: p.formatOneEMail, + FormatOneLineText: p.formatOneLineText, + FormatList: p.formatList, + Cnf: config.Conf.EMail, + }) + } + + if p.toHTTP { + reports = append(reports, reporter.HTTPRequestWriter{URL: config.Conf.HTTP.URL}) + } + + if p.toLocalFile { + reports = append(reports, reporter.LocalFileWriter{ + CurrentDir: dir, + DiffPlus: config.Conf.DiffPlus, + DiffMinus: config.Conf.DiffMinus, + FormatJSON: p.formatJSON, + FormatCsv: p.formatCsv, + FormatFullText: p.formatFullText, + FormatOneLineText: p.formatOneLineText, + FormatList: p.formatList, + FormatCycloneDXJSON: p.formatCycloneDXJSON, + FormatCycloneDXXML: p.formatCycloneDXXML, + Gzip: p.gzip, + }) + } + + if p.toS3 { + w := reporter.S3Writer{ + FormatJSON: p.formatJSON, + FormatFullText: p.formatFullText, + FormatOneLineText: p.formatOneLineText, + FormatList: p.formatList, + Gzip: p.gzip, + AWSConf: config.Conf.AWS, + } + if err := w.Validate(); err != nil { + logging.Log.Errorf("Check if there is a bucket beforehand: %s, err: %+v", config.Conf.AWS.S3Bucket, err) + return subcommands.ExitUsageError + } + reports = append(reports, w) + } + + if p.toAzureBlob { + w := reporter.AzureBlobWriter{ + FormatJSON: p.formatJSON, + FormatFullText: p.formatFullText, + FormatOneLineText: p.formatOneLineText, + FormatList: p.formatList, + Gzip: p.gzip, + AzureConf: config.Conf.Azure, + } + if err := w.Validate(); err != nil { + logging.Log.Errorf("Check if there is a container beforehand: %s, err: %+v", config.Conf.Azure.ContainerName, err) + return subcommands.ExitUsageError + } + reports = append(reports, w) + } + + for _, w := range reports { + if err := w.Write(res...); err != nil { + logging.Log.Errorf("Failed to report. err: %+v", err) + return subcommands.ExitFailure + } + } + + if hasError { + return subcommands.ExitFailure + } + + return subcommands.ExitSuccess +} diff --git a/tui/tui.go b/tui/tui.go index 7376075450..b98e231754 100644 --- a/tui/tui.go +++ b/tui/tui.go @@ -614,6 +614,7 @@ func summaryLines(r models.ScanResult) string { pkgNames = append(pkgNames, vinfo.GitHubSecurityAlerts.Names()...) pkgNames = append(pkgNames, vinfo.WpPackageFixStats.Names()...) pkgNames = append(pkgNames, vinfo.LibraryFixedIns.Names()...) + pkgNames = append(pkgNames, vinfo.WindowsKBFixedIns...) av := vinfo.AttackVector() for _, pname := range vinfo.AffectedPackages.Names() {