feat(windows): support Windows (#1581)

* chore(deps): mod update

* fix(scanner): do not attach tty because there is no need to enter ssh password

* feat(windows): support Windows
This commit is contained in:
MaineK00n
2023-03-28 19:00:33 +09:00
committed by GitHub
parent db21149f00
commit 947d668452
37 changed files with 6938 additions and 341 deletions

View File

@@ -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)
}
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)
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 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)
}
case nil:
logging.Log.Warnf("This Windows has no option(InstalledProducts). UUID: %s", r.ServerUUID)
}
}
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)
continue
}
unapplied[strings.TrimPrefix(s, "KB")] = struct{}{}
}
case []string:
for _, kbID := range kbIDs {
unapplied[strings.TrimPrefix(kbID, "KB")] = struct{}{}
}
case nil:
logging.Log.Warnf("This Windows has no option(KBID). UUID: %s", r.ServerUUID)
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)
}
for _, pkg := range r.Packages {
matches := kbIDPattern.FindAllStringSubmatch(pkg.Name, -1)
for _, match := range matches {
applied[match[1]] = struct{}{}
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)
}
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 {
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)
}
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
}
unapplied[strings.TrimPrefix(s, "KB")] = struct{}{}
}
case []string:
for _, kbID := range kbIDs {
unapplied[strings.TrimPrefix(kbID, "KB")] = struct{}{}
}
case nil:
logging.Log.Warnf("This Windows has no option(UnappliedKBID). UUID: %s", r.ServerUUID)
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
}
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)
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
}
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))
}
}
default:
}
}
filtered = unique(append(filtered, maps.Keys(m)...))
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 {
cves, err = ms.driver.GetFilteredCvesMicrosoft(filtered, append(applied, unapplied...))
if err != nil {
return 0, xerrors.Errorf("Failed to detect CVEs. err: %w", err)
}
}
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)
}
}
}
if len(kbs) > 0 {
p.KBs = kbs
ps = append(ps, p)
}
}
cve.Products = ps
if len(cve.Products) == 0 {
continue
}
nCVEs++
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

View File

@@ -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)
}