Skip to content

Commit

Permalink
Merge branch 'feat-fleet-app-library' into fleet-managed-apps-list-apps
Browse files Browse the repository at this point in the history
  • Loading branch information
dantecatalfamo authored Sep 18, 2024
2 parents d2669a6 + 92e0da0 commit b510c95
Show file tree
Hide file tree
Showing 65 changed files with 1,294 additions and 233 deletions.
1 change: 1 addition & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ define HELP_TEXT
make generate-go - Generate and bundle required go code
make generate-js - Generate and bundle required js code
make generate-dev - Generate and bundle required code in a watch loop
make generate-doc - Generate updated API documentation for activities, osquery flags

make clean - Clean all build artifacts
make clean-assets - Clean assets only
Expand Down
6 changes: 3 additions & 3 deletions articles/mdm-migration.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ This guide provides instructions for migrating devices from your current MDM sol

<img width="1400" alt="My device page - turn on MDM" src="https://user-images.githubusercontent.com/5359586/229950406-98343bf7-9653-4117-a8f5-c03359ba0d86.png">

## Migrate automatically enrolled (DEP) hosts
## Migrate automatically enrolled (ADE) hosts

> Automatic enrollment is available in Fleet Premium or Ultimate
Expand Down Expand Up @@ -100,7 +100,7 @@ Configuring the end user migration workflow requires a few additional steps.

> Available in Fleet Premium or Ultimate
The end user migration workflow is supported for automatically enrolled (DEP) hosts.
The end user migration workflow is supported for automatically enrolled (ADE) hosts.

To watch a GIF that walks through the end user experience during the migration workflow, in the Fleet UI, head to **Settings > Integrations > Mobile device management (MDM)**, and scroll down to the **End user migration workflow** section.

Expand Down Expand Up @@ -188,7 +188,7 @@ After turning on disk encryption in Fleet, share [these guided instructions](#ho

## Activation Lock

In Fleet, the [Activation Lock](https://support.apple.com/en-us/HT208987) feature is disabled by default for automatically enrolled (DEP) hosts.
In Fleet, the [Activation Lock](https://support.apple.com/en-us/HT208987) feature is disabled by default for automatically enrolled (ADE) hosts.

In 2024, Apple added the ability to manage activation lock in Apple Business Manager (ABM). For devices that are owned by the business and available in ABM, you can [turn off activation lock remotely](https://support.apple.com/en-ca/guide/apple-business-manager/axm812df1dd8/web).

Expand Down
8 changes: 4 additions & 4 deletions articles/tales-from-fleet-security-securing-the-startup.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ I thought using Apple’s Automated Device Enrollment (or Device Enrollment Prog

Technically, I was not wrong, but there are non-technical challenges.

1. The requirements to establish a DEP account vary by country. In the US, for example, it requires a [DUNS](https://en.wikipedia.org/wiki/Data_Universal_Numbering_System) number. Getting a DUNS number is simple for US companies, but what is not easy is to fulfill similar requirements in every country where you would like to use DEP. We could not register for DEP in Canada. We have people in many other countries with a similar situation.
1. The requirements to establish a ADE account vary by country. In the US, for example, it requires a [DUNS](https://en.wikipedia.org/wiki/Data_Universal_Numbering_System) number. Getting a DUNS number is simple for US companies, but what is not easy is to fulfill similar requirements in every country where you would like to use ADE. We could not register for ADE in Canada. We have people in many other countries with a similar situation.
2. The delays for obtaining hardware are very long. When planning endpoint deployment strategies, we must consider this, as supply chain issues will not disappear soon.
3. The benchmarks made by the Center for Internet Security (CIS) are excellent but are incredibly long (700+ pages) and written for experts. We wanted to be transparent about why we configured company devices a certain way and explain it so everyone could understand without Googling for hours.

Expand All @@ -55,9 +55,9 @@ Google should offer more granularity than on/off for third-party cookies, such a

## Solutions

### DEP in other countries
### ADE in other countries

First, we enrolled in DEP in the US. Once we had our customer numbers and Mobile Device Management (MDM) system linked up, we were ready to buy laptops in the US that would get configured out of the box. Then, we found a workaround for Canada. If you add Apple’s Reseller ID to [Apple Business Manager](https://business.apple.com/), you can order computers over the phone and have them linked to your business account. The Reseller ID part is critical. I learned that the hard way, by receiving a laptop ordered like this to find it not part of DEP. Fortunately, it was easy for me to [add it to DEP manually](https://support.apple.com/en-ca/guide/apple-configurator/welcome/ios).
First, we enrolled in ADE in the US. Once we had our customer numbers and Mobile Device Management (MDM) system linked up, we were ready to buy laptops in the US that would get configured out of the box. Then, we found a workaround for Canada. If you add Apple’s Reseller ID to [Apple Business Manager](https://business.apple.com/), you can order computers over the phone and have them linked to your business account. The Reseller ID part is critical. I learned that the hard way, by receiving a laptop ordered like this to find it not part of ADE. Fortunately, it was easy for me to [add it to ADE manually](https://support.apple.com/en-ca/guide/apple-configurator/welcome/ios).

We will keep trying the same approach in every country where we need Macs, though we know it will not be possible everywhere. We will either obtain equipment from a nearby country or rely on manual MDM enrollment by end-users for those countries.

Expand All @@ -76,7 +76,7 @@ Using the [CIS Benchmark for macOS 12](https://www.cisecurity.org/benchmark/appl

### Effort

Implementing our own security baseline, configuring our MDM and DEP required a couple of days of effort, mostly because I insisted on reviewing all of the CIS Benchmark to be certain I didn’t miss something important. Having everything published in our handbook required additional effort, but if you were to use our baseline, you could get started very quickly. The main thing that will slow you down is getting onboarded to DEP, and receiving your first laptop ordered!
Implementing our own security baseline, configuring our MDM and ADE required a couple of days of effort, mostly because I insisted on reviewing all of the CIS Benchmark to be certain I didn’t miss something important. Having everything published in our handbook required additional effort, but if you were to use our baseline, you could get started very quickly. The main thing that will slow you down is getting onboarded to ADE, and receiving your first laptop ordered!

## What's next?

Expand Down
1 change: 1 addition & 0 deletions changes/21612-edit-software-gitops
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Reset install counts and cancel pending installs/uninstalls when GitOps installer updates change package contents
1 change: 1 addition & 0 deletions changes/21776-add-software
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Adds the `POST /software/fleet_maintained` endpoint for adding Fleet-maintained apps.
1 change: 1 addition & 0 deletions changes/22106-fix-software-package-name
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Fixed UI design bug where software package file name was not displayed as expected.
1 change: 1 addition & 0 deletions changes/22136-software-status-no-teams-hosts-page
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Support the software status filter for 'No teams' on the hosts page
1 change: 1 addition & 0 deletions changes/22158-scep
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
* Allow custom SCEP CA certificates with any kind of extendedKeyUsage attributes.
2 changes: 1 addition & 1 deletion cmd/fleetctl/get_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2320,7 +2320,7 @@ func TestGetTeamsYAMLAndApply(t *testing.T) {
declaration.DeclarationUUID = uuid.NewString()
return declaration, nil
}
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}

Expand Down
41 changes: 32 additions & 9 deletions cmd/fleetctl/gitops_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
ds.NewJobFunc = func(ctx context.Context, job *fleet.Job) (*fleet.Job, error) {
return &fleet.Job{}, nil
}
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}

Expand Down Expand Up @@ -373,7 +373,7 @@ func TestGitOpsBasicTeam(t *testing.T) {
ds.DeleteMDMAppleDeclarationByNameFunc = func(ctx context.Context, teamID *uint, name string) error {
return nil
}
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
ds.ApplyEnrollSecretsFunc = func(ctx context.Context, teamID *uint, secrets []*fleet.EnrollSecret) error {
Expand Down Expand Up @@ -804,7 +804,7 @@ func TestGitOpsFullTeam(t *testing.T) {
return nil
}
var appliedSoftwareInstallers []*fleet.UploadSoftwareInstallerPayload
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
appliedSoftwareInstallers = installers
return nil, nil
}
Expand Down Expand Up @@ -1055,7 +1055,7 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
savedTeam = team
return team, nil
}
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
Expand Down Expand Up @@ -1317,7 +1317,7 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
savedTeam = team
return team, nil
}
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
Expand Down Expand Up @@ -1442,6 +1442,20 @@ software:
`)
require.NoError(t, err)

noTeamFilePathPoliciesCalendarPath := filepath.Join(t.TempDir(), "no-team.yml")
noTeamFilePathPoliciesCalendar, err := os.Create(noTeamFilePathPoliciesCalendarPath)
require.NoError(t, err)
_, err = noTeamFilePathPoliciesCalendar.WriteString(`
controls:
policies:
- name: Foobar
query: SELECT 1 FROM osquery_info WHERE start_time < 0;
calendar_events_enabled: true
name: No team
software:
`)
require.NoError(t, err)

noTeamFilePathWithControls := filepath.Join(t.TempDir(), "no-team.yml")
noTeamFileWithControls, err := os.Create(noTeamFilePathWithControls)
require.NoError(t, err)
Expand Down Expand Up @@ -1480,16 +1494,25 @@ software:
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml"))
// Real run, both global and no-team.yml define controls.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithControls.Name(), "--dry-run"})
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithControls.Name()})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'controls' cannot be set on both global config and on no-team.yml"))

// Dry run, both global and no-team.yml defines policy with calendar events enabled.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFilePathPoliciesCalendar.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())
// Real run, both global and no-team.yml define controls.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithControls.Name(), "-f", teamFile.Name(), "-f", noTeamFilePathPoliciesCalendar.Name()})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "calendar events are not supported on \"No team\" policies: \"Foobar\""), err.Error())

// Dry run, controls should be defined somewhere, either in no-team.yml or global.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name(), "--dry-run"})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml"))
// Real run, both global and no-team.yml define controls.
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name(), "--dry-run"})
_, err = runAppNoChecks([]string{"gitops", "-f", globalFileWithoutControlsAndSoftwareKeys.Name(), "-f", teamFile.Name(), "-f", noTeamFileWithoutControls.Name()})
require.Error(t, err)
assert.True(t, strings.Contains(err.Error(), "'controls' must be set on global config or no-team.yml"))

Expand Down Expand Up @@ -1645,7 +1668,7 @@ func TestGitOpsTeamSoftwareInstallersQueryEnv(t *testing.T) {

t.Setenv("QUERY_VAR", "IT_WORKS")

ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, tmID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
if installers[0].PreInstallQuery != "select IT_WORKS" {
return nil, fmt.Errorf("Missing env var, got %s", installers[0].PreInstallQuery)
}
Expand Down Expand Up @@ -2158,7 +2181,7 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
declaration.DeclarationUUID = uuid.NewString()
return declaration, nil
}
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwareInstaller, error) {
ds.BatchSetSoftwareInstallersFunc = func(ctx context.Context, teamID *uint, installers []*fleet.UploadSoftwareInstallerPayload) ([]fleet.SoftwarePackageResponse, error) {
return nil, nil
}

Expand Down
2 changes: 1 addition & 1 deletion docs/Contributing/Audit-logs.md
Original file line number Diff line number Diff line change
Expand Up @@ -1222,7 +1222,7 @@ Generated when a software installer is updated in Fleet.

This activity contains the following fields:
- "software_title": Name of the software.
- "software_package": Filename of the installer. `null` if the installer package was not modified.
- "software_package": Filename of the installer as of this update (including if unchanged).
- "team_name": Name of the team on which this software was updated. `null` if it was updated on no team.
- "team_id": The ID of the team on which this software was updated. `null` if it was updated on no team.
- "self_service": Whether the software is available for installation by the end user.
Expand Down
103 changes: 103 additions & 0 deletions ee/server/service/maintained_apps.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package service

import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"

"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/mdm/maintainedapps"
)

func (svc *Service) AddFleetMaintainedApp(ctx context.Context, teamID *uint, appID uint, installScript, preInstallQuery, postInstallScript string, selfService bool) error {
if err := svc.authz.Authorize(ctx, &fleet.SoftwareInstaller{TeamID: teamID}, fleet.ActionWrite); err != nil {
return err
}

vc, ok := viewer.FromContext(ctx)
if !ok {
return fleet.ErrNoContext
}

app, err := svc.ds.GetMaintainedAppByID(ctx, appID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting maintained app by id")
}

// Download installer from the URL
installerBytes, filename, err := maintainedapps.DownloadInstaller(ctx, app.InstallerURL)
if err != nil {
return ctxerr.Wrap(ctx, err, "downloading app installer")
}

// Validate the bytes we got are what we expected
h := sha256.New()
_, err = h.Write(installerBytes)
if err != nil {
return ctxerr.Wrap(ctx, err, "generating SHA256 of maintained app installer")
}
gotHash := hex.EncodeToString(h.Sum(nil))

if gotHash != app.SHA256 {
return ctxerr.New(ctx, "mismatch in maintained app SHA256 hash")
}

// Fall back to the filename if we weren't able to extract a filename from the installer response
if filename == "" {
filename = app.Name
}

installerReader := bytes.NewReader(installerBytes)
payload := &fleet.UploadSoftwareInstallerPayload{
InstallerFile: installerReader,
Title: app.Name,
UserID: vc.UserID(),
TeamID: teamID,
Version: app.Version,
Filename: filename,
Platform: string(app.Platform),
BundleIdentifier: app.BundleIdentifier,
StorageID: app.SHA256,
FleetLibraryAppID: &app.ID,
PreInstallQuery: preInstallQuery,
PostInstallScript: postInstallScript,
SelfService: selfService,
InstallScript: installScript,
}

// Create record in software installers table
_, err = svc.ds.MatchOrCreateSoftwareInstaller(ctx, payload)
if err != nil {
return ctxerr.Wrap(ctx, err, "setting downloaded installer")
}

// Save in S3
if err := svc.storeSoftware(ctx, payload); err != nil {
return ctxerr.Wrap(ctx, err, "upload maintained app installer to S3")
}

// Create activity
var teamName *string
if payload.TeamID != nil && *payload.TeamID != 0 {
t, err := svc.ds.Team(ctx, *payload.TeamID)
if err != nil {
return ctxerr.Wrap(ctx, err, "getting team")
}
teamName = &t.Name
}

if err := svc.NewActivity(ctx, vc.User, fleet.ActivityTypeAddedSoftware{
SoftwareTitle: payload.Title,
SoftwarePackage: payload.Filename,
TeamName: teamName,
TeamID: payload.TeamID,
SelfService: payload.SelfService,
}); err != nil {
return ctxerr.Wrap(ctx, err, "creating activity for added software")
}

return nil
}
Loading

0 comments on commit b510c95

Please sign in to comment.