From 2699c221432fd3e97fda80845c24eb7a17e88767 Mon Sep 17 00:00:00 2001 From: Juan Fernandez Date: Tue, 30 Aug 2022 16:39:50 -0400 Subject: [PATCH] Feature 7077: Add MSRC feed parser (#7424) Added parser for MSRC --- changes/feature-7077-msrc-parser | 2 + server/ptr/ptr.go | 4 + server/vulnerabilities/msrc/io/fs.go | 47 + server/vulnerabilities/msrc/io/fs_test.go | 47 + server/vulnerabilities/msrc/io/github.go | 70 + .../msrc/io/security_bulletin_name.go | 57 + .../msrc/io/security_bulletin_name_test.go | 34 + server/vulnerabilities/msrc/parsed/product.go | 77 + .../msrc/parsed/product_test.go | 375 +++++ .../msrc/parsed/security_bulletin.go | 55 + server/vulnerabilities/msrc/parser.go | 153 ++ server/vulnerabilities/msrc/parser_test.go | 1467 +++++++++++++++++ server/vulnerabilities/msrc/sync.go | 106 ++ server/vulnerabilities/msrc/sync_test.go | 197 +++ .../vulnerabilities/msrc/xml/feed_result.go | 7 + server/vulnerabilities/msrc/xml/product.go | 48 + .../vulnerabilities/msrc/xml/product_test.go | 53 + .../vulnerabilities/msrc/xml/vulnerability.go | 73 + .../msrc/xml/vulnerability_test.go | 89 + .../testdata/msrc-2022-may.xml.bz2 | Bin 0 -> 42992 bytes 20 files changed, 2961 insertions(+) create mode 100644 changes/feature-7077-msrc-parser create mode 100644 server/vulnerabilities/msrc/io/fs.go create mode 100644 server/vulnerabilities/msrc/io/fs_test.go create mode 100644 server/vulnerabilities/msrc/io/github.go create mode 100644 server/vulnerabilities/msrc/io/security_bulletin_name.go create mode 100644 server/vulnerabilities/msrc/io/security_bulletin_name_test.go create mode 100644 server/vulnerabilities/msrc/parsed/product.go create mode 100644 server/vulnerabilities/msrc/parsed/product_test.go create mode 100644 server/vulnerabilities/msrc/parsed/security_bulletin.go create mode 100644 server/vulnerabilities/msrc/parser.go create mode 100644 server/vulnerabilities/msrc/parser_test.go create mode 100644 server/vulnerabilities/msrc/sync.go create mode 100644 server/vulnerabilities/msrc/sync_test.go create mode 100644 server/vulnerabilities/msrc/xml/feed_result.go create mode 100644 server/vulnerabilities/msrc/xml/product.go create mode 100644 server/vulnerabilities/msrc/xml/product_test.go create mode 100644 server/vulnerabilities/msrc/xml/vulnerability.go create mode 100644 server/vulnerabilities/msrc/xml/vulnerability_test.go create mode 100644 server/vulnerabilities/testdata/msrc-2022-may.xml.bz2 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 0000000000000000000000000000000000000000..bbb35680698dad85a5ec40faf51326a4e3e08d71 GIT binary patch literal 42992 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

SXY<1uZPN@n0>>UGLQviaXf#&X`X5>FMH;ve>x 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%0Ds*-@am+PF3 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 literal 0 HcmV?d00001