mirror of
https://github.com/empayre/fleet.git
synced 2024-11-06 08:55:24 +00:00
205 lines
5.2 KiB
Go
205 lines
5.2 KiB
Go
package macoffice
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/vulnerabilities/io"
|
|
"github.com/fleetdm/fleet/v4/server/vulnerabilities/utils"
|
|
)
|
|
|
|
// getLatestReleaseNotes returns the most recent Mac Office release notes asset (based on the date in the
|
|
// filename) contained in 'vulnPath'
|
|
func getLatestReleaseNotes(vulnPath string) (ReleaseNotes, error) {
|
|
fs := io.NewFSClient(vulnPath)
|
|
|
|
files, err := fs.MacOfficeReleaseNotes()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(files) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
sort.Slice(files, func(i, j int) bool { return files[j].Before(files[i]) })
|
|
filePath := filepath.Join(vulnPath, files[0].String())
|
|
|
|
payload, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
relNotes := ReleaseNotes{}
|
|
err = json.Unmarshal(payload, &relNotes)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Ensure the release notes are sorted by release date, this is because the vuln. processing
|
|
// algo. will stop when a release note older than the current software version is found.
|
|
sort.Slice(relNotes, func(i, j int) bool { return relNotes[j].Date.Before(relNotes[i].Date) })
|
|
|
|
return relNotes, nil
|
|
}
|
|
|
|
// collectVulnerabilities compares 'software' against all 'release notes' returning all detected
|
|
// vulnerabilities.
|
|
func collectVulnerabilities(
|
|
software *fleet.Software,
|
|
product ProductType,
|
|
relNotes ReleaseNotes,
|
|
) []fleet.SoftwareVulnerability {
|
|
var vulns []fleet.SoftwareVulnerability
|
|
for _, relNote := range relNotes {
|
|
// We only care about release notes with set versions and with security updates,
|
|
// 'relNotes' should only contain valid release notes, but this check not expensive.
|
|
if !relNote.Valid() {
|
|
continue
|
|
}
|
|
|
|
if relNote.CmpVersion(software.Version) <= 0 {
|
|
return vulns
|
|
}
|
|
|
|
for _, cve := range relNote.CollectVulnerabilities(product) {
|
|
vulns = append(vulns, fleet.SoftwareVulnerability{
|
|
SoftwareID: software.ID,
|
|
CVE: cve,
|
|
})
|
|
}
|
|
}
|
|
return vulns
|
|
}
|
|
|
|
// getStoredVulnerabilities return all stored vulnerabilities for 'softwareID'
|
|
func getStoredVulnerabilities(
|
|
ctx context.Context,
|
|
ds fleet.Datastore,
|
|
softwareID uint,
|
|
) ([]fleet.SoftwareVulnerability, error) {
|
|
storedSoftware, err := ds.SoftwareByID(ctx, softwareID, nil, false, nil)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var result []fleet.SoftwareVulnerability
|
|
for _, v := range storedSoftware.Vulnerabilities {
|
|
result = append(result, fleet.SoftwareVulnerability{
|
|
SoftwareID: storedSoftware.ID,
|
|
CVE: v.CVE,
|
|
})
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func updateVulnsInDB(
|
|
ctx context.Context,
|
|
ds fleet.Datastore,
|
|
detected []fleet.SoftwareVulnerability,
|
|
existing []fleet.SoftwareVulnerability,
|
|
) ([]fleet.SoftwareVulnerability, error) {
|
|
toInsert, toDelete := utils.VulnsDelta(detected, existing)
|
|
|
|
// Remove any possible dups...
|
|
toInsertSet := make(map[string]fleet.SoftwareVulnerability, len(toInsert))
|
|
for _, i := range toInsert {
|
|
toInsertSet[i.Key()] = i
|
|
}
|
|
|
|
err := ds.DeleteSoftwareVulnerabilities(ctx, toDelete)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inserted := make([]fleet.SoftwareVulnerability, 0, len(toInsertSet))
|
|
err = utils.BatchProcess(toInsertSet, func(vulns []fleet.SoftwareVulnerability) error {
|
|
for _, v := range vulns {
|
|
ok, err := ds.InsertSoftwareVulnerability(ctx, v, fleet.MacOfficeReleaseNotesSource)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if ok {
|
|
inserted = append(inserted, v)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
// Since we are only detecting Mac Office vulnerabilities 'toInsertSet' should be small, so
|
|
// inserting the whole batch in one go should be ok.
|
|
}, len(toInsertSet))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return inserted, nil
|
|
}
|
|
|
|
// Analyze uses the most recent Mac Office release notes asset in 'vulnPath' for detecting
|
|
// vulnerabilities on Mac Office apps.
|
|
func Analyze(
|
|
ctx context.Context,
|
|
ds fleet.Datastore,
|
|
vulnPath string,
|
|
collectVulns bool,
|
|
) ([]fleet.SoftwareVulnerability, error) {
|
|
relNotes, err := getLatestReleaseNotes(vulnPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if len(relNotes) == 0 {
|
|
return nil, nil
|
|
}
|
|
|
|
queryParams := fleet.SoftwareIterQueryOptions{IncludedSources: []string{"apps"}}
|
|
iter, err := ds.AllSoftwareIterator(ctx, queryParams)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer iter.Close()
|
|
|
|
var vulnerabilities []fleet.SoftwareVulnerability
|
|
for iter.Next() {
|
|
software, err := iter.Value()
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "getting software from iterator")
|
|
}
|
|
|
|
product, ok := OfficeProductFromBundleId(software.BundleIdentifier)
|
|
// If we don't have an Office Product ...
|
|
if !ok {
|
|
continue
|
|
}
|
|
|
|
detected := collectVulnerabilities(software, product, relNotes)
|
|
// The 'software' instance we get back from the iterator does not include vulnerabilities...
|
|
existing, err := getStoredVulnerabilities(ctx, ds, software.ID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
inserted, err := updateVulnsInDB(ctx, ds, detected, existing)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if collectVulns {
|
|
vulnerabilities = append(vulnerabilities, inserted...)
|
|
}
|
|
}
|
|
|
|
if err := iter.Err(); err != nil {
|
|
return nil, fmt.Errorf("iter: %w", err)
|
|
}
|
|
|
|
return vulnerabilities, nil
|
|
}
|