mirror of
https://github.com/fleetdm/fleet
synced 2026-05-24 09:28:54 +00:00
Some checks are pending
Build binaries / build-binaries (push) Waiting to run
Check automated documentation is up-to-date / check-doc-gen (push) Waiting to run
CodeQL / Analyze (push) Waiting to run
Deploy Fleet website / build (20.x) (push) Waiting to run
Apply latest configuration to dogfood with GitOps / fleet-gitops (push) Waiting to run
Test latest changes in fleetctl preview / test-preview (ubuntu-latest) (push) Waiting to run
golangci-lint / lint (push) Waiting to run
golangci-lint / lint-incremental (push) Waiting to run
Docker publish / publish (push) Waiting to run
Ingest maintained apps / build (push) Waiting to run
OSSF Scorecard / Validate Gradle wrapper (push) Waiting to run
OSSF Scorecard / Scorecard analysis (push) Waiting to run
Sync Maintained Apps Outputs to R2 / sync-to-r2 (push) Waiting to run
Test DB Changes / test-db-changes (push) Waiting to run
Run fleetd-chrome tests / test-fleetd-chrome (ubuntu-latest) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, main) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, mysql) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, service) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, vuln) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, fleetctl) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-core) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-enterprise) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, main) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, mysql) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, service) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.4.8, vuln) (push) Waiting to run
Go Tests / test-go-nanomdm (push) Waiting to run
Go Tests / test-go-no-db (fast) (push) Waiting to run
Go Tests / test-go-no-db (scripts) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, main) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, mysql) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, service) (push) Waiting to run
Go Tests / test-go (mysql:8.0.44, vuln) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, fleetctl) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-core) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, integration-mdm) (push) Waiting to run
Go Tests / test-go-extended-mysql (mysql:8.0.42, integration-enterprise) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, main) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, mysql) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, service) (push) Waiting to run
Go Tests / test-go (mysql:9.5.0, vuln) (push) Waiting to run
Go Tests / upload-coverage (push) Blocked by required conditions
Go Tests / aggregate-result (push) Blocked by required conditions
JavaScript Tests / test-js (ubuntu-latest) (push) Waiting to run
JavaScript Tests / lint-js (ubuntu-latest) (push) Waiting to run
Test Mock Changes / test-mock-changes (push) Waiting to run
Test native tooling packaging / test-packaging (local, ubuntu-latest) (push) Waiting to run
Test native tooling packaging / test-packaging (remote, ubuntu-latest) (push) Waiting to run
Test Puppet / test-puppet (push) Waiting to run
<!-- Add the related story/sub-task/bug number, like Resolves #123, or remove if NA --> **Related issue:** Resolves #44723 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [x] 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. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements), JS inline code is prevented especially for url redirects, and untrusted data interpolated into shell scripts/commands is validated against shell metacharacters. - [x] If paths of existing endpoints are modified without backwards compatibility, checked the frontend/CLI for any necessary changes ## Testing - [x] Added/updated automated tests - [x] Where appropriate, [automated tests simulate multiple hosts and test for host isolation](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/reference/patterns-backend.md#unit-testing) (updates to one hosts's records do not affect another) - [ ] QA'd all new/changed functionality manually <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Bug Fixes** * Strengthened validation of sorting/order parameters across many list and cursor-based endpoints — unsupported sort keys now return explicit errors and prevent unsafe queries. * Labels listing: label-list pagination query name changed; ordering by host_count is rejected when host counts are disabled (validated at request parsing). * **Tests** * Added/expanded tests covering allowed order keys, rejection of unknown keys, and pagination behavior for multiple listing APIs. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Lucas Manuel Rodriguez <lucas@fleetdm.com>
273 lines
8.6 KiB
Go
273 lines
8.6 KiB
Go
package mysql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
|
|
"github.com/fleetdm/fleet/v4/server/fleet"
|
|
common_mysql "github.com/fleetdm/fleet/v4/server/platform/mysql"
|
|
"github.com/jmoiron/sqlx"
|
|
)
|
|
|
|
var maintainedAppsAllowedOrderKeys = common_mysql.OrderKeyAllowlist{
|
|
"id": "fma.id",
|
|
"name": "fma.name",
|
|
"platform": "fma.platform",
|
|
"slug": "fma.slug",
|
|
}
|
|
|
|
func (ds *Datastore) UpsertMaintainedApp(ctx context.Context, app *fleet.MaintainedApp) (*fleet.MaintainedApp, error) {
|
|
const upsertStmt = `
|
|
INSERT INTO
|
|
fleet_maintained_apps (name, slug, platform, unique_identifier)
|
|
VALUES
|
|
(?, ?, ?, ?)
|
|
ON DUPLICATE KEY UPDATE
|
|
name = VALUES(name),
|
|
platform = VALUES(platform),
|
|
unique_identifier = VALUES(unique_identifier)
|
|
`
|
|
|
|
var appID uint
|
|
err := ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
|
|
var err error
|
|
|
|
// upsert the maintained app
|
|
res, err := tx.ExecContext(ctx, upsertStmt, app.Name, app.Slug, app.Platform, app.UniqueIdentifier)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "upsert maintained app")
|
|
}
|
|
id, _ := res.LastInsertId()
|
|
appID = uint(id) //nolint:gosec // dismiss G115
|
|
|
|
// For darwin apps, update existing software_titles and software entries
|
|
// to use the FMA canonical name. This ensures consistency when an FMA
|
|
// is added for software that was previously ingested with osquery-reported names.
|
|
//
|
|
// We only run these UPDATEs when the FMA was actually inserted or modified.
|
|
// MySQL's ON DUPLICATE KEY UPDATE returns RowsAffected:
|
|
// 0 = duplicate key, no changes (existing FMA with same values)
|
|
// 1 = new row inserted
|
|
// 2 = duplicate key, values changed
|
|
// Skip if RowsAffected == 0 since nothing changed.
|
|
rowsAffected, _ := res.RowsAffected()
|
|
if app.Platform == "darwin" && app.UniqueIdentifier != "" && rowsAffected > 0 {
|
|
_, err = tx.ExecContext(ctx, `
|
|
UPDATE software_titles
|
|
SET name = ?
|
|
WHERE bundle_identifier = ?
|
|
AND name != ?
|
|
`, app.Name, app.UniqueIdentifier, app.Name)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "update software_titles names for FMA")
|
|
}
|
|
|
|
_, err = tx.ExecContext(ctx, `
|
|
UPDATE software
|
|
SET name = ?
|
|
WHERE bundle_identifier = ?
|
|
AND name != ?
|
|
`, app.Name, app.UniqueIdentifier, app.Name)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "update software names for FMA")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
app.ID = appID
|
|
return app, nil
|
|
}
|
|
|
|
const teamFMATitlesJoin = `
|
|
team_titles.id software_title_id FROM fleet_maintained_apps fma
|
|
LEFT JOIN (
|
|
SELECT DISTINCT st.id, st.unique_identifier, st.name, si.platform
|
|
FROM software_titles st
|
|
LEFT JOIN
|
|
software_installers si
|
|
ON si.title_id = st.id AND si.global_or_team_id = ?
|
|
AND si.platform IN ('darwin','windows')
|
|
LEFT JOIN
|
|
vpp_apps va
|
|
ON va.title_id = st.id
|
|
AND va.platform = 'darwin'
|
|
LEFT JOIN
|
|
vpp_apps_teams vat
|
|
ON vat.adam_id = va.adam_id
|
|
AND vat.platform = va.platform
|
|
AND vat.global_or_team_id = ?
|
|
WHERE si.id IS NOT NULL OR vat.id IS NOT NULL
|
|
) team_titles
|
|
ON team_titles.unique_identifier = fma.unique_identifier
|
|
-- pattern match fma name to a similar title name, since upgrade_code is not surfaced in fma table
|
|
OR (
|
|
team_titles.platform = fma.platform
|
|
AND fma.platform = 'windows'
|
|
-- Box Drive is the only FMA at the point of writing this where unique_identifier is shorter than name
|
|
AND team_titles.name LIKE CONCAT(LEAST(fma.name, fma.unique_identifier), '%')
|
|
)
|
|
`
|
|
|
|
func (ds *Datastore) GetMaintainedAppByID(ctx context.Context, appID uint, teamID *uint) (*fleet.MaintainedApp, error) {
|
|
stmt := `SELECT fma.id, fma.name, fma.platform, fma.unique_identifier, fma.slug, `
|
|
var args []any
|
|
|
|
if teamID != nil {
|
|
stmt += teamFMATitlesJoin
|
|
args = []any{teamID, teamID}
|
|
} else {
|
|
stmt += `NULL software_title_id FROM fleet_maintained_apps fma`
|
|
}
|
|
|
|
stmt += ` WHERE fma.id = ?`
|
|
args = append(args, appID)
|
|
|
|
var app fleet.MaintainedApp
|
|
if err := sqlx.GetContext(ctx, ds.reader(ctx), &app, stmt, args...); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ctxerr.Wrap(ctx, notFound("MaintainedApp"), "no matching maintained app found")
|
|
}
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "getting maintained app by id")
|
|
}
|
|
|
|
return &app, nil
|
|
}
|
|
|
|
func (ds *Datastore) GetMaintainedAppBySlug(ctx context.Context, slug string, teamID *uint) (*fleet.MaintainedApp, error) {
|
|
stmt := `SELECT fma.id, fma.name, fma.platform, fma.unique_identifier, fma.slug, `
|
|
var args []any
|
|
|
|
if teamID != nil {
|
|
stmt += teamFMATitlesJoin
|
|
args = []any{teamID, teamID}
|
|
} else {
|
|
stmt += `NULL software_title_id FROM fleet_maintained_apps fma`
|
|
}
|
|
|
|
stmt += ` WHERE fma.slug = ?`
|
|
args = append(args, slug)
|
|
|
|
var app fleet.MaintainedApp
|
|
if err := sqlx.GetContext(ctx, ds.reader(ctx), &app, stmt, args...); err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
return nil, ctxerr.Wrap(ctx, notFound("MaintainedApp"), "no matching maintained app found")
|
|
}
|
|
|
|
return nil, ctxerr.Wrap(ctx, err, "getting maintained app by slug")
|
|
}
|
|
|
|
return &app, nil
|
|
}
|
|
|
|
func (ds *Datastore) ListAvailableFleetMaintainedApps(ctx context.Context, teamID *uint, opt fleet.ListOptions) ([]fleet.MaintainedApp, *fleet.PaginationMetadata, error) {
|
|
stmt := `SELECT fma.id, fma.name, fma.platform, fma.slug, `
|
|
var args []any
|
|
|
|
if teamID != nil {
|
|
stmt += teamFMATitlesJoin + ` WHERE TRUE`
|
|
args = []any{teamID, teamID}
|
|
} else {
|
|
stmt += `NULL software_title_id FROM fleet_maintained_apps fma`
|
|
}
|
|
|
|
if match := opt.MatchQuery; match != "" {
|
|
match = likePattern(match)
|
|
stmt += ` AND (fma.name LIKE ?)`
|
|
args = append(args, match)
|
|
}
|
|
|
|
// perform a second query to grab the filtered count. Build the count statement before
|
|
// adding the pagination constraints to the stmt but after including the
|
|
// MatchQuery option sql.
|
|
dbReader := ds.reader(ctx)
|
|
getAppsCountStmt := fmt.Sprintf(`SELECT COUNT(DISTINCT s.id) FROM (%s) AS s`, stmt)
|
|
var filteredCount int
|
|
if err := sqlx.GetContext(ctx, dbReader, &filteredCount, getAppsCountStmt, args...); err != nil {
|
|
return nil, nil, ctxerr.Wrap(ctx, err, "get fleet maintained apps count")
|
|
}
|
|
|
|
if filteredCount == 0 { // check if we have nothing in the full apps list, in which case provide an error back
|
|
var totalCount int
|
|
if err := sqlx.GetContext(ctx, dbReader, &totalCount, `SELECT COUNT(id) FROM fleet_maintained_apps`); err != nil {
|
|
return nil, nil, ctxerr.Wrap(ctx, err, "get fleet maintained apps total count")
|
|
}
|
|
|
|
if totalCount == 0 {
|
|
return nil, nil, &fleet.NoMaintainedAppsInDatabaseError{}
|
|
}
|
|
}
|
|
|
|
stmtPaged, args, err := appendListOptionsWithCursorToSQLSecure(stmt, args, &opt, maintainedAppsAllowedOrderKeys)
|
|
if err != nil {
|
|
return nil, nil, ctxerr.Wrap(ctx, err, "list fleet maintained apps")
|
|
}
|
|
|
|
var avail []fleet.MaintainedApp
|
|
if err := sqlx.SelectContext(ctx, ds.reader(ctx), &avail, stmtPaged, args...); err != nil {
|
|
return nil, nil, ctxerr.Wrap(ctx, err, "selecting available fleet maintained apps")
|
|
}
|
|
|
|
meta := &fleet.PaginationMetadata{HasPreviousResults: opt.Page > 0, TotalResults: uint(filteredCount)} //nolint:gosec // dismiss G115
|
|
if len(avail) > int(opt.PerPage) { //nolint:gosec // dismiss G115
|
|
meta.HasNextResults = true
|
|
avail = avail[:len(avail)-1]
|
|
}
|
|
|
|
return avail, meta, nil
|
|
}
|
|
|
|
func (ds *Datastore) GetFMANamesByIdentifier(ctx context.Context) (map[string]string, error) {
|
|
query := `SELECT unique_identifier, name FROM fleet_maintained_apps WHERE platform = 'darwin'`
|
|
|
|
rows, err := ds.reader(ctx).QueryContext(ctx, query)
|
|
if err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "query FMA names by identifier")
|
|
}
|
|
defer rows.Close()
|
|
|
|
result := make(map[string]string)
|
|
for rows.Next() {
|
|
var identifier, name string
|
|
if err := rows.Scan(&identifier, &name); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "scan FMA name row")
|
|
}
|
|
result[identifier] = name
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, ctxerr.Wrap(ctx, err, "iterate FMA name rows")
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
func (ds *Datastore) ClearRemovedFleetMaintainedApps(ctx context.Context, slugsToKeep []string) error {
|
|
stmt := `DELETE FROM fleet_maintained_apps WHERE slug NOT IN (?)`
|
|
|
|
var err error
|
|
var args []any
|
|
switch len(slugsToKeep) {
|
|
case 0:
|
|
stmt = `DELETE FROM fleet_maintained_apps`
|
|
default:
|
|
stmt, args, err = sqlx.In(stmt, slugsToKeep)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "building sqlx.In statement for clearing removed maintained apps")
|
|
}
|
|
}
|
|
|
|
_, err = ds.writer(ctx).ExecContext(ctx, stmt, args...)
|
|
if err != nil {
|
|
return ctxerr.Wrap(ctx, err, "clearing removed maintained apps")
|
|
}
|
|
|
|
return nil
|
|
}
|