Files
Magnus Jensen cef60cee25 support management in osquery-perf DDM and update stats output (#50561)
<!-- Add the related story/sub-task/bug number, like Resolves #123, or
remove if NA -->
**Related issue:** Resolves #50282


# 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. (Part of a bigger story)

- [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] Timeouts are implemented and retries are limited to avoid infinite
loops
- [x] If paths of existing endpoints are modified without backwards
compatibility, checked the frontend/CLI for any necessary changes

## Testing

- [ ] Added/updated automated tests

- [x] QA'd all new/changed functionality manually

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

- **New Features**
- Added support for tracking and reporting management declarations
alongside other device management statuses.
- Added success and error metrics for management declaration processing.
- Included management declarations in synchronization, caching, and
status reporting across device and user channels.

- **Improvements**
- Updated performance output with clearer, structured sections for host,
MDM, DDM, Android, and PSSO metrics.
  - Added consolidated reporting for management declaration activity.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
2026-08-06 17:38:29 +02:00

316 lines
9.7 KiB
Go

package main
import (
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"github.com/fleetdm/fleet/v4/server/fleet"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
)
// ddmMethods is a helper struct to abstract method calls into their respective user/device methods.
type ddmMethods struct {
DeclarativeManagement func(endpoint string, data ...fleet.MDMAppleDDMStatusReport) (*http.Response, error)
IncrementTokensErrors func()
IncrementTokensSuccess func()
IncrementDeclarationItemsErrors func()
IncrementDeclarationItemsSuccess func()
IncrementConfigurationErrors func()
IncrementConfigurationSuccess func()
IncrementManagementErrors func()
IncrementManagementSuccess func()
IncrementActivationErrors func()
IncrementActivationSuccess func()
IncrementAssetErrors func()
IncrementAssetSuccess func()
IncrementStatusErrors func()
IncrementStatusSuccess func()
getGlobalToken func() string
setGlobalToken func(token string)
getDeclTokens func() map[string]string
setDeclTokens func(tokens map[string]string)
}
func (a *agent) doDeclarativeManagement(cmd *mdm.Command, methods ddmMethods) {
const maxAttempts = 3
// prevToken starts as the last-applied global token. On each iteration,
// a tokens fetch is compared to it: if it matches, the server has settled
// (or nothing changed on the first pass). If it differs, we sync
// declaration-items and fetch changed declarations, then loop to check
// again (mimicking the real device behavior, see
// https://github.com/fleetdm/fleet/issues/43050#issuecomment-4252241277).
prevToken := methods.getGlobalToken()
var items *fleet.MDMAppleDDMDeclarationItemsResponse
var currentTokens map[string]string
changed := false
for range maxAttempts {
// Fetch tokens — on the first pass this is the initial check against
// the cached token; on subsequent passes it is the convergence check
// for the previous iteration's sync.
globalToken, err := a.ddmFetchTokens(methods)
if err != nil {
return
}
if globalToken == prevToken {
break // nothing changed, or server has settled
}
// Fetch declaration-items manifest
items, err = a.ddmFetchDeclarationItems(methods)
if err != nil {
return
}
// Check each manifest item against cached tokens, fetch changed ones.
currentTokens = make(map[string]string, len(items.Declarations.Activations)+len(items.Declarations.Configurations)+len(items.Declarations.Assets)+len(items.Declarations.Management))
for _, d := range items.Declarations.Activations {
currentTokens[d.Identifier] = d.ServerToken
if methods.getDeclTokens()[d.Identifier] != d.ServerToken {
if err := a.ddmFetchDeclaration("activation", d.Identifier, methods); err != nil {
return
}
changed = true
}
}
for _, d := range items.Declarations.Assets {
currentTokens[d.Identifier] = d.ServerToken
if methods.getDeclTokens()[d.Identifier] != d.ServerToken {
if err := a.ddmFetchDeclaration("asset", d.Identifier, methods); err != nil {
return
}
changed = true
}
}
for _, d := range items.Declarations.Configurations {
currentTokens[d.Identifier] = d.ServerToken
if methods.getDeclTokens()[d.Identifier] != d.ServerToken {
if err := a.ddmFetchDeclaration("configuration", d.Identifier, methods); err != nil {
return
}
changed = true
}
}
for _, d := range items.Declarations.Management {
currentTokens[d.Identifier] = d.ServerToken
if methods.getDeclTokens()[d.Identifier] != d.ServerToken {
if err := a.ddmFetchDeclaration("management", d.Identifier, methods); err != nil {
return
}
changed = true
}
}
// Check for removed items (in cache but not in manifest) - no need to check if changes are
// already detected, as the whole set of declaration tokens will get replaced with the current ones.
// This is just to detect the case where the only change is a removal.
if !changed {
for id := range methods.getDeclTokens() {
if _, ok := currentTokens[id]; !ok {
changed = true
break
}
}
}
prevToken = globalToken
}
if !changed || items == nil {
return
}
// Server has settled (or max attempts exhausted) and declarations
// changed — send a single consolidated status report and update cache.
if err := a.ddmSendStatus(items, methods); err != nil {
return
}
methods.setGlobalToken(prevToken)
methods.setDeclTokens(currentTokens)
}
func (a *agent) ddmFetchTokens(methods ddmMethods) (string, error) {
r, err := methods.DeclarativeManagement("tokens")
if err != nil {
log.Printf("DDM tokens request failed: %s", err)
methods.IncrementTokensErrors()
return "", err
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("DDM tokens read body failed: %s", err)
methods.IncrementTokensErrors()
return "", err
}
var resp fleet.MDMAppleDDMTokensResponse
if err := json.Unmarshal(body, &resp); err != nil {
log.Printf("DDM tokens unmarshal failed: %s", err)
methods.IncrementTokensErrors()
return "", err
}
methods.IncrementTokensSuccess()
return resp.SyncTokens.DeclarationsToken, nil
}
func (a *agent) ddmFetchDeclarationItems(methods ddmMethods) (*fleet.MDMAppleDDMDeclarationItemsResponse, error) {
r, err := methods.DeclarativeManagement("declaration-items")
if err != nil {
log.Printf("DDM declaration-items request failed: %s", err)
methods.IncrementDeclarationItemsErrors()
return nil, err
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("DDM declaration-items read body failed: %s", err)
methods.IncrementDeclarationItemsErrors()
return nil, err
}
var items fleet.MDMAppleDDMDeclarationItemsResponse
if err := json.Unmarshal(body, &items); err != nil {
log.Printf("DDM declaration-items unmarshal failed: %s", err)
methods.IncrementDeclarationItemsErrors()
return nil, err
}
methods.IncrementDeclarationItemsSuccess()
return &items, nil
}
func (a *agent) ddmFetchDeclaration(kind, identifier string, methods ddmMethods) error {
path := fmt.Sprintf("declaration/%s/%s", kind, identifier)
r, err := methods.DeclarativeManagement(path)
if err != nil {
log.Printf("DDM %s request failed: %s", path, err)
a.ddmIncrementDeclError(kind, methods)
return err
}
defer r.Body.Close()
body, err := io.ReadAll(r.Body)
if err != nil {
log.Printf("DDM %s read body failed: %s", path, err)
a.ddmIncrementDeclError(kind, methods)
return err
}
switch kind {
case "activation":
var act fleet.MDMAppleDDMActivation
if err := json.Unmarshal(body, &act); err != nil {
log.Printf("DDM %s unmarshal failed: %s", path, err)
a.ddmIncrementDeclError(kind, methods)
return err
}
methods.IncrementActivationSuccess()
case "configuration":
var decl fleet.MDMAppleDeclaration
if err := json.Unmarshal(body, &decl); err != nil {
log.Printf("DDM %s unmarshal failed: %s", path, err)
a.ddmIncrementDeclError(kind, methods)
return err
}
methods.IncrementConfigurationSuccess()
case "asset":
var asset fleet.RawDDMAsset
if err := json.Unmarshal(body, &asset); err != nil {
log.Printf("DDM %s unmarshal failed: %s", path, err)
a.ddmIncrementDeclError(kind, methods)
return err
}
methods.IncrementAssetSuccess()
case "management":
var decl fleet.MDMAppleDeclaration
if err := json.Unmarshal(body, &decl); err != nil {
log.Printf("DDM %s unmarshal failed: %s", path, err)
a.ddmIncrementDeclError(kind, methods)
return err
}
methods.IncrementManagementSuccess()
}
return nil
}
func (a *agent) ddmIncrementDeclError(kind string, methods ddmMethods) {
switch kind {
case "activation":
methods.IncrementActivationErrors()
case "configuration":
methods.IncrementConfigurationErrors()
case "asset":
methods.IncrementAssetErrors()
case "management":
methods.IncrementManagementErrors()
}
}
func (a *agent) ddmSendStatus(items *fleet.MDMAppleDDMDeclarationItemsResponse, methods ddmMethods) error {
report := fleet.MDMAppleDDMStatusReport{}
for _, d := range items.Declarations.Activations {
report.StatusItems.Management.Declarations.Activations = append(
report.StatusItems.Management.Declarations.Activations,
fleet.MDMAppleDDMStatusDeclaration{
Active: true, Valid: fleet.MDMAppleDeclarationValid,
Identifier: d.Identifier, ServerToken: d.ServerToken,
},
)
}
for _, d := range items.Declarations.Assets {
report.StatusItems.Management.Declarations.Assets = append(
report.StatusItems.Management.Declarations.Assets,
fleet.MDMAppleDDMStatusDeclaration{
Active: true, Valid: fleet.MDMAppleDeclarationValid,
Identifier: d.Identifier, ServerToken: d.ServerToken,
},
)
}
for _, d := range items.Declarations.Configurations {
report.StatusItems.Management.Declarations.Configurations = append(
report.StatusItems.Management.Declarations.Configurations,
fleet.MDMAppleDDMStatusDeclaration{
Active: true, Valid: fleet.MDMAppleDeclarationValid,
Identifier: d.Identifier, ServerToken: d.ServerToken,
},
)
}
for _, d := range items.Declarations.Management {
report.StatusItems.Management.Declarations.Management = append(
report.StatusItems.Management.Declarations.Management,
fleet.MDMAppleDDMStatusDeclaration{
Active: true, Valid: fleet.MDMAppleDeclarationValid,
Identifier: d.Identifier, ServerToken: d.ServerToken,
},
)
}
r, err := methods.DeclarativeManagement("status", report)
if err != nil {
log.Printf("DDM status request failed: %s", err)
methods.IncrementStatusErrors()
return err
}
defer r.Body.Close()
_, _ = io.Copy(io.Discard, r.Body)
if r.StatusCode != http.StatusOK {
log.Printf("DDM status response unexpected: %d", r.StatusCode)
methods.IncrementStatusErrors()
return fmt.Errorf("unexpected status code: %d", r.StatusCode)
}
methods.IncrementStatusSuccess()
return nil
}