fleet/server/service/campaigns.go
Lucas Manuel Rodriguez 02ea8b104b
Remove attach check on queries and return proper bad request error (#10427)
Fixes both #10378 and
https://github.com/fleetdm/confidential/issues/2133

On `main`:
```sh
curl -v -k -X POST -H "Authorization: Bearer $TEST_TOKEN" \
https://localhost:8080/api/latest/fleet/queries/run \
-d '{ "query": "select \"With automounting enabled anyone with physical access could attach a USB drive or disc and have its contents available in system even if they lacked permissions to mount it themselves.\" as Rationale;" }'

< HTTP/2 500
< content-type: application/json; charset=utf-8
< content-length: 130
< date: Fri, 10 Mar 2023 17:50:40 GMT
<
{
  "message": "invalid query's SQL",
  "errors": [
    {
      "name": "base",
      "reason": "invalid query's SQL"
    }
  ]
}
```
With changes in this PR:
```sh
curl -v -k -X POST -H "Authorization: Bearer $TEST_TOKEN" \
https://localhost:8080/api/latest/fleet/queries/run \
-d '{ "query": "select \"With automounting enabled anyone with physical access could attach a USB drive or disc and have its contents available in system even if they lacked permissions to mount it themselves.\" as Rationale;", "selected": { "hosts": [57] } }'

< HTTP/2 200
< content-type: application/json; charset=utf-8
< content-length: 325
< date: Fri, 10 Mar 2023 17:49:40 GMT
<
{
  "campaign": {
    "created_at": "0001-01-01T00:00:00Z",
    "updated_at": "0001-01-01T00:00:00Z",
    "Metrics": {
      "TotalHosts": 1,
      "OnlineHosts": 1,
      "OfflineHosts": 0,
      "MissingInActionHosts": 0,
      "NewHosts": 0
    },
    "id": 87,
    "query_id": 85,
    "status": 0,
    "user_id": 1
  }
}
```

- [X] Changes file added for user-visible changes in `changes/` or
`orbit/changes/`.
See [Changes
files](https://fleetdm.com/docs/contributing/committing-changes#changes-files)
for more information.
- ~[ ] Documented any API changes (docs/Using-Fleet/REST-API.md or
docs/Contributing/API-for-contributors.md)~
- ~[ ] Documented any permissions changes~
- ~[ ] Input data is properly validated, `SELECT *` is avoided, SQL
injection is prevented (using placeholders for values in statements)~
- ~[ ] Added support on fleet's osquery simulator `cmd/osquery-perf` for
new osquery data ingestion features.~
- [X] Added/updated tests
- [X] Manual QA for all new/changed functionality
  - ~For Orbit and Fleet Desktop changes:~
- ~[ ] Manual QA must be performed in the three main OSs, macOS, Windows
and Linux.~
- ~[ ] Auto-update manual QA, from released version of component to new
version (see [tools/tuf/test](../tools/tuf/test/README.md)).~
2023-03-13 11:42:26 -03:00

229 lines
7.2 KiB
Go

package service
import (
"context"
"fmt"
"strconv"
"strings"
"time"
"github.com/fleetdm/fleet/v4/server/authz"
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/contexts/logging"
"github.com/fleetdm/fleet/v4/server/contexts/viewer"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/ptr"
)
////////////////////////////////////////////////////////////////////////////////
// Create Distributed Query Campaign
////////////////////////////////////////////////////////////////////////////////
type createDistributedQueryCampaignRequest struct {
QuerySQL string `json:"query"`
QueryID *uint `json:"query_id"`
Selected fleet.HostTargets `json:"selected"`
}
type createDistributedQueryCampaignResponse struct {
Campaign *fleet.DistributedQueryCampaign `json:"campaign,omitempty"`
Err error `json:"error,omitempty"`
}
func (r createDistributedQueryCampaignResponse) error() error { return r.Err }
func createDistributedQueryCampaignEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*createDistributedQueryCampaignRequest)
campaign, err := svc.NewDistributedQueryCampaign(ctx, req.QuerySQL, req.QueryID, req.Selected)
if err != nil {
return createDistributedQueryCampaignResponse{Err: err}, nil
}
return createDistributedQueryCampaignResponse{Campaign: campaign}, nil
}
func (svc *Service) NewDistributedQueryCampaign(ctx context.Context, queryString string, queryID *uint, targets fleet.HostTargets) (*fleet.DistributedQueryCampaign, error) {
if err := svc.StatusLiveQuery(ctx); err != nil {
return nil, err
}
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, fleet.ErrNoContext
}
if queryID == nil && strings.TrimSpace(queryString) == "" {
return nil, fleet.NewInvalidArgumentError("query", "one of query or query_id must be specified")
}
var query *fleet.Query
var err error
if queryID != nil {
query, err = svc.ds.Query(ctx, *queryID)
if err != nil {
return nil, err
}
queryString = query.Query
} else {
if err := svc.authz.Authorize(ctx, &fleet.Query{}, fleet.ActionRunNew); err != nil {
return nil, err
}
query = &fleet.Query{
Name: fmt.Sprintf("distributed_%s_%d", vc.Email(), time.Now().UnixNano()),
Query: queryString,
Saved: false,
AuthorID: ptr.Uint(vc.UserID()),
}
if err := query.Verify(); err != nil {
return nil, ctxerr.Wrap(ctx, &fleet.BadRequestError{
Message: fmt.Sprintf("query payload verification: %s", err),
})
}
query, err = svc.ds.NewQuery(ctx, query)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "new query")
}
}
tq := &fleet.TargetedQuery{Query: query, HostTargets: targets}
if err := svc.authz.Authorize(ctx, tq, fleet.ActionRun); err != nil {
return nil, err
}
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: query.ObserverCanRun}
campaign, err := svc.ds.NewDistributedQueryCampaign(ctx, &fleet.DistributedQueryCampaign{
QueryID: query.ID,
Status: fleet.QueryWaiting,
UserID: vc.UserID(),
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "new campaign")
}
defer func() {
var numHosts uint
if campaign != nil {
numHosts = campaign.Metrics.TotalHosts
}
logging.WithExtras(ctx, "sql", queryString, "query_id", queryID, "numHosts", numHosts)
}()
// Add host targets
for _, hid := range targets.HostIDs {
_, err = svc.ds.NewDistributedQueryCampaignTarget(ctx, &fleet.DistributedQueryCampaignTarget{
Type: fleet.TargetHost,
DistributedQueryCampaignID: campaign.ID,
TargetID: hid,
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "adding host target")
}
}
// Add label targets
for _, lid := range targets.LabelIDs {
_, err = svc.ds.NewDistributedQueryCampaignTarget(ctx, &fleet.DistributedQueryCampaignTarget{
Type: fleet.TargetLabel,
DistributedQueryCampaignID: campaign.ID,
TargetID: lid,
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "adding label target")
}
}
// Add team targets
for _, tid := range targets.TeamIDs {
_, err = svc.ds.NewDistributedQueryCampaignTarget(ctx, &fleet.DistributedQueryCampaignTarget{
Type: fleet.TargetTeam,
DistributedQueryCampaignID: campaign.ID,
TargetID: tid,
})
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "adding team target")
}
}
hostIDs, err := svc.ds.HostIDsInTargets(ctx, filter, targets)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "get target IDs")
}
if len(hostIDs) == 0 {
return nil, &fleet.BadRequestError{
Message: "no hosts targeted",
}
}
err = svc.liveQueryStore.RunQuery(strconv.Itoa(int(campaign.ID)), queryString, hostIDs)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "run query")
}
campaign.Metrics, err = svc.ds.CountHostsInTargets(ctx, filter, targets, time.Now())
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "counting hosts")
}
activityData := fleet.ActivityTypeLiveQuery{
TargetsCount: campaign.Metrics.TotalHosts,
QuerySQL: query.Query,
}
if queryID != nil {
activityData.QueryName = &query.Name
}
if err := svc.ds.NewActivity(
ctx,
authz.UserFromContext(ctx),
activityData,
); err != nil {
return nil, ctxerr.Wrap(ctx, err, "create activity for campaign creation")
}
return campaign, nil
}
////////////////////////////////////////////////////////////////////////////////
// Create Distributed Query Campaign By Names
////////////////////////////////////////////////////////////////////////////////
type createDistributedQueryCampaignByNamesRequest struct {
QuerySQL string `json:"query"`
QueryID *uint `json:"query_id"`
Selected distributedQueryCampaignTargetsByNames `json:"selected"`
}
type distributedQueryCampaignTargetsByNames struct {
Labels []string `json:"labels"`
Hosts []string `json:"hosts"`
}
func createDistributedQueryCampaignByNamesEndpoint(ctx context.Context, request interface{}, svc fleet.Service) (errorer, error) {
req := request.(*createDistributedQueryCampaignByNamesRequest)
campaign, err := svc.NewDistributedQueryCampaignByNames(ctx, req.QuerySQL, req.QueryID, req.Selected.Hosts, req.Selected.Labels)
if err != nil {
return createDistributedQueryCampaignResponse{Err: err}, nil
}
return createDistributedQueryCampaignResponse{Campaign: campaign}, nil
}
func (svc *Service) NewDistributedQueryCampaignByNames(ctx context.Context, queryString string, queryID *uint, hosts []string, labels []string) (*fleet.DistributedQueryCampaign, error) {
vc, ok := viewer.FromContext(ctx)
if !ok {
return nil, fleet.ErrNoContext
}
filter := fleet.TeamFilter{User: vc.User, IncludeObserver: true}
hostIDs, err := svc.ds.HostIDsByName(ctx, filter, hosts)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "finding host IDs")
}
labelIDs, err := svc.ds.LabelIDsByName(ctx, labels)
if err != nil {
return nil, ctxerr.Wrap(ctx, err, "finding label IDs")
}
targets := fleet.HostTargets{HostIDs: hostIDs, LabelIDs: labelIDs}
return svc.NewDistributedQueryCampaign(ctx, queryString, queryID, targets)
}