mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** For #39344 # Details As a first step to deprecating API params like `team_id` in favor of `fleet_id` and `query_id` in favor of `report_id`, this PR adds `renameto` tags to all deprecated keys. There is no logic in this PR to actually use these tags in any way. The logic and test fixes will be in the next PR, but in the interest of keeping things manageable I'm pushing this out first. There were definitely params with "query" in them that we don't want to change (mainly osquery-related), and I think I kept them all out but it's worth double-checking here. The team -> fleet changes are pretty safe in comparison. # Checklist for submitter If some of the following don't apply, delete the relevant line. - [ ] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/guides/committing-changes.md#changes-files) for more information. Deferring changelog to PR with logic changes ## Testing - [ ] Added/updated automated tests This should be a no-op. All existing tests shoud pass. - [X] QA'd all new/changed functionality manually
264 lines
8.6 KiB
Go
264 lines
8.6 KiB
Go
package service
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
"github.com/fleetdm/fleet/v4/server/ptr"
|
|
)
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// List
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type listSoftwareRequest struct {
|
|
fleet.SoftwareListOptions
|
|
}
|
|
|
|
// Deprecated: listSoftwareResponse is the response struct for the deprecated
|
|
// listSoftwareEndpoint. It differs from listSoftwareVersionsResponse in that
|
|
// the latter includes a count of the total number of software items.
|
|
type listSoftwareResponse struct {
|
|
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
|
|
Software []fleet.Software `json:"software,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r listSoftwareResponse) Error() error { return r.Err }
|
|
|
|
// Deprecated: use listSoftwareVersionsEndpoint instead
|
|
func listSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*listSoftwareRequest)
|
|
resp, _, err := svc.ListSoftware(ctx, req.SoftwareListOptions)
|
|
if err != nil {
|
|
return listSoftwareResponse{Err: err}, nil
|
|
}
|
|
|
|
// calculate the latest counts_updated_at
|
|
var latest time.Time
|
|
for _, sw := range resp {
|
|
if !sw.CountsUpdatedAt.IsZero() && sw.CountsUpdatedAt.After(latest) {
|
|
latest = sw.CountsUpdatedAt
|
|
}
|
|
}
|
|
listResp := listSoftwareResponse{Software: resp}
|
|
if !latest.IsZero() {
|
|
listResp.CountsUpdatedAt = &latest
|
|
}
|
|
|
|
return listResp, nil
|
|
}
|
|
|
|
type listSoftwareVersionsResponse struct {
|
|
Count int `json:"count"`
|
|
CountsUpdatedAt *time.Time `json:"counts_updated_at"`
|
|
Software []fleet.Software `json:"software,omitempty"`
|
|
Meta *fleet.PaginationMetadata `json:"meta"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r listSoftwareVersionsResponse) Error() error { return r.Err }
|
|
|
|
func listSoftwareVersionsEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*listSoftwareRequest)
|
|
|
|
// always include pagination for new software versions endpoint (not included by default in
|
|
// legacy endpoint for backwards compatibility)
|
|
req.SoftwareListOptions.ListOptions.IncludeMetadata = true
|
|
|
|
resp, meta, err := svc.ListSoftware(ctx, req.SoftwareListOptions)
|
|
if err != nil {
|
|
return listSoftwareVersionsResponse{Err: err}, nil
|
|
}
|
|
|
|
// calculate the latest counts_updated_at
|
|
var latest time.Time
|
|
for _, sw := range resp {
|
|
if !sw.CountsUpdatedAt.IsZero() && sw.CountsUpdatedAt.After(latest) {
|
|
latest = sw.CountsUpdatedAt
|
|
}
|
|
}
|
|
listResp := listSoftwareVersionsResponse{Software: resp, Meta: meta}
|
|
if !latest.IsZero() {
|
|
listResp.CountsUpdatedAt = &latest
|
|
}
|
|
|
|
c, err := svc.CountSoftware(ctx, req.SoftwareListOptions)
|
|
if err != nil {
|
|
return listSoftwareVersionsResponse{Err: err}, nil
|
|
}
|
|
listResp.Count = c
|
|
|
|
return listResp, nil
|
|
}
|
|
|
|
func (svc *Service) ListSoftware(ctx context.Context, opt fleet.SoftwareListOptions) ([]fleet.Software, *fleet.PaginationMetadata, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
|
|
TeamID: opt.TeamID,
|
|
}, fleet.ActionRead); err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
// Vulnerability filters are only available in premium (opt.IncludeCVEScores is only true in premium)
|
|
lic, err := svc.License(ctx)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if !lic.IsPremium() && (opt.MaximumCVSS > 0 || opt.MinimumCVSS > 0 || opt.KnownExploit) {
|
|
return nil, nil, fleet.ErrMissingLicense
|
|
}
|
|
|
|
// default sort order to hosts_count descending
|
|
if opt.ListOptions.OrderKey == "" {
|
|
opt.ListOptions.OrderKey = "hosts_count"
|
|
opt.ListOptions.OrderDirection = fleet.OrderDescending
|
|
}
|
|
opt.WithHostCounts = true
|
|
|
|
softwares, meta, err := svc.ds.ListSoftware(ctx, opt)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
|
|
return softwares, meta, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Get Software
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type getSoftwareRequest struct {
|
|
ID uint `url:"id"`
|
|
TeamID *uint `query:"team_id,optional" renameto:"fleet_id"`
|
|
}
|
|
|
|
type getSoftwareResponse struct {
|
|
Software *fleet.Software `json:"software,omitempty"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r getSoftwareResponse) Error() error { return r.Err }
|
|
|
|
func getSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*getSoftwareRequest)
|
|
|
|
software, err := svc.SoftwareByID(ctx, req.ID, req.TeamID, false)
|
|
if err != nil {
|
|
return getSoftwareResponse{Err: err}, nil
|
|
}
|
|
|
|
return getSoftwareResponse{Software: software}, nil
|
|
}
|
|
|
|
func (svc *Service) SoftwareByID(ctx context.Context, id uint, teamID *uint, includeCVEScores bool) (*fleet.Software, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.Host{TeamID: teamID}, fleet.ActionList); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if teamID != nil && *teamID > 0 {
|
|
// This auth check ensures we return 403 if the user doesn't have access to the team
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{TeamID: teamID}, fleet.ActionRead); err != nil {
|
|
return nil, err
|
|
}
|
|
exists, err := svc.ds.TeamExists(ctx, *teamID)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "checking if team exists")
|
|
} else if !exists {
|
|
return nil, fleet.NewInvalidArgumentError("team_id", fmt.Sprintf("team %d does not exist", *teamID)).
|
|
WithStatus(http.StatusNotFound)
|
|
}
|
|
}
|
|
vc, ok := viewer.FromContext(ctx)
|
|
if !ok {
|
|
return nil, fleet.ErrNoContext
|
|
}
|
|
|
|
// NOTE: The following logic relaxes the restriction so that software metadata (e.g. id, name)
|
|
// can be returned even if the requesting user does not have permission to view any hosts where the software
|
|
// is installed. However, host-specific information remains protected and is only available if the
|
|
// user is authorized for the relevant teams. This ensures basic metadata visibility across all users
|
|
// while preserving team-based access control for sensitive, host-linked data.
|
|
software, err := svc.ds.SoftwareByID(ctx, id, teamID, includeCVEScores, &fleet.TeamFilter{
|
|
User: vc.User,
|
|
IncludeObserver: true,
|
|
})
|
|
if err != nil {
|
|
if fleet.IsNotFound(err) && teamID == nil {
|
|
// here we use a global admin as filter because we want
|
|
// to check if the software version exists
|
|
filter := fleet.TeamFilter{User: &fleet.User{GlobalRole: ptr.String(fleet.RoleAdmin)}}
|
|
sw, err := svc.ds.SoftwareByID(ctx, id, teamID, includeCVEScores, &filter)
|
|
if err != nil {
|
|
// Not found anywhere
|
|
return nil, ctxerr.Wrap(ctx, err, "software not found for any team")
|
|
}
|
|
// Found, but user has no permission to hosts it's installed on.
|
|
// Instead of PermissionError, return a stub with the name.
|
|
stub := &fleet.Software{
|
|
ID: id,
|
|
Name: sw.Name,
|
|
}
|
|
return stub, nil
|
|
}
|
|
return nil, ctxerr.Wrap(ctx, err, "getting software version by id")
|
|
}
|
|
|
|
return software, nil
|
|
}
|
|
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
// Count
|
|
/////////////////////////////////////////////////////////////////////////////////
|
|
|
|
type countSoftwareRequest struct {
|
|
fleet.SoftwareListOptions
|
|
}
|
|
|
|
type countSoftwareResponse struct {
|
|
Count int `json:"count"`
|
|
Err error `json:"error,omitempty"`
|
|
}
|
|
|
|
func (r countSoftwareResponse) Error() error { return r.Err }
|
|
|
|
// Deprecated: counts are now included directly in the listSoftwareVersionsResponse. This
|
|
// endpoint is retained for backwards compatibility.
|
|
func countSoftwareEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (fleet.Errorer, error) {
|
|
req := request.(*countSoftwareRequest)
|
|
count, err := svc.CountSoftware(ctx, req.SoftwareListOptions)
|
|
if err != nil {
|
|
return countSoftwareResponse{Err: err}, nil
|
|
}
|
|
return countSoftwareResponse{Count: count}, nil
|
|
}
|
|
|
|
func (svc Service) CountSoftware(ctx context.Context, opt fleet.SoftwareListOptions) (int, error) {
|
|
if err := svc.authz.Authorize(ctx, &fleet.AuthzSoftwareInventory{
|
|
TeamID: opt.TeamID,
|
|
}, fleet.ActionRead); err != nil {
|
|
return 0, err
|
|
}
|
|
|
|
lic, err := svc.License(ctx)
|
|
if err != nil {
|
|
return 0, ctxerr.Wrap(ctx, err, "get license")
|
|
}
|
|
|
|
// Vulnerability filters are only available in premium
|
|
if !lic.IsPremium() && (opt.MaximumCVSS > 0 || opt.MinimumCVSS > 0 || opt.KnownExploit) {
|
|
return 0, fleet.ErrMissingLicense
|
|
}
|
|
|
|
// required for vulnerability filters
|
|
if lic.IsPremium() {
|
|
opt.IncludeCVEScores = true
|
|
}
|
|
|
|
return svc.ds.CountSoftware(ctx, opt)
|
|
}
|