diff --git a/changes/feature-7077-msrc-parser b/changes/feature-7077-msrc-parser new file mode 100644 index 0000000000..b08fc2fdec --- /dev/null +++ b/changes/feature-7077-msrc-parser @@ -0,0 +1,2 @@ +- Added the MSRC feed parser that we will be using for generating the MSRC artifacts. +- Added sync logic for keeping the local MSRC artifacts up to date. diff --git a/server/ptr/ptr.go b/server/ptr/ptr.go index 137166c1e3..d70a21661a 100644 --- a/server/ptr/ptr.go +++ b/server/ptr/ptr.go @@ -52,3 +52,7 @@ func Float64Ptr(x float64) **float64 { p := Float64(x) return &p } + +func Int64(x int64) *int64 { + return &x +} diff --git a/server/vulnerabilities/msrc/io/fs.go b/server/vulnerabilities/msrc/io/fs.go new file mode 100644 index 0000000000..5ade017705 --- /dev/null +++ b/server/vulnerabilities/msrc/io/fs.go @@ -0,0 +1,47 @@ +package io + +import ( + "os" + "path/filepath" + "strings" +) + +type MSRCFSAPI interface { + Bulletins() ([]SecurityBulletinName, error) + Delete(SecurityBulletinName) error +} + +type MSRCFSClient struct { + dir string +} + +func NewMSRCFSClient(dir string) MSRCFSClient { + return MSRCFSClient{ + dir: dir, + } +} + +// Delete deletes the provided security bulletin name from 'dir'. +func (fs MSRCFSClient) Delete(b SecurityBulletinName) error { + path := filepath.Join(fs.dir, string(b)) + return os.Remove(path) +} + +// Bulletins walks 'dir' returning all security bulletin names. +func (fs MSRCFSClient) Bulletins() ([]SecurityBulletinName, error) { + var result []SecurityBulletinName + + err := filepath.WalkDir(fs.dir, func(path string, d os.DirEntry, err error) error { + if err != nil { + return err + } + + filePath := filepath.Base(path) + if strings.HasPrefix(filePath, MSRCFilePrefix) { + result = append(result, NewSecurityBulletinName(filePath)) + } + + return nil + }) + return result, err +} diff --git a/server/vulnerabilities/msrc/io/fs_test.go b/server/vulnerabilities/msrc/io/fs_test.go new file mode 100644 index 0000000000..9d8c8660da --- /dev/null +++ b/server/vulnerabilities/msrc/io/fs_test.go @@ -0,0 +1,47 @@ +package io + +import ( + "fmt" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMSRCFSClient(t *testing.T) { + t.Run("#Bulletins", func(t *testing.T) { + t.Run("directory does not exists", func(t *testing.T) { + sut := NewMSRCFSClient("asdf") + _, err := sut.Bulletins() + require.Error(t, err) + }) + + t.Run("returns a list of file matching the MSRC file prefix", func(t *testing.T) { + path := t.TempDir() + sut := NewMSRCFSClient(path) + + file1 := filepath.Join(path, "my_lyrics.json") + bulletin1 := filepath.Join(path, fmt.Sprintf("%sWindows_10-2022_10_10.json", MSRCFilePrefix)) + bulletin2 := filepath.Join(path, fmt.Sprintf("%sWindows_11-2022_10_10.json", MSRCFilePrefix)) + + f1, err := os.Create(bulletin1) + require.NoError(t, err) + f1.Close() + + f2, err := os.Create(bulletin2) + require.NoError(t, err) + f2.Close() + + f3, err := os.Create(file1) + require.NoError(t, err) + f3.Close() + + r, err := sut.Bulletins() + require.NoError(t, err) + require.NotContains(t, r, NewSecurityBulletinName(filepath.Base(file1))) + require.Contains(t, r, NewSecurityBulletinName(filepath.Base(bulletin1))) + require.Contains(t, r, NewSecurityBulletinName(filepath.Base(bulletin2))) + }) + }) +} diff --git a/server/vulnerabilities/msrc/io/github.go b/server/vulnerabilities/msrc/io/github.go new file mode 100644 index 0000000000..e58437e87f --- /dev/null +++ b/server/vulnerabilities/msrc/io/github.go @@ -0,0 +1,70 @@ +package io + +import ( + "context" + "fmt" + "net/http" + "net/url" + "path/filepath" + "strings" + "time" + + "github.com/fleetdm/fleet/v4/pkg/download" + "github.com/google/go-github/v37/github" +) + +type MSRCGithubAPI interface { + Download(SecurityBulletinName, string) error + Bulletins() (map[SecurityBulletinName]string, error) +} + +type MSRCGithubClient struct { + client *http.Client + dstDir string +} + +func NewMSRCGithubClient(client *http.Client, dir string) MSRCGithubClient { + return MSRCGithubClient{client: client, dstDir: dir} +} + +// Downloads the security bulletin to 'dir'. +func (gh MSRCGithubClient) Download(b SecurityBulletinName, urlStr string) error { + u, err := url.Parse(urlStr) + if err != nil { + return err + } + path := filepath.Join(gh.dstDir, string(b)) + return download.DownloadAndExtract(gh.client, u, path) +} + +// Bulletins returns a map of 'name' => 'download URL' of the parsed security bulletins stored as assets on Github. +func (gh MSRCGithubClient) Bulletins() (map[SecurityBulletinName]string, error) { + ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) + defer cancel() + + releases, r, err := github.NewClient(gh.client).Repositories.ListReleases( + ctx, + "fleetdm", + "nvd", + &github.ListOptions{Page: 0, PerPage: 10}, + ) + if err != nil { + return nil, err + } + + if r.StatusCode != http.StatusOK { + return nil, fmt.Errorf("github http status error: %d", r.StatusCode) + } + + results := make(map[SecurityBulletinName]string) + + // TODO (juan): Since the nvd repo includes both NVD and MSRC assets, we will need to do some + // filtering logic here. To be done in https://github.com/fleetdm/fleet/issues/7394. + for _, e := range releases[0].Assets { + name := e.GetName() + if strings.HasPrefix(name, MSRCFilePrefix) { + results[NewSecurityBulletinName(name)] = e.GetBrowserDownloadURL() + } + } + return results, nil +} diff --git a/server/vulnerabilities/msrc/io/security_bulletin_name.go b/server/vulnerabilities/msrc/io/security_bulletin_name.go new file mode 100644 index 0000000000..d163b24907 --- /dev/null +++ b/server/vulnerabilities/msrc/io/security_bulletin_name.go @@ -0,0 +1,57 @@ +package io + +import ( + "errors" + "strings" + "time" +) + +const ( + MSRCFilePrefix = "fleet_msrc_" + fileExt = "json" + dateLayout = "2006_01_02" +) + +// Bulletins are published as assets to GH and copies are downloaded to the local FS. The file name +// of those assets contain some useful information like the 'product name' and the date the asset was modified. This type +// provides an abstration around the asset 'file name' to allow us to easy extract/compare the encoded info. +type SecurityBulletinName string + +func NewSecurityBulletinName(str string) SecurityBulletinName { + return SecurityBulletinName(str) +} + +func (sbn SecurityBulletinName) date() (time.Time, error) { + parts := strings.Split(string(sbn), "-") + + if len(parts) != 2 { + return time.Now(), errors.New("invalid security bulletin name") + } + timeRaw := strings.TrimSuffix(parts[1], "."+fileExt) + return time.Parse(dateLayout, timeRaw) +} + +func (sbn SecurityBulletinName) Before(other SecurityBulletinName) bool { + a, err := sbn.date() + if err != nil { + return false + } + + b, err := other.date() + if err != nil { + return false + } + + return a.Before(b) +} + +func (sbn SecurityBulletinName) ProductName() string { + pName := strings.TrimPrefix(string(sbn), MSRCFilePrefix) + parts := strings.Split(pName, "-") + + if len(parts) != 2 { + return "" + } + + return strings.Replace(parts[0], "_", " ", -1) +} diff --git a/server/vulnerabilities/msrc/io/security_bulletin_name_test.go b/server/vulnerabilities/msrc/io/security_bulletin_name_test.go new file mode 100644 index 0000000000..52c2b07b0f --- /dev/null +++ b/server/vulnerabilities/msrc/io/security_bulletin_name_test.go @@ -0,0 +1,34 @@ +package io + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestSecurityBulletinName(t *testing.T) { + t.Run("#date", func(t *testing.T) { + sut := NewSecurityBulletinName("Windows_10-2022_09_10.json") + result, err := sut.date() + require.NoError(t, err) + require.Equal(t, 2022, result.Year()) + require.Equal(t, time.Month(9), result.Month()) + require.Equal(t, 10, result.Day()) + }) + + t.Run("#ProductName", func(t *testing.T) { + a := NewSecurityBulletinName("Windows_10-2022_09_10.json") + require.Equal(t, "Windows 10", a.ProductName()) + }) + + t.Run("#Before", func(t *testing.T) { + a := NewSecurityBulletinName("Windows_10-2022_09_10.json") + b := NewSecurityBulletinName("Windows_10-2022_10_10.json") + c := NewSecurityBulletinName("Windows_10-2022_10_10.json") + require.True(t, a.Before(b)) + require.False(t, b.Before(a)) + require.False(t, b.Before(c)) + require.False(t, c.Before(b)) + }) +} diff --git a/server/vulnerabilities/msrc/parsed/product.go b/server/vulnerabilities/msrc/parsed/product.go new file mode 100644 index 0000000000..d9eaefda52 --- /dev/null +++ b/server/vulnerabilities/msrc/parsed/product.go @@ -0,0 +1,77 @@ +package parsed + +import "strings" + +// Product abstracts a MS full product name. +// A full product name includes the name of the product plus its arch +// (if any) and its version (if any). +type Product string + +func NewProduct(fullName string) Product { + return Product(fullName) +} + +// Arch returns the archicture for the current Microsoft product, if none can +// be found then "all" is returned. Returned values are meant to match the values returned from +// `SELECT arch FROM os_version` in OSQuery. +// eg: +// "Windows 10 Version 1803 for 32-bit Systems" => "32-bit" +func (p Product) Arch() string { + val := string(p) + switch { + case strings.Index(val, "32-bit") != -1: + return "32-bit" + case strings.Index(val, "x64") != -1: + return "64-bit" + case strings.Index(val, "ARM64") != -1: + return "arm64" + case strings.Index(val, "Itanium-Based") != -1: + return "itanium" + default: + return "all" + } +} + +// Name returns the name for the current Microsoft product, if none can +// be found then "" is returned. +// eg: +// "Windows 10 Version 1803 for 32-bit Systems" => "Windows 10" +// "Windows Server 2008 R2 for Itanium-Based Systems Service Pack 1" => "Windows Server 2008 R2" +func (p Product) Name() string { + val := string(p) + switch { + // Desktop versions + case strings.Index(val, "Windows 7") != -1: + return "Windows 7" + case strings.Index(val, "Windows 8.1") != -1: + return "Windows 8.1" + case strings.Index(val, "Windows RT 8.1") != -1: + return "Windows RT 8.1" + case strings.Index(val, "Windows 10") != -1: + return "Windows 10" + case strings.Index(val, "Windows 11") != -1: + return "Windows 11" + + // Server versions + case strings.Index(val, "Windows Server 2008 R2") != -1: + return "Windows Server 2008 R2" + case strings.Index(val, "Windows Server 2012 R2") != -1: + return "Windows Server 2012 R2" + + case strings.Index(val, "Windows Server 2008") != -1: + return "Windows Server 2008" + case strings.Index(val, "Windows Server 2012") != -1: + return "Windows Server 2012" + case strings.Index(val, "Windows Server 2016") != -1: + return "Windows Server 2016" + case strings.Index(val, "Windows Server 2019") != -1: + return "Windows Server 2019" + case strings.Index(val, "Windows Server 2022") != -1: + return "Windows Server 2022" + case strings.Index(val, "Windows Server,") != -1: + return "Windows Server" + + default: + return "" + } +} diff --git a/server/vulnerabilities/msrc/parsed/product_test.go b/server/vulnerabilities/msrc/parsed/product_test.go new file mode 100644 index 0000000000..854c512904 --- /dev/null +++ b/server/vulnerabilities/msrc/parsed/product_test.go @@ -0,0 +1,375 @@ +package parsed + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFullProductName(t *testing.T) { + testCases := []struct { + fullName string + arch string + prodName string + }{ + { + fullName: "Windows 10 Version 1809 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1809 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1809 for ARM64-based Systems", + arch: "arm64", + prodName: "Windows 10", + }, + { + fullName: "Windows Server 2019", + arch: "all", + prodName: "Windows Server 2019", + }, + { + fullName: "Windows Server 2019 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2019", + }, + { + fullName: "Windows 10 Version 1909 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1909 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1909 for ARM64-based Systems", + arch: "arm64", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 21H1 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 21H1 for ARM64-based Systems", + arch: "arm64", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 21H1 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows Server 2022", + arch: "all", + prodName: "Windows Server 2022", + }, + { + fullName: "Windows Server 2022 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2022", + }, + { + fullName: "Windows 10 Version 20H2 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 20H2 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 20H2 for ARM64-based Systems", + arch: "arm64", + prodName: "Windows 10", + }, + { + fullName: "Windows Server, version 20H2 (Server Core Installation)", + arch: "all", + prodName: "Windows Server", + }, + { + fullName: "Windows 11 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 11", + }, + { + fullName: "Windows 11 for ARM64-based Systems", + arch: "arm64", + prodName: "Windows 11", + }, + { + fullName: "Windows 10 Version 21H2 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 21H2 for ARM64-based Systems", + arch: "arm64", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 21H2 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1607 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1607 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows Server 2016", + arch: "all", + prodName: "Windows Server 2016", + }, + { + fullName: "Windows Server 2016 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2016", + }, + { + fullName: "Windows 8.1 for 32-bit systems", + arch: "32-bit", + prodName: "Windows 8.1", + }, + { + fullName: "Windows 8.1 for x64-based systems", + arch: "64-bit", + prodName: "Windows 8.1", + }, + { + fullName: "Windows RT 8.1", + arch: "all", + prodName: "Windows RT 8.1", + }, + { + fullName: "Windows Server 2012", + arch: "all", + prodName: "Windows Server 2012", + }, + { + fullName: "Windows Server 2012 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2012", + }, + { + fullName: "Windows Server 2012 R2", + arch: "all", + prodName: "Windows Server 2012 R2", + }, + { + fullName: "Windows Server 2012 R2 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2012 R2", + }, + { + fullName: "Windows 7 for 32-bit Systems Service Pack 1", + arch: "32-bit", + prodName: "Windows 7", + }, + { + fullName: "Windows 7 for x64-based Systems Service Pack 1", + arch: "64-bit", + prodName: "Windows 7", + }, + { + fullName: "Windows Server 2008 for 32-bit Systems Service Pack 2", + arch: "32-bit", + prodName: "Windows Server 2008", + }, + { + fullName: "Windows Server 2008 for 32-bit Systems Service Pack 2 (Server Core installation)", + arch: "32-bit", + prodName: "Windows Server 2008", + }, + { + fullName: "Windows Server 2008 for x64-based Systems Service Pack 2", + arch: "64-bit", + prodName: "Windows Server 2008", + }, + { + fullName: "Windows Server 2008 for x64-based Systems Service Pack 2 (Server Core installation)", + arch: "64-bit", + prodName: "Windows Server 2008", + }, + { + fullName: "Windows Server 2008 R2 for x64-based Systems Service Pack 1", + arch: "64-bit", + prodName: "Windows Server 2008 R2", + }, + { + fullName: "Windows Server 2008 R2 for x64-based Systems Service Pack 1 (Server Core installation)", + arch: "64-bit", + prodName: "Windows Server 2008 R2", + }, + { + fullName: "Windows 10 Version 1803 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows Server, version 1803 (Server Core Installation)", + arch: "all", + prodName: "Windows Server", + }, + { + fullName: "Windows 10 Version 1809 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows Server 2019", + arch: "all", + prodName: "Windows Server 2019", + }, + { + fullName: "Windows Server 2019 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2019", + }, + { + fullName: "Windows 10 Version 1709 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1903 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows Server, version 1903 (Server Core installation)", + arch: "all", + prodName: "Windows Server", + }, + { + fullName: "Windows 10 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1607 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows Server 2016", + arch: "all", + prodName: "Windows Server 2016", + }, + { + fullName: "Windows Server 2016 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2016", + }, + { + fullName: "Windows 8.1 for x64-based systems", + arch: "64-bit", + prodName: "Windows 8.1", + }, + { + fullName: "Windows Server 2012", + arch: "all", + prodName: "Windows Server 2012", + }, + { + fullName: "Windows Server 2012 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2012", + }, + { + fullName: "Windows Server 2012 R2", + arch: "all", + prodName: "Windows Server 2012 R2", + }, + { + fullName: "Windows Server 2012 R2 (Server Core installation)", + arch: "all", + prodName: "Windows Server 2012 R2", + }, + { + fullName: "Windows 10 Version 1909 for x64-based Systems", + arch: "64-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows Server, version 1909 (Server Core installation)", + arch: "all", + prodName: "Windows Server", + }, + { + fullName: "Windows 10 Version 1803 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1803 for ARM64-based Systems", + arch: "arm64", + prodName: "Windows 10", + }, + { + fullName: "Windows 10 Version 1809 for 32-bit Systems", + arch: "32-bit", + prodName: "Windows 10", + }, + { + fullName: "None Available", + arch: "all", + prodName: "", + }, + { + fullName: "Windows Server 2008 for 32-bit Systems Service Pack 2 (Server Core installation)", + arch: "32-bit", + prodName: "Windows Server 2008", + }, + { + fullName: "Windows Server 2008 for Itanium-Based Systems Service Pack 2", + arch: "itanium", + prodName: "Windows Server 2008", + }, + { + fullName: "Windows Server 2008 R2 for Itanium-Based Systems Service Pack 1", + arch: "itanium", + prodName: "Windows Server 2008 R2", + }, + } + + t.Run("#ArchFromProdName", func(t *testing.T) { + for _, tCase := range testCases { + sut := NewProduct(tCase.fullName) + require.Equal(t, tCase.arch, sut.Arch(), tCase) + } + }) + + t.Run("#NameFromFullProdName", func(t *testing.T) { + for _, tCase := range testCases { + sut := NewProduct(tCase.fullName) + require.Equal(t, tCase.prodName, sut.Name(), tCase) + } + }) +} diff --git a/server/vulnerabilities/msrc/parsed/security_bulletin.go b/server/vulnerabilities/msrc/parsed/security_bulletin.go new file mode 100644 index 0000000000..a004903d4b --- /dev/null +++ b/server/vulnerabilities/msrc/parsed/security_bulletin.go @@ -0,0 +1,55 @@ +package parsed + +type SecurityBulletin struct { + // The 'product' name this bulletin targets (e.g. Windows 10) + ProductName string + // All products contained in this bulletin (Product ID => Product full name). + // We can have many different 'products' under a single name, for example, for 'Windows 10': + // - Windows 10 Version 1809 for 32-bit Systems + // - Windows 10 Version 1909 for x64-based Systems + Products map[string]string + // All vulnerabilities contained in this bulletin, by CVE + Vulnerabities map[string]Vulnerability + // All vendor fixes for remediating the vulnerabilities contained in this bulletin, by KBID + VendorFixes map[int]VendorFix +} + +func NewSecurityBulletin(pName string) *SecurityBulletin { + return &SecurityBulletin{ + ProductName: pName, + Products: make(map[string]string), + Vulnerabities: make(map[string]Vulnerability), + VendorFixes: make(map[int]VendorFix), + } +} + +type Vulnerability struct { + PublishedEpoch *int64 + // Set of products that are susceptible to this vuln. + ProductIDs map[string]bool + // Set of Vendor fixes that remediate this vuln. + RemediatedBy map[int]bool +} + +func NewVulnerability(publishedDateEpoch *int64) Vulnerability { + return Vulnerability{ + PublishedEpoch: publishedDateEpoch, + ProductIDs: make(map[string]bool), + RemediatedBy: make(map[int]bool), + } +} + +type VendorFix struct { + // TODO (juan): Do we need this? + FixedBuild string + ProductIDs map[string]bool + // A Reference to what vendor fix this particular vendor fix 'replaces'. + Supersedes *int `json:",omitempty"` +} + +func NewVendorFix(fixedBuild string) VendorFix { + return VendorFix{ + FixedBuild: fixedBuild, + ProductIDs: make(map[string]bool), + } +} diff --git a/server/vulnerabilities/msrc/parser.go b/server/vulnerabilities/msrc/parser.go new file mode 100644 index 0000000000..7fac3d5550 --- /dev/null +++ b/server/vulnerabilities/msrc/parser.go @@ -0,0 +1,153 @@ +package msrc + +import ( + "encoding/xml" + "fmt" + "io" + "os" + "strconv" + + "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/parsed" + msrcxml "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/xml" +) + +func parseFeed(feedFilePath string) (map[string]*parsed.SecurityBulletin, error) { + r, err := os.Open(feedFilePath) + if err != nil { + return nil, fmt.Errorf("msrc parser: %w", err) + } + defer r.Close() + + feedResultXML, err := parseXML(r) + if err != nil { + return nil, fmt.Errorf("msrc parser: %w", err) + } + + bulletins, err := mapToSecurityBulletins(feedResultXML) + if err != nil { + return nil, fmt.Errorf("msrc parser: %w", err) + } + + return bulletins, nil +} + +func mapToSecurityBulletins(rXML *msrcxml.FeedResult) (map[string]*parsed.SecurityBulletin, error) { + // We will have one bulletin for each product. + bulletins := make(map[string]*parsed.SecurityBulletin) + pIDToPName := make(map[string]string, len(rXML.WinProducts)) + + for pID, p := range rXML.WinProducts { + name := parsed.NewProduct(p.FullName).Name() + if bulletins[name] == nil { + bulletins[name] = parsed.NewSecurityBulletin(name) + } + bulletins[name].Products[pID] = p.FullName + pIDToPName[pID] = name + } + + for _, v := range rXML.WinVulnerabities { + for _, rem := range v.Remediations { + // We will only be able to detect vulns for which they are vendor fixes. + if !rem.IsVendorFix() { + continue + } + + // We assume that rem.Description will contain the ID portion of a KBID, which should + // be always a numeric value. + remediatedKBID, err := strconv.Atoi(rem.Description) + if err != nil { + return nil, fmt.Errorf("invalid remediation KBID %q for %s", rem.Description, v.CVE) + } + + // rem.Supercedence should have the ID portion of a KBID which the current vendor fix replaces. + var supersedes *int + if rem.Supercedence != "" { + r, err := strconv.Atoi(rem.Supercedence) + if err != nil { + return nil, fmt.Errorf("invalid supercedence KBID %q for %s", rem.Supercedence, v.CVE) + } + supersedes = &r + } + + for _, pID := range rem.ProductIDs { + // Get the bulletin for the current product ID, skip further processing if is a + // non-windows product. + b, ok := bulletins[pIDToPName[pID]] + if !ok { + continue + } + + // Check if the vulnerability referenced by this remediation exists, if not + // initialize it. + var vuln parsed.Vulnerability + if vuln, ok = b.Vulnerabities[v.CVE]; !ok { + vuln = parsed.NewVulnerability(v.PublishedDateEpoch()) + } + vuln.ProductIDs[pID] = true + vuln.RemediatedBy[remediatedKBID] = true + + // Check if the vendor fix referenced by this remediation exists, if not + // initialize it. + var vFix parsed.VendorFix + if vFix, ok = b.VendorFixes[remediatedKBID]; !ok { + vFix = parsed.NewVendorFix(rem.FixedBuild) + } + vFix.Supersedes = supersedes + vFix.ProductIDs[pID] = true + + // Update the bulletin + b.Vulnerabities[v.CVE] = vuln + b.VendorFixes[remediatedKBID] = vFix + } + } + } + + return bulletins, nil +} + +func parseXML(reader io.Reader) (*msrcxml.FeedResult, error) { + r := &msrcxml.FeedResult{ + WinProducts: map[string]msrcxml.Product{}, + } + d := xml.NewDecoder(reader) + + for { + t, err := d.Token() + if err != nil { + if err == io.EOF { + return r, nil + } + return nil, fmt.Errorf("decoding token: %v", err) + } + + switch t := t.(type) { + case xml.StartElement: + if t.Name.Local == "Branch" { + branch := msrcxml.ProductBranch{} + if err = d.DecodeElement(&branch, &t); err != nil { + return nil, err + } + + for _, p := range branch.WinProducts() { + r.WinProducts[p.ProductID] = p + } + } + + if t.Name.Local == "Vulnerability" { + vuln := msrcxml.Vulnerability{} + if err = d.DecodeElement(&vuln, &t); err != nil { + return nil, err + } + + for pID := range r.WinProducts { + // We only care about vulnerabilities that have a vendor fix targeting a Windows + // product. + if vuln.IncludesVendorFix(pID) { + r.WinVulnerabities = append(r.WinVulnerabities, vuln) + break + } + } + } + } + } +} diff --git a/server/vulnerabilities/msrc/parser_test.go b/server/vulnerabilities/msrc/parser_test.go new file mode 100644 index 0000000000..1271ca3111 --- /dev/null +++ b/server/vulnerabilities/msrc/parser_test.go @@ -0,0 +1,1467 @@ +package msrc + +import ( + "compress/bzip2" + "io" + "os" + "path/filepath" + "testing" + + "github.com/fleetdm/fleet/v4/server/ptr" + msrc_parsed "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/parsed" + msrc_xml "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/xml" + "github.com/stretchr/testify/require" +) + +func extractXMLFixtureFile(t *testing.T, src, dst string) { + srcF, err := os.Open(src) + require.NoError(t, err) + defer srcF.Close() + dstF, err := os.Create(dst) + require.NoError(t, err) + defer dstF.Close() + r := bzip2.NewReader(srcF) + // ignoring "G110: Potential DoS vulnerability via decompression bomb", as this is test code. + _, err = io.Copy(dstF, r) //nolint:gosec + require.NoError(t, err) +} + +func TestParser(t *testing.T) { + xmlSrcPath := filepath.Join("..", "testdata", "msrc-2022-may.xml.bz2") + xmlDstPath := filepath.Join(t.TempDir(), "msrc-2022-may.xml") + + extractXMLFixtureFile(t, xmlSrcPath, xmlDstPath) + f, err := os.Open(xmlDstPath) + require.NoError(t, err) + + // Parse XML + xmlResult, err := parseXML(f) + f.Close() + require.NoError(t, err) + + // All the products we expect to see, grouped by their product name + expectedProducts := map[string]map[string]string{ + "Windows 10": { + "11568": "Windows 10 Version 1809 for 32-bit Systems", + "11569": "Windows 10 Version 1809 for x64-based Systems", + "11570": "Windows 10 Version 1809 for ARM64-based Systems", + "11712": "Windows 10 Version 1909 for 32-bit Systems", + "11713": "Windows 10 Version 1909 for x64-based Systems", + "11714": "Windows 10 Version 1909 for ARM64-based Systems", + "11896": "Windows 10 Version 21H1 for x64-based Systems", + "11897": "Windows 10 Version 21H1 for ARM64-based Systems", + "11898": "Windows 10 Version 21H1 for 32-bit Systems", + "11800": "Windows 10 Version 20H2 for x64-based Systems", + "11801": "Windows 10 Version 20H2 for 32-bit Systems", + "11802": "Windows 10 Version 20H2 for ARM64-based Systems", + "11929": "Windows 10 Version 21H2 for 32-bit Systems", + "11930": "Windows 10 Version 21H2 for ARM64-based Systems", + "11931": "Windows 10 Version 21H2 for x64-based Systems", + "10729": "Windows 10 for 32-bit Systems", + "10735": "Windows 10 for x64-based Systems", + "10852": "Windows 10 Version 1607 for 32-bit Systems", + "10853": "Windows 10 Version 1607 for x64-based Systems", + }, + "Windows Server 2019": { + "11571": "Windows Server 2019", + "11572": "Windows Server 2019 (Server Core installation)", + }, + "Windows Server 2022": { + "11923": "Windows Server 2022", + "11924": "Windows Server 2022 (Server Core installation)", + }, + "Windows Server": { + "11803": "Windows Server, version 20H2 (Server Core Installation)", + }, + "Windows 11": { + "11926": "Windows 11 for x64-based Systems", + "11927": "Windows 11 for ARM64-based Systems", + }, + "Windows Server 2016": { + "10816": "Windows Server 2016", + "10855": "Windows Server 2016 (Server Core installation)", + }, + "Windows 8.1": { + "10481": "Windows 8.1 for 32-bit systems", + "10482": "Windows 8.1 for x64-based systems", + }, + "Windows RT 8.1": { + "10484": "Windows RT 8.1", + }, + "Windows Server 2012": { + "10378": "Windows Server 2012", + "10379": "Windows Server 2012 (Server Core installation)", + }, + "Windows Server 2012 R2": { + "10483": "Windows Server 2012 R2", + "10543": "Windows Server 2012 R2 (Server Core installation)", + }, + "Windows 7": { + "10047": "Windows 7 for 32-bit Systems Service Pack 1", + "10048": "Windows 7 for x64-based Systems Service Pack 1", + }, + "Windows Server 2008": { + "9312": "Windows Server 2008 for 32-bit Systems Service Pack 2", + "10287": "Windows Server 2008 for 32-bit Systems Service Pack 2 (Server Core installation)", + "9318": "Windows Server 2008 for x64-based Systems Service Pack 2", + "9344": "Windows Server 2008 for x64-based Systems Service Pack 2 (Server Core installation)", + }, + "Windows Server 2008 R2": { + "10051": "Windows Server 2008 R2 for x64-based Systems Service Pack 1", + "10049": "Windows Server 2008 R2 for x64-based Systems Service Pack 1 (Server Core installation)", + }, + } + + expectedCVEs := map[string][]string{ + "Windows 10": { + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-23279", + "CVE-2022-29142", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-29140", + "CVE-2022-21972", + "CVE-2022-22713", + "CVE-2022-23270", + "CVE-2022-24466", + "CVE-2022-26913", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26927", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-22016", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29105", + "CVE-2022-29112", + "CVE-2022-29113", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29131", + "CVE-2022-29132", + "CVE-2022-29137", + "CVE-2022-29139", + }, + "Windows Server 2019": { + "CVE-2022-26927", + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-29142", + "CVE-2022-29150", + "CVE-2022-29151", + "CVE-2022-29122", + "CVE-2022-29120", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-29140", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-24466", + "CVE-2022-26913", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26932", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-26937", + "CVE-2022-26938", + "CVE-2022-26939", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-22016", + "CVE-2022-29102", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29105", + "CVE-2022-29106", + "CVE-2022-29112", + "CVE-2022-29113", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29131", + "CVE-2022-29132", + "CVE-2022-29134", + "CVE-2022-29135", + "CVE-2022-29137", + "CVE-2022-29138", + "CVE-2022-29123", + "CVE-2022-29139", + }, + "Windows Server 2022": { + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-23279", + "CVE-2022-29142", + "CVE-2022-29150", + "CVE-2022-29151", + "CVE-2022-29122", + "CVE-2022-29120", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-29140", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-24466", + "CVE-2022-26913", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26927", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26932", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-26937", + "CVE-2022-26938", + "CVE-2022-26939", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-22016", + "CVE-2022-29102", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29106", + "CVE-2022-29112", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29131", + "CVE-2022-29132", + "CVE-2022-29134", + "CVE-2022-29135", + "CVE-2022-29137", + "CVE-2022-29138", + "CVE-2022-29123", + "CVE-2022-22017", + "CVE-2022-26940", + "CVE-2022-29139", + }, + "Windows Server": { + "CVE-2022-24466", + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-23279", + "CVE-2022-29142", + "CVE-2022-29150", + "CVE-2022-29151", + "CVE-2022-29122", + "CVE-2022-29120", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-29140", + "CVE-2022-21972", + "CVE-2022-22713", + "CVE-2022-23270", + "CVE-2022-26913", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26927", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26932", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-26937", + "CVE-2022-26938", + "CVE-2022-26939", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-22016", + "CVE-2022-29102", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29105", + "CVE-2022-29106", + "CVE-2022-29112", + "CVE-2022-29113", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29131", + "CVE-2022-29132", + "CVE-2022-29134", + "CVE-2022-29135", + "CVE-2022-29137", + "CVE-2022-29138", + "CVE-2022-29123", + "CVE-2022-29139", + }, + "Windows 11": { + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-23279", + "CVE-2022-29116", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-29140", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-24466", + "CVE-2022-26913", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26927", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-22016", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29112", + "CVE-2022-29113", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29131", + "CVE-2022-29132", + "CVE-2022-29133", + "CVE-2022-29137", + "CVE-2022-22017", + "CVE-2022-26940", + "CVE-2022-29139", + }, + "Windows Server 2016": { + "CVE-2022-29137", + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-29150", + "CVE-2022-29151", + "CVE-2022-29122", + "CVE-2022-29120", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-24466", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26932", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-26937", + "CVE-2022-26938", + "CVE-2022-26939", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-22016", + "CVE-2022-29102", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29105", + "CVE-2022-29106", + "CVE-2022-29112", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29132", + "CVE-2022-29134", + "CVE-2022-29135", + "CVE-2022-29138", + "CVE-2022-29123", + "CVE-2022-29139", + "CVE-2022-29140", + }, + "Windows 8.1": { + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29105", + "CVE-2022-29112", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29132", + "CVE-2022-29137", + "CVE-2022-29139", + }, + "Windows RT 8.1": { + "CVE-2022-26934", + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26933", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29105", + "CVE-2022-29112", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29132", + "CVE-2022-29137", + "CVE-2022-29139", + }, + "Windows Server 2012": { + "CVE-2022-26936", + "CVE-2022-30190", + "CVE-2022-29150", + "CVE-2022-29151", + "CVE-2022-29122", + "CVE-2022-29120", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26937", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-29102", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29105", + "CVE-2022-29112", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29132", + "CVE-2022-29135", + "CVE-2022-29137", + "CVE-2022-29138", + "CVE-2022-29123", + "CVE-2022-29139", + }, + "Windows Server 2012 R2": { + "CVE-2022-30190", + "CVE-2022-26923", + "CVE-2022-29150", + "CVE-2022-29151", + "CVE-2022-29122", + "CVE-2022-29120", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26930", + "CVE-2022-26931", + "CVE-2022-26933", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26937", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-29102", + "CVE-2022-29103", + "CVE-2022-29104", + "CVE-2022-29105", + "CVE-2022-29112", + "CVE-2022-29114", + "CVE-2022-29115", + "CVE-2022-29125", + "CVE-2022-29126", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29132", + "CVE-2022-29134", + "CVE-2022-29135", + "CVE-2022-29137", + "CVE-2022-29138", + "CVE-2022-29123", + "CVE-2022-29139", + "CVE-2022-26936", + }, + "Windows 7": { + "CVE-2022-29105", + "CVE-2022-30190", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26931", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-29103", + "CVE-2022-29112", + "CVE-2022-29115", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29132", + "CVE-2022-29137", + "CVE-2022-29139", + }, + "Windows Server 2008": { + "CVE-2022-29115", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26931", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-26937", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-29103", + "CVE-2022-29112", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29132", + "CVE-2022-29137", + "CVE-2022-29139", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + }, + "Windows Server 2008 R2": { + "CVE-2022-30190", + "CVE-2022-21972", + "CVE-2022-23270", + "CVE-2022-26925", + "CVE-2022-26926", + "CVE-2022-26931", + "CVE-2022-26934", + "CVE-2022-26935", + "CVE-2022-26936", + "CVE-2022-26937", + "CVE-2022-22011", + "CVE-2022-22012", + "CVE-2022-22013", + "CVE-2022-22014", + "CVE-2022-22015", + "CVE-2022-29103", + "CVE-2022-29112", + "CVE-2022-29115", + "CVE-2022-29127", + "CVE-2022-29128", + "CVE-2022-29129", + "CVE-2022-29130", + "CVE-2022-29132", + "CVE-2022-29137", + "CVE-2022-29139", + "CVE-2022-29141", + "CVE-2022-22019", + "CVE-2022-29121", + "CVE-2022-30138", + "CVE-2022-29105", + }, + } + + // A random vulnerability ("CVE-2022-29137") + expectedVulns := map[string]map[string]msrc_parsed.Vulnerability{ + "Windows 10": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "11568": true, + "11569": true, + "11570": true, + "11712": true, + "11713": true, + "11714": true, + "11896": true, + "11897": true, + "11898": true, + "11800": true, + "11801": true, + "11802": true, + "11929": true, + "11930": true, + "11931": true, + "10729": true, + "10735": true, + "10852": true, + "10853": true, + }, + RemediatedBy: map[int]bool{ + 5013941: true, + 5013952: true, + 5013942: true, + 5013963: true, + 5013945: true, + }, + }, + }, + "Windows Server 2019": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "11571": true, + "11572": true, + }, + RemediatedBy: map[int]bool{ + 5013941: true, + }, + }, + }, + + "Windows Server 2022": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "11923": true, + "11924": true, + }, + RemediatedBy: map[int]bool{ + 5013944: true, + }, + }, + }, + + "Windows Server": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "11803": true, + }, + RemediatedBy: map[int]bool{ + 5013942: true, + }, + }, + }, + + "Windows Server 2008": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "9312": true, + "10287": true, + "9318": true, + "9344": true, + }, + RemediatedBy: map[int]bool{ + 5014010: true, + 5014006: true, + }, + }, + }, + + "Windows Server 2008 R2": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "10051": true, + "10049": true, + }, + RemediatedBy: map[int]bool{ + 5014012: true, + 5013999: true, + }, + }, + }, + + "Windows Server 2012": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "10378": true, + "10379": true, + }, + RemediatedBy: map[int]bool{ + 5014017: true, + 5014018: true, + }, + }, + }, + + "Windows Server 2012 R2": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "10483": true, + "10543": true, + }, + RemediatedBy: map[int]bool{ + 5014011: true, + 5014001: true, + }, + }, + }, + + "Windows 7": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "10047": true, + "10048": true, + }, + RemediatedBy: map[int]bool{ + 5014012: true, + 5013999: true, + }, + }, + }, + + "Windows Server 2016": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "10816": true, + "10855": true, + }, + RemediatedBy: map[int]bool{ + 5013952: true, + }, + }, + }, + + "Windows 11": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "11926": true, + "11927": true, + }, + RemediatedBy: map[int]bool{ + 5013943: true, + }, + }, + }, + + "Windows RT 8.1": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "10484": true, + }, + RemediatedBy: map[int]bool{ + 5014025: true, + }, + }, + }, + + "Windows 8.1": { + "CVE-2022-29137": { + PublishedEpoch: ptr.Int64(1652169600), + ProductIDs: map[string]bool{ + "10481": true, + "10482": true, + }, + RemediatedBy: map[int]bool{ + 5014011: true, + 5014001: true, + }, + }, + }, + } + + // A random vulnerability ("CVE-2022-29137") + expectedVendorFixes := map[string]map[int]msrc_parsed.VendorFix{ + "Windows 10": { + 5013941: { + FixedBuild: "10.0.17763.2928", + ProductIDs: map[string]bool{ + "11568": true, + "11569": true, + "11570": true, + }, + Supersedes: ptr.Int(5012647), + }, + 5013952: { + FixedBuild: "10.0.14393.5125", + ProductIDs: map[string]bool{ + "10852": true, + "10853": true, + }, + Supersedes: ptr.Int(5012596), + }, + 5013942: { + FixedBuild: "10.0.19043.1706", + ProductIDs: map[string]bool{ + "11896": true, + "11897": true, + "11898": true, + "11929": true, + "11800": true, + "11801": true, + "11802": true, + "11930": true, + "11931": true, + }, + Supersedes: ptr.Int(5012599), + }, + 5013963: { + FixedBuild: "10.0.10240.19297", + ProductIDs: map[string]bool{ + "10729": true, + "10735": true, + }, + Supersedes: ptr.Int(5012653), + }, + + 5013945: { + FixedBuild: "10.0.18363.2274", + ProductIDs: map[string]bool{ + "11712": true, + "11713": true, + "11714": true, + }, + Supersedes: ptr.Int(5012591), + }, + }, + "Windows Server 2019": { + 5013941: { + FixedBuild: "10.0.17763.2928", + ProductIDs: map[string]bool{ + "11571": true, + "11572": true, + }, + Supersedes: ptr.Int(5012647), + }, + }, + + "Windows Server 2022": { + 5013944: { + FixedBuild: "10.0.20348.707", + ProductIDs: map[string]bool{ + "11923": true, + "11924": true, + }, + Supersedes: ptr.Int(5012604), + }, + }, + + "Windows Server": { + 5013942: { + FixedBuild: "10.0.19042.1706", + ProductIDs: map[string]bool{ + "11803": true, + }, + Supersedes: ptr.Int(5012599), + }, + }, + + "Windows Server 2008": { + 5014010: { + ProductIDs: map[string]bool{ + "9312": true, + "10287": true, + "9318": true, + "9344": true, + }, + FixedBuild: "6.0.6003.21481", + Supersedes: ptr.Int(5012658), + }, + 5014006: { + ProductIDs: map[string]bool{ + "9312": true, + "10287": true, + "9318": true, + "9344": true, + }, + FixedBuild: "6.0.6003.21481", + }, + }, + + "Windows Server 2008 R2": { + 5014012: { + ProductIDs: map[string]bool{ + "10051": true, + "10049": true, + }, + Supersedes: ptr.Int(5012626), + FixedBuild: "6.1.7601.25954", + }, + 5013999: { + ProductIDs: map[string]bool{ + "10051": true, + "10049": true, + }, + FixedBuild: "6.1.7601.25954", + }, + }, + + "Windows Server 2012": { + 5014017: { + ProductIDs: map[string]bool{ + "10378": true, + "10379": true, + }, + Supersedes: ptr.Int(5012650), + FixedBuild: "6.2.9200.23714", + }, + 5014018: { + ProductIDs: map[string]bool{ + "10378": true, + "10379": true, + }, + FixedBuild: "6.2.9200.23714", + }, + }, + + "Windows Server 2012 R2": { + 5014011: { + ProductIDs: map[string]bool{ + "10483": true, + "10543": true, + }, + FixedBuild: "6.3.9600.20371", + Supersedes: ptr.Int(5012670), + }, + 5014001: { + ProductIDs: map[string]bool{ + "10483": true, + "10543": true, + }, + FixedBuild: "6.3.9600.20365", + }, + }, + + "Windows 7": { + 5014012: { + ProductIDs: map[string]bool{ + "10047": true, + "10048": true, + }, + Supersedes: ptr.Int(5012626), + FixedBuild: "6.1.7601.25954", + }, + 5013999: { + ProductIDs: map[string]bool{ + "10047": true, + "10048": true, + }, + FixedBuild: "6.1.7601.25954", + }, + }, + + "Windows Server 2016": { + 5013952: { + ProductIDs: map[string]bool{ + "10816": true, + "10855": true, + }, + FixedBuild: "10.0.14393.5125", + }, + }, + + "Windows 11": { + 5013943: { + ProductIDs: map[string]bool{ + "11926": true, + "11927": true, + }, + FixedBuild: "10.0.22000.675", + Supersedes: ptr.Int(5012592), + }, + }, + + "Windows RT 8.1": { + 5014025: { + ProductIDs: map[string]bool{ + "10484": true, + }, + FixedBuild: "6.3.9600.20367", + }, + }, + + "Windows 8.1": { + 5014011: { + ProductIDs: map[string]bool{ + "10481": true, + "10482": true, + }, + FixedBuild: "6.3.9600.20371", + Supersedes: ptr.Int(5012670), + }, + 5014001: { + ProductIDs: map[string]bool{ + "10481": true, + "10482": true, + }, + FixedBuild: "6.3.9600.20365", + }, + }, + } + + t.Run("parseFeed", func(t *testing.T) { + t.Run("errors out if file does not exists", func(t *testing.T) { + _, err := parseFeed("asdcv") + require.Error(t, err) + }) + }) + + t.Run("mapToSecurityBulletins", func(t *testing.T) { + bulletins, err := mapToSecurityBulletins(xmlResult) + require.NoError(t, err) + + t.Run("should map the vendor fixes entries correctly", func(t *testing.T) { + for pName, vF := range expectedVendorFixes { + bulletin := bulletins[pName] + + for KBID, fix := range vF { + sut := bulletin.VendorFixes[KBID] + require.Equal(t, fix.FixedBuild, sut.FixedBuild, pName, KBID) + require.Equal(t, fix.ProductIDs, sut.ProductIDs, pName, KBID) + // We want to check that either both are nil or that both are not nil + require.False(t, (fix.Supersedes == nil || sut.Supersedes == nil) && !(fix.Supersedes == nil || sut.Supersedes == nil), pName, KBID) + if fix.Supersedes != nil { + require.Equal(t, *fix.Supersedes, *sut.Supersedes, pName, KBID) + } + } + } + }) + + t.Run("should map the vulnerability entries correctly", func(t *testing.T) { + for pName, v := range expectedVulns { + bulletin := bulletins[pName] + + for cve, vuln := range v { + sut := bulletin.Vulnerabities[cve] + require.Equal(t, *vuln.PublishedEpoch, *sut.PublishedEpoch, pName) + require.Equal(t, vuln.RemediatedBy, sut.RemediatedBy, pName) + require.Equal(t, vuln.ProductIDs, sut.ProductIDs, pName) + } + } + }) + + t.Run("should have one bulletin per product", func(t *testing.T) { + var expected []string + for p := range expectedProducts { + expected = append(expected, p) + } + + var actual []string + for _, g := range bulletins { + actual = append(actual, g.ProductName) + } + + require.Len(t, bulletins, len(expected)) + require.ElementsMatch(t, expected, actual) + }) + + t.Run("each bulletin should have the right products", func(t *testing.T) { + for _, g := range bulletins { + require.Equal(t, g.Products, expectedProducts[g.ProductName], g.ProductName) + } + }) + + t.Run("each bulletin should have the right vulnerabilities", func(t *testing.T) { + for _, g := range bulletins { + var actual []string + for v := range g.Vulnerabities { + actual = append(actual, v) + } + require.ElementsMatch(t, actual, expectedCVEs[g.ProductName], g.ProductName) + } + }) + }) + + t.Run("parseXML", func(t *testing.T) { + t.Run("only windows products are included", func(t *testing.T) { + var expected []msrc_xml.Product + for _, grp := range expectedProducts { + for pID, pFn := range grp { + expected = append( + expected, + msrc_xml.Product{ProductID: pID, FullName: pFn}, + ) + } + } + + var actual []msrc_xml.Product + for _, v := range xmlResult.WinProducts { + actual = append(actual, v) + } + require.ElementsMatch(t, actual, expected) + }) + + t.Run("only CVEs for windows products are included", func(t *testing.T) { + expected := make(map[string]bool) + for _, p := range expectedCVEs { + for _, v := range p { + expected[v] = true + } + } + actual := make(map[string]bool) + for _, v := range xmlResult.WinVulnerabities { + actual[v.CVE] = true + } + require.Equal(t, expected, actual) + }) + + t.Run("scores are parsed correctly", func(t *testing.T) { + // Check the score of a random CVE (CVE-2022-24466) + for _, v := range xmlResult.WinVulnerabities { + if v.CVE == "CVE-2022-24466" { + require.Equal(t, 4.1, v.Score) + } + } + }) + + t.Run("the revision history is parsed correctly", func(t *testing.T) { + // Check the revision history of a random CVE (CVE-2022-29114) + for _, v := range xmlResult.WinVulnerabities { + if v.CVE == "CVE-2022-29114" { + require.Len(t, v.Revisions, 1) + require.Equal(t, "2022-05-10T08:00:00", v.Revisions[0].Date) + require.Equal(t, "

Information published.

\n", v.Revisions[0].Description) + } + } + }) + + t.Run("the remediations are parsed correctly", func(t *testing.T) { + // Check the remediations of a random CVE (CVE-2022-29126) + expectedRemediations := []msrc_xml.VulnerabilityRemediation{ + { + Type: "Vendor Fix", + FixedBuild: "10.0.17763.2928", + ProductIDs: []string{"11568", "11569", "11570", "11571", "11572"}, + Description: "5013941", + Supercedence: "5012647", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013941", + }, + { + Type: "Known Issue", + ProductIDs: []string{"11568", "11569", "11570", "11571", "11572"}, + Description: "5013941", + URL: "https://support.microsoft.com/help/5013941", + }, + { + Type: "Vendor Fix", + FixedBuild: "10.0.18363.2274", + ProductIDs: []string{"11712", "11713", "11714"}, + Description: "5013945", + Supercedence: "5012591", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013945", + }, + { + Type: "Vendor Fix", + FixedBuild: "10.0.19043.1706", + ProductIDs: []string{"11896", "11897", "11898", "11929"}, + Description: "5013942", + Supercedence: "5012599", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013942", + }, + { + Type: "Known Issue", + ProductIDs: []string{"11896", "11897", "11898", "11800", "11801", "11802", "11803", "11929", "11930", "11931"}, + Description: "5013942", + URL: "https://support.microsoft.com/help/5013942", + }, + { + Type: "Vendor Fix", + FixedBuild: "10.0.20348.707", + ProductIDs: []string{"11923", "11924"}, + Description: "5013944", + Supercedence: "5012604", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013944", + }, + { + Type: "Known Issue", + ProductIDs: []string{"11923", "11924"}, + Description: "5013944", + URL: "https://support.microsoft.com/help/5013944", + }, + { + Type: "Vendor Fix", + FixedBuild: "10.0.19042.1706", + ProductIDs: []string{"11800", "11801", "11802", "11803"}, + Description: "5013942", + Supercedence: "5012599", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013942", + }, + { + Type: "Known Issue", + ProductIDs: []string{"11896", "11897", "11898", "11800", "11801", "11802", "11803", "11929", "11930", "11931"}, + Description: "5013942", + URL: "https://support.microsoft.com/help/5013942", + }, + { + Type: "Vendor Fix", + FixedBuild: "10.0.22000.675", + ProductIDs: []string{"11926", "11927"}, + Description: "5013943", + Supercedence: "5012592", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013943", + }, + { + Type: "Known Issue", + ProductIDs: []string{"11926", "11927"}, + Description: "5013943", + URL: "https://support.microsoft.com/help/5013943", + }, + { + Type: "Vendor Fix", + FixedBuild: "10.0.19044.1706", + ProductIDs: []string{"11930", "11931"}, + Description: "5013942", + Supercedence: "5012599", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013942", + }, + { + Type: "Known Issue", + ProductIDs: []string{"11896", "11897", "11898", "11800", "11801", "11802", "11803", "11929", "11930", "11931"}, + Description: "5013942", + URL: "https://support.microsoft.com/help/5013942", + }, + { + Type: "Vendor Fix", + FixedBuild: "10.0.10240.19297", + ProductIDs: []string{"10729", "10735"}, + Description: "5013963", + Supercedence: "5012653", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013963", + }, + { + Type: "Vendor Fix", + FixedBuild: "10.0.14393.5125", + ProductIDs: []string{"10852", "10853", "10816", "10855"}, + Description: "5013952", + Supercedence: "5012596", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013952", + }, + { + Type: "Known Issue", + ProductIDs: []string{"10852", "10853", "10816", "10855"}, + Description: "5013952", + URL: "https://support.microsoft.com/help/5013952", + }, + { + Type: "Vendor Fix", + FixedBuild: "6.3.9600.20371", + ProductIDs: []string{"10481", "10482", "10483", "10543"}, + Description: "5014011", + Supercedence: "5012670", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5014011", + }, + { + Type: "Known Issue", + ProductIDs: []string{"10481", "10482", "10483", "10543"}, + Description: "5014011", + URL: "https://support.microsoft.com/help/5014011", + }, + { + Type: "Vendor Fix", + FixedBuild: "6.3.9600.20365", + ProductIDs: []string{"10481", "10482", "10483", "10543"}, + Description: "5014001", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5014001", + }, + { + Type: "Known Issue", + ProductIDs: []string{"10481", "10482", "10483", "10543"}, + Description: "5014001", + URL: "https://support.microsoft.com/help/5014001", + }, + { + Type: "Vendor Fix", + FixedBuild: "6.3.9600.20367", + ProductIDs: []string{"10484"}, + Description: "5014025", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5014025", + }, + { + Type: "Vendor Fix", + FixedBuild: "6.2.9200.23714", + ProductIDs: []string{"10378", "10379"}, + Description: "5014017", + Supercedence: "5012650", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5014017", + }, + { + Type: "Known Issue", + ProductIDs: []string{"10378", "10379"}, + Description: "5014017", + URL: "https://support.microsoft.com/help/5014017", + }, + { + Type: "Vendor Fix", + FixedBuild: "6.2.9200.23714", + ProductIDs: []string{"10378", "10379"}, + Description: "5014018", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5014018", + }, + { + Type: "Known Issue", + ProductIDs: []string{"10378", "10379"}, + Description: "5014018", + URL: "https://support.microsoft.com/help/5014018", + }, + } + for _, v := range xmlResult.WinVulnerabities { + if v.CVE == "CVE-2022-29126" { + require.Len(t, v.Remediations, len(expectedRemediations)) + require.ElementsMatch(t, v.Remediations, expectedRemediations) + } + } + }) + }) +} diff --git a/server/vulnerabilities/msrc/sync.go b/server/vulnerabilities/msrc/sync.go new file mode 100644 index 0000000000..dedff85a4f --- /dev/null +++ b/server/vulnerabilities/msrc/sync.go @@ -0,0 +1,106 @@ +package msrc + +import ( + "fmt" + "net/http" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/io" + "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/parsed" +) + +// bulletinsDelta returns what bulletins should be download from GH and what bulletins should be removed +// from the local file system based what OS are installed, what local bulletins we have and what +// remote bulletins exist. +func bulletinsDelta( + os []fleet.OperatingSystem, + local []io.SecurityBulletinName, + remote []io.SecurityBulletinName, +) ( + []io.SecurityBulletinName, + []io.SecurityBulletinName, +) { + if len(os) == 0 { + return remote, nil + } + + var matching []io.SecurityBulletinName + for _, r := range remote { + for _, o := range os { + product := parsed.NewProduct(o.Name) + if r.ProductName() == product.Name() { + matching = append(matching, r) + } + } + } + + var toDownload []io.SecurityBulletinName + var toDelete []io.SecurityBulletinName + for _, m := range matching { + var found bool + for _, l := range local { + if m.ProductName() == l.ProductName() { + found = true + // out of date + if l.Before(m) { + toDownload = append(toDownload, m) + toDelete = append(toDelete, l) + } + break + } + } + if !found { + toDownload = append(toDownload, m) + } + } + return toDownload, toDelete +} + +// Sync syncs the local msrc security bulletins (contained in dstDir) for one or more operating systems with the security +// bulletin published in Github. +// If 'os' is nil, then all security bulletins will be synched. +func Sync(client *http.Client, dstDir string, os []fleet.OperatingSystem) error { + gh := io.NewMSRCGithubClient(client, dstDir) + fs := io.NewMSRCFSClient(dstDir) + + if err := sync(os, fs, gh); err != nil { + return fmt.Errorf("msrc sync: %w", err) + } + + return nil +} + +func sync( + os []fleet.OperatingSystem, + fsClient io.MSRCFSAPI, + ghClient io.MSRCGithubAPI, +) error { + remoteURLs, err := ghClient.Bulletins() + if err != nil { + return err + } + + var remote []io.SecurityBulletinName + for r := range remoteURLs { + remote = append(remote, r) + } + + local, err := fsClient.Bulletins() + if err != nil { + return err + } + + toDownload, toDelete := bulletinsDelta(os, local, remote) + for _, b := range toDownload { + if err := ghClient.Download(b, remoteURLs[b]); err != nil { + return err + } + } + for _, d := range toDelete { + if err := fsClient.Delete(d); err != nil { + return err + } + } + + return nil +} diff --git a/server/vulnerabilities/msrc/sync_test.go b/server/vulnerabilities/msrc/sync_test.go new file mode 100644 index 0000000000..6bf60244d5 --- /dev/null +++ b/server/vulnerabilities/msrc/sync_test.go @@ -0,0 +1,197 @@ +package msrc + +import ( + "testing" + + "github.com/fleetdm/fleet/v4/server/fleet" + "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/io" + "github.com/stretchr/testify/require" +) + +type testData struct { + remoteList map[io.SecurityBulletinName]string + remoteListError error + remoteDownloaded []string + remoteDownloadError error + localList []io.SecurityBulletinName + localListError error + localDeleted []io.SecurityBulletinName + localDeleteError error +} + +type ghMock struct{ testData *testData } + +func (gh ghMock) Bulletins() (map[io.SecurityBulletinName]string, error) { + return gh.testData.remoteList, gh.testData.remoteListError +} + +func (gh ghMock) Download(b io.SecurityBulletinName, url string) error { + gh.testData.remoteDownloaded = append(gh.testData.remoteDownloaded, url) + return gh.testData.remoteDownloadError +} + +type fsMock struct{ testData *testData } + +func (fs fsMock) Bulletins() ([]io.SecurityBulletinName, error) { + return fs.testData.localList, fs.testData.localListError +} + +func (fs fsMock) Delete(d io.SecurityBulletinName) error { + fs.testData.localDeleted = append(fs.testData.localDeleted, d) + return fs.testData.localDeleteError +} + +func TestSync(t *testing.T) { + t.Run("#sync", func(t *testing.T) { + os := []fleet.OperatingSystem{ + { + Name: "Microsoft Windows 11 Enterprise", + Version: "21H2", + Arch: "64-bit", + KernelVersion: "10.0.22000.795", + }, + { + Name: "Microsoft Windows 10 Pro", + Version: "10.0.19044", + Arch: "64-bit", + KernelVersion: "10.0.19044", + }, + } + + testData := testData{ + remoteList: map[io.SecurityBulletinName]string{ + io.NewSecurityBulletinName("Windows_10-2022_10_10.json"): "http://somebulletin.com", + }, + localList: []io.SecurityBulletinName{"Windows_10-2022_09_10.json"}, + } + + err := sync(os, fsMock{testData: &testData}, ghMock{testData: &testData}) + require.NoError(t, err) + require.ElementsMatch(t, testData.remoteDownloaded, []string{"http://somebulletin.com"}) + require.ElementsMatch(t, testData.localDeleted, []io.SecurityBulletinName{"Windows_10-2022_09_10.json"}) + }) + + t.Run("#bulletinsDelta", func(t *testing.T) { + t.Run("win OS provided", func(t *testing.T) { + os := []fleet.OperatingSystem{ + { + Name: "Microsoft Windows 11 Enterprise", + Version: "21H2", + Arch: "64-bit", + KernelVersion: "10.0.22000.795", + }, + { + Name: "Microsoft Windows 10 Pro", + Version: "10.0.19044", + Arch: "64-bit", + KernelVersion: "10.0.19044", + }, + } + t.Run("without remote bulletins", func(t *testing.T) { + var remote []io.SecurityBulletinName + local := []io.SecurityBulletinName{ + "Windows_10-2022_10_10.json", + } + toDownload, toDelete := bulletinsDelta(os, local, remote) + require.Empty(t, toDownload) + require.Empty(t, toDelete) + }) + + t.Run("with remote bulletins", func(t *testing.T) { + remote := []io.SecurityBulletinName{ + "Windows_10-2022_10_10.json", + "Windows_11-2022_10_10.json", + "Windows_Server_2016-2022_10_10.json", + "Windows_8.1-2022_10_10.json", + } + t.Run("no local bulletins", func(t *testing.T) { + var local []io.SecurityBulletinName + toDownload, toDelete := bulletinsDelta(os, local, remote) + + require.ElementsMatch(t, toDownload, []io.SecurityBulletinName{ + "Windows_10-2022_10_10.json", + "Windows_11-2022_10_10.json", + }) + require.Empty(t, toDelete) + }) + + t.Run("missing some local bulletin", func(t *testing.T) { + local := []io.SecurityBulletinName{ + "Windows_10-2022_10_10.json", + } + toDownload, toDelete := bulletinsDelta(os, local, remote) + + require.ElementsMatch(t, toDownload, []io.SecurityBulletinName{ + "Windows_11-2022_10_10.json", + }) + require.Empty(t, toDelete) + }) + + t.Run("out of date local bulletin", func(t *testing.T) { + local := []io.SecurityBulletinName{ + "Windows_10-2022_09_10.json", + "Windows_11-2022_10_10.json", + } + + toDownload, toDelete := bulletinsDelta(os, local, remote) + + require.ElementsMatch(t, toDownload, []io.SecurityBulletinName{ + "Windows_10-2022_10_10.json", + }) + require.ElementsMatch(t, toDelete, []io.SecurityBulletinName{ + "Windows_10-2022_09_10.json", + }) + }) + + t.Run("up to date local bulletins", func(t *testing.T) { + local := []io.SecurityBulletinName{ + "Windows_10-2022_10_10.json", + "Windows_11-2022_10_10.json", + } + + toDownload, toDelete := bulletinsDelta(os, local, remote) + + require.Empty(t, toDownload) + require.Empty(t, toDelete) + }) + }) + }) + + t.Run("no Win OS provided", func(t *testing.T) { + os := []fleet.OperatingSystem{ + { + Name: "CentOS", + Version: "8.0.0", + Platform: "rhel", + KernelVersion: "5.10.76-linuxkit", + }, + } + local := []io.SecurityBulletinName{"Windows_11-2022_10_10.json"} + remote := []io.SecurityBulletinName{"Windows_10-2022_10_10.json"} + + t.Run("nothing to download, nothing to delete", func(t *testing.T) { + toDownload, toDelete := bulletinsDelta(os, local, remote) + require.Empty(t, toDownload) + require.Empty(t, toDelete) + }) + }) + + t.Run("no OS provided", func(t *testing.T) { + var os []fleet.OperatingSystem + t.Run("no local bulletins", func(t *testing.T) { + var local []io.SecurityBulletinName + + t.Run("returns all remote", func(t *testing.T) { + remote := []io.SecurityBulletinName{ + "Windows_10-2022_10_10.json", + "Windows_11-2022_10_10.json", + } + + toDownload, toDelete := bulletinsDelta(os, local, remote) + require.ElementsMatch(t, toDownload, remote) + require.Empty(t, toDelete) + }) + }) + }) + }) +} diff --git a/server/vulnerabilities/msrc/xml/feed_result.go b/server/vulnerabilities/msrc/xml/feed_result.go new file mode 100644 index 0000000000..3608116250 --- /dev/null +++ b/server/vulnerabilities/msrc/xml/feed_result.go @@ -0,0 +1,7 @@ +package xml + +// FeedResult groups together products and their vulnerabilities. +type FeedResult struct { + WinVulnerabities []Vulnerability + WinProducts map[string]Product +} diff --git a/server/vulnerabilities/msrc/xml/product.go b/server/vulnerabilities/msrc/xml/product.go new file mode 100644 index 0000000000..3a0f75220a --- /dev/null +++ b/server/vulnerabilities/msrc/xml/product.go @@ -0,0 +1,48 @@ +package xml + +import "strings" + +// XML elements related to the 'prod' namespace used to describe Microsoft products + +// Describes a product three see +// http://docs.oasis-open.org/csaf/csaf-cvrf/v1.2/cs01/csaf-cvrf-v1.2-cs01.html#_Toc493508797 +// for more details +type ProductBranch struct { + Type string `xml:"Type,attr"` + Name string `xml:"Name,attr"` + Branches []ProductBranch `xml:"Branch"` + Products []Product `xml:"FullProductName"` +} + +// Describes a full product name +// http://docs.oasis-open.org/csaf/csaf-cvrf/v1.2/cs01/csaf-cvrf-v1.2-cs01.html#_Toc493508797 +type Product struct { + ProductID string `xml:"ProductID,attr"` + FullName string `xml:",chardata"` +} + +// WinProducts traverses the ProductBranchXML tree returning only 'Windows' products. +func (b *ProductBranch) WinProducts() []Product { + var r []Product + queue := []ProductBranch{*b} + + for len(queue) > 0 { + next := queue[0] + + // We want only products from the 'Windows' and the 'Extended Security Update (ESU)' branches + if next.Type == "Product Family" && (next.Name == "Windows" || next.Name == "ESU") { + for _, p := range next.Products { + // Even if the product branch is for 'Windows/ESU', there could be a non-OS + // product like 'Remote Desktop client for Windows Desktop' inside the branch. + if strings.HasPrefix(p.FullName, "Windows") { + r = append(r, p) + } + } + } + + queue = queue[1:] + queue = append(queue, next.Branches...) + } + + return r +} diff --git a/server/vulnerabilities/msrc/xml/product_test.go b/server/vulnerabilities/msrc/xml/product_test.go new file mode 100644 index 0000000000..3bae024516 --- /dev/null +++ b/server/vulnerabilities/msrc/xml/product_test.go @@ -0,0 +1,53 @@ +package xml + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestProdXML(t *testing.T) { + t.Run("ProductBranchXML", func(t *testing.T) { + t.Run("#WindowsProducts", func(t *testing.T) { + windowsBranch := ProductBranch{ + Type: "Product Family", Name: "Windows", + Products: []Product{ + {ProductID: "11572", FullName: "Windows Server 2019 (Server Core installation)"}, + {ProductID: "11712", FullName: "Windows 10 Version 1909 for 32-bit Systems"}, + }, + } + + esuBranch := ProductBranch{ + Type: "Product Family", Name: "ESU", + Products: []Product{ + {ProductID: "10051", FullName: "Windows Server 2008 R2 for x64-based Systems Service Pack 1"}, + {ProductID: "10049", FullName: "Windows Server 2008 R2 for x64-based Systems Service Pack 1 (Server Core installation)"}, + }, + } + + devToolsBranch := ProductBranch{ + Type: "Product Family", Name: "Developer Tools", + Products: []Product{ + {ProductID: "11676-11927", FullName: "Microsoft .NET Framework 3.5 AND 4.8 on Windows 11 for ARM64-based Systems"}, + {ProductID: "9495-10047", FullName: "Microsoft .NET Framework 3.5.1 on Windows 7 for 32-bit Systems Service Pack 1"}, + {ProductID: "9495-10048", FullName: "Microsoft .NET Framework 3.5.1 on Windows 7 for x64-based Systems Service Pack 1"}, + {ProductID: "9495-10051", FullName: "Microsoft .NET Framework 3.5.1 on Windows Server 2008 R2 for x64-based Systems Service Pack 1"}, + }, + } + + rootBranch := &ProductBranch{ + Type: "Vendor", Name: "Microsoft", + Branches: []ProductBranch{ + windowsBranch, + esuBranch, + devToolsBranch, + }, + } + + winProds := rootBranch.WinProducts() + require.Subset(t, winProds, windowsBranch.Products) + require.Subset(t, winProds, esuBranch.Products) + require.NotSubset(t, winProds, devToolsBranch.Products) + }) + }) +} diff --git a/server/vulnerabilities/msrc/xml/vulnerability.go b/server/vulnerabilities/msrc/xml/vulnerability.go new file mode 100644 index 0000000000..817dabed77 --- /dev/null +++ b/server/vulnerabilities/msrc/xml/vulnerability.go @@ -0,0 +1,73 @@ +package xml + +import ( + "fmt" + "strings" + "time" +) + +// XML elements related to the 'vuln' namespace used to describe vulnerabilities and their remediations. + +// Vulnerability see +// http://docs.oasis-open.org/csaf/csaf-cvrf/v1.2/cs01/csaf-cvrf-v1.2-cs01.html#_Toc493508834 +// for more details. +type Vulnerability struct { + CVE string `xml:"CVE"` + Score float64 `xml:"CVSSScoreSets>ScoreSet>BaseScore"` + Revisions []RevisionHistory `xml:"RevisionHistory>Revision"` + Remediations []VulnerabilityRemediation `xml:"Remediations>Remediation"` +} + +type RevisionHistory struct { + Date string `xml:"Date"` + Description string `xml:"Description"` +} + +// VulnerabilityRemediation See http://docs.oasis-open.org/csaf/csaf-cvrf/v1.2/cs01/csaf-cvrf-v1.2-cs01.html#_Toc493508854 +// for more details. +type VulnerabilityRemediation struct { + Type string `xml:"Type,attr"` + FixedBuild string `xml:"FixedBuild"` + RestartRequired string `xml:"RestartRequired"` + ProductIDs []string `xml:"ProductID"` + Description string `xml:"Description"` + URL string `xml:"URL"` + Supercedence string `xml:"Supercedence"` +} + +// IncludesVendorFix returns true if the vulnerability has a vendor fix targeting the product +// identified by pID. +func (v *Vulnerability) IncludesVendorFix(pID string) bool { + for _, rem := range v.Remediations { + if rem.IsVendorFix() { + for _, vfPID := range rem.ProductIDs { + if vfPID == pID { + return true + } + } + } + } + + return false +} + +// PublishedDateEpoch returns the date the vuln was published (if any) as an epoch +func (v *Vulnerability) PublishedDateEpoch() *int64 { + for _, rev := range v.Revisions { + if strings.Index(rev.Description, "Information published") != -1 { + dPublished, err := time.Parse("2006-01-02T15:04:05", rev.Date) + if err != nil { + return nil + } + epoch := dPublished.Unix() + return &epoch + } + } + return nil +} + +func (rem *VulnerabilityRemediation) IsVendorFix() bool { + return rem.Type == "Vendor Fix" && + strings.HasPrefix(rem.URL, "https://catalog.update") && + strings.HasSuffix(rem.URL, fmt.Sprintf("q=KB%s", rem.Description)) +} diff --git a/server/vulnerabilities/msrc/xml/vulnerability_test.go b/server/vulnerabilities/msrc/xml/vulnerability_test.go new file mode 100644 index 0000000000..fefed72771 --- /dev/null +++ b/server/vulnerabilities/msrc/xml/vulnerability_test.go @@ -0,0 +1,89 @@ +package xml + +import ( + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestVulnunerability(t *testing.T) { + t.Run("VulnerabilityXML", func(t *testing.T) { + t.Run("#PublishedDateEpoch", func(t *testing.T) { + sut := Vulnerability{ + Revisions: []RevisionHistory{ + { + Description: "

Information published.

", + Date: "2022-05-10T07:00:00", + }, + }, + } + + resultEpoch := sut.PublishedDateEpoch() + require.NotNil(t, resultEpoch) + + resultDate := time.Unix(*resultEpoch, 0) + require.Equal(t, 2022, resultDate.Year()) + require.Equal(t, time.May, resultDate.Month()) + require.Equal(t, 10, resultDate.Day()) + }) + + t.Run("#IncludesVendorFix", func(t *testing.T) { + t.Run("no remediations", func(t *testing.T) { + sut := Vulnerability{} + require.False(t, sut.IncludesVendorFix("1")) + }) + + t.Run("no vendor fixes", func(t *testing.T) { + sut := Vulnerability{ + Remediations: []VulnerabilityRemediation{ + { + Type: "Known Issue", + ProductIDs: []string{"11896", "11897"}, + Description: "5013942", + URL: "https://support.microsoft.com/help/5013942", + }, + }, + } + + require.False(t, sut.IncludesVendorFix("11896")) + }) + + t.Run("no vendor fix matches", func(t *testing.T) { + sut := Vulnerability{ + Remediations: []VulnerabilityRemediation{ + { + Type: "Vendor Fix", + FixedBuild: "10.0.17763.2928", + ProductIDs: []string{"11568", "11569"}, + Description: "5013941", + Supercedence: "5012647", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013941", + }, + }, + } + + require.False(t, sut.IncludesVendorFix("123")) + }) + + t.Run("vendor fix matches", func(t *testing.T) { + sut := Vulnerability{ + Remediations: []VulnerabilityRemediation{ + { + Type: "Vendor Fix", + FixedBuild: "10.0.17763.2928", + ProductIDs: []string{"11568", "11569"}, + Description: "5013941", + Supercedence: "5012647", + RestartRequired: "Yes", + URL: "https://catalog.update.microsoft.com/v7/site/Search.aspx?q=KB5013941", + }, + }, + } + + require.True(t, sut.IncludesVendorFix("11568")) + }) + }) + }) +} diff --git a/server/vulnerabilities/testdata/msrc-2022-may.xml.bz2 b/server/vulnerabilities/testdata/msrc-2022-may.xml.bz2 new file mode 100644 index 0000000000..bbb3568069 Binary files /dev/null and b/server/vulnerabilities/testdata/msrc-2022-may.xml.bz2 differ