fleet/server/vulnerabilities/msrc/parser.go
2024-10-18 12:38:26 -05:00

170 lines
4.8 KiB
Go

package msrc
import (
"encoding/xml"
"fmt"
"io"
"os"
"strconv"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/parsed"
msrcxml "github.com/fleetdm/fleet/v4/server/vulnerabilities/msrc/xml"
)
func ParseFeed(fPath string) (map[string]*parsed.SecurityBulletin, error) {
r, err := os.Open(fPath)
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 name.
bulletins := make(map[string]*parsed.SecurityBulletin)
pIDToPName := make(map[string]string, len(rXML.WinProducts))
for pID, p := range rXML.WinProducts {
name := parsed.NewProductFromFullName(p.FullName).Name()
// If the name could not be determined means that we have an un-supported Windows product
if name == "" {
continue
}
if bulletins[name] == nil {
bulletins[name] = parsed.NewSecurityBulletin(name)
}
bulletins[name].Products[pID] = parsed.NewProductFromFullName(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.
remediatedKBIDRaw, err := strconv.Atoi(rem.Description)
if err != nil {
return nil, fmt.Errorf("invalid remediation KBID %q for %s", rem.Description, v.CVE)
}
if remediatedKBIDRaw < 0 {
return nil, fmt.Errorf("invalid remediation KBID %q for %s", rem.Description, v.CVE)
}
remediatedKBID := uint(remediatedKBIDRaw)
// rem.Supercedence should have the ID portion of a KBID which the current vendor fix replaces.
var supersedes *uint
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)
}
if r < 0 {
return nil, fmt.Errorf("invalid supercedence KBID %q for %s", rem.Supercedence, v.CVE)
}
supersedes = ptr.Uint(uint(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())
}
// At this point we know that the remediation is a vendor fix that targets a windows
// product, so we add the remediation's product ID to the vulnerability's targeted products.
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)
} else {
vFix.AddFixedBuild(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) { //nolint:gocritic // ignore singleCaseSwitch
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
}
}
}
}
}
}