2024-02-10 03:54:44 +00:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"fmt"
|
2024-08-21 17:52:28 +00:00
|
|
|
|
"net/http"
|
2024-08-08 19:37:25 +00:00
|
|
|
|
"regexp"
|
2024-02-14 21:42:16 +00:00
|
|
|
|
"time"
|
2024-02-10 03:54:44 +00:00
|
|
|
|
|
2024-02-20 15:49:11 +00:00
|
|
|
|
"github.com/fleetdm/fleet/v4/server/authz"
|
|
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
2024-02-10 03:54:44 +00:00
|
|
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
var freeValidVulnSortColumns = []string{
|
|
|
|
|
|
"cve",
|
2024-02-20 16:17:07 +00:00
|
|
|
|
"hosts_count",
|
2024-02-10 03:54:44 +00:00
|
|
|
|
"host_count_updated_at",
|
|
|
|
|
|
"created_at",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-21 17:52:28 +00:00
|
|
|
|
type cveNotFoundError struct{}
|
|
|
|
|
|
|
|
|
|
|
|
var _ fleet.NotFoundError = (*cveNotFoundError)(nil)
|
|
|
|
|
|
|
|
|
|
|
|
func (p cveNotFoundError) Error() string {
|
2024-08-28 01:38:13 +00:00
|
|
|
|
return "This is not a known CVE. None of Fleet’s vulnerability sources are aware of this CVE."
|
2024-08-21 17:52:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (p cveNotFoundError) IsNotFound() bool {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-10 03:54:44 +00:00
|
|
|
|
type listVulnerabilitiesRequest struct {
|
|
|
|
|
|
fleet.VulnListOptions
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type listVulnerabilitiesResponse struct {
|
2024-08-21 17:52:28 +00:00
|
|
|
|
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"`
|
2024-02-10 03:54:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-21 17:52:28 +00:00
|
|
|
|
// Allow formats like: CVE-2017-12345, cve-2017-12345
|
|
|
|
|
|
var cveRegex = regexp.MustCompile(`(?i)^CVE-\d{4}-\d{4}\d*$`)
|
2024-08-08 19:37:25 +00:00
|
|
|
|
|
2025-02-03 17:23:26 +00:00
|
|
|
|
func (r listVulnerabilitiesResponse) Error() error { return r.Err }
|
2024-02-10 03:54:44 +00:00
|
|
|
|
|
2025-02-14 22:19:34 +00:00
|
|
|
|
func listVulnerabilitiesEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
2024-02-10 03:54:44 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-03-11 20:14:29 +00:00
|
|
|
|
updatedAt := time.Now()
|
|
|
|
|
|
for _, vuln := range vulns {
|
|
|
|
|
|
if vuln.HostsCountUpdatedAt.Before(updatedAt) {
|
|
|
|
|
|
updatedAt = vuln.HostsCountUpdatedAt
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-10 03:54:44 +00:00
|
|
|
|
return listVulnerabilitiesResponse{
|
2024-08-21 17:52:28 +00:00
|
|
|
|
Vulnerabilities: vulns,
|
|
|
|
|
|
Meta: meta,
|
|
|
|
|
|
Count: count,
|
|
|
|
|
|
CountsUpdatedAt: updatedAt,
|
2024-02-10 03:54:44 +00:00
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *Service) ListVulnerabilities(ctx context.Context, opt fleet.VulnListOptions) ([]fleet.VulnerabilityWithMetadata, *fleet.PaginationMetadata, error) {
|
|
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
|
2024-07-30 17:19:05 +00:00
|
|
|
|
TeamID: opt.TeamID,
|
2024-02-10 03:54:44 +00:00
|
|
|
|
}, 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 {
|
2024-05-14 23:00:33 +00:00
|
|
|
|
vulns[i].DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE.CVE)
|
2024-02-10 03:54:44 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return vulns, meta, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func (svc *Service) CountVulnerabilities(ctx context.Context, opts fleet.VulnListOptions) (uint, error) {
|
|
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
|
2024-07-30 17:19:05 +00:00
|
|
|
|
TeamID: opts.TeamID,
|
2024-02-10 03:54:44 +00:00
|
|
|
|
}, fleet.ActionRead); err != nil {
|
|
|
|
|
|
return 0, err
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return svc.ds.CountVulnerabilities(ctx, opts)
|
|
|
|
|
|
}
|
2024-02-14 21:42:16 +00:00
|
|
|
|
|
2024-08-08 19:37:25 +00:00
|
|
|
|
func (svc *Service) IsCVEKnownToFleet(ctx context.Context, cve string) (bool, error) {
|
|
|
|
|
|
return svc.ds.IsCVEKnownToFleet(ctx, cve)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-14 21:42:16 +00:00
|
|
|
|
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"`
|
2024-08-21 17:52:28 +00:00
|
|
|
|
statusCode int
|
2024-02-14 21:42:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-03 17:23:26 +00:00
|
|
|
|
func (r getVulnerabilityResponse) Error() error { return r.Err }
|
2024-02-14 21:42:16 +00:00
|
|
|
|
|
2024-08-21 17:52:28 +00:00
|
|
|
|
func (r getVulnerabilityResponse) Status() int {
|
|
|
|
|
|
if r.statusCode == 0 {
|
|
|
|
|
|
return http.StatusOK
|
|
|
|
|
|
}
|
|
|
|
|
|
return r.statusCode
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-02-14 22:19:34 +00:00
|
|
|
|
func getVulnerabilityEndpoint(ctx context.Context, req interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
2024-02-14 21:42:16 +00:00
|
|
|
|
request := req.(*getVulnerabilityRequest)
|
|
|
|
|
|
|
2024-08-21 17:52:28 +00:00
|
|
|
|
vuln, known, err := svc.Vulnerability(ctx, request.CVE, request.TeamID, false)
|
2024-02-14 21:42:16 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return getVulnerabilityResponse{Err: err}, nil
|
|
|
|
|
|
}
|
2024-08-21 17:52:28 +00:00
|
|
|
|
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
|
|
|
|
|
|
}
|
2024-02-14 21:42:16 +00:00
|
|
|
|
|
2024-05-14 23:00:33 +00:00
|
|
|
|
vuln.DetailsLink = fmt.Sprintf("https://nvd.nist.gov/vuln/detail/%s", vuln.CVE.CVE)
|
2024-02-14 21:42:16 +00:00
|
|
|
|
|
2024-02-26 18:29:59 +00:00
|
|
|
|
osVersions, _, err := svc.ListOSVersionsByCVE(ctx, vuln.CVE.CVE, request.TeamID)
|
2024-02-14 21:42:16 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return getVulnerabilityResponse{Err: err}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-02-26 18:29:59 +00:00
|
|
|
|
software, _, err := svc.ListSoftwareByCVE(ctx, vuln.CVE.CVE, request.TeamID)
|
2024-02-14 21:42:16 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return getVulnerabilityResponse{Err: err}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return getVulnerabilityResponse{
|
|
|
|
|
|
Vulnerability: vuln,
|
|
|
|
|
|
OSVersions: osVersions,
|
|
|
|
|
|
Software: software,
|
|
|
|
|
|
}, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-21 17:52:28 +00:00
|
|
|
|
func (svc *Service) Vulnerability(ctx context.Context, cve string, teamID *uint, useCVSScores bool) (vuln *fleet.VulnerabilityWithMetadata,
|
2024-08-28 01:38:13 +00:00
|
|
|
|
known bool, err error,
|
|
|
|
|
|
) {
|
2024-02-14 21:42:16 +00:00
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{TeamID: teamID}, fleet.ActionRead); err != nil {
|
2024-08-21 17:52:28 +00:00
|
|
|
|
return nil, false, err
|
2024-02-14 21:42:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionRead); err != nil {
|
2024-08-21 17:52:28 +00:00
|
|
|
|
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>\"")
|
2024-02-14 21:42:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-07-30 17:19:05 +00:00
|
|
|
|
if teamID != nil && *teamID != 0 {
|
2024-02-20 15:49:11 +00:00
|
|
|
|
exists, err := svc.ds.TeamExists(ctx, *teamID)
|
|
|
|
|
|
if err != nil {
|
2024-08-21 17:52:28 +00:00
|
|
|
|
return nil, false, ctxerr.Wrap(ctx, err, "checking if team exists")
|
2024-02-20 15:49:11 +00:00
|
|
|
|
} else if !exists {
|
2024-08-21 17:52:28 +00:00
|
|
|
|
return nil, false, authz.ForbiddenWithInternal("team does not exist", nil, nil, nil)
|
2024-02-20 15:49:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-21 17:52:28 +00:00
|
|
|
|
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
|
2024-02-14 21:42:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
2024-08-21 17:52:28 +00:00
|
|
|
|
return vuln, known, nil
|
2024-02-14 21:42:16 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
|
}
|