fleet/server/service/vulnerabilities.go
2025-02-14 16:19:34 -06:00

232 lines
6.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"context"
"fmt"
"net/http"
"regexp"
"time"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/fleet"
)
var freeValidVulnSortColumns = []string{
"cve",
"hosts_count",
"host_count_updated_at",
"created_at",
}
type cveNotFoundError struct{}
var _ fleet.NotFoundError = (*cveNotFoundError)(nil)
func (p cveNotFoundError) Error() string {
return "This is not a known CVE. None of Fleets vulnerability sources are aware of this CVE."
}
func (p cveNotFoundError) IsNotFound() bool {
return true
}
type listVulnerabilitiesRequest struct {
fleet.VulnListOptions
}
type listVulnerabilitiesResponse struct {
Vulnerabilities []fleet.VulnerabilityWithMetadata `json:"vulnerabilities"`
Count uint `json:"count"`
CountsUpdatedAt time.Time `json:"counts_updated_at"`
Meta *fleet.PaginationMetadata `json:"meta,omitempty"`
Err error `json:"error,omitempty"`
}
// Allow formats like: CVE-2017-12345, cve-2017-12345
var cveRegex = regexp.MustCompile(`(?i)^CVE-\d{4}-\d{4}\d*$`)
func (r listVulnerabilitiesResponse) Error() error { return r.Err }
func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (fleet.Errorer, error) {
request := req.(*listVulnerabilitiesRequest)
vulns, meta, err := svc.ListVulnerabilities(ctx, request.VulnListOptions)
if err != nil {
return listVulnerabilitiesResponse{Err: err}, nil
}
count, err := svc.CountVulnerabilities(ctx, request.VulnListOptions)
if err != nil {
return listVulnerabilitiesResponse{Err: err}, nil
}
updatedAt := time.Now()
for _, vuln := range vulns {
if vuln.HostsCountUpdatedAt.Before(updatedAt) {
updatedAt = vuln.HostsCountUpdatedAt
}
}
return listVulnerabilitiesResponse{
Vulnerabilities: vulns,
Meta: meta,
Count: count,
CountsUpdatedAt: updatedAt,
}, nil
}
func (svc *Service) ListVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
TeamID: opt.TeamID,
}, fleet.ActionRead); err != nil {
return nil, nil, err
}
if len(opt.ValidSortColumns) == 0 {
opt.ValidSortColumns = freeValidVulnSortColumns
}
if !opt.HasValidSortColumn() {
return nil, nil, badRequest("invalid order key")
}
if opt.KnownExploit && !opt.IsEE {
return nil, nil, fleet.ErrMissingLicense
}
vulns, meta, err := svc.ds.ListVulnerabilities(ctx, opt)
if err != nil {
return nil, nil, err
}
for i, vuln := range vulns {
vulns[i].DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE.CVE)
}
return vulns, meta, nil
}
func (svc *Service) CountVulnerabilities(ctx context.Context, opts fleet.VulnListOptions) (uint, error) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
TeamID: opts.TeamID,
}, fleet.ActionRead); err != nil {
return 0, err
}
return svc.ds.CountVulnerabilities(ctx, opts)
}
func (svc *Service) IsCVEKnownToFleet(ctx context.Context, cve string) (bool, error) {
return svc.ds.IsCVEKnownToFleet(ctx, cve)
}
type getVulnerabilityRequest struct {
CVE string `url:"cve"`
TeamID *uint `query:"team_id,optional"`
}
type getVulnerabilityResponse struct {
Vulnerability *fleet.VulnerabilityWithMetadata `json:"vulnerability"`
OSVersions []*fleet.VulnerableOS `json:"os_versions"`
Software []*fleet.VulnerableSoftware `json:"software"`
Err error `json:"error,omitempty"`
statusCode int
}
func (r getVulnerabilityResponse) Error() error { return r.Err }
func (r getVulnerabilityResponse) Status() int {
if r.statusCode == 0 {
return http.StatusOK
}
return r.statusCode
}
func getVulnerabilityEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (fleet.Errorer, error) {
request := req.(*getVulnerabilityRequest)
vuln, known, err := svc.Vulnerability(ctx, request.CVE, request.TeamID, false)
if err != nil {
return getVulnerabilityResponse{Err: err}, nil
}
if vuln == nil && known {
// Return 204 status code if the vulnerability is known to Fleet but does not match any host software/OS
return getVulnerabilityResponse{statusCode: http.StatusNoContent}, nil
}
vuln.DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE.CVE)
osVersions, _, err := svc.ListOSVersionsByCVE(ctx, vuln.CVE.CVE, request.TeamID)
if err != nil {
return getVulnerabilityResponse{Err: err}, nil
}
software, _, err := svc.ListSoftwareByCVE(ctx, vuln.CVE.CVE, request.TeamID)
if err != nil {
return getVulnerabilityResponse{Err: err}, nil
}
return getVulnerabilityResponse{
Vulnerability: vuln,
OSVersions: osVersions,
Software: software,
}, nil
}
func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (vuln *fleet.VulnerabilityWithMetadata,
known bool, err error,
) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, false, err
}
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, false, err
}
if !cveRegex.Match([]byte(cve)) {
return nil, false, badRequest("That vulnerability (CVE) is not valid. Try updating your search to use CVE format: \"CVE-YYYY-<4 or more digits>\"")
}
if teamID != nil && *teamID != 0 {
exists, err := svc.ds.TeamExists(ctx, *teamID)
if err != nil {
return nil, false, ctxerr.Wrap(ctx, err, "checking if team exists")
} else if !exists {
return nil, false, authz.ForbiddenWithInternal("team does not exist", nil, nil, nil)
}
}
vuln, err = svc.ds.Vulnerability(ctx, cve, teamID, useCVSScores)
switch {
case fleet.IsNotFound(err):
var errKnown error
known, errKnown = svc.ds.IsCVEKnownToFleet(ctx, cve)
if errKnown != nil {
return nil, false, errKnown
}
if !known {
return nil, false, cveNotFoundError{}
}
case err != nil:
return nil, false, err
default:
known = true
}
return vuln, known, nil
}
func (svc *Service) ListOSVersionsByCVE(ctx context.Context, cve string, teamID *uint) (result []*fleet.VulnerableOS, updatedAt time.Time, err error) {
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, updatedAt, err
}
return svc.ds.OSVersionsByCVE(ctx, cve, teamID)
}
func (svc *Service) ListSoftwareByCVE(ctx context.Context, cve string, teamID *uint) (result []*fleet.VulnerableSoftware, updatedAt time.Time, err error) {
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{TeamID: teamID}, fleet.ActionRead); err != nil {
return nil, updatedAt, err
}
return svc.ds.SoftwareByCVE(ctx, cve, teamID)
}