diff --git a/.github/workflows/build-binaries.yaml b/.github/workflows/build-binaries.yaml
index 278f958b2842..8499171c1583 100644
--- a/.github/workflows/build-binaries.yaml
+++ b/.github/workflows/build-binaries.yaml
@@ -37,11 +37,10 @@ jobs:
with:
go-version-file: 'go.mod'
- # Set the Node.js version
- - name: Set up Node.js ${{ vars.NODE_VERSION }}
+ - name: Set up Node.js
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
- node-version: ${{ vars.NODE_VERSION }}
+ node-version-file: package.json
- name: JS Dependency Cache
id: js-cache
@@ -51,7 +50,7 @@ jobs:
**/node_modules
# Use a separate cache for this from other JS jobs since we run the
# webpack steps and will have more to cache.
- key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}-node_version-${{ vars.NODE_VERSION }}
+ key: ${{ runner.os }}-node_modules-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-node_modules-
diff --git a/.github/workflows/fleet-and-orbit.yml b/.github/workflows/fleet-and-orbit.yml
index f4dfb2780eb7..22be676c7868 100644
--- a/.github/workflows/fleet-and-orbit.yml
+++ b/.github/workflows/fleet-and-orbit.yml
@@ -79,11 +79,10 @@ jobs:
with:
go-version-file: 'go.mod'
- # Set the Node.js version
- - name: Set up Node.js ${{ vars.NODE_VERSION }}
+ - name: Set up Node.js
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
- node-version: ${{ vars.NODE_VERSION }}
+ node-version-file: package.json
- name: Start tunnel
env:
diff --git a/.github/workflows/goreleaser-fleet.yaml b/.github/workflows/goreleaser-fleet.yaml
index 6ba9aff8f08f..7a23296ef2fe 100644
--- a/.github/workflows/goreleaser-fleet.yaml
+++ b/.github/workflows/goreleaser-fleet.yaml
@@ -46,11 +46,10 @@ jobs:
with:
go-version-file: 'go.mod'
- # Set the Node.js version
- - name: Set up Node.js ${{ vars.NODE_VERSION }}
+ - name: Set up Node.js
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
- node-version: ${{ vars.NODE_VERSION }}
+ node-version-file: package.json
- name: Install JS Dependencies
run: make deps-js
diff --git a/.github/workflows/goreleaser-snapshot-fleet.yaml b/.github/workflows/goreleaser-snapshot-fleet.yaml
index 927cf31be1da..f3c6a9340c96 100644
--- a/.github/workflows/goreleaser-snapshot-fleet.yaml
+++ b/.github/workflows/goreleaser-snapshot-fleet.yaml
@@ -60,10 +60,10 @@ jobs:
go-version-file: 'go.mod'
# Set the Node.js version
- - name: Set up Node.js ${{ vars.NODE_VERSION }}
+ - name: Set up Node.js
uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
with:
- node-version: ${{ vars.NODE_VERSION }}
+ node-version-file: package.json
- name: Install Dependencies
run: make deps
diff --git a/.github/workflows/test-db-changes.yml b/.github/workflows/test-db-changes.yml
index 2bd89ab82b1e..10d1ec4ddea2 100644
--- a/.github/workflows/test-db-changes.yml
+++ b/.github/workflows/test-db-changes.yml
@@ -40,33 +40,6 @@ jobs:
with:
fetch-depth: 0
- - name: Install Go
- uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
- with:
- go-version-file: 'go.mod'
-
- - name: Start Infra Dependencies
- # Use & to background this
- run: docker compose up -d mysql_test &
-
- - name: Wait for mysql
- run: |
- echo "waiting for mysql..."
- until docker compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
- echo "."
- sleep 1
- done
- echo "mysql is ready"
-
- - name: Verify test schema changes
- run: |
- make dump-test-schema
- if [[ $(git diff server/datastore/mysql/schema.sql) ]]; then
- echo "❌ fail: uncommited changes in schema.sql"
- echo "please run `make dump-test-schema` and commit the changes"
- exit 1
- fi
-
# TODO: This doesn't cover all scenarios since other PRs might
# be merged into `main` after this check has passed.
#
@@ -84,8 +57,8 @@ jobs:
base_ref=$(git tag --list "fleet-v*" --sort=-creatordate | head -n 1)
fi
- all_migrations=($(ls server/datastore/mysql/migrations/tables/20*_*.go | sort -r))
- new_migrations=($(git diff --find-renames --name-only --diff-filter=A $base_ref -- server/datastore/mysql/migrations/tables/20\*_\*.go | sort -r))
+ all_migrations=($(ls server/datastore/mysql/migrations/tables/20*_*.go | sort -r | grep -v '_test.go'))
+ new_migrations=($(git diff --find-renames --name-only --diff-filter=A $base_ref -- server/datastore/mysql/migrations/tables/20\*_\*.go ':(exclude,glob)server/datastore/mysql/migrations/tables/20*_*_test.go' | sort -r))
index=0
for migration in "${new_migrations[@]}"; do
@@ -110,3 +83,31 @@ jobs:
echo "Ref: https://github.com/fleetdm/fleet/blob/main/handbook/engineering/scaling-fleet.md#foreign-keys-and-locking"
exit 1
fi
+
+
+ - name: Install Go
+ uses: actions/setup-go@93397bea11091df50f3d7e59dc26a7711a8bcfbe # v4.1.0
+ with:
+ go-version-file: 'go.mod'
+
+ - name: Start Infra Dependencies
+ # Use & to background this
+ run: docker compose up -d mysql_test &
+
+ - name: Wait for mysql
+ run: |
+ echo "waiting for mysql..."
+ until docker compose exec -T mysql_test sh -c "mysql -uroot -p\"\${MYSQL_ROOT_PASSWORD}\" -e \"SELECT 1=1\" fleet" &> /dev/null; do
+ echo "."
+ sleep 1
+ done
+ echo "mysql is ready"
+
+ - name: Verify test schema changes
+ run: |
+ make dump-test-schema
+ if [[ $(git diff server/datastore/mysql/schema.sql) ]]; then
+ echo "❌ fail: uncommited changes in schema.sql"
+ echo "please run `make dump-test-schema` and commit the changes"
+ exit 1
+ fi
\ No newline at end of file
diff --git a/.github/workflows/test-js.yml b/.github/workflows/test-js.yml
index 15b4fd05cee5..ab60d81939a2 100644
--- a/.github/workflows/test-js.yml
+++ b/.github/workflows/test-js.yml
@@ -42,14 +42,14 @@ jobs:
with:
egress-policy: audit
- - name: Set up Node.js ${{ vars.NODE_VERSION }}
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
- with:
- node-version: ${{ vars.NODE_VERSION }}
-
- name: Checkout Code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - name: Set up Node.js
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ with:
+ node-version-file: package.json
+
- name: JS Dependency Cache
id: js-cache
uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v2
@@ -87,14 +87,14 @@ jobs:
with:
egress-policy: audit
- - name: Set up Node.js ${{ vars.NODE_VERSION }}
- uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
- with:
- node-version: ${{ vars.NODE_VERSION }}
-
- name: Checkout Code
uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3
+ - name: Set up Node.js
+ uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1
+ with:
+ node-version-file: package.json
+
- name: JS Dependency Cache
id: js-cache
uses: actions/cache@69d9d449aced6a2ede0bc19182fadc3a0a42d2b0 # v2
diff --git a/CHANGELOG.md b/CHANGELOG.md
index f1f4dd2cee3e..056966f521be 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+## Fleet 4.57.1 (Oct 01, 2024)
+
+### Bug fixes
+
+* Improved performance of SQL queries used to determine MDM profile status for Apple hosts.
+* Ensured request timeouts for software installer edits were just as high as for initial software installer uploads.
+* Fixed an issue with the migration that added support for multiple VPP tokens, which would happen if a token was removed prior to upgrading Fleet.
+* Fixed a "no rows" error when adding a software installer that matched an existing title's name and source but not its bundle ID.
+
## Fleet 4.57.0 (Sep 23, 2024)
**Endpoint Operations**
diff --git a/articles/config-less-fleetd-agent-deployment.md b/articles/config-less-fleetd-agent-deployment.md
index eb37159fdcf5..2ca3b5cd7c93 100644
--- a/articles/config-less-fleetd-agent-deployment.md
+++ b/articles/config-less-fleetd-agent-deployment.md
@@ -4,7 +4,7 @@
Deploying Fleet's agent across a diverse range of devices often involves the crucial step of enrolling each device. Traditionally, this involves [packaging](https://fleetdm.com/docs/using-fleet/fleetd#packaging) `fleetd` with configuration including the enroll secret and server URL. While effective, an alternative offers more flexibility in your deployment process. This guide introduces a different approach for deploying Fleet's agent without embedding configuration settings directly into `fleetd`. Ideal for IT administrators who prefer to generate a single package and maintain greater control over the distribution of enrollment secrets and server URLs, this method simplifies the enrollment process across macOS and Windows hosts.
-Emphasizing adaptability and convenience, this approach allows for a more efficient way to manage device enrollments. Let’s dive into how to deploy Fleet's agent using this alternative method, ensuring a more open and flexible deployment process.
+This approach emphasizes adaptability and convenience and allows for a more efficient way to manage device enrollments. Let’s explore how to deploy Fleet's agent using this alternative method, ensuring a more open and flexible deployment process.
## For macOS:
@@ -44,6 +44,18 @@ fleetctl package --type=pkg --use-system-configuration --fleet-desktop
PayloadVersion1
+
+ EndUserEmail
+ END_USER_EMAIL_HERE
+ PayloadIdentifier
+ com.fleetdm.fleet.mdm.apple.mdm
+ PayloadType
+ com.apple.mdm
+ PayloadUUID
+ 29713130-1602-4D27-90C9-B822A295E44E
+ PayloadVersion
+ 1
+ PayloadDisplayNameFleetd configuration
@@ -56,11 +68,38 @@ fleetctl package --type=pkg --use-system-configuration --fleet-desktop
PayloadVersion1PayloadDescription
- Default configuration for the fleetd agent.
+ Configuration for the fleetd agent.
```
+You can optionally specify the `END_USER_EMAIL` that will be added to the host's [human-device mapping](https://fleetdm.com/docs/rest-api/rest-api#get-human-device-mapping):
+
+```xml
+
+
+
+
+ PayloadContent
+
+ ...
+
+ EndUserEmail
+ END_USER_EMAIL
+ PayloadIdentifier
+ com.fleetdm.fleet.mdm.apple.mdm
+ PayloadType
+ com.apple.mdm
+ PayloadUUID
+ 29713130-1602-4D27-90C9-B822A295E44E
+ PayloadVersion
+ 1
+
+
+ ...
+
+
+```
## For Windows:
diff --git a/articles/enroll-hosts.md b/articles/enroll-hosts.md
index 0635855596ab..f15f44236b29 100644
--- a/articles/enroll-hosts.md
+++ b/articles/enroll-hosts.md
@@ -320,7 +320,7 @@ Fleetd will send stdout/stderr logs to the following directories:
- macOS: `/private/var/log/orbit/orbit.std{out|err}.log`.
- Windows: `C:\Windows\system32\config\systemprofile\AppData\Local\FleetDM\Orbit\Logs\orbit-osquery.log` (the log file is rotated).
- - Linux: Orbit and osqueryd stdout/stderr output is sent to syslog (`/var/log/syslog` on Debian systems and `/var/log/messages` on CentOS).
+ - Linux: Orbit and osqueryd stdout/stderr output is sent to syslog (`/var/log/syslog` on Debian systems, `/var/log/messages` on CentOS, and `journalctl -u orbit` on Fedora).
If the `logger_path` agent configuration is set to `filesystem`, fleetd will send osquery's "result" and "status" logs to the following directories:
- Windows: C:\Program Files\Orbit\osquery_log
diff --git a/articles/macos-mdm-setup.md b/articles/macos-mdm-setup.md
index 209deead8b00..0f32ccb09033 100644
--- a/articles/macos-mdm-setup.md
+++ b/articles/macos-mdm-setup.md
@@ -58,17 +58,17 @@ If no default team is set for a host platform (macOS, iOS, or iPadOS), then newl
To connect Fleet to Apple's VPP, head to the guide [here](https://fleetdm.com/guides/install-vpp-apps-on-macos-using-fleet).
-### Best practice
+## Best practice
-Most organizations need only one ABM token and one VPP token to manage their macOS, iOS, and iPadOS hosts.
+Most organizations only need one ABM token and one VPP token to manage their macOS, iOS, and iPadOS hosts.
-Some organizations need multiple ABM and VPP tokens:
+These organizations may need multiple ABM and VPP tokens:
- Managed Service Providers (MSPs)
- Enterprises that acquire new businesses and as a result inherit new hosts
- Umbrella organizations that preside over entities with separated purchasing authority (i.e. a hospital or university)
-For MSPs, the best practice is to have one ABM and VPP connection per client.
+For **MSPs**, the best practice is to have one ABM and VPP connection per client.
The default teams in Fleet for each client's ABM token in Fleet will look like this:
- macOS: 💻 Client A - Workstations
@@ -77,7 +77,7 @@ The default teams in Fleet for each client's ABM token in Fleet will look like t
Client A's VPP token will be assigned to the above teams.
-For enterprises that acquire, the best practice is to add a new ABM and VPP connection for each acquisition.
+For **enterprises that acquire**, the best practice is to add a new ABM and VPP connection for each acquisition.
These will default teams in Fleet:
diff --git a/changes/19619-win-battery b/changes/19619-win-battery
new file mode 100644
index 000000000000..124e58114048
--- /dev/null
+++ b/changes/19619-win-battery
@@ -0,0 +1 @@
+- Windows host details now include battery status
\ No newline at end of file
diff --git a/changes/20537-add-rpm-support b/changes/20537-add-rpm-support
new file mode 100644
index 000000000000..2238298b2413
--- /dev/null
+++ b/changes/20537-add-rpm-support
@@ -0,0 +1 @@
+* Added support for uploading RPM packages.
diff --git a/changes/21409-fedora-label b/changes/21409-fedora-label
new file mode 100644
index 000000000000..2f0ca7cfd61e
--- /dev/null
+++ b/changes/21409-fedora-label
@@ -0,0 +1 @@
+- added builtin label for Fedora Linux. Warning: migrations will fail if a pre-existing 'Fedora Linux' label exists. To resolve, delete the existing 'Fedora Linux' label.
\ No newline at end of file
diff --git a/changes/21923-switch-exact-search-focus-bug b/changes/21923-switch-exact-search-focus-bug
new file mode 100644
index 000000000000..0ae8b45da921
--- /dev/null
+++ b/changes/21923-switch-exact-search-focus-bug
@@ -0,0 +1 @@
+- UI fix: Switching vulnerability search types does not cause page re-render
diff --git a/changes/22094-cleanup-queries b/changes/22094-cleanup-queries
new file mode 100644
index 000000000000..af8e9ab77da8
--- /dev/null
+++ b/changes/22094-cleanup-queries
@@ -0,0 +1 @@
+- updated activity cleanup job to remove all expired live queries to improve API performance in environment using large volumes of live queries. To note, the cleanup cron may take longer on the first run after upgrade.
\ No newline at end of file
diff --git a/changes/22094-query-optimization b/changes/22094-query-optimization
new file mode 100644
index 000000000000..bd779b451346
--- /dev/null
+++ b/changes/22094-query-optimization
@@ -0,0 +1 @@
+- Increased performance for Host details and Fleet Desktop, particularly in environments using high volumes of live queries
\ No newline at end of file
diff --git a/changes/22198-defaults b/changes/22198-defaults
new file mode 100644
index 000000000000..ec243e9a48e4
--- /dev/null
+++ b/changes/22198-defaults
@@ -0,0 +1,2 @@
+- Fixes a bug where removing a VPP or ABM token from a GitOps YAML file would leave the team
+ assignments unchanged.
\ No newline at end of file
diff --git a/charts/fleet/Chart.yaml b/charts/fleet/Chart.yaml
index c23438bf22aa..f9abb8e29901 100644
--- a/charts/fleet/Chart.yaml
+++ b/charts/fleet/Chart.yaml
@@ -8,7 +8,7 @@ version: v6.2.0
home: https://github.com/fleetdm/fleet
sources:
- https://github.com/fleetdm/fleet.git
-appVersion: v4.57.0
+appVersion: v4.57.1
dependencies:
- name: mysql
condition: mysql.enabled
diff --git a/charts/fleet/values.yaml b/charts/fleet/values.yaml
index 03539df9da98..15eda9b9e8ce 100644
--- a/charts/fleet/values.yaml
+++ b/charts/fleet/values.yaml
@@ -3,7 +3,7 @@
hostName: fleet.localhost
replicas: 3 # The number of Fleet instances to deploy
imageRepository: fleetdm/fleet
-imageTag: v4.57.0 # Version of Fleet to deploy
+imageTag: v4.57.1 # Version of Fleet to deploy
podAnnotations: {} # Additional annotations to add to the Fleet pod
serviceAccountAnnotations: {} # Additional annotations to add to the Fleet service account
resources:
diff --git a/cmd/fleetctl/apply_test.go b/cmd/fleetctl/apply_test.go
index f35b39dc84f1..bd80bb84347c 100644
--- a/cmd/fleetctl/apply_test.go
+++ b/cmd/fleetctl/apply_test.go
@@ -657,6 +657,18 @@ func TestApplyAppConfig(t *testing.T) {
return []*fleet.TeamSummary{{Name: "team1", ID: 1}}, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{{OrganizationName: t.Name()}}, nil
+ }
+
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
@@ -782,6 +794,18 @@ func TestApplyAppConfigDryRunIssue(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
// first, set the default app config's agent options as set after fleetctl setup
name := writeTmpYml(t, `---
apiVersion: v1
@@ -914,6 +938,18 @@ func TestApplyAppConfigDeprecatedFields(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
@@ -1316,6 +1352,14 @@ func TestApplyAsGitOps(t *testing.T) {
return []*fleet.ABMToken{{ID: 1}}, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
// Apply global config.
name := writeTmpYml(t, `---
apiVersion: v1
@@ -1873,6 +1917,18 @@ func TestCanApplyIntervalsInNanoseconds(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
@@ -1908,6 +1964,18 @@ func TestCanApplyIntervalsUsingDurations(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
name := writeTmpYml(t, `---
apiVersion: v1
kind: config
@@ -2091,6 +2159,18 @@ func TestApplyMacosSetup(t *testing.T) {
return []*fleet.ABMToken{{ID: 1}}, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
return ds
}
@@ -2764,6 +2844,17 @@ func TestApplySpecs(t *testing.T) {
ds.DeleteMDMWindowsConfigProfileByTeamAndNameFunc = func(ctx context.Context, teamID *uint, profileName string) error {
return nil
}
+
+ // VPP/AMB
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
}
cases := []struct {
diff --git a/cmd/fleetctl/gitops_test.go b/cmd/fleetctl/gitops_test.go
index 1a054482f618..295172612d74 100644
--- a/cmd/fleetctl/gitops_test.go
+++ b/cmd/fleetctl/gitops_test.go
@@ -83,6 +83,18 @@ func TestGitOpsBasicGlobalFree(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -238,6 +250,18 @@ func TestGitOpsBasicGlobalPremium(t *testing.T) {
return nil, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
tmpFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -591,6 +615,17 @@ func TestGitOpsFullGlobal(t *testing.T) {
return nil
}
+ // Needed for checking tokens
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
const (
fleetServerURL = "https://fleet.example.com"
orgName = "GitOps Test"
@@ -1079,6 +1114,18 @@ func TestGitOpsBasicGlobalAndTeam(t *testing.T) {
return nil, 0, nil, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
globalFile, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -1345,6 +1392,18 @@ func TestGitOpsBasicGlobalAndNoTeam(t *testing.T) {
return nil, 0, nil, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
globalFileBasic, err := os.CreateTemp(t.TempDir(), "*.yml")
require.NoError(t, err)
@@ -1604,6 +1663,18 @@ func TestGitOpsFullGlobalAndTeam(t *testing.T) {
return team, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
apnsCert, apnsKey, err := mysql.GenerateTestCertBytes()
require.NoError(t, err)
crt, key, err := apple_mdm.NewSCEPCACertKey()
@@ -1656,7 +1727,7 @@ func TestGitOpsTeamSofwareInstallers(t *testing.T) {
wantErr string
}{
{"testdata/gitops/team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
- {"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
+ {"testdata/gitops/team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb or .rpm."},
{"testdata/gitops/team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
{"testdata/gitops/team_software_installer_valid.yml", ""},
{"testdata/gitops/team_software_installer_valid_apply.yml", ""},
@@ -1711,7 +1782,7 @@ func TestGitOpsNoTeamSoftwareInstallers(t *testing.T) {
wantErr string
}{
{"testdata/gitops/no_team_software_installer_not_found.yml", "Please make sure that URLs are reachable from your Fleet server."},
- {"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe or .deb."},
+ {"testdata/gitops/no_team_software_installer_unsupported.yml", "The file should be .pkg, .msi, .exe, .deb or .rpm."},
{"testdata/gitops/no_team_software_installer_too_large.yml", "The maximum file size is 500 MiB"},
{"testdata/gitops/no_team_software_installer_valid.yml", ""},
{"testdata/gitops/no_team_software_installer_pre_condition_multiple_queries.yml", "should have only one query."},
@@ -2234,6 +2305,15 @@ func setupFullGitOpsPremiumServer(t *testing.T) (*mock.Store, **fleet.AppConfig,
ds.ListSoftwareTitlesFunc = func(ctx context.Context, opt fleet.SoftwareTitleListOptions, tmFilter fleet.TeamFilter) ([]fleet.SoftwareTitleListResult, int, *fleet.PaginationMetadata, error) {
return nil, 0, nil, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
t.Setenv("FLEET_SERVER_URL", fleetServerURL)
t.Setenv("ORG_NAME", orgName)
diff --git a/docs/Configuration/yaml-files.md b/docs/Configuration/yaml-files.md
index 7599fd259f9a..fae1c212c192 100644
--- a/docs/Configuration/yaml-files.md
+++ b/docs/Configuration/yaml-files.md
@@ -354,7 +354,9 @@ software:
- `app_store_id` is the ID of the Apple App Store app. You can find this at the end of the app's App Store URL. For example, "Bear - Markdown Notes" URL is "https://apps.apple.com/us/app/bear-markdown-notes/id1016366447" and the `app_store_id` is `1016366447`.
-> Make sure to include only the ID itself, and not the `id` prefix shown in the URL. The ID must be wrapped in quotes as shown in the example so that it is processed as a string.
+> Make sure to include only the ID itself, and not the `id` prefix shown in the URL. The ID must be wrapped in quotes as shown in the example so that it is processed as a string.
+
+`self_service` only applies to macOS, and is ignored for other platforms. For example, if the app is supported on macOS, iOS, and iPadOS, and `self_service` is set to `true`, it will be self-service on macOS workstations but not iPhones or iPads.
##### Separate file
diff --git a/docs/Contributing/API-for-contributors.md b/docs/Contributing/API-for-contributors.md
index 074c3338b4c2..27f819f72467 100644
--- a/docs/Contributing/API-for-contributors.md
+++ b/docs/Contributing/API-for-contributors.md
@@ -541,6 +541,10 @@ The MDM endpoints exist to support the related command-line interface sub-comman
- [Renew VPP token](#renew-vpp-token)
- [Delete VPP token](#delete-vpp-token)
- [Batch-apply MDM custom settings](#batch-apply-mdm-custom-settings)
+- [Batch-apply packages](#batch-apply-packages)
+- [Batch-apply App Store apps](#batch-apply-app-store-apps)
+- [Get token to download package](#get-token-to-download-package)
+- [Download package using a token](#download-package-using-a-token)
- [Initiate SSO during DEP enrollment](#initiate-sso-during-dep-enrollment)
- [Complete SSO during DEP enrollment](#complete-sso-during-dep-enrollment)
- [Over the air enrollment](#over-the-air-enrollment)
@@ -549,7 +553,6 @@ The MDM endpoints exist to support the related command-line interface sub-comman
- [Get FileVault statistics](#get-filevault-statistics)
- [Upload VPP content token](#upload-vpp-content-token)
- [Disable VPP](#disable-vpp)
-- [Get an over the air (OTA) enrollment profile](#get-an-over-the-air-ota-enrollment-profile)
### Generate Apple Business Manager public key (ADE)
@@ -1744,7 +1747,7 @@ If the `name` is not already associated with an existing team, this API route cr
| scripts | list | body | A list of script files to add to this team so they can be executed at a later time. |
| software | object | body | The team's software that will be available for install. |
| software.packages | list | body | An array of objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `self_service` boolean. |
-| software.app_store_apps | list | body | An array objects. Each object consists of `app_store_id` - ID of the App Store app formatted as a string (in quotes) rather than a number. |
+| software.app_store_apps | list | body | An array of objects. Each object consists of `app_store_id` - ID of the App Store app and `self_service` boolean. |
| mdm.macos_settings.enable_disk_encryption | bool | body | Whether disk encryption should be enabled for hosts that belong to this team. |
| force | bool | query | Force apply the spec even if there are (ignorable) validation errors. Those are unknown keys and agent options-related validations. |
| dry_run | bool | query | Validate the provided JSON for unknown keys and invalid value types and return any validation errors, but do not apply the changes. |
@@ -1837,6 +1840,7 @@ If the `name` is not already associated with an existing team, this API route cr
"app_store_apps": [
{
"app_store_id": "12464567",
+ "self_service": true
}
]
}
@@ -3377,3 +3381,187 @@ Run a live script and get results back (5 minute timeout). Live scripts only run
"exit_code": 0
}
```
+## Software
+
+### Batch-apply software
+
+_Available in Fleet Premium._
+
+`POST /api/v1/fleet/software/batch`
+
+This endpoint is asynchronous, meaning it will start a background process to download and apply the software and return a `request_uuid` in the JSON response that can be used to query the status of the batch-apply (using the `GET /api/v1/fleet/software/batch/:request_uuid` endpoint defined below).
+
+#### Parameters
+
+| Name | Type | In | Description |
+| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| team_name | string | query | The name of the team to add the software package to. Ommitting these parameters will add software to 'No Team'. |
+| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. |
+| software | object | body | The team's software that will be available for install. |
+| software.packages | list | body | An array of objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `uninstall_script` - command that Fleet runs to uninstall software. |
+
+#### Example
+
+`POST /api/v1/fleet/software/batch`
+
+##### Default response
+
+`Status: 200`
+```json
+{
+ "request_uuid": "ec23c7b6-c336-4109-b89d-6afd859659b4",
+}
+```
+
+### Get status of software batch-apply request
+
+_Available in Fleet Premium._
+
+`GET /api/v1/fleet/software/batch/:request_uuid`
+
+This endpoint allows querying the status of a batch-apply software request (`POST /api/v1/fleet/software/batch`).
+Returns `"status"` field that can be one of `"processing"`, `"complete"` or `"failed"`.
+If `"status"` is `"completed"` then the `"packages"` field contains the applied packages.
+If `"status"` is `"processing"` then the operation is ongoing and the request should be retried.
+If `"status"` is `"failed"` then the `"message"` field contains the error message.
+
+#### Parameters
+
+| Name | Type | In | Description |
+| ------------ | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| request_uuid | string | query | The request_uuid returned by the `POST /api/v1/fleet/software/batch` endpoint. |
+| team_name | string | query | The name of the team to add the software package to. Ommitting these parameters will add software to 'No Team'. |
+| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. |
+
+##### Default responses
+
+`Status: 200`
+```json
+{
+ "status": "processing",
+ "message": "",
+ "packages": null
+}
+```
+
+`Status: 200`
+```json
+{
+ "status": "completed",
+ "message": "",
+ "packages": [
+ {
+ "team_id": 1,
+ "title_id": 2751,
+ "url": "https://ftp.mozilla.org/pub/firefox/releases/129.0.2/win64/en-US/Firefox%20Setup%20129.0.2.msi"
+ }
+ ]
+}
+```
+
+`Status: 200`
+```json
+{
+ "status": "failed",
+ "message": "validation failed: software.url Couldn't edit software. URL (\"https://foobar.does.not.exist.com\") returned \"Not Found\". Please make sure that URLs are reachable from your Fleet server.",
+ "packages": null
+}
+```
+
+### Batch-apply App Store apps
+
+_Available in Fleet Premium._
+
+`POST /api/latest/fleet/software/app_store_apps/batch`
+
+#### Parameters
+
+| Name | Type | In | Description |
+| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| team_name | string | query | The name of the team to add the software package to. Ommitting this parameter will add software to "No team". |
+| dry_run | bool | query | If `true`, will validate the provided VPP apps and return any validation errors, but will not apply the changes. |
+| app_store_apps | list | body | An array of objects. Each object contains `app_store_id` and `self_service`. |
+| app_store_apps.app_store_id | string | body | ID of the App Store app. |
+| app_store_apps.self_service | boolean | body | Whether the VPP app is "Self-service" or not. |
+
+#### Example
+
+`POST /api/latest/fleet/software/app_store_apps/batch`
+```json
+{
+ "team_name": "Foobar",
+ "app_store_apps": {
+ {
+ "app_store_id": "597799333",
+ "self_service": false,
+ },
+ {
+ "app_store_id": "497799835",
+ "self_service": true,
+ }
+ }
+}
+```
+
+##### Default response
+
+`Status: 204`
+
+### Get token to download package
+
+_Available in Fleet Premium._
+
+`POST /api/v1/fleet/software/titles/:software_title_id/package/token?alt=media`
+
+The returned token is a one-time use token that expires after 10 minutes.
+
+#### Parameters
+
+| Name | Type | In | Description |
+|-------------------|---------|-------|------------------------------------------------------------------|
+| software_title_id | integer | path | **Required**. The ID of the software title for software package. |
+| team_id | integer | query | **Required**. The team ID containing the software package. |
+| alt | integer | query | **Required**. Must be specified and set to "media". |
+
+#### Example
+
+`POST /api/v1/fleet/software/titles/123/package/token?alt=media&team_id=2`
+
+##### Default response
+
+`Status: 200`
+
+```json
+{
+ "token": "e905e33e-07fe-4f82-889c-4848ed7eecb7"
+}
+```
+
+### Download package using a token
+
+_Available in Fleet Premium._
+
+`GET /api/v1/fleet/software/titles/:software_title_id/package/token/:token?alt=media`
+
+#### Parameters
+
+| Name | Type | In | Description |
+|-------------------|---------|------|--------------------------------------------------------------------------|
+| software_title_id | integer | path | **Required**. The ID of the software title to download software package. |
+| token | string | path | **Required**. The token to download the software package. |
+
+#### Example
+
+`GET /api/v1/fleet/software/titles/123/package/token/e905e33e-07fe-4f82-889c-4848ed7eecb7`
+
+##### Default response
+
+`Status: 200`
+
+```http
+Status: 200
+Content-Type: application/octet-stream
+Content-Disposition: attachment
+Content-Length:
+Body:
+```
diff --git a/docs/Contributing/Understanding-host-vitals.md b/docs/Contributing/Understanding-host-vitals.md
index 3cdabf2fde06..3aa2078574f1 100644
--- a/docs/Contributing/Understanding-host-vitals.md
+++ b/docs/Contributing/Understanding-host-vitals.md
@@ -3,7 +3,7 @@
Following is a summary of the detail queries hardcoded in Fleet used to populate the device details:
-## battery
+## battery_macos
- Platforms: darwin
@@ -12,6 +12,20 @@ Following is a summary of the detail queries hardcoded in Fleet used to populate
SELECT serial_number, cycle_count, health FROM battery;
```
+## battery_windows
+
+- Platforms: windows
+
+- Discovery query:
+```sql
+SELECT 1 FROM osquery_registry WHERE active = true AND registry = 'table' AND name = 'battery'
+```
+
+- Query:
+```sql
+SELECT serial_number, cycle_count, designed_capacity, max_capacity FROM battery
+```
+
## chromeos_profile_user_info
- Platforms: chrome
diff --git a/docs/Get started/why-fleet.md b/docs/Get started/why-fleet.md
index e0db75d68759..6ee517a43da3 100644
--- a/docs/Get started/why-fleet.md
+++ b/docs/Get started/why-fleet.md
@@ -4,9 +4,9 @@ Fleet is an open-source device management platform for Linux, macOS, Windows, Ch
## What's it for?
-Managing computers today is getting harder. You have to juggle a mix of operating systems and devices, with a whole bunch of middleman vendors in between.
+Managing computers today is getting harder. You have to juggle a mix of operating systems and devices, with a bunch of middleman vendors in between.
-Fleet makes things easier by giving you a single system to manage and secure all your computing devices. You can do MDM, patch stuff, and verify anything—all from one dashboard. It's like having a universal remote control for all your organization's computers.
+Fleet makes things easier by giving you a single system to secure and maintain all your computing devices over the air. You can do MDM, patch stuff, and verify anything—all from one system. It's like having a universal remote control for all your organization's computers.
Fleet is open source, and free features will always be free.
@@ -15,7 +15,7 @@ Fleet is open source, and free features will always be free.
Fleet is used in production by IT and security teams with thousands of laptops and servers. Many deployments support tens of thousands of hosts, and a few large organizations manage deployments as large as 400,000+ hosts.
- **Get what you need:** Fleet lets you work directly with [data](https://fleetdm.com/integrations) and events from the native operating system. It lets you go all the way down to the bare metal. It’s also modular. (You can turn off features you are not using.)
-- **Out of the box:** Ready-to-use integrations exist for the [most common tools](https://fleetdm.com/integrations). You can also build custom workflows with the REST API, webhook events, and the fleetctl command line tool. Or go all in and manage computers [with GitOps](https://fleetdm.com/handbook/company#history).
+- **Out of the box:** Ready-to-use integrations exist for the [most common tools](https://fleetdm.com/integrations). You can also build custom workflows with the REST API, webhook events, and the fleetctl command line tool. Or go all in and govern computers [with GitOps](https://github.com/fleetdm/fleet-gitops).
- **Good neighbors:** We think tools should be as easy as possible for everyone to understand. We helped [create osquery](https://fleetdm.com/handbook/company#history), and we are committed to improving it.
- **Free as in free:** The free version of Fleet will [always be free](https://fleetdm.com/pricing). Fleet is independently backed and actively maintained with the help of many amazing contributors.
diff --git a/docs/REST API/rest-api.md b/docs/REST API/rest-api.md
index 0a760c32d5fc..cb23ae838055 100644
--- a/docs/REST API/rest-api.md
+++ b/docs/REST API/rest-api.md
@@ -4295,8 +4295,10 @@ Resends a configuration profile for the specified host.
"name": "Logic Pro",
"software_package": null
"app_store_app": {
- "app_store_id": "1091189122"
+ "app_store_id": "1091189122",
+ "icon_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple221/v4/f4/25/1f/f4251f60-e27a-6f05-daa7-9f3a63aac929/AppIcon-0-0-85-220-0-0-4-0-0-2x-0-0-0-0-0.png/512x512bb.png"
"version": "2.04",
+ "self_service": false,
"last_install": {
"command_uuid": "0aa14ae5-58fe-491a-ac9a-e4ee2b3aac40",
"installed_at": "2024-05-15T15:23:57Z"
@@ -5573,6 +5575,7 @@ solely on the response status code returned by this endpoint.
```
###### Example response body
+
```xml
@@ -5720,6 +5723,7 @@ Get aggregate status counts of profiles for to macOS and Windows hosts that are
- [Set custom MDM setup enrollment profile](#set-custom-mdm-setup-enrollment-profile)
- [Get custom MDM setup enrollment profile](#get-custom-mdm-setup-enrollment-profile)
- [Delete custom MDM setup enrollment profile](#delete-custom-mdm-setup-enrollment-profile)
+- [Get Over-the-Air (OTA) enrollment profile](#get-over-the-air-ota-enrollment-profile)
- [Get manual enrollment profile](#get-manual-enrollment-profile)
- [Upload a bootstrap package](#upload-a-bootstrap-package)
- [Get metadata about a bootstrap package](#get-metadata-about-a-bootstrap-package)
@@ -5827,10 +5831,83 @@ Deletes the custom MDM setup enrollment profile assigned to a team or no team.
`Status: 204`
+### Get Over-the-Air (OTA) enrollment profile
+
+`GET /api/v1/fleet/enrollment_profiles/ota`
+
+The returned value is a signed `.mobileconfig` OTA enrollment profile. Install this profile on macOS, iOS, or iPadOS hosts to enroll them to a specific team in Fleet and turn on MDM features.
+
+To enroll macOS hosts, turn on MDM features, and add [human-device mapping](#get-human-device-mapping), install the [manual enrollment profile](#get-manual-enrollment-profile) instead.
+
+Learn more about OTA profiles [here](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/iPhoneOTAConfiguration/OTASecurity/OTASecurity.html).
+
+#### Parameters
+
+| Name | Type | In | Description |
+|-------------------|---------|-------|----------------------------------------------------------------------------------|
+| enroll_secret | string | query | **Required**. The enroll secret of the team this host will be assigned to. |
+
+#### Example
+
+`GET /api/v1/fleet/enrollment_profiles/ota?enroll_secret=foobar`
+
+##### Default response
+
+`Status: 200`
+
+> **Note:** To confirm success, it is important for clients to match content length with the response header (this is done automatically by most clients, including the browser) rather than relying solely on the response status code returned by this endpoint.
+
+##### Example response headers
+
+```http
+ Content-Length: 542
+ Content-Type: application/x-apple-aspen-config; charset=urf-8
+ Content-Disposition: attachment;filename="fleet-mdm-enrollment-profile.mobileconfig"
+ X-Content-Type-Options: nosniff
+```
+
+###### Example response body
+
+```xml
+
+
+
+
+ PayloadContent
+
+ URL
+ https://foo.example.com/api/fleet/ota_enrollment?enroll_secret=foobar
+ DeviceAttributes
+
+ UDID
+ VERSION
+ PRODUCT
+ SERIAL
+
+
+ PayloadOrganization
+ Acme Inc.
+ PayloadDisplayName
+ Acme Inc. enrollment
+ PayloadVersion
+ 1
+ PayloadUUID
+ fdb376e5-b5bb-4d8c-829e-e90865f990c9
+ PayloadIdentifier
+ com.fleetdm.fleet.mdm.apple.ota
+ PayloadType
+ Profile Service
+
+
+```
+
+
### Get manual enrollment profile
Retrieves an unsigned manual enrollment profile for macOS hosts. Install this profile on macOS hosts to turn on MDM features manually.
+To add [human-device mapping](#get-human-device-mapping), add the end user's email to the enrollment profle. Learn how [here](https://fleetdm.com/guides/config-less-fleetd-agent-deployment#basic-article).
+
`GET /api/v1/fleet/enrollment_profiles/manual`
##### Example
@@ -6442,7 +6519,8 @@ None.
]
```
-Get Volume Purchasing Program (VPP)
+### Get Volume Purchasing Program (VPP)
+
> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
@@ -8652,10 +8730,6 @@ Deletes the session specified by ID. When the user associated with the session n
- [Get package install result](#get-package-install-result)
- [Download package](#download-package)
- [Delete package or App Store app](#delete-package-or-app-store-app)
-- [Batch-apply software](#batch-apply-software)
-- [Batch-apply app store apps](#batch-apply-app-store-apps)
-- [Get token to download package](#get-token-to-download-package)
-- [Download package using a token](#download-package-using-a-token)
### List software
@@ -9026,9 +9100,10 @@ Returns information about the specified software. By default, `versions` are sor
"software_package": null,
"app_store_app": {
"name": "Logic Pro",
- "app_store_id": "1091189122",
+ "app_store_id": 1091189122,
"latest_version": "2.04",
"icon_url": "https://is1-ssl.mzstatic.com/image/thumb/Purple211/v4/f1/65/1e/a4844ccd-486d-455f-bb31-67336fe46b14/AppIcon-1x_U007emarketing-0-7-0-85-220-0.png/512x512bb.jpg",
+ "self_service": true,
"status": {
"installed": 3,
"pending": 1,
@@ -9107,6 +9182,7 @@ Returns information about the specified software version.
}
```
+
### Get operating system version
Retrieves information about the specified operating system (OS) version.
@@ -9379,6 +9455,7 @@ Add App Store (VPP) app purchased in Apple Business Manager.
| app_store_id | string | body | **Required.** The ID of App Store app. |
| team_id | integer | body | **Required**. The team ID. Adds VPP software to the specified team. |
| platform | string | body | The platform of the app (`darwin`, `ios`, or `ipados`). Default is `darwin`. |
+| self_service | boolean | body | Self-service software is optional and can be installed by the end user. |
#### Example
@@ -9391,6 +9468,7 @@ Add App Store (VPP) app purchased in Apple Business Manager.
"app_store_id": "497799835",
"team_id": 2,
"platform": "ipados"
+ "self_service": true
}
```
@@ -9398,38 +9476,6 @@ Add App Store (VPP) app purchased in Apple Business Manager.
`Status: 200`
-### Download package
-
-> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
-
-_Available in Fleet Premium._
-
-`GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media`
-
-#### Parameters
-
-| Name | Type | In | Description |
-| ---- | ------- | ---- | -------------------------------------------- |
-| software_title_id | integer | path | **Required**. The ID of the software title to download software package.|
-| team_id | integer | query | **Required**. The team ID. Downloads a software package added to the specified team. |
-| alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. |
-
-#### Example
-
-`GET /api/v1/fleet/software/titles/123/package?alt=media?team_id=2`
-
-##### Default response
-
-`Status: 200`
-
-```http
-Status: 200
-Content-Type: application/octet-stream
-Content-Disposition: attachment
-Content-Length:
-Body:
-```
-
### Install package or App Store app
> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
@@ -9487,7 +9533,7 @@ _Available in Fleet Premium._
`GET /api/v1/fleet/software/install/:install_uuid/results`
-Get the results of a software package install.
+Get the results of a software package install.
To get the results of an App Store app install, use the [List MDM commands](#list-mdm-commands) and [Get MDM command results](#get-mdm-command-results) API enpoints. Fleet uses an MDM command to install App Store apps.
@@ -9518,141 +9564,62 @@ To get the results of an App Store app install, use the [List MDM commands](#lis
}
```
-### Delete package or App Store app
+### Download package
> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
_Available in Fleet Premium._
-Deletes software that's available for install (package or App Store app).
-
-`DELETE /api/v1/fleet/software/titles/:software_title_id/available_for_install`
+`GET /api/v1/fleet/software/titles/:software_title_id/package?alt=media`
#### Parameters
| Name | Type | In | Description |
| ---- | ------- | ---- | -------------------------------------------- |
-| software_title_id | integer | path | **Required**. The ID of the software title to delete software available for install. |
-| team_id | integer | query | **Required**. The team ID. Deletes a software package added to the specified team. |
-
-#### Example
-
-`DELETE /api/v1/fleet/software/titles/24/available_for_install?team_id=2`
-
-##### Default response
-
-`Status: 204`
-
-### Batch-apply software
-
-_Available in Fleet Premium._
-
-`POST /api/v1/fleet/software/batch`
-
-#### Parameters
-
-| Name | Type | In | Description |
-| --------- | ------ | ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| team_id | number | query | The ID of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request; omit this parameter if using `team_name`. Omitting these parameters will add software to "No Team". |
-| team_name | string | query | The name of the team to add the software package to. Only one team identifier (`team_id` or `team_name`) can be included in the request; omit this parameter if using `team_id`. Omitting these parameters will add software to "No Team". |
-| dry_run | bool | query | If `true`, will validate the provided software packages and return any validation errors, but will not apply the changes. |
-| software | object | body | The team's software that will be available for install. |
-| software.packages | list | body | An array of objects. Each object consists of:`url`- URL to the software package (PKG, MSI, EXE or DEB),`install_script` - command that Fleet runs to install software, `pre_install_query` - condition query that determines if the install will proceed, `post_install_script` - script that runs after software install, and `uninstall_script` - command that Fleet runs to uninstall software. |
-| software.app_store_apps | list | body | An array objects. Each object consists of `app_store_id` - ID of the App Store app. |
-
-If both `team_id` and `team_name` parameters are included, this endpoint will respond with an error. If no `team_name` or `team_id` is provided, the scripts will be applied for **all hosts**.
-
-#### Example
-
-`POST /api/v1/fleet/software/batch`
-
-##### Default response
-
-`Status: 204`
-
-### Batch-apply app store apps
-
-_Available in Fleet Premium._
-
-`POST /api/v1/fleet/software/app_store_apps/batch`
-
-#### Parameters
-
-| Name | Type | In | Description |
-|-----------------|---------|-------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
-| team_name | integer | query | **Required**. The name of the team to add the app to. |
-| dry_run | bool | query | If `true`, will validate the provided apps and return any validation errors, but will not apply the changes. |
-| apps_store_apps | list | body | The list of objects containing `app_store_id`: a string representation of the app's App ID, `self_service`: a bool indicating if the app's installation can be initiated by end users. |
-
-> Note that this endpoint replaces all apps associated with a team.
-
-#### Example
-
-`POST /api/v1/fleet/software/app_store_apps/batch`
-
-#### Default response
-
-`Status: 204`
-
-### Get token to download package
-
-_Available in Fleet Premium._
-
-`POST /api/v1/fleet/software/titles/:software_title_id/package/token?alt=media`
-
-The returned token is a one-time use token that expires after 10 minutes.
-
-#### Parameters
-
-| Name | Type | In | Description |
-|-------------------|---------|-------|------------------------------------------------------------------|
-| software_title_id | integer | path | **Required**. The ID of the software title for software package. |
-| team_id | integer | query | **Required**. The team ID containing the software package. |
-| alt | integer | query | **Required**. Must be specified and set to "media". |
+| software_title_id | integer | path | **Required**. The ID of the software title to download software package.|
+| team_id | integer | query | **Required**. The team ID. Downloads a software package added to the specified team. |
+| alt | integer | query | **Required**. If specified and set to "media", downloads the specified software package. |
#### Example
-`POST /api/v1/fleet/software/titles/123/package/token?alt=media&team_id=2`
+`GET /api/v1/fleet/software/titles/123/package?alt=media?team_id=2`
##### Default response
`Status: 200`
-```json
-{
- "token": "e905e33e-07fe-4f82-889c-4848ed7eecb7"
-}
+```http
+Status: 200
+Content-Type: application/octet-stream
+Content-Disposition: attachment
+Content-Length:
+Body:
```
-### Download package using a token
+### Delete package or App Store app
+
+> **Experimental feature**. This feature is undergoing rapid improvement, which may result in breaking changes to the API or configuration surface. It is not recommended for use in automated workflows.
_Available in Fleet Premium._
-`GET /api/v1/fleet/software/titles/:software_title_id/package/token/:token?alt=media`
+Deletes software that's available for install (package or App Store app).
+
+`DELETE /api/v1/fleet/software/titles/:software_title_id/available_for_install`
#### Parameters
-| Name | Type | In | Description |
-|-------------------|---------|------|--------------------------------------------------------------------------|
-| software_title_id | integer | path | **Required**. The ID of the software title to download software package. |
-| token | string | path | **Required**. The token to download the software package. |
+| Name | Type | In | Description |
+| ---- | ------- | ---- | -------------------------------------------- |
+| software_title_id | integer | path | **Required**. The ID of the software title to delete software available for install. |
+| team_id | integer | query | **Required**. The team ID. Deletes a software package added to the specified team. |
#### Example
-`GET /api/v1/fleet/software/titles/123/package/token/e905e33e-07fe-4f82-889c-4848ed7eecb7`
+`DELETE /api/v1/fleet/software/titles/24/available_for_install?team_id=2`
##### Default response
-`Status: 200`
-
-```http
-Status: 200
-Content-Type: application/octet-stream
-Content-Disposition: attachment
-Content-Length:
-Body:
-```
-
+`Status: 204`
## Vulnerabilities
diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go
index ac4461a592b2..a2dea22138bb 100644
--- a/ee/server/service/software_installers.go
+++ b/ee/server/service/software_installers.go
@@ -1054,7 +1054,7 @@ func (svc *Service) addMetadataToSoftwarePayload(ctx context.Context, payload *f
if err != nil {
if errors.Is(err, file.ErrUnsupportedType) {
return "", &fleet.BadRequestError{
- Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe or .deb.",
+ Message: "Couldn't edit software. File type not supported. The file should be .pkg, .msi, .exe, .deb or .rpm.",
InternalErr: ctxerr.Wrap(ctx, err, "extracting metadata from installer"),
}
}
@@ -1517,7 +1517,7 @@ func packageExtensionToPlatform(ext string) string {
requiredPlatform = "windows"
case ".pkg":
requiredPlatform = "darwin"
- case ".deb":
+ case ".deb", ".rpm":
requiredPlatform = "linux"
default:
return ""
diff --git a/frontend/interfaces/package_type.ts b/frontend/interfaces/package_type.ts
index 8afb43cef382..b8b83b93fe9d 100644
--- a/frontend/interfaces/package_type.ts
+++ b/frontend/interfaces/package_type.ts
@@ -1,4 +1,4 @@
-const unixPackageTypes = ["pkg", "deb"] as const;
+const unixPackageTypes = ["pkg", "deb", "rpm"] as const;
const windowsPackageTypes = ["msi", "exe"] as const;
export const packageTypes = [
...unixPackageTypes,
diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts
index 77b4c91767cb..5db289051a3a 100644
--- a/frontend/interfaces/software.ts
+++ b/frontend/interfaces/software.ts
@@ -270,7 +270,7 @@ export interface ISoftwareInstallResults {
// ISoftwareInstallerType defines the supported installer types for
// software uploaded by the IT admin.
-export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "exe";
+export type ISoftwareInstallerType = "pkg" | "msi" | "deb" | "rpm" | "exe";
export interface ISoftwareLastInstall {
install_uuid: string;
diff --git a/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx b/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx
index eef37919dc87..8b5fec069426 100644
--- a/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx
+++ b/frontend/pages/ManageControlsPage/Scripts/components/DeleteScriptModal/DeleteScriptModal.tsx
@@ -44,8 +44,8 @@ const DeleteScriptModal = ({
The script{" "}
{scriptName} will
- run on pending hosts. After the script runs, its output and
- exit code will appear in the activity feed.
+ run on pending hosts. After the script runs, its output and exit code
+ will appear in the activity feed.
;
}
diff --git a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx
index 5758b67b0320..76cd4323502f 100644
--- a/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx
+++ b/frontend/pages/SoftwarePage/components/PackageAdvancedOptions/PackageAdvancedOptions.tsx
@@ -24,6 +24,7 @@ const getSupportedScriptTypeText = (pkgType: PackageType) => {
const PKG_TYPE_TO_ID_TEXT = {
pkg: "package IDs",
deb: "package name",
+ rpm: "package name",
msi: "product code",
exe: "software name",
} as const;
diff --git a/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx b/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx
index 343423d2d829..4e9d7aa74de0 100644
--- a/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx
+++ b/frontend/pages/SoftwarePage/components/PackageForm/PackageForm.tsx
@@ -58,7 +58,7 @@ interface IPackageFormProps {
defaultSelfService?: boolean;
}
-const ACCEPTED_EXTENSIONS = ".pkg,.msi,.exe,.deb";
+const ACCEPTED_EXTENSIONS = ".pkg,.msi,.exe,.deb,.rpm";
const PackageForm = ({
isUploading,
@@ -173,7 +173,7 @@ const PackageForm = ({
canEdit={isEditingSoftware}
graphicName={"file-pkg"}
accept={ACCEPTED_EXTENSIONS}
- message=".pkg, .msi, .exe, or .deb"
+ message=".pkg, .msi, .exe, .deb, or .rpm"
onFileUpload={onFileSelect}
buttonMessage="Choose file"
buttonType="link"
diff --git a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/TeamsCell/TeamsCell.tsx b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/TeamsCell/TeamsCell.tsx
index 57a21e7e48b1..570d7af8ed0f 100644
--- a/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/TeamsCell/TeamsCell.tsx
+++ b/frontend/pages/admin/IntegrationsPage/cards/MdmSettings/VppPage/components/VppTable/TeamsCell/TeamsCell.tsx
@@ -17,7 +17,7 @@ const generateCell = (teams: ITokenTeam[] | null) => {
}
if (teams.length === 0) {
- return ;
+ return ;
}
let text = "";
@@ -83,7 +83,7 @@ const TeamsCell = ({ teams, className }: ITeamsCellProps) => {
}
if (teams.length === 0) {
- return ;
+ return ;
}
if (teams.length === 1) {
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx
index 84b8c01ffa42..483a786e62eb 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/InstallSoftwareModal/InstallSoftwareModal.tsx
@@ -28,6 +28,7 @@ const getPlatformDisplayFromPackageSuffix = (packageName: string) => {
case "pkg":
return "macOS";
case "deb":
+ case "rpm":
return "Linux";
case "exe":
return "Windows";
diff --git a/frontend/utilities/file/fileUtils.tests.ts b/frontend/utilities/file/fileUtils.tests.ts
index 8d650638920a..f21f84001959 100644
--- a/frontend/utilities/file/fileUtils.tests.ts
+++ b/frontend/utilities/file/fileUtils.tests.ts
@@ -2,15 +2,22 @@ import { getPlatformDisplayName } from "./fileUtils";
describe("fileUtils", () => {
describe("getPlatformDisplayName", () => {
- it("should return the correct platform display name depending on the file extension", () => {
- const file = new File([""], "test.pkg");
- expect(getPlatformDisplayName(file)).toEqual("macOS");
+ const testCases = [
+ { extension: "pkg", platform: "macOS" },
+ { extension: "json", platform: "macOS" },
+ { extension: "mobileconfig", platform: "macOS" },
+ { extension: "exe", platform: "Windows" },
+ { extension: "msi", platform: "Windows" },
+ { extension: "xml", platform: "Windows" },
+ { extension: "deb", platform: "Linux" },
+ { extension: "rpm", platform: "Linux" },
+ ];
- const file2 = new File([""], "test.exe");
- expect(getPlatformDisplayName(file2)).toEqual("Windows");
-
- const file3 = new File([""], "test.deb");
- expect(getPlatformDisplayName(file3)).toEqual("linux");
+ testCases.forEach(({ extension, platform }) => {
+ it(`should return ${platform} for .${extension} files`, () => {
+ const file = new File([""], `test.${extension}`);
+ expect(getPlatformDisplayName(file)).toEqual(platform);
+ });
});
});
});
diff --git a/frontend/utilities/file/fileUtils.ts b/frontend/utilities/file/fileUtils.ts
index ca7b74fdd12b..6853a68a69c3 100644
--- a/frontend/utilities/file/fileUtils.ts
+++ b/frontend/utilities/file/fileUtils.ts
@@ -1,4 +1,4 @@
-type IPlatformDisplayName = "macOS" | "Windows" | "linux";
+type IPlatformDisplayName = "macOS" | "Windows" | "Linux";
const getFileExtension = (file: File) => {
const nameParts = file.name.split(".");
@@ -15,7 +15,8 @@ export const FILE_EXTENSIONS_TO_PLATFORM_DISPLAY_NAME: Record<
exe: "Windows",
msi: "Windows",
xml: "Windows",
- deb: "linux",
+ deb: "Linux",
+ rpm: "Linux",
};
/**
diff --git a/frontend/utilities/software_install_scripts.ts b/frontend/utilities/software_install_scripts.ts
index 5c21e8a1f465..be0a9f30d3ce 100644
--- a/frontend/utilities/software_install_scripts.ts
+++ b/frontend/utilities/software_install_scripts.ts
@@ -6,6 +6,8 @@ import installMsi from "../../pkg/file/scripts/install_msi.ps1";
import installExe from "../../pkg/file/scripts/install_exe.ps1";
// @ts-ignore
import installDeb from "../../pkg/file/scripts/install_deb.sh";
+// @ts-ignore
+import installRPM from "../../pkg/file/scripts/install_rpm.sh";
/*
* getInstallScript returns a string with a script to install the
@@ -20,6 +22,8 @@ const getDefaultInstallScript = (fileName: string): string => {
return installMsi;
case "deb":
return installDeb;
+ case "rpm":
+ return installRPM;
case "exe":
return installExe;
default:
diff --git a/frontend/utilities/software_uninstall_scripts.ts b/frontend/utilities/software_uninstall_scripts.ts
index 041d2519c4c1..172c53c14f68 100644
--- a/frontend/utilities/software_uninstall_scripts.ts
+++ b/frontend/utilities/software_uninstall_scripts.ts
@@ -6,6 +6,8 @@ import uninstallMsi from "../../pkg/file/scripts/uninstall_msi.ps1";
import uninstallExe from "../../pkg/file/scripts/uninstall_exe.ps1";
// @ts-ignore
import uninstallDeb from "../../pkg/file/scripts/uninstall_deb.sh";
+// @ts-ignore
+import uninstallRPM from "../../pkg/file/scripts/uninstall_rpm.sh";
/*
* getUninstallScript returns a string with a script to uninstall the
@@ -20,6 +22,8 @@ const getDefaultUninstallScript = (fileName: string): string => {
return uninstallMsi;
case "deb":
return uninstallDeb;
+ case "rpm":
+ return uninstallRPM;
case "exe":
return uninstallExe;
default:
diff --git a/go.mod b/go.mod
index 056fe1b18413..83670b64fdd1 100644
--- a/go.mod
+++ b/go.mod
@@ -199,6 +199,8 @@ require (
github.com/caarlos0/env/v6 v6.7.0 // indirect
github.com/caarlos0/go-shellwords v1.0.12 // indirect
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e // indirect
+ github.com/cavaliergopher/cpio v1.0.1 // indirect
+ github.com/cavaliergopher/rpm v1.2.0 // indirect
github.com/cespare/xxhash v1.1.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cloudflare/circl v1.3.8 // indirect
diff --git a/go.sum b/go.sum
index beec8ba3e049..8bf177e83697 100644
--- a/go.sum
+++ b/go.sum
@@ -320,6 +320,10 @@ github.com/caarlos0/testfs v0.4.3 h1:q1zEM5hgsssqWanAfevJYYa0So60DdK6wlJeTc/yfUE
github.com/caarlos0/testfs v0.4.3/go.mod h1:bRN55zgG4XCUVVHZCeU+/Tz1Q6AxEJOEJTliBy+1DMk=
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e h1:hHg27A0RSSp2Om9lubZpiMgVbvn39bsUmW9U5h0twqc=
github.com/cavaliercoder/go-cpio v0.0.0-20180626203310-925f9528c45e/go.mod h1:oDpT4efm8tSYHXV5tHSdRvBet/b/QzxZ+XyyPehvm3A=
+github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM=
+github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc=
+github.com/cavaliergopher/rpm v1.2.0 h1:s0h+QeVK252QFTolkhGiMeQ1f+tMeIMhGl8B1HUmGUc=
+github.com/cavaliergopher/rpm v1.2.0/go.mod h1:R0q3vTqa7RUvPofAZYrnjJ63hh2vngjFfphuXiExVos=
github.com/cenkalti/backoff v2.1.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4=
github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM=
diff --git a/handbook/company/leadership.md b/handbook/company/leadership.md
index 1cad2e84de28..90c9d0b2a70c 100644
--- a/handbook/company/leadership.md
+++ b/handbook/company/leadership.md
@@ -415,7 +415,11 @@ Although it's sad to see someone go, Fleet understands that not everything is me
## Changing someone's position
-From time to time, someone's job title changes. To do this, reach out to [Digital Experience](https://fleetdm.com/handbook/digital-experience).
+
+From time to time, someone's job title changes. Use the following steps to change someone's position:
+1. Create Slack channel: Create a private "#YYYY-change-title-for-xxxxxx" Slack channel (where "xxxxxx" is the Fleetie's name and YYYY is the current year) for discussion and invite the CEO and Head of Digital Experience.
+2. At-mention the Head of Digital Experience in the new channel with any context regarding the title change. Share any related documents with the Head of Digital Experience and the CEO.
+3. After getting approval from the [Head of People](https://fleetdm.com/handbook/digital-experience#team), the Digital Experience team will take the necessary steps to [change the fleetie's job title](https://fleetdm.com/handbook/digital-experience#change-a-fleeties-job-title).
diff --git a/handbook/demand/README.md b/handbook/demand/README.md
index 69dd7a2d843f..9d3f6eb5d561 100644
--- a/handbook/demand/README.md
+++ b/handbook/demand/README.md
@@ -31,20 +31,6 @@ Fleet's public relations firm is directly responsible for the accuracy of event
2. Update the workbook with the latest location, dates, and CFP deadlines from the website.
-### Respond to a "Contact us" submission
-
-1. Check the [_from-prospective-customers](https://fleetdm.slack.com/archives/C01HE9GQW6B) Slack channel for "Contact us" submissions.
-2. Mark submission as seen with the "👀" emoji.
-3. Within 4 business hours, use the [best practices template (private Google doc)](https://docs.google.com/document/d/1D02k0tc5v-sEJ4uahAouuqnvZ6phxA_gP-IqmkBdMTE/edit) to respond to general asks.
-4. Answer any technical questions to the best of your ability. If you are unable to answer a technical/product question, ask a Solutions Consultant in `#help-solutions-consulting`. If an SC is unavailable, post in `#g-mdm`or `#g-endpoint-ops`and notify @on-call.
-5. log in to [Salesforce](https://fleetdm.lightning.force.com/lightning/o/Lead/list?filterName=00B4x00000DtaRDEAZ) and search the lead list by first name and match the corresponding email to find the right lead.
-6. Enrich each lead with company information and buying situation.
-7. If a lead is completed or out of ICP, update the lead status in Salesforce to "Closed" or "Disqualified". If within ICP at-mention the [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) Slack channel and move lead to their name in SFDC.
-8. Mark the Slack message as complete with the "✅" emoji.
-
-> For any support-related questions, forward the submission to [Fleet's support team](https://docs.google.com/document/d/1tE-NpNfw1icmU2MjYuBRib0VWBPVAdmq4NiCrpuI0F0/edit#heading=h.wqalwz1je6rq).
-
-
### Begin or modify an advertising campaign
Any new ads or changes to current running ads are approved in ["🦢🗣 Design review (#g-digital-experience)"](https://app.zenhub.com/workspaces/-g-digital-experience-6451748b4eb15200131d4bab/board?sprints=none).
diff --git a/handbook/digital-experience/README.md b/handbook/digital-experience/README.md
index 34f77f2b9fe3..ab17f0cf5c89 100644
--- a/handbook/digital-experience/README.md
+++ b/handbook/digital-experience/README.md
@@ -91,7 +91,8 @@ When a Fleetie, consultant or advisor requests an update to their personnel deta
### Change a Fleetie's job title
When Digital Experience receives notification of a Fleetie's job title changing, follow these steps to ensure accurate recording of the change across our systems.
-1. Update ["🧑🚀 Fleeties"](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0):
+1. The Head of Digital Experience will bring the proposed title change to the next Roundup meeting with the CEO for approval. If the proposed change is rejected, the Head of Digital Experience will inform the requesting manager as to why.
+1. If approved, update ["🧑🚀 Fleeties"](https://docs.google.com/spreadsheets/d/1OSLn-ZCbGSjPusHPiR5dwQhheH1K8-xqyZdsOe9y7qc/edit#gid=0):
- Search the spreadsheet for the Fleetie in need of a job title change.
- Input the new job title in the Fleetie's row in the "Job title" cell.
- Navigate to the "Org chart" tab of the spreadsheet, and verify that the Fleetie's title appears correctly in the org chart.
@@ -765,6 +766,20 @@ Fleet has several brand fronts that need to be updated from time to time. Check
- The current [brand imagery](https://www.figma.com/design/1J2yxqH8Q7u8V7YTtA1iej/Social-media-(logos%2C-covers%2C-banners)?node-id=3962-65895). Check this [Loom video](https://www.loom.com/share/4432646cc9614046aaa4a74da1c0adb5?sid=2f84779f-f0bd-4055-be69-282c5a16f5c5) for more info.
+### Respond to a "Contact us" submission
+
+1. Check the [_from-prospective-customers](https://fleetdm.slack.com/archives/C01HE9GQW6B) Slack channel for "Contact us" submissions.
+2. Mark submission as seen with the "👀" emoji.
+3. Within 4 business hours, use the [best practices template (private Google doc)](https://docs.google.com/document/d/1D02k0tc5v-sEJ4uahAouuqnvZ6phxA_gP-IqmkBdMTE/edit) to respond to general asks.
+4. Answer any technical questions to the best of your ability. If you are unable to answer a technical/product question, ask a Solutions Consultant in `#help-solutions-consulting`. If an SC is unavailable, post in `#g-mdm`or `#g-endpoint-ops`and notify @on-call.
+5. log in to [Salesforce](https://fleetdm.lightning.force.com/lightning/o/Lead/list?filterName=00B4x00000DtaRDEAZ) and search the lead list by first name and match the corresponding email to find the right lead.
+6. Enrich each lead with company information and buying situation.
+7. If a lead is completed or out of ICP, update the lead status in Salesforce to "Closed" or "Disqualified". If within ICP at-mention the [Head of Digital Experience](https://fleetdm.com/handbook/digital-experience#team) in the [#g-digital-experience](https://fleetdm.slack.com/archives/C058S8PFSK0) Slack channel and move lead to their name in SFDC.
+8. Mark the Slack message as complete with the "✅" emoji.
+
+> For any support-related questions, forward the submission to [Fleet's support team](https://docs.google.com/document/d/1tE-NpNfw1icmU2MjYuBRib0VWBPVAdmq4NiCrpuI0F0/edit#heading=h.wqalwz1je6rq).
+
+
## Rituals
- Note: Some rituals (⏰) are especially time-sensitive and require attention multiple times (3+) per day. Set reminders for the following times (CT):
diff --git a/handbook/digital-experience/digital-experience.rituals.yml b/handbook/digital-experience/digital-experience.rituals.yml
index c36e75db31ec..f7f1f8d5aedc 100644
--- a/handbook/digital-experience/digital-experience.rituals.yml
+++ b/handbook/digital-experience/digital-experience.rituals.yml
@@ -9,6 +9,16 @@
autoIssue:
labels: [ "#g-digital-experience" ]
repo: "fleet"
+-
+ task: "Confirm consultant hours"
+ startedOn: "2024-09-30"
+ frequency: "Weekly"
+ description: "Perform step three in “Inform managers about hours worked” responsibility"
+ moreInfoUrl: "https://fleetdm.com/handbook/digital-experience#inform-managers-about-hours-worked"
+ dri: "SFriendLee"
+ autoIssue:
+ labels: [ "#g-digital-experience" ]
+ repo: "fleet"
-
task: "Prep 1:1s for OKR planning"
startedOn: "2024-09-09"
diff --git a/handbook/product-design/README.md b/handbook/product-design/README.md
index a81f4376f9e7..6b9a74ce64fe 100644
--- a/handbook/product-design/README.md
+++ b/handbook/product-design/README.md
@@ -137,6 +137,8 @@ Next, the API design DRI reviews all user stories and bugs with the release mile
To signal that the reference docs branch is ready for release, the API design DRI opens a PR to `main`, adds the DRI for what goes in a release as the reviewer, and adds the release milestone.
+> Anytime there is a missing or incorrect configuration option or REST API endpoint in the docs, it is treated as a released bug to be filed and fixed ASAP.
+
### Interview a Product Designer candidate
Ensure the interview process follows these steps in order. This process must follow [creating a new position](https://fleetdm.com/handbook/company/leadership#creating-a-new-position) through [receiving job applications](https://fleetdm.com/handbook/company/leadership#receiving-job-applications).
diff --git a/infrastructure/dogfood/terraform/aws/variables.tf b/infrastructure/dogfood/terraform/aws/variables.tf
index 2020de2f8306..d61f8819ec4b 100644
--- a/infrastructure/dogfood/terraform/aws/variables.tf
+++ b/infrastructure/dogfood/terraform/aws/variables.tf
@@ -56,7 +56,7 @@ variable "database_name" {
variable "fleet_image" {
description = "the name of the container image to run"
- default = "fleetdm/fleet:v4.57.0"
+ default = "fleetdm/fleet:v4.57.1"
}
variable "software_inventory" {
diff --git a/infrastructure/dogfood/terraform/gcp/variables.tf b/infrastructure/dogfood/terraform/gcp/variables.tf
index 906a58c153f8..a8877ab5e00f 100644
--- a/infrastructure/dogfood/terraform/gcp/variables.tf
+++ b/infrastructure/dogfood/terraform/gcp/variables.tf
@@ -68,7 +68,7 @@ variable "redis_mem" {
}
variable "image" {
- default = "fleetdm/fleet:v4.57.0"
+ default = "fleetdm/fleet:v4.57.1"
}
variable "software_installers_bucket_name" {
diff --git a/it-and-security/lib/configuration-profiles/macos-passcode-settings.json b/it-and-security/lib/configuration-profiles/macos-passcode-settings.json
new file mode 100644
index 000000000000..d433a91826fe
--- /dev/null
+++ b/it-and-security/lib/configuration-profiles/macos-passcode-settings.json
@@ -0,0 +1,12 @@
+{
+ "Type": "com.apple.configuration.passcode.settings",
+ "Identifier": "com.fleetdm.config.passcode.settings",
+ "Payload": {
+ "RequireAlphanumericPasscode": true,
+ "MinimumLength": 10,
+ "MinimumComplexCharacters": 1,
+ "MaximumFailedAttempts": 11,
+ "MaximumGracePeriodInMinutes": 1,
+ "MaximumInactivityInMinutes": 15
+ }
+}
diff --git a/it-and-security/lib/linux-device-health.policies.yml b/it-and-security/lib/linux-device-health.policies.yml
index 0d9e2f8aa2fb..b7093c02acdc 100644
--- a/it-and-security/lib/linux-device-health.policies.yml
+++ b/it-and-security/lib/linux-device-health.policies.yml
@@ -1,6 +1,6 @@
- name: Linux - Enable disk encryption
- query: SELECT 1 FROM disk_encryption WHERE encrypted=1 AND name LIKE '/dev/dm-1';
+ query: SELECT 1 FROM mounts m, disk_encryption d WHERE m.device_alias = d.name AND d.encrypted = 1 AND m.path = '/';
critical: false
description: This policy checks if disk encryption is enabled.
resolution: As an IT admin, deploy an image that includes disk encryption.
- platform: linux
\ No newline at end of file
+ platform: linux
diff --git a/it-and-security/teams/workstations.yml b/it-and-security/teams/workstations.yml
index 0ad266f905d9..f586b2b5d724 100644
--- a/it-and-security/teams/workstations.yml
+++ b/it-and-security/teams/workstations.yml
@@ -38,6 +38,7 @@ controls:
- path: ../lib/configuration-profiles/macos-password.mobileconfig
- path: ../lib/configuration-profiles/macos-prevent-autologon.mobileconfig
- path: ../lib/configuration-profiles/macos-secure-terminal-keyboard.mobileconfig
+ - path: ../lib/configuration-profiles/macos-passcode-settings.json
macos_setup:
bootstrap_package: ""
enable_end_user_authentication: true
diff --git a/orbit/pkg/installer/installer.go b/orbit/pkg/installer/installer.go
index c8fac3fcca4a..4059416f31ca 100644
--- a/orbit/pkg/installer/installer.go
+++ b/orbit/pkg/installer/installer.go
@@ -22,8 +22,10 @@ import (
"github.com/rs/zerolog/log"
)
-type QueryResponse = osquery_gen.ExtensionResponse
-type QueryResponseStatus = osquery_gen.ExtensionStatus
+type (
+ QueryResponse = osquery_gen.ExtensionResponse
+ QueryResponseStatus = osquery_gen.ExtensionStatus
+)
// Client defines the methods required for the API requests to the server. The
// fleet.OrbitClient type satisfies this interface.
@@ -202,7 +204,7 @@ func (r *Runner) installSoftware(ctx context.Context, installID string) (*fleet.
return payload, fmt.Errorf("creating temporary directory: %w", err)
}
- log.Debug().Msgf("about to download software installer")
+ log.Debug().Str("install_id", installID).Msgf("about to download software installer")
installerPath, err := r.OrbitClient.DownloadSoftwareInstaller(installer.InstallerID, tmpDir)
if err != nil {
return payload, err
@@ -233,7 +235,7 @@ func (r *Runner) installSoftware(ctx context.Context, installID string) (*fleet.
}
if installer.PostInstallScript != "" {
- log.Debug().Msgf("about to run post-install script")
+ log.Debug().Msgf("about to run post-install script for %s", installerPath)
postOutput, postExitCode, postErr := r.runInstallerScript(ctx, installer.PostInstallScript, installerPath, "post-install-script"+scriptExtension)
payload.PostInstallScriptOutput = &postOutput
payload.PostInstallScriptExitCode = &postExitCode
diff --git a/pkg/file/file.go b/pkg/file/file.go
index 7abd79169f1d..45774a3e9d8c 100644
--- a/pkg/file/file.go
+++ b/pkg/file/file.go
@@ -42,6 +42,8 @@ func ExtractInstallerMetadata(r io.Reader) (*InstallerMetadata, error) {
switch extension {
case "deb":
meta, err = ExtractDebMetadata(br)
+ case "rpm":
+ meta, err = ExtractRPMMetadata(br)
case "exe":
meta, err = ExtractPEMetadata(br)
case "pkg":
@@ -59,12 +61,16 @@ func ExtractInstallerMetadata(r io.Reader) (*InstallerMetadata, error) {
return meta, err
}
+// typeFromBytes deduces the type from the magic bytes.
+// See https://en.wikipedia.org/wiki/List_of_file_signatures.
func typeFromBytes(br *bufio.Reader) (string, error) {
switch {
case hasPrefix(br, []byte{0x78, 0x61, 0x72, 0x21}):
return "pkg", nil
case hasPrefix(br, []byte("!\ndebian")):
return "deb", nil
+ case hasPrefix(br, []byte{0xed, 0xab, 0xee, 0xdb}):
+ return "rpm", nil
case hasPrefix(br, []byte{0xd0, 0xcf}):
return "msi", nil
case hasPrefix(br, []byte("MZ")):
diff --git a/pkg/file/management.go b/pkg/file/management.go
index 26d1294952a9..3f83255142d2 100644
--- a/pkg/file/management.go
+++ b/pkg/file/management.go
@@ -16,6 +16,9 @@ var installExeScript string
//go:embed scripts/install_deb.sh
var installDebScript string
+//go:embed scripts/install_rpm.sh
+var installRPMScript string
+
// GetInstallScript returns a script that can be used to install the given extension
func GetInstallScript(extension string) string {
switch extension {
@@ -23,6 +26,8 @@ func GetInstallScript(extension string) string {
return installMsiScript
case "deb":
return installDebScript
+ case "rpm":
+ return installRPMScript
case "pkg":
return installPkgScript
case "exe":
@@ -44,6 +49,9 @@ var removeMsiScript string
//go:embed scripts/remove_deb.sh
var removeDebScript string
+//go:embed scripts/remove_rpm.sh
+var removeRPMScript string
+
// GetRemoveScript returns a script that can be used to remove an
// installer with the given extension.
func GetRemoveScript(extension string) string {
@@ -52,6 +60,8 @@ func GetRemoveScript(extension string) string {
return removeMsiScript
case "deb":
return removeDebScript
+ case "rpm":
+ return removeRPMScript
case "pkg":
return removePkgScript
case "exe":
@@ -73,6 +83,9 @@ var uninstallMsiScript string
//go:embed scripts/uninstall_deb.sh
var uninstallDebScript string
+//go:embed scripts/uninstall_rpm.sh
+var uninstallRPMScript string
+
// GetUninstallScript returns a script that can be used to uninstall a
// software item with the given extension.
func GetUninstallScript(extension string) string {
@@ -81,6 +94,8 @@ func GetUninstallScript(extension string) string {
return uninstallMsiScript
case "deb":
return uninstallDebScript
+ case "rpm":
+ return uninstallRPMScript
case "pkg":
return uninstallPkgScript
case "exe":
diff --git a/pkg/file/management_test.go b/pkg/file/management_test.go
index 8b5d8608c589..6f94fb293458 100644
--- a/pkg/file/management_test.go
+++ b/pkg/file/management_test.go
@@ -11,9 +11,7 @@ import (
"github.com/stretchr/testify/require"
)
-var (
- update = flag.Bool("update", false, "update the golden files of this test")
-)
+var update = flag.Bool("update", false, "update the golden files of this test")
func TestMain(m *testing.M) {
flag.Parse()
@@ -41,6 +39,11 @@ func TestGetInstallAndRemoveScript(t *testing.T) {
"remove": "./scripts/remove_deb.sh",
"uninstall": "./scripts/uninstall_deb.sh",
},
+ "rpm": {
+ "install": "./scripts/install_rpm.sh",
+ "remove": "./scripts/remove_rpm.sh",
+ "uninstall": "./scripts/uninstall_rpm.sh",
+ },
"exe": {
"install": "./scripts/install_exe.ps1",
"remove": "./scripts/remove_exe.ps1",
@@ -64,7 +67,7 @@ func assertGoldenMatches(t *testing.T, goldenFile string, actual string, update
t.Helper()
goldenPath := filepath.Join("testdata", goldenFile+".golden")
- f, err := os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE, 0644)
+ f, err := os.OpenFile(goldenPath, os.O_RDWR|os.O_CREATE, 0o644)
require.NoError(t, err)
defer f.Close()
diff --git a/pkg/file/rpm.go b/pkg/file/rpm.go
new file mode 100644
index 000000000000..b82221a46b70
--- /dev/null
+++ b/pkg/file/rpm.go
@@ -0,0 +1,33 @@
+package file
+
+import (
+ "crypto/sha256"
+ "fmt"
+ "io"
+
+ "github.com/cavaliergopher/rpm"
+)
+
+func ExtractRPMMetadata(r io.Reader) (*InstallerMetadata, error) {
+ h := sha256.New()
+ r = io.TeeReader(r, h)
+
+ // Read the package headers
+ pkg, err := rpm.Read(r)
+ if err != nil {
+ return nil, fmt.Errorf("read headers: %w", err)
+ }
+ // r is now positioned at the RPM payload.
+
+ // Ensure the whole file is read to get the correct hash
+ if _, err := io.Copy(io.Discard, r); err != nil {
+ return nil, fmt.Errorf("read all RPM content: %w", err)
+ }
+
+ return &InstallerMetadata{
+ Name: pkg.Name(),
+ Version: pkg.Version(),
+ SHASum: h.Sum(nil),
+ PackageIDs: []string{pkg.Name()},
+ }, nil
+}
diff --git a/pkg/file/rpm_test.go b/pkg/file/rpm_test.go
new file mode 100644
index 000000000000..d9d3127250e2
--- /dev/null
+++ b/pkg/file/rpm_test.go
@@ -0,0 +1,94 @@
+package file
+
+import (
+ "crypto/sha256"
+ "io"
+ "os"
+ "path/filepath"
+ "testing"
+
+ "github.com/fleetdm/fleet/v4/orbit/pkg/constant"
+ "github.com/goreleaser/nfpm/v2"
+ "github.com/goreleaser/nfpm/v2/files"
+ "github.com/goreleaser/nfpm/v2/rpm"
+ "github.com/stretchr/testify/require"
+)
+
+func TestExtractRPMMetadata(t *testing.T) {
+ //
+ // Build an RPM package on the fly with nfpm.
+ //
+ tmpDir := t.TempDir()
+ err := os.WriteFile(filepath.Join(tmpDir, "foo.sh"), []byte("#!/bin/sh\n\necho \"Foo!\"\n"), constant.DefaultFileMode)
+ require.NoError(t, err)
+ contents := files.Contents{
+ &files.Content{
+ Source: filepath.Join(tmpDir, "**"),
+ Destination: "/",
+ },
+ }
+ postInstallPath := filepath.Join(t.TempDir(), "postinstall.sh")
+ err = os.WriteFile(postInstallPath, []byte("#!/bin/sh\n\necho \"Hello world!\"\n"), constant.DefaultFileMode)
+ require.NoError(t, err)
+ info := &nfpm.Info{
+ Name: "foobar",
+ Version: "1.2.3",
+ Description: "Foo bar",
+ Arch: "x86_64",
+ Maintainer: "Fleet Device Management",
+ Vendor: "Fleet Device Management",
+ License: "LICENSE",
+ Homepage: "https://example.com",
+ Overridables: nfpm.Overridables{
+ Contents: contents,
+ Scripts: nfpm.Scripts{
+ PostInstall: postInstallPath,
+ },
+ },
+ }
+ rpmPath := filepath.Join(t.TempDir(), "foobar.rpm")
+ out, err := os.OpenFile(rpmPath, os.O_CREATE|os.O_RDWR, constant.DefaultFileMode)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ out.Close()
+ })
+ err = rpm.Default.Package(info, out)
+ require.NoError(t, err)
+ err = out.Close()
+ require.NoError(t, err)
+
+ //
+ // Test ExtractRPMMetadata with the generated package.
+ // Using ExtractInstallerMetadata for broader testing (for a file
+ // with rpm extension it will call ExtractRPMMetadata).
+ //
+ f, err := os.Open(rpmPath)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ f.Close()
+ })
+ m, err := ExtractInstallerMetadata(f)
+ require.NoError(t, err)
+ err = f.Close()
+ require.NoError(t, err)
+ require.Empty(t, m.BundleIdentifier)
+ require.Equal(t, "rpm", m.Extension)
+ require.Equal(t, "foobar", m.Name)
+ require.Equal(t, []string{"foobar"}, m.PackageIDs)
+ require.Equal(t, sha256FilePath(t, rpmPath), m.SHASum)
+ require.Equal(t, "1.2.3", m.Version)
+}
+
+func sha256FilePath(t *testing.T, path string) []byte {
+ f, err := os.Open(path)
+ require.NoError(t, err)
+ t.Cleanup(func() {
+ f.Close()
+ })
+ h := sha256.New()
+ _, err = io.Copy(h, f)
+ require.NoError(t, err)
+ err = f.Close()
+ require.NoError(t, err)
+ return h.Sum(nil)
+}
diff --git a/pkg/file/scripts/README.md b/pkg/file/scripts/README.md
index 606801ce682c..c2e0bff612bb 100644
--- a/pkg/file/scripts/README.md
+++ b/pkg/file/scripts/README.md
@@ -3,10 +3,15 @@
This folder contains scripts to install/remove software for different types of installers.
Scripts are stored on their own files for two reasons:
-
1. Some of them are read and displayed in the UI.
2. It's helpful to have good syntax highlighting and easy ways to run them.
+#### Scripts
+
+- `install_*.*`: Default installer scripts for each platform.
+- `uninstall_*.*`: Default uinstaller scripts for each platform.
+- `remove_*.*`: Uninstaller scripts used when the uninstall script is not set (for packages added before the uninstall feature was released) or empty uninstaller scripts.
+
#### Variables
The scripts in this folder accept variables like `$VAR_NAME` that will be replaced/populated by `fleetd` when they run.
@@ -14,4 +19,3 @@ The scripts in this folder accept variables like `$VAR_NAME` that will be replac
Supported variables are:
- `$INSTALLER_PATH` path to the installer file.
-
diff --git a/pkg/file/scripts/install_rpm.sh b/pkg/file/scripts/install_rpm.sh
new file mode 100644
index 000000000000..fb01bc2acda5
--- /dev/null
+++ b/pkg/file/scripts/install_rpm.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+dnf install --assumeyes "$INSTALLER_PATH"
diff --git a/pkg/file/scripts/remove_rpm.sh b/pkg/file/scripts/remove_rpm.sh
new file mode 100644
index 000000000000..efd8e0bb0c89
--- /dev/null
+++ b/pkg/file/scripts/remove_rpm.sh
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+package_name=$PACKAGE_ID
+
+# Fleet uninstalls app using product name that's extracted on upload
+dnf remove --assumeyes "$package_name"
diff --git a/pkg/file/scripts/uninstall_rpm.sh b/pkg/file/scripts/uninstall_rpm.sh
new file mode 100644
index 000000000000..0d664e32f439
--- /dev/null
+++ b/pkg/file/scripts/uninstall_rpm.sh
@@ -0,0 +1,4 @@
+package_name=$PACKAGE_ID
+
+# Fleet uninstalls app using product name that's extracted on upload
+dnf remove --assumeyes "$package_name"
diff --git a/pkg/file/testdata/scripts/install_rpm.sh.golden b/pkg/file/testdata/scripts/install_rpm.sh.golden
new file mode 100644
index 000000000000..fb01bc2acda5
--- /dev/null
+++ b/pkg/file/testdata/scripts/install_rpm.sh.golden
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+dnf install --assumeyes "$INSTALLER_PATH"
diff --git a/pkg/file/testdata/scripts/remove_rpm.sh.golden b/pkg/file/testdata/scripts/remove_rpm.sh.golden
new file mode 100644
index 000000000000..efd8e0bb0c89
--- /dev/null
+++ b/pkg/file/testdata/scripts/remove_rpm.sh.golden
@@ -0,0 +1,6 @@
+#!/bin/sh
+
+package_name=$PACKAGE_ID
+
+# Fleet uninstalls app using product name that's extracted on upload
+dnf remove --assumeyes "$package_name"
diff --git a/pkg/file/testdata/scripts/uninstall_rpm.sh.golden b/pkg/file/testdata/scripts/uninstall_rpm.sh.golden
new file mode 100644
index 000000000000..0d664e32f439
--- /dev/null
+++ b/pkg/file/testdata/scripts/uninstall_rpm.sh.golden
@@ -0,0 +1,4 @@
+package_name=$PACKAGE_ID
+
+# Fleet uninstalls app using product name that's extracted on upload
+dnf remove --assumeyes "$package_name"
diff --git a/proposals/Fleet-Installers-4-Sandbox.md b/proposals/Fleet-Installers-4-Sandbox.md
deleted file mode 100644
index 7043c05fe987..000000000000
--- a/proposals/Fleet-Installers-4-Sandbox.md
+++ /dev/null
@@ -1,50 +0,0 @@
-# Fleet Sandbox & Pre-Packaged Fleet-osquery Installers
-
-## Goals
-
-1. Improve UX on Fleet Sandbox by offering pre-packaged Fleet-osquery installers.
-2. Add the "Pre-Packaged installers" feature to "Fleet Sandbox" as soon as possible (i.e. not block on having a fully functional "Fleet Packager" service).
-
-## Fleet Sandbox Assumptions
-
-- We will limit number of teams to T.
-- Sandbox has good root CA trusted certificates
-- Users won't be allowed to change enroll secrets.
-
-## Pre-Packaged Installers Plan
-
-We will need some changes to fleetctl, the pre-provisioner, the Fleet server and UI.
-
-### fleetctl
-
-Make all functionality in `fleetctl package` to run in linux. (This change will be needed for the Packager service anyways.)
-
-PS: Abstract in its own package so that it can be used by Packager service in a next iteration.
-
-### Pre-provisioner
-
-Following are the pre-provisioner steps to generate the pre-packaged installers:
-
-1. Generate T+1 random enroll secrets.
-
-2. Run `fleetctl package --type={pkg|rpm|deb|msi}` with T+1 enroll secrets (i.e. one for Global and one for each team).
-PS: There's some complexity in storing/handling credentials for macOS Signing and Notarization of the packages.
-
-3. The generated packages will be stored in a S3 bucket accessible by the Fleet server with the following object name format
-`$INSTALLERS_DIR/$ENROLL_SECRET/fleet-osquery.$TYPE`, e.g. `/fleet-installers/FzRCZWTlEY2kqzIwk1BE9fru5KuhrlYP/fleet-osquery.pkg`.
-We propose using S3 to support multiple Fleet instances serving the requests.
-
-4. Set comma-separated `FLEET_ENROLL_POOL` environment variable to Fleet server config (Fleet would use those secrets instead of randomly generating one).
-The Fleet server will only serve the installers with enroll secret listed in this variable (security check).
-
-### Fleet Server and UI
-
-- Fleet server new configuration and new functionality:
- - `FLEET_MAX_TEAMS`: Maximum number of teams to allow in the deployment (default 0, disabled).
- - `FLEET_DISABLE_ENROLL_CHANGE`: Disallow users from changing enroll secrets (default false).
- - `FLEET_PACKAGES_S3_*`: S3 configuration for the retrieval of the pre-packaged installers (default empty).
- - `FLEET_ENROLL_POOL`: comma-separated enrolls to use when needed (default empty), must have equals to FLEET_MAX_TEAMS+1 items (default empty).
-
-- Fleet will serve a new authenticated API (for Sandbox-only): `{GET|HEAD} /api/v1/fleet/download_installer/{enroll}/{type}`, e.g. `GET /api/v1/fleet/download_installer/FzRCZWTlEY2kqzIwk1BE9fru5KuhrlYP/rpm`.
- - The UI can make a `HEAD` request to check if an installer exists, if so, then it can display a download button for it, (if not, "show the current UI"? TBD with UI team)
- - The API looks for the installer corresponding to the Global/Team the user is looking at, and returns it for download.
diff --git a/proposals/Fleet-Installers.md b/proposals/Fleet-Installers.md
deleted file mode 100644
index f6604e36c431..000000000000
--- a/proposals/Fleet-Installers.md
+++ /dev/null
@@ -1,469 +0,0 @@
-# Fleet Installers
-
-## Goal
-
-[#5757](https://github.com/fleetdm/fleet/issues/5757)
-
-```
-As a user, I want to be able to download a Fleet-osquery installer (aka Orbit) in the Fleet UI
-so that I can add hosts to Fleet without having to know how to successfully generate an
-installer with the `fleetctl package` command.
-
-Figma wireframes: https://www.figma.com/file/hdALBDsrti77QuDNSzLdkx/?node-id=6740%3A267448
-```
-
-## Command `fleetctl package`
-
-Currently users use the `fleetctl package` command to generate Fleet-osquery packages.
-
-The `fleetctl package` command has the following *required* configuration that is specific to a "Fleet Deployment":
-- `--fleet-url`: The URL that the hosts will use to connect to the Fleet server.
-- `--enroll-secret`: Global or team enroll secret to use when enrolling the host to Fleet.
-
-As the goal states, we would like to provide functionality in Fleet to automatically generate and download such packages from the UI.
-
-## How
-
-We can implement such functionality in two ways:
-
-- Option A. Fleet Server to generate such packages itself.
-- Option B. Separate "Packager" service.
-
-There's a lot of platform specific logic and tooling involved in packaging, and one of Fleet's primary goals is to keep deployment/infrastructure simple for On-Prem deployments.
-To that end, we believe the best option is Option B, implementing the functionality as a separate service.
-
-## Packager Service
-
-The "Fleet Packager" service will implement all the package generation logic. Think of it like offering the `fleetctl package` command functionality but as a REST service.
-
-```mermaid
-graph LR;
- A[User-Agent/ Browser]-- Fleet API -->B[Fleet UI/Server]
- A-- Packager API -->C
- subgraph FleetDM Hosted Infrastructure
- direction LR
- subgraph pkg.fleetctl.com
- direction TB;
- C[Packager Service]
- packages[(Generated packages)]
- end
- direction TB;
- D[tuf.fleetctl.com]
- C-- Fetch Targets -->D;
- end
- E[Apple's Notary Server]
- C-- Notary API ---->E
-```
-
-Configurations:
-- The `Fleet UI/Server` will allow configuring the "Fleet Packager" URL (default value being FleetDM Hosted Packager service, something like `pkg.fleetctl.com`).
-- The `Packager Service` will allow configuring an alternative TUF server URL and TUF roots via an environmental variable (default value being FleetDM Hosted TUF, `tuf.fleetctl.com`).
-
-Both of these configurations will allow users to deploy their own `Packager Service`.
-
-### Storage
-
-The generated packages should be stored on encrypted disk and will expire (will be deleted) after a configurable amount of time (default 30m).
-There are two reasons we want to expire generated packages soon:
-1. To not store user credentials for too long (URL and enroll secret).
-2. To free up space.
-
-We can use "Storage Optimized" instances (see https://aws.amazon.com/ec2/instance-types/).
-
-#### Notes
-
-- As a possible future optimization, we could use "Memory Optimized" instances and store packages in RAM instead of using hard disk.
-- S3 could also be used to store such packages, but disk storage is needed to generate the packages in the first place.
-So hard-disk will be a dependency anyways (and ideally we would like these packages with sensitive credentials to be stored in one location).
-- From Roberto: "sounds like the main bottleneck here will be transferring the data over the network to the user doing the request.".
-In other words, we should apply all optimizations on the network (like caching, reducing package size, etc.), that will be our main bottleneck
-(not CPU or hard-disk access).
-
-#### Back of the Envelope
-
-- `.pkg`s use ~70MB of storage.
-- `.msi`s use ~20MB of storage.
-- `.deb`s and `.rpm`s use ~75MB of storage.
-
-Assuming the worst case of ~75 MB for each package:
-If we have a ~30TB hard disk, it would allow storing ~400_000 packages simultaneously.
-
-### Network & Credentials
-
-The service will require network access to (URLs provided via config):
-
-- TUF server.
-- Apple's Notary Server (for generating `.pkg`).
-
-The packaging service will need the following credentials (provided via config):
-
-- Apple credentials:
- - Codesign identity.
- - Username and Password for notarization.
-- TUF server update roots (default will be the hardcoded one for FleetDM's hosted TUF server, tuf.fleetctl.com).
-
-### Packager REST API
-
-For the MVF (Minimum Viable Feature) we'll need three APIs: one for creation/submission, one for checking status and another one for the actual download of the package.
-All the APIs must be rate-limited to prevent abuse of the system.
-
-#### 1. Package creation
-
-`POST /create`
-
-This endpoint will perform the following operations:
-1. Check if a `package_id` already exists (and hasn't been expired) with the exact same arguments, if so, return HTTP 200 with the `package_id`.
-2. Generate a [random](https://en.wikipedia.org/wiki/Universally_unique_identifier#Version_4_(random)) `package_id`.
-3. Dispatch the creation of a package with ID set to `package_id` and the given request parameters.
-4. Return HTTP 200 with the `package_id`.
-
-This endpoint, which is the entrypoint, should be rate-limited by IP.
-
-##### Request Fields
-
-| Name | Type | In | Description |
-| ----------------- | ------- | ---- | -------------------------------------------------------------------------------------- |
-| type | string | body | **Required.** One of the following values "pkg", "msi", "deb", "rpm" |
-| fleet_url | string | body | **Required.** The URL that the hosts will use to connect to the Fleet server |
-| enroll_secret | string | body | **Required.** Global or team enroll secret to use when enrolling the host to Fleet |
-| retry | boolean | body | Retry a failed package generation (default: false) |
-| fleet-certificate | string | body | Server certificate chain |
-| insecure | boolean | body | Disable TLS certificate verification (default: false) |
-| osqueryd-channel | string | body | Update channel of osqueryd to use (default: "stable") |
-| desktop-channel | string | body | Update channel of desktop to use (default: "stable") |
-| orbit-channel | string | body | Update channel of Orbit to use (default: "stable") |
-| disable-updates | boolean | body | Disable auto updates on the generated package (default: false) |
-| debug | boolean | body | Enable debug logging in orbit (default: false) |
-| fleet-desktop | boolean | body | Include the Fleet Desktop Application in the package (default: false) |
-| update-interval | string | body | Interval that Orbit will use to check for new updates (10s, 1h, etc.) (default: 15m0s) |
-| osquery-flagfile | string | body | Flagfile to package and provide to osquery (default: empty) |
-| service | boolean | body | Install with a persistence service (launchd, systemd, etc.) (default: true) |
-
-##### Error in Package Generation
-
-When the set of arguments correspond to a `package_id` that failed to generate, then:
-- If `retry` is `false` (default) it will return such `package_id`.
-- If `retry` is set to `true`, the service will dispatch a new package build and return a new `package_id`.
-
-##### Response Fields
-
-| Name | Type | In | Description |
-| ----------------- | ------- | ---- | -------------------------------- |
-| package_id | string | body | ID of the package being created. |
-
-#### 2. Package Status Check
-
-`GET /status`
-
-This endpoint allows checking the status of a package being created.
-Clients can poll for the status of a package using this API.
-
-This endpoint should be rate-limited by `package_id`.
-
-##### Request Fields
-
-| Name | Type | In | Description |
-| ----------------- | ------- | ----- | ------------------------------------------------------------------------ |
-| package_id | string | query | **Required.** ID of the package created with `POST /create` |
-
-##### Response Fields
-
-| Name | Type | In | Description |
-| ----------------- | ------- | ---- | ---------------------------------------------------------------------------- |
-| status | string | body | One of the following values "success", "fail", "in-progress", "expired" |
-| download_url | string | body | Set to the download URL if status field is "success" |
-| stage | string | body | Set to a "stage" string if status field is "in-progress" (e.g. "notarizing") |
-| logs | string | body | Contains logs if status is "failed" |
-
-#### 3. Package Download
-
-`GET /download/{package_id}`
-
-This is the API to download the generated package.
-
-This endpoint should be rate-limited by `package_id`.
-
-## Sequence Diagram
-
-Following is the sequence diagram for the happy-path.
-
-```mermaid
-sequenceDiagram
- User-Agent/Browser->>Fleet: GET /api/v1/fleet/config
- Fleet-->>User-Agent/Browser: packager_url
- User-Agent/Browser->>Packager: POST /create
- Packager-->>User-Agent/Browser: package_id
- Packager-->>TUF Server: Fetch targets
- loop
- User-Agent/Browser->>Packager: GET /status
- Packager-->>User-Agent/Browser: status == "in-progress"
- Note over User-Agent/Browser: Retry every ~10 seconds, until status is "success"
- TUF Server->>Packager: Targets
- Note over Packager: Build package
- Note over Packager: Sign package
- rect rgb(128, 128, 128)
- Note right of Packager: (When generating macOS pkgs)
- Packager-->>Apple's Notary Server: New SubmissionRequest (upload)
- Apple's Notary Server->>Packager: NewSubmissionResponse
- Note over Packager: Upload to S3 (see Apple docs)
- loop Every ~30 seconds, until status is "Accepted"
- Packager->>Apple's Notary Server: Get submission status
- Apple's Notary Server-->>Packager: status
- end
- end
- end
- User-Agent/Browser->>Packager: GET /download/{package_id}
- Packager-->>User-Agent/Browser: Package File
-```
-
-## Package Generation Time
-
-Some back of the envelope calculations for the time the user clicks Download to the time the installer is downloaded fully.
-
-### macOS
-
-Test running `time fleetctl package --type=pkg --fleet-url=... --enroll-secret=... --fleet-desktop` (from Argentina).
-
-1. Download packages from TUF and generate raw `.pkg` (16 seconds).
-2. Signing (assuming this is negligible).
-3. Notarization (~3 minutes, from Zach's tests mentioned below).
-4. Download from Packager service (~15 seconds to download a 100 MB file).
-
-Total: ~4 minutes
-
-### Windows
-
-Test running `time fleetctl package --type=msi --fleet-url=... --enroll-secret=... --fleet-desktop` (from Argentina).
-
-1. Download packages from TUF and generate raw `.msi` (~18 seconds).
-2. Download from Packager service (~15 seconds to download a 100 MB file).
-
-Total: ~30 seconds.
-
-### Linux
-
-Test running `time fleetctl package --type={deb|rpm} --fleet-url=... --enroll-secret=... --fleet-desktop` (from Argentina).
-
-1. Download packages from TUF and generate raw `.{deb|rpm}` (~30 seconds).
-2. Download from Packager service (~30 seconds to download a 200 MB file).
-
-Total: ~1 minute.
-
-### Possible Download Time Optimization
-
-- The Packager service could cache the TUF targets for a couple of minutes (given that they usually change ~ every three weeks).
-This would reduce 15s-30s the time it takes to generate the packages.
-
-## Platform Specifics
-
-### macOS
-
-There are two operations **required** to have a proper macOS installer package (`.pkg`):
-
-1. Code signing: Used by Gatekeeper to verify the author of the package (identified by Developer ID)
-2. Notarization: "Gives users more confidence that the Developer ID-signed software you distribute has been checked by Apple for malicious components."
-
-The packages are composed by three TUF targets: osquery, Orbit and Fleet Desktop.
-
-All the TUF targets served by FleetDM's TUF server are signed and notarized:
-
-- osquery: signed and notarized .app (by Osquery)
-- Orbit: signed and notarized executable (by Fleet DM)
-- fleet-desktop: signed and notarized .app (by Fleet DM)
-
-Even if all targets are signed and notarized we must still sign and notarize the `.pkg` installer as a whole, see [#122045](https://developer.apple.com/forums/thread/122045).
-
-#### Notarization
-
-The Notarization process can be summarized to the following steps:
-
-1. Submit/Upload the **signed** package to the Notary Server.
-2. Notary server performs automated security checks.
-3. Notary generates a ticket, publishes ticket online (so that Gatekeeper can find it) and returns the ticket.
-4. Ticket can be stapled to your software to let Gatekeeper know it has been notarized.
-
-##### Notarization Limitations
-
-- Notarization completes for most software within 5 minutes, and for 98 percent of software within 15 minutes. Thus our workflow must support users leaving the "Add hosts" page and returning later.
-- To help avoid long response times they suggest: to "limit notarizations to 75 per day." So it doesn't look like a hard limit, just something that would help reduce upload response times.
-Source: https://developer.apple.com/documentation/security/notarizing_macos_software_before_distribution/customizing_the_notarization_workflow
-Though Zach has ran the following test with no issues:
-> I was able to Notarize ~200 packages in under 12 hours yesterday with no apparent limiting.
-> Note this was using the older Notarization API (slower, about 3 mins per notarization).
-
-#### Future Optimizations for Notarization
-
-This is the current structure of a `.pkg` generated with `fleetctl package` (unsigned and unnotarized):
-```sh
-pkg_expanded
-├── Distribution
-└── base
- ├── Bom
- ├── Library
- │ └── LaunchDaemons
- │ └── com.fleetdm.orbit.plist
- ├── PackageInfo
- ├── Payload
- ├── Scripts
- │ └── postinstall
- └── opt
- └── orbit
- ├── bin
- │ ├── desktop
- │ │ └── macos
- │ │ └── stable
- │ │ ├── Fleet Desktop.app
- │ │ │ └── Contents
- │ │ │ ├── CodeResources
- │ │ │ ├── Info.plist
- │ │ │ ├── MacOS
- │ │ │ │ └── fleet-desktop
- │ │ │ └── _CodeSignature
- │ │ │ └── CodeResources
- │ │ └── desktop.app.tar.gz
- │ ├── orbit
- │ │ └── macos
- │ │ └── stable
- │ │ └── orbit
- │ └── osqueryd
- │ └── macos-app
- │ └── stable
- │ ├── osquery.app
- │ │ └── Contents
- │ │ ├── Info.plist
- │ │ ├── MacOS
- │ │ │ └── osqueryd
- │ │ ├── PkgInfo
- │ │ ├── Resources
- │ │ │ └── osqueryctl
- │ │ ├── _CodeSignature
- │ │ │ └── CodeResources
- │ │ └── embedded.provisionprofile
- │ └── osqueryd.app.tar.gz
- ├── certs.pem
- ├── osquery.flags
- ├── secret.txt
- ├── staging
- └── tuf-metadata.json
-```
-
-##### Remove Unnecessary Files
-
-The `osqueryd.app.tar.gz` and `desktop.app.tar.gz` files are used by Orbit to compare with remote targets when checking for updates.
-A future optimization could replace those `.app.tar.gz` files with a txt file with the hash of such file
-(would reduce ~30MB of uncompressed size reduction for the `.pkg`).
-
-##### Notarized Package without Configs
-
-The only files that really change when users generate a pkg are:
-- Library/LaunchDaemons/com.fleetdm.orbit.plist (contains the configuration, like URLs, update channels, etc.)
-- opt/orbit/secret.txt (enroll secret)
-
-Another idea to consider could be packaging and notarizing code and scripts, but leave specific configs out of the `.pkg`.
-Problem: We would need to solve how to apply the additional configuration (the two files above) to installed packages.
-
-From [#122512](https://developer.apple.com/forums/thread/122512):
-
-> I thought there might be some apple approved way of adding extra information to a package.
-
->> No. The notarisation ticket covers the code signature of the package, and the code signature of the package covers the
->> effective contents of that package. This is pretty much required. For example, one of your goals is to tweak the install scripts,
->> but such scripts execute with enormous privileges and thus must be covered by the code signature, and thus covered by the notarisation ticket.
-
----
-
-> Is there a Apple recommended method to provide custom packages for different customers?
-> Some way to add additional parameters to a package without breaking the notarizartion/signing of it?
-
->> Option 2) Change your distribution strategy to distribute a static executable.
->> Download any customer-specific resources and store them in /Library/Application Support.
-
-### Windows
-
-Currently we don't support signing of the MSI installers in `fleetctl package`. But this functionality could be added both to the command and the service.
-From Zach:
-> When we decide to support this (which we should do soon), we can use https://github.com/mtrojnar/osslsigncode.
-
-We should also research https://github.com/sassoftware/relic (it mentions Windows signature support)
-
-## Service Implementation
-
-It looks like we will be able to implement the first iteration of the Packager Service as a Go service running on a Linux server.
-The only limitation will be macOS stapling (see below).
-
-### Scale
-
-MVP of the service will support running one instance of the service. Probably ok as the majority of the load will be IO, network and disk.
-
-Nothing prevent us from horizontal scaling by sharding requests by `package_id` (or a `client_id`) in the future.
-
-### State storing
-
-For the first iteration, we should pick one of the following simple options:
-1. Store state in-memory. Simplest, but not resilient to crashes/restarts.
-2. Store state in an embedded disk database, such as:
- - https://github.com/dgraph-io/badger (Pure Go)
- - https://github.com/etcd-io/bbolt (Pure Go)
- - Sqlite3 (Cgo 🙈), see https://www.youtube.com/watch?v=XcAYkriuQ1o
-
-- We already need disk storage for storing the packages, so option (2) is feasible to do from the get-go.
-
-### macOS requirements
-
-- Code Signing: Looks like https://github.com/sassoftware/relic would allow us to sign a `.pkg` in pure Go.
-- Notarization: We can implement a Go package that uses the new [Notary API](https://developer.apple.com/documentation/notaryapi).
-Limitation: Such API does not offer a way to "staple" the package, thus we would depend on the `stapler` tool for this last step (such tool is only available on macOS).
-So if we run the Packager service on a Linux server, we won't be able to support stapling. However, it seems stapling is recommended but not a must, see [#116812](https://developer.apple.com/forums/thread/116812).
-> If Gatekeeper can’t find a notarisation ticket stapled to the item, it attempts to get that ticket from the Apple notarisation servers.
-> Assuming the Mac is online, this typically works and thus the Gatekeeper check succeeds.
-
-### Windows requirements
-
-We need Docker to generate the MSI for Windows (the `fleetctl package` command uses the `fleetdm/wix:latest` docker image to generate them).
-We will need to pre-fetch such Docker Image during initialization (to not delay the first request indefinitely).
-
-PS: We could alternatively try a PoC that uses Wine+WiX on Linux without Docker?
-
-### Lambda?
-
-Depending on dependencies we could implement the service as a Lambda Function. Though we could make this a full service to not be tied to a specific provider.
-
-## Security / Threat model
-
-What could go wrong?
-
-### Fleet Credentials
-
-The MVP of the service will have access to:
-- Fleet's Developer Signing Key (for `.pkg` signing).
-- Fleet's Apple Connect Username and Password (for `.pkg` notarization).
-
-From Guillaume:
-> If someone manages to compromise this service, they could potentially sign AND notarize malware under Fleet's identity.
-
-Remediation: Fleet credentials should be stored on a secrets manager, e.g. [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/latest/userguide/intro.html).
-
-### User Credentials
-
-The Fleet-URL and enroll secret are stored within the generated packages.
-If the unexpired installers are leaked, users' Fleet URLs and enroll secrets would be compromised.
-Attackers could enroll their devices to users' Fleet deployment.
-
-An attacker with access to an enroll secret can perform the following attacks:
-- Feed a Fleet server with fake data (possibly DoS the service).
-- Leak the queries configured in Fleet.
-
-Remediation: All generated packages should be securely deleted when they expire.
-
-## Sandbox/Demo & Cloud
-
-The design supports the following deployments:
-
-- Fleet On-Prem running with FleetDM's Packager and TUF.
-- Fleet On-Prem running with On-Prem Packager with FleetDM's TUF server.
-- Fleet On-Prem running with On-Prem Packager with On-Prem TUF server.
-- Fleet Sandbox/Demo & Cloud (basically all hosted by FleetDM).
-
-## New Fleetctl Command
-
-We could add new flags (e.g. `--remote`) to `fleetctl package` to generate the packages using a Packager Service instead of building locally.
\ No newline at end of file
diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go
index 650c097cb985..ccaeb4f16ae0 100644
--- a/server/datastore/mysql/activities.go
+++ b/server/datastore/mysql/activities.go
@@ -539,35 +539,65 @@ func (ds *Datastore) CleanupActivitiesAndAssociatedData(ctx context.Context, max
// `activities` and `queries` are not tied because the activity itself holds
// the query SQL so they don't need to be executed on the same transaction.
//
- if err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
- // Delete temporary queries (aka "not saved").
- if _, err := tx.ExecContext(ctx,
- `DELETE FROM queries
+ // All expired live queries are deleted in batch sizes of `maxCount` to ensure
+ // the table size is kept in check with high volumes of live queries (zero-trust workflows).
+ // This differs from the `activities` cleanup which uses maxCount as a limit to
+ // the number of activities to delete.
+ //
+ for {
+ if ctx.Err() != nil {
+ return ctx.Err()
+ }
+
+ var rowsAffected int64
+
+ // Start a new transaction for each batch of deletions.
+ err := ds.withTx(ctx, func(tx sqlx.ExtContext) error {
+ // Delete expired live queries (aka "not saved")
+ result, err := tx.ExecContext(ctx,
+ `DELETE FROM queries
WHERE NOT saved AND created_at < DATE_SUB(NOW(), INTERVAL ? DAY)
LIMIT ?`,
- expiredWindowDays, maxCount,
- ); err != nil {
- return ctxerr.Wrap(ctx, err, "delete expired non-saved queries")
- }
- // Delete distributed campaigns that reference unexisting query (removed in the previous query).
- if _, err := tx.ExecContext(ctx,
- `DELETE distributed_query_campaigns FROM distributed_query_campaigns
+ expiredWindowDays, maxCount,
+ )
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired non-saved queries")
+ }
+
+ rowsAffected, err = result.RowsAffected()
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "retrieving rows affected from delete query")
+ }
+
+ // Cleanup orphaned distributed campaigns that reference non-existing queries.
+ if _, err := tx.ExecContext(ctx,
+ `DELETE distributed_query_campaigns FROM distributed_query_campaigns
LEFT JOIN queries ON (distributed_query_campaigns.query_id=queries.id)
WHERE queries.id IS NULL`,
- ); err != nil {
- return ctxerr.Wrap(ctx, err, "delete expired orphaned distributed_query_campaigns")
- }
- // Delete distributed campaign targets that reference unexisting distributed campaign (removed in the previous query).
- if _, err := tx.ExecContext(ctx,
- `DELETE distributed_query_campaign_targets FROM distributed_query_campaign_targets
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired orphaned distributed_query_campaigns")
+ }
+
+ // Cleanup orphaned distributed campaign targets that reference non-existing distributed campaigns.
+ if _, err := tx.ExecContext(ctx,
+ `DELETE distributed_query_campaign_targets FROM distributed_query_campaign_targets
LEFT JOIN distributed_query_campaigns ON (distributed_query_campaign_targets.distributed_query_campaign_id=distributed_query_campaigns.id)
WHERE distributed_query_campaigns.id IS NULL`,
- ); err != nil {
- return ctxerr.Wrap(ctx, err, "delete expired orphaned distributed_query_campaign_targets")
+ ); err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired orphaned distributed_query_campaign_targets")
+ }
+
+ return nil
+ })
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "delete expired queries in batch")
+ }
+
+ // Break the loop if no rows were deleted in the current batch.
+ if rowsAffected == 0 {
+ break
}
- return nil
- }); err != nil {
- return ctxerr.Wrap(ctx, err, "delete expired distributed queries")
}
+
return nil
}
diff --git a/server/datastore/mysql/activities_test.go b/server/datastore/mysql/activities_test.go
index 17bd72e6ee8c..669680127633 100644
--- a/server/datastore/mysql/activities_test.go
+++ b/server/datastore/mysql/activities_test.go
@@ -991,7 +991,7 @@ func testCleanupActivitiesAndAssociatedDataBatch(t *testing.T, ds *Datastore) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &queriesLen, `SELECT COUNT(*) FROM queries WHERE NOT saved;`)
})
- require.Equal(t, 1000, queriesLen)
+ require.Equal(t, 250, queriesLen) // All expired queries should be cleaned up.
err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
require.NoError(t, err)
@@ -1002,7 +1002,7 @@ func testCleanupActivitiesAndAssociatedDataBatch(t *testing.T, ds *Datastore) {
ExecAdhocSQL(t, ds, func(q sqlx.ExtContext) error {
return sqlx.GetContext(ctx, q, &queriesLen, `SELECT COUNT(*) FROM queries WHERE NOT saved;`)
})
- require.Equal(t, 500, queriesLen)
+ require.Equal(t, 250, queriesLen)
err = ds.CleanupActivitiesAndAssociatedData(ctx, maxCount, 1)
require.NoError(t, err)
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index 610a25db52fa..ad18242eb754 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -291,7 +291,9 @@ WHERE
}
func (ds *Datastore) DeleteMDMAppleConfigProfileByDeprecatedID(ctx context.Context, profileID uint) error {
- return ds.deleteMDMAppleConfigProfileByIDOrUUID(ctx, profileID, "")
+ return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
+ return deleteMDMAppleConfigProfileByIDOrUUID(ctx, tx, profileID, "")
+ })
}
func (ds *Datastore) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID string) error {
@@ -299,10 +301,20 @@ func (ds *Datastore) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUI
if strings.HasPrefix(profileUUID, fleet.MDMAppleDeclarationUUIDPrefix) {
return ds.deleteMDMAppleDeclaration(ctx, profileUUID)
}
- return ds.deleteMDMAppleConfigProfileByIDOrUUID(ctx, 0, profileUUID)
+ return ds.withRetryTxx(ctx, func(tx sqlx.ExtContext) error {
+ if err := deleteMDMAppleConfigProfileByIDOrUUID(ctx, tx, 0, profileUUID); err != nil {
+ return err
+ }
+
+ if err := deleteUnsentAppleHostMDMProfile(ctx, tx, profileUUID); err != nil {
+ return err
+ }
+
+ return nil
+ })
}
-func (ds *Datastore) deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context, id uint, uuid string) error {
+func deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context, tx sqlx.ExtContext, id uint, uuid string) error {
var arg any
stmt := `DELETE FROM mdm_apple_configuration_profiles WHERE `
if uuid != "" {
@@ -312,7 +324,7 @@ func (ds *Datastore) deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context,
arg = id
stmt += `profile_id = ?`
}
- res, err := ds.writer(ctx).ExecContext(ctx, stmt, arg)
+ res, err := tx.ExecContext(ctx, stmt, arg)
if err != nil {
return ctxerr.Wrap(ctx, err)
}
@@ -328,6 +340,15 @@ func (ds *Datastore) deleteMDMAppleConfigProfileByIDOrUUID(ctx context.Context,
return nil
}
+func deleteUnsentAppleHostMDMProfile(ctx context.Context, tx sqlx.ExtContext, uuid string) error {
+ const stmt = `DELETE FROM host_mdm_apple_profiles WHERE profile_uuid = ? AND status IS NULL AND operation_type = ? AND command_uuid = ''`
+ if _, err := tx.ExecContext(ctx, stmt, uuid, fleet.MDMOperationTypeInstall); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting host profile that has not been sent to host")
+ }
+
+ return nil
+}
+
func (ds *Datastore) DeleteMDMAppleDeclarationByName(ctx context.Context, teamID *uint, name string) error {
const stmt = `DELETE FROM mdm_apple_declarations WHERE team_id = ? AND name = ?`
diff --git a/server/datastore/mysql/hosts.go b/server/datastore/mysql/hosts.go
index ee23749de80a..6026dcd9b140 100644
--- a/server/datastore/mysql/hosts.go
+++ b/server/datastore/mysql/hosts.go
@@ -403,13 +403,17 @@ func loadHostPackStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint,
return ps, nil
}
+// loadHostScheduledQueryStatsDB will load all the scheduled query stats for the given host.
+// The filter is split into two statements joined by a UNION ALL to take advantage of indexes.
+// Using an OR in the WHERE clause causes a full table scan which causes issues with a large
+// queries table due to the high volume of live queries (created by zero trust workflows)
func loadHostScheduledQueryStatsDB(ctx context.Context, db sqlx.QueryerContext, hid uint, hostPlatform string, teamID *uint) ([]fleet.QueryStats, error) {
var teamID_ uint
if teamID != nil {
teamID_ = *teamID
}
- sqlQuery := `
+ baseQuery := `
SELECT
q.id,
q.name,
@@ -442,12 +446,19 @@ func loadHostScheduledQueryStatsDB(ctx context.Context, db sqlx.QueryerContext,
SUM(stats.wall_time) AS wall_time
FROM scheduled_query_stats stats WHERE stats.host_id = ? GROUP BY stats.scheduled_query_id) as sqs ON (q.id = sqs.scheduled_query_id)
LEFT JOIN query_results qr ON (q.id = qr.query_id AND qr.host_id = ?)
+ `
+
+ filter1 := `
WHERE
(q.platform = '' OR q.platform IS NULL OR FIND_IN_SET(?, q.platform) != 0)
- AND q.schedule_interval > 0
+ AND q.is_scheduled = 1
AND (q.automations_enabled IS TRUE OR (q.discard_data IS FALSE AND q.logging_type = ?))
AND (q.team_id IS NULL OR q.team_id = ?)
- OR EXISTS (
+ GROUP BY q.id
+ `
+
+ filter2 := `
+ WHERE EXISTS (
SELECT 1 FROM query_results
WHERE query_results.query_id = q.id
AND query_results.host_id = ?
@@ -455,6 +466,8 @@ func loadHostScheduledQueryStatsDB(ctx context.Context, db sqlx.QueryerContext,
GROUP BY q.id
`
+ sqlQuery := baseQuery + filter1 + " UNION ALL " + baseQuery + filter2
+
args := []interface{}{
pastDate,
hid,
@@ -462,8 +475,12 @@ func loadHostScheduledQueryStatsDB(ctx context.Context, db sqlx.QueryerContext,
fleet.PlatformFromHost(hostPlatform),
fleet.LoggingSnapshot,
teamID_,
+ pastDate,
+ hid,
+ hid,
hid,
}
+
var stats []fleet.QueryStats
if err := sqlx.SelectContext(ctx, db, &stats, sqlQuery, args...); err != nil {
return nil, ctxerr.Wrap(ctx, err, "load query stats")
diff --git a/server/datastore/mysql/migrations/tables/20240927081858_CreateFedoraBuiltinLabel.go b/server/datastore/mysql/migrations/tables/20240927081858_CreateFedoraBuiltinLabel.go
new file mode 100644
index 000000000000..f69faa0a7b8f
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240927081858_CreateFedoraBuiltinLabel.go
@@ -0,0 +1,57 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+ "time"
+
+ "github.com/VividCortex/mysqlerr"
+ "github.com/fleetdm/fleet/v4/server/fleet"
+ "github.com/go-sql-driver/mysql"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20240927081858, Down_20240927081858)
+}
+
+func Up_20240927081858(tx *sql.Tx) error {
+ const stmt = `
+ INSERT INTO labels (
+ name,
+ description,
+ query,
+ platform,
+ label_type,
+ label_membership_type,
+ created_at,
+ updated_at
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
+`
+
+ // hard-coded timestamps are used so that schema.sql is stable
+ ts := time.Date(2024, 9, 27, 0, 0, 0, 0, time.UTC)
+ _, err := tx.Exec(
+ stmt,
+ fleet.BuiltinLabelFedoraLinux,
+ "All Fedora hosts",
+ `select 1 from os_version where name = 'Fedora Linux';`,
+ "rhel",
+ fleet.LabelTypeBuiltIn,
+ fleet.LabelMembershipTypeDynamic,
+ ts,
+ ts,
+ )
+ if err != nil {
+ if driverErr, ok := err.(*mysql.MySQLError); ok {
+ if driverErr.Number == mysqlerr.ER_DUP_ENTRY {
+ return fmt.Errorf("a label with the name %q already exists, please rename it before applying this migration: %w", fleet.BuiltinLabelFedoraLinux, err)
+ }
+ }
+ return err
+ }
+ return nil
+}
+
+func Down_20240927081858(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20240927081858_CreateFedoraBuiltinLabel_test.go b/server/datastore/mysql/migrations/tables/20240927081858_CreateFedoraBuiltinLabel_test.go
new file mode 100644
index 000000000000..1610b0f0b149
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240927081858_CreateFedoraBuiltinLabel_test.go
@@ -0,0 +1,19 @@
+package tables
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestUp_20240927081858(t *testing.T) {
+ db := applyUpToPrev(t)
+
+ applyNext(t, db)
+
+ var names []string
+ err := db.Select(&names, `SELECT name FROM labels`)
+ require.NoError(t, err)
+
+ require.Contains(t, names, "Fedora Linux")
+}
diff --git a/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex.go b/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex.go
new file mode 100644
index 000000000000..592e8349df32
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex.go
@@ -0,0 +1,33 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20240930171917, Down_20240930171917)
+}
+
+func Up_20240930171917(tx *sql.Tx) error {
+ _, err := tx.Exec(`
+ ALTER TABLE queries
+ ADD COLUMN is_scheduled BOOLEAN GENERATED ALWAYS AS (schedule_interval > 0) STORED NOT NULL
+ `)
+ if err != nil {
+ return fmt.Errorf("error creating generated column is_scheduled: %w", err)
+ }
+
+ _, err = tx.Exec(`
+ CREATE INDEX idx_queries_schedule_automations ON queries (is_scheduled, automations_enabled)
+ `)
+ if err != nil {
+ return fmt.Errorf("error creating index idx_queries_schedule_automations: %w", err)
+ }
+
+ return nil
+}
+
+func Down_20240930171917(tx *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex_test.go b/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex_test.go
new file mode 100644
index 000000000000..27f7d814c52b
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20240930171917_AddScheduleAutomationsIndex_test.go
@@ -0,0 +1,104 @@
+package tables
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestUp_20240930171917(t *testing.T) {
+ db := applyUpToPrev(t)
+
+ //
+ // Insert data to test the migration
+ //
+ // ...
+
+ // Apply current migration.
+ applyNext(t, db)
+
+ // Assert the index was created.
+ rows, err := db.Query("SHOW INDEX FROM queries WHERE Key_name = 'idx_queries_schedule_automations'")
+ require.NoError(t, err)
+ defer rows.Close()
+
+ var indexCount int
+ for rows.Next() {
+ indexCount++
+ }
+
+ require.NoError(t, rows.Err())
+ require.Greater(t, indexCount, 0)
+
+ //
+ // Assert the index is used when there are rows in the queries table
+ // (wrong index is used when there are no rows in the queries table)
+ //
+
+ stmtPrefix := "INSERT INTO `queries` (`saved`, `name`, `description`, `query`, `author_id`, `observer_can_run`, `team_id`, `team_id_char`, `platform`, `min_osquery_version`, `schedule_interval`, `automations_enabled`, `logging_type`, `discard_data`) VALUES "
+ stmtSuffix := ";"
+
+ var valueStrings []string
+ var valueArgs []interface{}
+
+ // Generate 10 records
+ for i := 0; i < 10; i++ {
+ queryID := i + 1
+ valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
+ valueArgs = append(valueArgs, 0, fmt.Sprintf("query_%d", queryID), "", "SELECT * FROM processes;", 1, 0, nil, "", "", "", 0, 0, "snapshot", 0)
+ }
+
+ // Disable foreign key checks to improve performance
+ _, err = db.Exec("SET FOREIGN_KEY_CHECKS=0")
+ require.NoError(t, err)
+
+ // Construct and execute the batch insert
+ stmt := stmtPrefix + strings.Join(valueStrings, ",") + stmtSuffix
+ _, err = db.Exec(stmt, valueArgs...)
+ require.NoError(t, err)
+
+ // Re-enable foreign key checks
+ _, err = db.Exec(`SET FOREIGN_KEY_CHECKS=1`)
+ require.NoError(t, err)
+
+ result := struct {
+ ID int `db:"id"`
+ SelectType string `db:"select_type"`
+ Table string `db:"table"`
+ Type string `db:"type"`
+ PossibleKeys *string `db:"possible_keys"`
+ Key *string `db:"key"`
+ KeyLen *int `db:"key_len"`
+ Ref *string `db:"ref"`
+ Rows int `db:"rows"`
+ Filtered float64 `db:"filtered"`
+ Extra *string `db:"Extra"`
+ Partitions *string `db:"partitions"`
+ }{}
+
+ // Query based on loadHostScheduledQueryStatsDB in server/datastore/mysql/hosts.go
+ err = db.Get(&result, `
+ EXPLAIN
+ SELECT
+ q.id
+ FROM
+ queries q
+ WHERE (q.platform = ''
+ OR q.platform IS NULL
+ OR FIND_IN_SET('darwin', q.platform) != 0)
+ AND q.is_scheduled = 1
+ AND(q.automations_enabled IS TRUE
+ OR(q.discard_data IS FALSE
+ AND q.logging_type = 'snapshot'))
+ AND(q.team_id IS NULL
+ OR q.team_id = 0)
+ GROUP BY
+ q.id
+`)
+ require.NoError(t, err)
+
+ // Assert the correct index is used
+ require.Equal(t, *result.Key, "idx_queries_schedule_automations")
+}
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 4b13126827de..fab1ea3878b2 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -769,9 +769,9 @@ CREATE TABLE `labels` (
PRIMARY KEY (`id`),
UNIQUE KEY `idx_label_unique_name` (`name`),
FULLTEXT KEY `labels_search` (`name`)
-) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `labels` VALUES (1,'2024-04-03 00:00:00','2024-04-03 00:00:00','macOS 14+ (Sonoma+)','macOS hosts with version 14 and above','select 1 from os_version where platform = \'darwin\' and major >= 14;','darwin',1,0),(2,'2024-06-28 00:00:00','2024-06-28 00:00:00','iOS','All iOS hosts','','ios',1,1),(3,'2024-06-28 00:00:00','2024-06-28 00:00:00','iPadOS','All iPadOS hosts','','ipados',1,1);
+INSERT INTO `labels` VALUES (1,'2024-04-03 00:00:00','2024-04-03 00:00:00','macOS 14+ (Sonoma+)','macOS hosts with version 14 and above','select 1 from os_version where platform = \'darwin\' and major >= 14;','darwin',1,0),(2,'2024-06-28 00:00:00','2024-06-28 00:00:00','iOS','All iOS hosts','','ios',1,1),(3,'2024-06-28 00:00:00','2024-06-28 00:00:00','iPadOS','All iPadOS hosts','','ipados',1,1),(4,'2024-09-27 00:00:00','2024-09-27 00:00:00','Fedora Linux','All Fedora hosts','select 1 from os_version where name = \'Fedora Linux\';','rhel',1,0);
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `locks` (
@@ -1061,9 +1061,9 @@ CREATE TABLE `migration_status_tables` (
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `id` (`id`)
-) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=314 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=315 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET character_set_client = @saved_cs_client */;
-INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20240909145426,1,'2020-01-01 01:01:01');
+INSERT INTO `migration_status_tables` VALUES (1,0,1,'2020-01-01 01:01:01'),(2,20161118193812,1,'2020-01-01 01:01:01'),(3,20161118211713,1,'2020-01-01 01:01:01'),(4,20161118212436,1,'2020-01-01 01:01:01'),(5,20161118212515,1,'2020-01-01 01:01:01'),(6,20161118212528,1,'2020-01-01 01:01:01'),(7,20161118212538,1,'2020-01-01 01:01:01'),(8,20161118212549,1,'2020-01-01 01:01:01'),(9,20161118212557,1,'2020-01-01 01:01:01'),(10,20161118212604,1,'2020-01-01 01:01:01'),(11,20161118212613,1,'2020-01-01 01:01:01'),(12,20161118212621,1,'2020-01-01 01:01:01'),(13,20161118212630,1,'2020-01-01 01:01:01'),(14,20161118212641,1,'2020-01-01 01:01:01'),(15,20161118212649,1,'2020-01-01 01:01:01'),(16,20161118212656,1,'2020-01-01 01:01:01'),(17,20161118212758,1,'2020-01-01 01:01:01'),(18,20161128234849,1,'2020-01-01 01:01:01'),(19,20161230162221,1,'2020-01-01 01:01:01'),(20,20170104113816,1,'2020-01-01 01:01:01'),(21,20170105151732,1,'2020-01-01 01:01:01'),(22,20170108191242,1,'2020-01-01 01:01:01'),(23,20170109094020,1,'2020-01-01 01:01:01'),(24,20170109130438,1,'2020-01-01 01:01:01'),(25,20170110202752,1,'2020-01-01 01:01:01'),(26,20170111133013,1,'2020-01-01 01:01:01'),(27,20170117025759,1,'2020-01-01 01:01:01'),(28,20170118191001,1,'2020-01-01 01:01:01'),(29,20170119234632,1,'2020-01-01 01:01:01'),(30,20170124230432,1,'2020-01-01 01:01:01'),(31,20170127014618,1,'2020-01-01 01:01:01'),(32,20170131232841,1,'2020-01-01 01:01:01'),(33,20170223094154,1,'2020-01-01 01:01:01'),(34,20170306075207,1,'2020-01-01 01:01:01'),(35,20170309100733,1,'2020-01-01 01:01:01'),(36,20170331111922,1,'2020-01-01 01:01:01'),(37,20170502143928,1,'2020-01-01 01:01:01'),(38,20170504130602,1,'2020-01-01 01:01:01'),(39,20170509132100,1,'2020-01-01 01:01:01'),(40,20170519105647,1,'2020-01-01 01:01:01'),(41,20170519105648,1,'2020-01-01 01:01:01'),(42,20170831234300,1,'2020-01-01 01:01:01'),(43,20170831234301,1,'2020-01-01 01:01:01'),(44,20170831234303,1,'2020-01-01 01:01:01'),(45,20171116163618,1,'2020-01-01 01:01:01'),(46,20171219164727,1,'2020-01-01 01:01:01'),(47,20180620164811,1,'2020-01-01 01:01:01'),(48,20180620175054,1,'2020-01-01 01:01:01'),(49,20180620175055,1,'2020-01-01 01:01:01'),(50,20191010101639,1,'2020-01-01 01:01:01'),(51,20191010155147,1,'2020-01-01 01:01:01'),(52,20191220130734,1,'2020-01-01 01:01:01'),(53,20200311140000,1,'2020-01-01 01:01:01'),(54,20200405120000,1,'2020-01-01 01:01:01'),(55,20200407120000,1,'2020-01-01 01:01:01'),(56,20200420120000,1,'2020-01-01 01:01:01'),(57,20200504120000,1,'2020-01-01 01:01:01'),(58,20200512120000,1,'2020-01-01 01:01:01'),(59,20200707120000,1,'2020-01-01 01:01:01'),(60,20201011162341,1,'2020-01-01 01:01:01'),(61,20201021104586,1,'2020-01-01 01:01:01'),(62,20201102112520,1,'2020-01-01 01:01:01'),(63,20201208121729,1,'2020-01-01 01:01:01'),(64,20201215091637,1,'2020-01-01 01:01:01'),(65,20210119174155,1,'2020-01-01 01:01:01'),(66,20210326182902,1,'2020-01-01 01:01:01'),(67,20210421112652,1,'2020-01-01 01:01:01'),(68,20210506095025,1,'2020-01-01 01:01:01'),(69,20210513115729,1,'2020-01-01 01:01:01'),(70,20210526113559,1,'2020-01-01 01:01:01'),(71,20210601000001,1,'2020-01-01 01:01:01'),(72,20210601000002,1,'2020-01-01 01:01:01'),(73,20210601000003,1,'2020-01-01 01:01:01'),(74,20210601000004,1,'2020-01-01 01:01:01'),(75,20210601000005,1,'2020-01-01 01:01:01'),(76,20210601000006,1,'2020-01-01 01:01:01'),(77,20210601000007,1,'2020-01-01 01:01:01'),(78,20210601000008,1,'2020-01-01 01:01:01'),(79,20210606151329,1,'2020-01-01 01:01:01'),(80,20210616163757,1,'2020-01-01 01:01:01'),(81,20210617174723,1,'2020-01-01 01:01:01'),(82,20210622160235,1,'2020-01-01 01:01:01'),(83,20210623100031,1,'2020-01-01 01:01:01'),(84,20210623133615,1,'2020-01-01 01:01:01'),(85,20210708143152,1,'2020-01-01 01:01:01'),(86,20210709124443,1,'2020-01-01 01:01:01'),(87,20210712155608,1,'2020-01-01 01:01:01'),(88,20210714102108,1,'2020-01-01 01:01:01'),(89,20210719153709,1,'2020-01-01 01:01:01'),(90,20210721171531,1,'2020-01-01 01:01:01'),(91,20210723135713,1,'2020-01-01 01:01:01'),(92,20210802135933,1,'2020-01-01 01:01:01'),(93,20210806112844,1,'2020-01-01 01:01:01'),(94,20210810095603,1,'2020-01-01 01:01:01'),(95,20210811150223,1,'2020-01-01 01:01:01'),(96,20210818151827,1,'2020-01-01 01:01:01'),(97,20210818151828,1,'2020-01-01 01:01:01'),(98,20210818182258,1,'2020-01-01 01:01:01'),(99,20210819131107,1,'2020-01-01 01:01:01'),(100,20210819143446,1,'2020-01-01 01:01:01'),(101,20210903132338,1,'2020-01-01 01:01:01'),(102,20210915144307,1,'2020-01-01 01:01:01'),(103,20210920155130,1,'2020-01-01 01:01:01'),(104,20210927143115,1,'2020-01-01 01:01:01'),(105,20210927143116,1,'2020-01-01 01:01:01'),(106,20211013133706,1,'2020-01-01 01:01:01'),(107,20211013133707,1,'2020-01-01 01:01:01'),(108,20211102135149,1,'2020-01-01 01:01:01'),(109,20211109121546,1,'2020-01-01 01:01:01'),(110,20211110163320,1,'2020-01-01 01:01:01'),(111,20211116184029,1,'2020-01-01 01:01:01'),(112,20211116184030,1,'2020-01-01 01:01:01'),(113,20211202092042,1,'2020-01-01 01:01:01'),(114,20211202181033,1,'2020-01-01 01:01:01'),(115,20211207161856,1,'2020-01-01 01:01:01'),(116,20211216131203,1,'2020-01-01 01:01:01'),(117,20211221110132,1,'2020-01-01 01:01:01'),(118,20220107155700,1,'2020-01-01 01:01:01'),(119,20220125105650,1,'2020-01-01 01:01:01'),(120,20220201084510,1,'2020-01-01 01:01:01'),(121,20220208144830,1,'2020-01-01 01:01:01'),(122,20220208144831,1,'2020-01-01 01:01:01'),(123,20220215152203,1,'2020-01-01 01:01:01'),(124,20220223113157,1,'2020-01-01 01:01:01'),(125,20220307104655,1,'2020-01-01 01:01:01'),(126,20220309133956,1,'2020-01-01 01:01:01'),(127,20220316155700,1,'2020-01-01 01:01:01'),(128,20220323152301,1,'2020-01-01 01:01:01'),(129,20220330100659,1,'2020-01-01 01:01:01'),(130,20220404091216,1,'2020-01-01 01:01:01'),(131,20220419140750,1,'2020-01-01 01:01:01'),(132,20220428140039,1,'2020-01-01 01:01:01'),(133,20220503134048,1,'2020-01-01 01:01:01'),(134,20220524102918,1,'2020-01-01 01:01:01'),(135,20220526123327,1,'2020-01-01 01:01:01'),(136,20220526123328,1,'2020-01-01 01:01:01'),(137,20220526123329,1,'2020-01-01 01:01:01'),(138,20220608113128,1,'2020-01-01 01:01:01'),(139,20220627104817,1,'2020-01-01 01:01:01'),(140,20220704101843,1,'2020-01-01 01:01:01'),(141,20220708095046,1,'2020-01-01 01:01:01'),(142,20220713091130,1,'2020-01-01 01:01:01'),(143,20220802135510,1,'2020-01-01 01:01:01'),(144,20220818101352,1,'2020-01-01 01:01:01'),(145,20220822161445,1,'2020-01-01 01:01:01'),(146,20220831100036,1,'2020-01-01 01:01:01'),(147,20220831100151,1,'2020-01-01 01:01:01'),(148,20220908181826,1,'2020-01-01 01:01:01'),(149,20220914154915,1,'2020-01-01 01:01:01'),(150,20220915165115,1,'2020-01-01 01:01:01'),(151,20220915165116,1,'2020-01-01 01:01:01'),(152,20220928100158,1,'2020-01-01 01:01:01'),(153,20221014084130,1,'2020-01-01 01:01:01'),(154,20221027085019,1,'2020-01-01 01:01:01'),(155,20221101103952,1,'2020-01-01 01:01:01'),(156,20221104144401,1,'2020-01-01 01:01:01'),(157,20221109100749,1,'2020-01-01 01:01:01'),(158,20221115104546,1,'2020-01-01 01:01:01'),(159,20221130114928,1,'2020-01-01 01:01:01'),(160,20221205112142,1,'2020-01-01 01:01:01'),(161,20221216115820,1,'2020-01-01 01:01:01'),(162,20221220195934,1,'2020-01-01 01:01:01'),(163,20221220195935,1,'2020-01-01 01:01:01'),(164,20221223174807,1,'2020-01-01 01:01:01'),(165,20221227163855,1,'2020-01-01 01:01:01'),(166,20221227163856,1,'2020-01-01 01:01:01'),(167,20230202224725,1,'2020-01-01 01:01:01'),(168,20230206163608,1,'2020-01-01 01:01:01'),(169,20230214131519,1,'2020-01-01 01:01:01'),(170,20230303135738,1,'2020-01-01 01:01:01'),(171,20230313135301,1,'2020-01-01 01:01:01'),(172,20230313141819,1,'2020-01-01 01:01:01'),(173,20230315104937,1,'2020-01-01 01:01:01'),(174,20230317173844,1,'2020-01-01 01:01:01'),(175,20230320133602,1,'2020-01-01 01:01:01'),(176,20230330100011,1,'2020-01-01 01:01:01'),(177,20230330134823,1,'2020-01-01 01:01:01'),(178,20230405232025,1,'2020-01-01 01:01:01'),(179,20230408084104,1,'2020-01-01 01:01:01'),(180,20230411102858,1,'2020-01-01 01:01:01'),(181,20230421155932,1,'2020-01-01 01:01:01'),(182,20230425082126,1,'2020-01-01 01:01:01'),(183,20230425105727,1,'2020-01-01 01:01:01'),(184,20230501154913,1,'2020-01-01 01:01:01'),(185,20230503101418,1,'2020-01-01 01:01:01'),(186,20230515144206,1,'2020-01-01 01:01:01'),(187,20230517140952,1,'2020-01-01 01:01:01'),(188,20230517152807,1,'2020-01-01 01:01:01'),(189,20230518114155,1,'2020-01-01 01:01:01'),(190,20230520153236,1,'2020-01-01 01:01:01'),(191,20230525151159,1,'2020-01-01 01:01:01'),(192,20230530122103,1,'2020-01-01 01:01:01'),(193,20230602111827,1,'2020-01-01 01:01:01'),(194,20230608103123,1,'2020-01-01 01:01:01'),(195,20230629140529,1,'2020-01-01 01:01:01'),(196,20230629140530,1,'2020-01-01 01:01:01'),(197,20230711144622,1,'2020-01-01 01:01:01'),(198,20230721135421,1,'2020-01-01 01:01:01'),(199,20230721161508,1,'2020-01-01 01:01:01'),(200,20230726115701,1,'2020-01-01 01:01:01'),(201,20230807100822,1,'2020-01-01 01:01:01'),(202,20230814150442,1,'2020-01-01 01:01:01'),(203,20230823122728,1,'2020-01-01 01:01:01'),(204,20230906152143,1,'2020-01-01 01:01:01'),(205,20230911163618,1,'2020-01-01 01:01:01'),(206,20230912101759,1,'2020-01-01 01:01:01'),(207,20230915101341,1,'2020-01-01 01:01:01'),(208,20230918132351,1,'2020-01-01 01:01:01'),(209,20231004144339,1,'2020-01-01 01:01:01'),(210,20231009094541,1,'2020-01-01 01:01:01'),(211,20231009094542,1,'2020-01-01 01:01:01'),(212,20231009094543,1,'2020-01-01 01:01:01'),(213,20231009094544,1,'2020-01-01 01:01:01'),(214,20231016091915,1,'2020-01-01 01:01:01'),(215,20231024174135,1,'2020-01-01 01:01:01'),(216,20231025120016,1,'2020-01-01 01:01:01'),(217,20231025160156,1,'2020-01-01 01:01:01'),(218,20231031165350,1,'2020-01-01 01:01:01'),(219,20231106144110,1,'2020-01-01 01:01:01'),(220,20231107130934,1,'2020-01-01 01:01:01'),(221,20231109115838,1,'2020-01-01 01:01:01'),(222,20231121054530,1,'2020-01-01 01:01:01'),(223,20231122101320,1,'2020-01-01 01:01:01'),(224,20231130132828,1,'2020-01-01 01:01:01'),(225,20231130132931,1,'2020-01-01 01:01:01'),(226,20231204155427,1,'2020-01-01 01:01:01'),(227,20231206142340,1,'2020-01-01 01:01:01'),(228,20231207102320,1,'2020-01-01 01:01:01'),(229,20231207102321,1,'2020-01-01 01:01:01'),(230,20231207133731,1,'2020-01-01 01:01:01'),(231,20231212094238,1,'2020-01-01 01:01:01'),(232,20231212095734,1,'2020-01-01 01:01:01'),(233,20231212161121,1,'2020-01-01 01:01:01'),(234,20231215122713,1,'2020-01-01 01:01:01'),(235,20231219143041,1,'2020-01-01 01:01:01'),(236,20231224070653,1,'2020-01-01 01:01:01'),(237,20240110134315,1,'2020-01-01 01:01:01'),(238,20240119091637,1,'2020-01-01 01:01:01'),(239,20240126020642,1,'2020-01-01 01:01:01'),(240,20240126020643,1,'2020-01-01 01:01:01'),(241,20240129162819,1,'2020-01-01 01:01:01'),(242,20240130115133,1,'2020-01-01 01:01:01'),(243,20240131083822,1,'2020-01-01 01:01:01'),(244,20240205095928,1,'2020-01-01 01:01:01'),(245,20240205121956,1,'2020-01-01 01:01:01'),(246,20240209110212,1,'2020-01-01 01:01:01'),(247,20240212111533,1,'2020-01-01 01:01:01'),(248,20240221112844,1,'2020-01-01 01:01:01'),(249,20240222073518,1,'2020-01-01 01:01:01'),(250,20240222135115,1,'2020-01-01 01:01:01'),(251,20240226082255,1,'2020-01-01 01:01:01'),(252,20240228082706,1,'2020-01-01 01:01:01'),(253,20240301173035,1,'2020-01-01 01:01:01'),(254,20240302111134,1,'2020-01-01 01:01:01'),(255,20240312103753,1,'2020-01-01 01:01:01'),(256,20240313143416,1,'2020-01-01 01:01:01'),(257,20240314085226,1,'2020-01-01 01:01:01'),(258,20240314151747,1,'2020-01-01 01:01:01'),(259,20240320145650,1,'2020-01-01 01:01:01'),(260,20240327115530,1,'2020-01-01 01:01:01'),(261,20240327115617,1,'2020-01-01 01:01:01'),(262,20240408085837,1,'2020-01-01 01:01:01'),(263,20240415104633,1,'2020-01-01 01:01:01'),(264,20240430111727,1,'2020-01-01 01:01:01'),(265,20240515200020,1,'2020-01-01 01:01:01'),(266,20240521143023,1,'2020-01-01 01:01:01'),(267,20240521143024,1,'2020-01-01 01:01:01'),(268,20240601174138,1,'2020-01-01 01:01:01'),(269,20240607133721,1,'2020-01-01 01:01:01'),(270,20240612150059,1,'2020-01-01 01:01:01'),(271,20240613162201,1,'2020-01-01 01:01:01'),(272,20240613172616,1,'2020-01-01 01:01:01'),(273,20240618142419,1,'2020-01-01 01:01:01'),(274,20240625093543,1,'2020-01-01 01:01:01'),(275,20240626195531,1,'2020-01-01 01:01:01'),(276,20240702123921,1,'2020-01-01 01:01:01'),(277,20240703154849,1,'2020-01-01 01:01:01'),(278,20240707134035,1,'2020-01-01 01:01:01'),(279,20240707134036,1,'2020-01-01 01:01:01'),(280,20240709124958,1,'2020-01-01 01:01:01'),(281,20240709132642,1,'2020-01-01 01:01:01'),(282,20240709183940,1,'2020-01-01 01:01:01'),(283,20240710155623,1,'2020-01-01 01:01:01'),(284,20240723102712,1,'2020-01-01 01:01:01'),(285,20240725152735,1,'2020-01-01 01:01:01'),(286,20240725182118,1,'2020-01-01 01:01:01'),(287,20240726100517,1,'2020-01-01 01:01:01'),(288,20240730171504,1,'2020-01-01 01:01:01'),(289,20240730174056,1,'2020-01-01 01:01:01'),(290,20240730215453,1,'2020-01-01 01:01:01'),(291,20240730374423,1,'2020-01-01 01:01:01'),(292,20240801115359,1,'2020-01-01 01:01:01'),(293,20240802101043,1,'2020-01-01 01:01:01'),(294,20240802113716,1,'2020-01-01 01:01:01'),(295,20240814135330,1,'2020-01-01 01:01:01'),(296,20240815000000,1,'2020-01-01 01:01:01'),(297,20240815000001,1,'2020-01-01 01:01:01'),(298,20240816103247,1,'2020-01-01 01:01:01'),(299,20240820091218,1,'2020-01-01 01:01:01'),(300,20240826111228,1,'2020-01-01 01:01:01'),(301,20240826160025,1,'2020-01-01 01:01:01'),(302,20240829165448,1,'2020-01-01 01:01:01'),(303,20240829165605,1,'2020-01-01 01:01:01'),(304,20240829165715,1,'2020-01-01 01:01:01'),(305,20240829165930,1,'2020-01-01 01:01:01'),(306,20240829170023,1,'2020-01-01 01:01:01'),(307,20240829170033,1,'2020-01-01 01:01:01'),(308,20240829170044,1,'2020-01-01 01:01:01'),(309,20240905105135,1,'2020-01-01 01:01:01'),(310,20240905140514,1,'2020-01-01 01:01:01'),(311,20240905200000,1,'2020-01-01 01:01:01'),(312,20240905200001,1,'2020-01-01 01:01:01'),(313,20240927081858,1,'2020-01-01 01:01:01'),(314,20240930171917,1,'2020-01-01 01:01:01');
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!50503 SET character_set_client = utf8mb4 */;
CREATE TABLE `mobile_device_management_solutions` (
@@ -1274,7 +1274,7 @@ CREATE TABLE `nano_users` (
/*!40101 SET character_set_client = @saved_cs_client */;
SET @saved_cs_client = @@character_set_client;
/*!50503 SET character_set_client = utf8mb4 */;
-/*!50001 CREATE VIEW `nano_view_queue` AS SELECT
+/*!50001 CREATE VIEW `nano_view_queue` AS SELECT
1 AS `id`,
1 AS `created_at`,
1 AS `active`,
@@ -1482,11 +1482,13 @@ CREATE TABLE `queries` (
`automations_enabled` tinyint unsigned NOT NULL DEFAULT '0',
`logging_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'snapshot',
`discard_data` tinyint(1) NOT NULL DEFAULT '1',
+ `is_scheduled` tinyint(1) GENERATED ALWAYS AS ((`schedule_interval` > 0)) STORED NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `idx_team_id_name_unq` (`team_id_char`,`name`),
UNIQUE KEY `idx_name_team_id_unq` (`name`,`team_id_char`),
KEY `author_id` (`author_id`),
KEY `idx_team_id_saved_auto_interval` (`team_id`,`saved`,`automations_enabled`,`schedule_interval`),
+ KEY `idx_queries_schedule_automations` (`is_scheduled`,`automations_enabled`),
CONSTRAINT `queries_ibfk_1` FOREIGN KEY (`author_id`) REFERENCES `users` (`id`) ON DELETE SET NULL,
CONSTRAINT `queries_ibfk_2` FOREIGN KEY (`team_id`) REFERENCES `teams` (`id`) ON DELETE CASCADE
) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go
index 3c4719d32407..fbf80bb80d46 100644
--- a/server/datastore/mysql/software_installers.go
+++ b/server/datastore/mysql/software_installers.go
@@ -560,6 +560,7 @@ SELECT
hsi.self_service,
hsi.host_deleted_at,
hsi.created_at as created_at,
+ hsi.updated_at as updated_at,
si.user_id AS software_installer_user_id,
si.user_name AS software_installer_user_name,
si.user_email AS software_installer_user_email
diff --git a/server/datastore/mysql/software_installers_test.go b/server/datastore/mysql/software_installers_test.go
index 49be5c29c220..487f2ddaadba 100644
--- a/server/datastore/mysql/software_installers_test.go
+++ b/server/datastore/mysql/software_installers_test.go
@@ -511,8 +511,18 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
+ beforeInstallRequest := time.Now()
installUUID, err := ds.InsertSoftwareInstallRequest(ctx, host.ID, installerID, false)
require.NoError(t, err)
+
+ res, err := ds.GetSoftwareInstallResults(ctx, installUUID)
+ require.NoError(t, err)
+ require.NotNil(t, res.UpdatedAt)
+ require.Less(t, beforeInstallRequest, res.CreatedAt)
+ createdAt := res.CreatedAt
+ require.Less(t, beforeInstallRequest, *res.UpdatedAt)
+
+ beforeInstallResult := time.Now()
err = ds.SetHostSoftwareInstallResult(ctx, &fleet.HostSoftwareInstallResultPayload{
HostID: host.ID,
InstallUUID: installUUID,
@@ -524,7 +534,7 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
})
require.NoError(t, err)
- res, err := ds.GetSoftwareInstallResults(ctx, installUUID)
+ res, err = ds.GetSoftwareInstallResults(ctx, installUUID)
require.NoError(t, err)
require.Equal(t, installUUID, res.InstallUUID)
@@ -534,6 +544,10 @@ func testGetSoftwareInstallResult(t *testing.T, ds *Datastore) {
require.Equal(t, tc.preInstallQueryOutput, res.PreInstallQueryOutput)
require.Equal(t, tc.postInstallScriptOutput, res.PostInstallScriptOutput)
require.Equal(t, tc.installScriptOutput, res.Output)
+ require.NotNil(t, res.CreatedAt)
+ require.Equal(t, createdAt, res.CreatedAt)
+ require.NotNil(t, res.UpdatedAt)
+ require.Less(t, beforeInstallResult, *res.UpdatedAt)
})
}
}
diff --git a/server/datastore/mysql/targets.go b/server/datastore/mysql/targets.go
index 16c185ed382b..cbb244acb774 100644
--- a/server/datastore/mysql/targets.go
+++ b/server/datastore/mysql/targets.go
@@ -60,7 +60,7 @@ func targetSQLCondAndArgs(targets fleet.HostTargets) (sql string, args []interfa
OR
(
/* 'All hosts' builtin label was selected. */
- id IN (SELECT DISTINCT host_id FROM label_membership WHERE label_id = 6 AND label_id IN (? /* queryLabelIDs */))
+ id IN (SELECT DISTINCT host_id FROM label_membership WHERE label_id = (SELECT id from labels WHERE name = 'All Hosts') AND label_id IN (? /* queryLabelIDs */))
)
OR
(
diff --git a/server/fleet/labels.go b/server/fleet/labels.go
index 001e50793046..e7fd5d93c5e9 100644
--- a/server/fleet/labels.go
+++ b/server/fleet/labels.go
@@ -158,6 +158,7 @@ const (
BuiltinLabelMacOS14Plus = "macOS 14+ (Sonoma+)"
BuiltinLabelIOS = "iOS"
BuiltinLabelIPadOS = "iPadOS"
+ BuiltinLabelFedoraLinux = "Fedora Linux"
)
// ReservedLabelNames returns a map of label name strings
@@ -175,5 +176,6 @@ func ReservedLabelNames() map[string]struct{} {
BuiltinLabelMacOS14Plus: {},
BuiltinLabelIOS: {},
BuiltinLabelIPadOS: {},
+ BuiltinLabelFedoraLinux: {},
}
}
diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go
index c1ea948f533a..866d41f6f29a 100644
--- a/server/fleet/software_installer.go
+++ b/server/fleet/software_installer.go
@@ -366,6 +366,8 @@ func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error
switch ext {
case "deb":
return "deb_packages", nil
+ case "rpm":
+ return "rpm_packages", nil
case "exe", "msi":
return "programs", nil
case "pkg":
@@ -381,7 +383,7 @@ func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error
func SofwareInstallerPlatformFromExtension(ext string) (string, error) {
ext = strings.TrimPrefix(ext, ".")
switch ext {
- case "deb":
+ case "deb", "rpm":
return "linux", nil
case "exe", "msi":
return "windows", nil
diff --git a/server/service/appconfig.go b/server/service/appconfig.go
index 17b83dcbbfa1..22998d24d601 100644
--- a/server/service/appconfig.go
+++ b/server/service/appconfig.go
@@ -545,15 +545,55 @@ func (svc *Service) ModifyAppConfig(ctx context.Context, p []byte, applyOpts fle
}
}
+ // Reset teams for ABM tokens that exist in Fleet but aren't present in the config being passed
+ tokensInCfg := make(map[string]struct{})
+ for _, t := range newAppConfig.MDM.AppleBusinessManager.Value {
+ tokensInCfg[t.OrganizationName] = struct{}{}
+ }
+
+ toks, err := svc.ds.ListABMTokens(ctx)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "listing ABM tokens")
+ }
+ for _, tok := range toks {
+ if _, ok := tokensInCfg[tok.OrganizationName]; !ok {
+ tok.MacOSDefaultTeamID = nil
+ tok.IOSDefaultTeamID = nil
+ tok.IPadOSDefaultTeamID = nil
+ if err := svc.ds.SaveABMToken(ctx, tok); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "saving ABM token assignments")
+ }
+ }
+ }
+
if (appConfig.MDM.AppleBusinessManager.Set && appConfig.MDM.AppleBusinessManager.Valid) || appConfig.MDM.DeprecatedAppleBMDefaultTeam != "" {
for _, tok := range abmAssignments {
- fmt.Println(tok.EncryptedToken)
if err := svc.ds.SaveABMToken(ctx, tok); err != nil {
return nil, ctxerr.Wrap(ctx, err, "saving ABM token assignments")
}
}
}
+ // Reset teams for VPP tokens that exist in Fleet but aren't present in the config being passed
+ clear(tokensInCfg)
+
+ for _, t := range newAppConfig.MDM.VolumePurchasingProgram.Value {
+ tokensInCfg[t.Location] = struct{}{}
+ }
+
+ vppToks, err := svc.ds.ListVPPTokens(ctx)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "listing VPP tokens")
+ }
+ for _, tok := range vppToks {
+ if _, ok := tokensInCfg[tok.Location]; !ok {
+ tok.Teams = nil
+ if _, err := svc.ds.UpdateVPPTokenTeams(ctx, tok.ID, nil); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "saving VPP token teams")
+ }
+ }
+ }
+
if appConfig.MDM.VolumePurchasingProgram.Set && appConfig.MDM.VolumePurchasingProgram.Valid {
for tokenID, tokenTeams := range vppAssignments {
if _, err := svc.ds.UpdateVPPTokenTeams(ctx, tokenID, tokenTeams); err != nil {
diff --git a/server/service/appconfig_test.go b/server/service/appconfig_test.go
index 0fb0d318d26f..2c6e6bc10849 100644
--- a/server/service/appconfig_test.go
+++ b/server/service/appconfig_test.go
@@ -51,6 +51,18 @@ func TestAppConfigAuth(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
testCases := []struct {
name string
user *fleet.User
@@ -647,6 +659,18 @@ func TestModifyAppConfigSMTPConfigured(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
// Disable SMTP.
newAppConfig := fleet.AppConfig{
SMTPSettings: &fleet.SMTPSettings{
@@ -751,6 +775,18 @@ func TestTransparencyURL(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err)
require.Equal(t, tt.initialURL, ac.FleetDesktop.TransparencyURL)
@@ -800,6 +836,18 @@ func TestTransparencyURLDowngradeLicense(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err)
require.Equal(t, "https://example.com/transparency", ac.FleetDesktop.TransparencyURL)
@@ -1090,6 +1138,15 @@ func TestMDMAppleConfig(t *testing.T) {
depStorage.StoreAssignerProfileFunc = func(ctx context.Context, name string, profileUUID string) error {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{{OrganizationName: t.Name()}}, nil
+ }
ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err)
@@ -1168,6 +1225,15 @@ func TestModifyAppConfigSMTPSSOAgentOptions(t *testing.T) {
) error {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
// Not sending smtp_settings, sso_settings or agent_settings will do nothing.
b := []byte(`{}`)
@@ -1297,6 +1363,18 @@ func TestModifyEnableAnalytics(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
ac, err := svc.AppConfigObfuscated(ctx)
require.NoError(t, err)
require.Equal(t, tt.initialEnabled, ac.ServerSettings.EnableAnalytics)
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index 4387367ef017..c96777ea580e 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -770,9 +770,12 @@ func (svc *Service) DeleteMDMAppleConfigProfile(ctx context.Context, profileUUID
}
}
+ // This call will also delete host_mdm_apple_profiles references IFF the profile has not been sent to
+ // the host yet.
if err := svc.ds.DeleteMDMAppleConfigProfile(ctx, profileUUID); err != nil {
return ctxerr.Wrap(ctx, err)
}
+
// cannot use the profile ID as it is now deleted
if _, err := svc.ds.BulkSetPendingMDMHostProfiles(ctx, nil, []uint{teamID}, nil, nil); err != nil {
return ctxerr.Wrap(ctx, err, "bulk set pending host profiles")
diff --git a/server/service/installer_test.go b/server/service/installer_test.go
deleted file mode 100644
index c66ae79ded2c..000000000000
--- a/server/service/installer_test.go
+++ /dev/null
@@ -1,179 +0,0 @@
-package service
-
-import (
- "context"
- "io"
- "strings"
- "testing"
-
- "github.com/fleetdm/fleet/v4/server/authz"
- "github.com/fleetdm/fleet/v4/server/config"
- "github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
- "github.com/fleetdm/fleet/v4/server/fleet"
- "github.com/fleetdm/fleet/v4/server/mock"
- "github.com/fleetdm/fleet/v4/server/test"
- "github.com/stretchr/testify/require"
-)
-
-func setup(t *testing.T) (context.Context, *mock.Store, *mock.InstallerStore, fleet.Service) {
- ds := new(mock.Store)
- is := new(mock.InstallerStore)
- cfg := config.TestConfig()
- cfg.Server.SandboxEnabled = true
- svc, ctx := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{Is: is, FleetConfig: &cfg})
- ctx = test.UserContext(ctx, test.UserAdmin)
- ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) {
- return &fleet.EnrollSecret{Secret: "xyz"}, nil
-
- }
- return ctx, ds, is, svc
-}
-
-func TestGetInstaller(t *testing.T) {
- t.Run("unauthorized access is not allowed", func(t *testing.T) {
- _, _, _, svc := setup(t)
- _, _, err := svc.GetInstaller(context.Background(), fleet.Installer{})
- require.Error(t, err)
- require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
- })
-
- t.Run("errors if store is not configured", func(t *testing.T) {
- ctx, ds, _, _ := setup(t)
- cfg := config.TestConfig()
- cfg.Server.SandboxEnabled = true
- svc, _ := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{Is: nil, FleetConfig: &cfg})
- _, _, err := svc.GetInstaller(ctx, fleet.Installer{})
- require.Error(t, err)
- require.ErrorContains(t, err, "installer storage has not been configured")
- })
-
- t.Run("errors if the provided enroll secret cannot be found", func(t *testing.T) {
- ctx, ds, _, svc := setup(t)
- ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) {
- return nil, newNotFoundError()
- }
- _, _, err := svc.GetInstaller(ctx, fleet.Installer{})
- require.Error(t, err)
- var nfe *notFoundError
- require.ErrorAs(t, err, &nfe)
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- })
-
- t.Run("errors if there's a problem verifying the enroll secret", func(t *testing.T) {
- ctx, ds, _, svc := setup(t)
- ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) {
- return nil, ctxerr.New(ctx, "test error")
- }
- _, _, err := svc.GetInstaller(ctx, fleet.Installer{})
- require.Error(t, err)
- require.ErrorContains(t, err, "test error")
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- })
-
- t.Run("errors if there's a problem checking the blob storage", func(t *testing.T) {
- ctx, ds, is, svc := setup(t)
- is.GetFunc = func(ctx context.Context, installer fleet.Installer) (io.ReadCloser, int64, error) {
- return nil, int64(0), ctxerr.New(ctx, "test error")
- }
- _, _, err := svc.GetInstaller(ctx, fleet.Installer{})
- require.Error(t, err)
- require.ErrorContains(t, err, "test error")
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- require.True(t, is.GetFuncInvoked)
- })
-
- t.Run("returns binary data with the installer", func(t *testing.T) {
- ctx, ds, is, svc := setup(t)
- is.GetFunc = func(ctx context.Context, installer fleet.Installer) (io.ReadCloser, int64, error) {
- str := "test"
- length := int64(len(str))
- reader := io.NopCloser(strings.NewReader(str))
- return reader, length, nil
- }
- blob, length, err := svc.GetInstaller(ctx, fleet.Installer{})
- require.NoError(t, err)
- body, err := io.ReadAll(blob)
- require.Equal(t, "test", string(body))
- require.EqualValues(t, length, len(body))
- require.NoError(t, err)
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- require.True(t, is.GetFuncInvoked)
- })
-}
-func TestCheckInstallerExistence(t *testing.T) {
- t.Run("unauthorized access is not allowed", func(t *testing.T) {
- _, _, _, svc := setup(t)
- err := svc.CheckInstallerExistence(context.Background(), fleet.Installer{})
- require.Error(t, err)
- require.Contains(t, err.Error(), authz.ForbiddenErrorMessage)
- })
-
- t.Run("errors if store is not configured", func(t *testing.T) {
- ctx, ds, _, _ := setup(t)
- cfg := config.TestConfig()
- cfg.Server.SandboxEnabled = true
- svc, _ := newTestServiceWithConfig(t, ds, cfg, nil, nil, &TestServerOpts{Is: nil, FleetConfig: &cfg})
- err := svc.CheckInstallerExistence(ctx, fleet.Installer{})
- require.Error(t, err)
- require.ErrorContains(t, err, "installer storage has not been configured")
- })
-
- t.Run("errors if the provided enroll secret cannot be found", func(t *testing.T) {
- ctx, ds, _, svc := setup(t)
- ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) {
- return nil, newNotFoundError()
- }
- err := svc.CheckInstallerExistence(ctx, fleet.Installer{})
- require.Error(t, err)
- var nfe *notFoundError
- require.ErrorAs(t, err, &nfe)
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- })
-
- t.Run("errors if there's a problem verifying the enroll secret", func(t *testing.T) {
- ctx, ds, _, svc := setup(t)
- ds.VerifyEnrollSecretFunc = func(ctx context.Context, enrollSecret string) (*fleet.EnrollSecret, error) {
- return nil, ctxerr.New(ctx, "test error")
- }
- err := svc.CheckInstallerExistence(ctx, fleet.Installer{})
- require.Error(t, err)
- require.ErrorContains(t, err, "test error")
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- })
-
- t.Run("errors if there's a problem checking the blob storage", func(t *testing.T) {
- ctx, ds, is, svc := setup(t)
- is.ExistsFunc = func(ctx context.Context, installer fleet.Installer) (bool, error) {
- return false, ctxerr.New(ctx, "test error")
- }
- err := svc.CheckInstallerExistence(ctx, fleet.Installer{})
- require.Error(t, err)
- require.ErrorContains(t, err, "test error")
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- require.True(t, is.ExistsFuncInvoked)
- })
-
- t.Run("errors with not found if the installer is not in the storage", func(t *testing.T) {
- ctx, ds, is, svc := setup(t)
- is.ExistsFunc = func(ctx context.Context, installer fleet.Installer) (bool, error) {
- return false, nil
- }
- err := svc.CheckInstallerExistence(ctx, fleet.Installer{})
- require.Error(t, err)
- var nfe *notFoundError
- require.ErrorAs(t, err, &nfe)
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- require.True(t, is.ExistsFuncInvoked)
- })
-
- t.Run("returns no errors if the installer exists", func(t *testing.T) {
- ctx, ds, is, svc := setup(t)
- is.ExistsFunc = func(ctx context.Context, installer fleet.Installer) (bool, error) {
- return true, nil
- }
- err := svc.CheckInstallerExistence(ctx, fleet.Installer{})
- require.NoError(t, err)
- require.True(t, ds.VerifyEnrollSecretFuncInvoked)
- require.True(t, is.ExistsFuncInvoked)
- })
-}
diff --git a/server/service/integration_core_test.go b/server/service/integration_core_test.go
index 8b1f84820506..8332e65eaf94 100644
--- a/server/service/integration_core_test.go
+++ b/server/service/integration_core_test.go
@@ -8310,24 +8310,6 @@ func (s *integrationTestSuite) TestSSODisabled() {
require.Contains(t, string(body), "/login?status=org_disabled") // html contains a script that redirects to this path
}
-func (s *integrationTestSuite) TestSandboxEndpoints() {
- t := s.T()
- validEmail := testUsers["user1"].Email
- validPwd := testUsers["user1"].PlaintextPassword
- hdrs := map[string]string{"Content-Type": "application/x-www-form-urlencoded"}
-
- // demo login endpoint always fails
- formBody := make(url.Values)
- formBody.Set("email", validEmail)
- formBody.Set("password", validPwd)
- res := s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusInternalServerError, hdrs)
- require.NotEqual(t, http.StatusOK, res.StatusCode)
-
- // installers endpoint is not enabled
- url, installersBody := installerPOSTReq(enrollSecret, "pkg", s.token, false)
- s.DoRaw("POST", url, installersBody, http.StatusInternalServerError)
-}
-
func (s *integrationTestSuite) TestGetHostBatteries() {
t := s.T()
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index e32eab6352b4..857299c8c9c9 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -14409,6 +14409,77 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallersWithoutBundleIden
s.uploadSoftwareInstaller(payload, http.StatusOK, "")
}
+func (s *integrationEnterpriseTestSuite) TestSoftwareUploadRPM() {
+ ctx := context.Background()
+ t := s.T()
+
+ // Fedora and RHEL have hosts.platform = 'rhel'.
+ host := createOrbitEnrolledHost(t, "rhel", "", s.ds)
+
+ // Upload an RPM package.
+ payload := &fleet.UploadSoftwareInstallerPayload{
+ InstallScript: "install script",
+ PreInstallQuery: "pre install query",
+ PostInstallScript: "post install script",
+ Filename: "ruby.rpm",
+ Title: "ruby",
+ }
+ s.uploadSoftwareInstaller(payload, http.StatusOK, "")
+ titleID := getSoftwareTitleID(t, s.ds, payload.Title, "rpm_packages")
+
+ latestInstallUUID := func() string {
+ var id string
+ mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
+ return sqlx.GetContext(ctx, q, &id, `SELECT execution_id FROM host_software_installs ORDER BY id DESC LIMIT 1`)
+ })
+ return id
+ }
+
+ // Send a request to the host to install the RPM package.
+ var installSoftwareResp installSoftwareResponse
+ beforeInstallRequest := time.Now()
+ s.DoJSON("POST", fmt.Sprintf("/api/v1/fleet/hosts/%d/software/%d/install", host.ID, titleID), nil, http.StatusAccepted, &installSoftwareResp)
+ installUUID := latestInstallUUID()
+
+ // Simulate host installing the RPM package.
+ beforeInstallResult := time.Now()
+ s.Do("POST", "/api/fleet/orbit/software_install/result",
+ json.RawMessage(fmt.Sprintf(`{
+ "orbit_node_key": %q,
+ "install_uuid": %q,
+ "pre_install_condition_output": "1",
+ "install_script_exit_code": 1,
+ "install_script_output": "failed"
+ }`, *host.OrbitNodeKey, installUUID)),
+ http.StatusNoContent,
+ )
+
+ var resp getSoftwareInstallResultsResponse
+ s.DoJSON("GET", fmt.Sprintf("/api/v1/fleet/software/install/%s/results", installUUID), nil, http.StatusOK, &resp)
+ assert.Equal(t, host.ID, resp.Results.HostID)
+ assert.Equal(t, installUUID, resp.Results.InstallUUID)
+ assert.Equal(t, fleet.SoftwareInstallFailed, resp.Results.Status)
+ assert.NotNil(t, resp.Results.PreInstallQueryOutput)
+ assert.Equal(t, fleet.SoftwareInstallerQuerySuccessCopy, *resp.Results.PreInstallQueryOutput)
+ assert.NotNil(t, resp.Results.Output)
+ assert.Equal(t, fmt.Sprintf(fleet.SoftwareInstallerInstallFailCopy, "failed"), *resp.Results.Output)
+ assert.Empty(t, resp.Results.PostInstallScriptOutput)
+ assert.Less(t, beforeInstallRequest, resp.Results.CreatedAt)
+ assert.Greater(t, time.Now(), resp.Results.CreatedAt)
+ assert.NotNil(t, resp.Results.UpdatedAt)
+ assert.Less(t, beforeInstallResult, *resp.Results.UpdatedAt)
+
+ wantAct := fleet.ActivityTypeInstalledSoftware{
+ HostID: host.ID,
+ HostDisplayName: host.DisplayName(),
+ SoftwareTitle: payload.Title,
+ SoftwarePackage: payload.Filename,
+ InstallUUID: installUUID,
+ Status: string(fleet.SoftwareInstallFailed),
+ }
+ s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0)
+}
+
func (s *integrationEnterpriseTestSuite) TestMaintainedApps() {
t := s.T()
ctx := context.Background()
diff --git a/server/service/integration_mdm_profiles_test.go b/server/service/integration_mdm_profiles_test.go
index 49a1f6eab0cf..1566fae3b114 100644
--- a/server/service/integration_mdm_profiles_test.go
+++ b/server/service/integration_mdm_profiles_test.go
@@ -4359,6 +4359,9 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() {
testTeam, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TestTeam"})
require.NoError(t, err)
+ teamDelete, err := s.ds.NewTeam(ctx, &fleet.Team{Name: "TeamDelete"})
+ require.NoError(t, err)
+
testProfiles := make(map[string]fleet.MDMAppleConfigProfile)
generateTestProfile := func(name string, identifier string) {
i := identifier
@@ -4402,6 +4405,12 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() {
require.Equal(t, expected.Identifier, actual.Identifier)
}
+ host, _ := createHostThenEnrollMDM(s.ds, s.server.URL, t)
+ s.Do("POST", "/api/latest/fleet/hosts/transfer", addHostsToTeamRequest{
+ TeamID: &teamDelete.ID,
+ HostIDs: []uint{host.ID},
+ }, http.StatusOK)
+
// create new profile (no team)
generateTestProfile("TestNoTeam", "")
body, headers := generateNewReq("TestNoTeam", nil)
@@ -4421,9 +4430,42 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() {
require.NotEmpty(t, newCP.ProfileID)
setTestProfileID("TestWithTeamID", newCP.ProfileID)
+ // Create a profile that we're going to remove immediately
+ generateTestProfile("TestImmediateDelete", "")
+ body, headers = generateNewReq("TestImmediateDelete", &teamDelete.ID)
+ newResp = s.DoRawWithHeaders("POST", "/api/latest/fleet/mdm/apple/profiles", body.Bytes(), http.StatusOK, headers)
+ newCP = fleet.MDMAppleConfigProfile{}
+ err = json.NewDecoder(newResp.Body).Decode(&newCP)
+ require.NoError(t, err)
+ require.NotEmpty(t, newCP.ProfileID)
+ setTestProfileID("TestImmediateDelete", newCP.ProfileID)
+
+ // check that host_mdm_apple_profiles entry was created
+ var hostResp getHostResponse
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
+ require.NotNil(t, hostResp.Host.MDM.Profiles)
+ require.Len(t, *hostResp.Host.MDM.Profiles, 1)
+ require.Equal(t, (*hostResp.Host.MDM.Profiles)[0].Name, "TestImmediateDelete")
+
+ // now delete the profile before it's sent, we should see the host_mdm_apple_profiles entry go
+ // away
+ deletedCP := testProfiles["TestImmediateDelete"]
+ deletePath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
+ var deleteResp deleteMDMAppleConfigProfileResponse
+ s.DoJSON("DELETE", deletePath, nil, http.StatusOK, &deleteResp)
+ // confirm deleted
+ var listResp listMDMAppleConfigProfilesResponse
+ s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", listMDMAppleConfigProfilesRequest{TeamID: teamDelete.ID}, http.StatusOK, &listResp)
+ require.Len(t, listResp.ConfigProfiles, 0)
+ getPath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
+ _ = s.DoRawWithHeaders("GET", getPath, nil, http.StatusNotFound, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)})
+ // confirm no host profiles
+ hostResp = getHostResponse{}
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &hostResp)
+ require.Nil(t, hostResp.Host.MDM.Profiles)
+
// list profiles (no team)
expectedCP := testProfiles["TestNoTeam"]
- var listResp listMDMAppleConfigProfilesResponse
s.DoJSON("GET", "/api/latest/fleet/mdm/apple/profiles", nil, http.StatusOK, &listResp)
require.Len(t, listResp.ConfigProfiles, 1)
respCP := listResp.ConfigProfiles[0]
@@ -4445,7 +4487,7 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() {
// get profile (no team)
expectedCP = testProfiles["TestNoTeam"]
- getPath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", expectedCP.ProfileID)
+ getPath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", expectedCP.ProfileID)
getResp := s.DoRawWithHeaders("GET", getPath, nil, http.StatusOK, map[string]string{"Authorization": fmt.Sprintf("Bearer %s", s.token)})
checkGetResponse(getResp, expectedCP)
@@ -4456,9 +4498,8 @@ func (s *integrationMDMTestSuite) TestMDMAppleConfigProfileCRUD() {
checkGetResponse(getResp, expectedCP)
// delete profile (no team)
- deletedCP := testProfiles["TestNoTeam"]
- deletePath := fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
- var deleteResp deleteMDMAppleConfigProfileResponse
+ deletedCP = testProfiles["TestNoTeam"]
+ deletePath = fmt.Sprintf("/api/latest/fleet/mdm/apple/profiles/%d", deletedCP.ProfileID)
s.DoJSON("DELETE", deletePath, nil, http.StatusOK, &deleteResp)
// confirm deleted
listResp = listMDMAppleConfigProfilesResponse{}
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index ce134a2bd5fe..6eee1327bb2b 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -879,6 +879,26 @@ func (s *integrationMDMTestSuite) TestAppleGetAppleMDM() {
require.Equal(t, tm.Name, tok.MacOSTeam.Name)
require.Equal(t, tm.Name, tok.IOSTeam.Name)
require.Equal(t, tm.Name, tok.IPadOSTeam.Name)
+
+ // Reset the teams via app config
+ acResp = appConfigResponse{}
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "mdm": {
+ "apple_business_manager": []
+ }
+ }`), http.StatusOK, &acResp)
+
+ tokensResp = listABMTokensResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/abm_tokens", nil, http.StatusOK, &tokensResp)
+ tok = s.getABMTokenByName(tmOrgName, tokensResp.Tokens)
+ require.NotNil(t, tok)
+ require.False(t, tok.TermsExpired)
+ require.Equal(t, "abc", tok.AppleID)
+ require.Equal(t, tmOrgName, tok.OrganizationName)
+ require.Equal(t, s.server.URL+"/mdm/apple/mdm", tok.MDMServerURL)
+ require.Equal(t, fleet.TeamNameNoTeam, tok.MacOSTeam.Name)
+ require.Equal(t, fleet.TeamNameNoTeam, tok.IOSTeam.Name)
+ require.Equal(t, fleet.TeamNameNoTeam, tok.IPadOSTeam.Name)
}
func (s *integrationMDMTestSuite) getABMTokenByName(orgName string, tokens []*fleet.ABMToken) *fleet.ABMToken {
@@ -10532,6 +10552,25 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
var resPatchVPP patchVPPTokensTeamsResponse
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)
+ // Reset the token's teams by omitting the token from app config
+ acResp := appConfigResponse{}
+ s.DoJSON("PATCH", "/api/latest/fleet/config", json.RawMessage(`{
+ "mdm": { "volume_purchasing_program": null }
+ }`), http.StatusOK, &acResp)
+
+ resp = getVPPTokensResponse{}
+ s.DoJSON("GET", "/api/latest/fleet/vpp_tokens", &getVPPTokensRequest{}, http.StatusOK, &resp)
+ require.NoError(t, resp.Err)
+ require.Len(t, resp.Tokens, 1)
+ require.Equal(t, orgName, resp.Tokens[0].OrgName)
+ require.Equal(t, location, resp.Tokens[0].Location)
+ require.Equal(t, expTime, resp.Tokens[0].RenewDate)
+ require.Empty(t, resp.Tokens[0].Teams)
+
+ // Add the team back
+ resPatchVPP = patchVPPTokensTeamsResponse{}
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", resp.Tokens[0].ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{}}, http.StatusOK, &resPatchVPP)
+
// Get list of VPP apps from "Apple"
// We're passing team 1 here, but we haven't added any app store apps to that team, so we get
// back all available apps in our VPP location.
@@ -10801,14 +10840,6 @@ func (s *integrationMDMTestSuite) TestVPPApps() {
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{TeamIDs: []uint{team.ID}}, http.StatusOK, &resPatchVPP)
- // mysql.ExecAdhocSQL(t, s.ds, func(q sqlx.ExtContext) error {
- // _, err := q.ExecContext(context.Background(), "UPDATE vpp_tokens SET renew_at = ? WHERE organization_name = ?", time.Now().Add(-1*time.Hour), "badtoken")
- // return err
- // })
-
- // r := s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/install/%d", mdmHost.ID, errTitleID), &installSoftwareRequest{}, http.StatusUnprocessableEntity)
- // require.Contains(t, extractServerErrorText(r.Body), "VPP token expired")
-
// Disable the token
s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/vpp_tokens/%d/teams", vppRes.Token.ID), patchVPPTokensTeamsRequest{}, http.StatusOK, &resPatchVPP)
diff --git a/server/service/integration_sandbox_test.go b/server/service/integration_sandbox_test.go
deleted file mode 100644
index 9016da8880db..000000000000
--- a/server/service/integration_sandbox_test.go
+++ /dev/null
@@ -1,148 +0,0 @@
-package service
-
-import (
- "context"
- "fmt"
- "io"
- "net/http"
- "net/url"
- "os"
- "testing"
-
- "github.com/fleetdm/fleet/v4/server/config"
- "github.com/fleetdm/fleet/v4/server/datastore/s3"
- "github.com/fleetdm/fleet/v4/server/fleet"
- kitlog "github.com/go-kit/log"
- "github.com/stretchr/testify/require"
- "github.com/stretchr/testify/suite"
-)
-
-const enrollSecret = "xyz/abc$@"
-
-type integrationSandboxTestSuite struct {
- suite.Suite
- withServer
- installers []fleet.Installer
-}
-
-func (s *integrationSandboxTestSuite) SetupSuite() {
- s.withDS.SetupSuite("integrationSandboxTestSuite")
- t := s.T()
-
- // make sure sandbox is enabled
- cfg := config.TestConfig()
- cfg.Server.SandboxEnabled = true
-
- is := s3.SetupTestInstallerStore(t, "integration-tests", "")
- opts := &TestServerOpts{FleetConfig: &cfg, Is: is}
- if os.Getenv("FLEET_INTEGRATION_TESTS_DISABLE_LOG") != "" {
- opts.Logger = kitlog.NewNopLogger()
- }
- users, server := RunServerForTestsWithDS(t, s.ds, opts)
- s.server = server
- s.users = users
- s.token = s.getTestAdminToken()
- s.installers = s3.SeedTestInstallerStore(t, is, enrollSecret)
-
- err := s.ds.ApplyEnrollSecrets(context.TODO(), nil, []*fleet.EnrollSecret{{Secret: enrollSecret}})
- require.NoError(t, err)
-}
-
-func TestIntegrationsSandbox(t *testing.T) {
- testingSuite := new(integrationSandboxTestSuite)
- testingSuite.s = &testingSuite.Suite
- suite.Run(t, testingSuite)
-}
-
-func (s *integrationSandboxTestSuite) TestDemoLogin() {
- t := s.T()
-
- validEmail := testUsers["user1"].Email
- validPwd := testUsers["user1"].PlaintextPassword
- wrongPwd := "nope"
- hdrs := map[string]string{"Content-Type": "application/x-www-form-urlencoded"}
-
- formBody := make(url.Values)
- formBody.Set("email", validEmail)
- formBody.Set("password", wrongPwd)
- res := s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusUnauthorized, hdrs)
- require.Equal(t, http.StatusUnauthorized, res.StatusCode)
-
- formBody.Set("email", validEmail)
- formBody.Set("password", validPwd)
- res = s.DoRawWithHeaders("POST", "/api/v1/fleet/demologin", []byte(formBody.Encode()), http.StatusOK, hdrs)
- resBody, err := io.ReadAll(res.Body)
- require.NoError(t, err)
- require.Equal(t, http.StatusOK, res.StatusCode)
- require.Contains(t, string(resBody), `window.location = "/"`)
- require.Regexp(t, `window.localStorage.setItem\('FLEET::auth_token', '[^']+'\)`, string(resBody))
-}
-
-func (s *integrationSandboxTestSuite) TestInstallerGet() {
- t := s.T()
-
- validURL, formBody := installerPOSTReq(enrollSecret, "pkg", s.token, false)
-
- r := s.DoRaw("POST", validURL, formBody, http.StatusOK)
- body, err := io.ReadAll(r.Body)
- require.NoError(t, err)
- require.Equal(t, "mock", string(body))
- require.Equal(t, "application/octet-stream", r.Header.Get("Content-Type"))
- require.Equal(t, "4", r.Header.Get("Content-Length"))
- require.Equal(t, `attachment;filename="fleet-osquery.pkg"`, r.Header.Get("Content-Disposition"))
- require.Equal(t, `nosniff`, r.Header.Get("X-Content-Type-Options"))
-
- // unauthorized requests
- s.DoRawNoAuth("POST", validURL, nil, http.StatusUnauthorized)
- s.token = "invalid"
- s.Do("POST", validURL, nil, http.StatusUnauthorized)
- s.token = s.cachedAdminToken
-
- // wrong enroll secret
- wrongURL, wrongFormBody := installerPOSTReq("wrong-enroll", "pkg", s.token, false)
- s.Do("POST", wrongURL, wrongFormBody, http.StatusNotFound)
-
- // non-existent package
- wrongURL, wrongFormBody = installerPOSTReq(enrollSecret, "exe", s.token, false)
- s.Do("POST", wrongURL, wrongFormBody, http.StatusNotFound)
-}
-
-func (s *integrationSandboxTestSuite) TestInstallerHeadCheck() {
- validURL := installerURL(enrollSecret, "pkg", false)
- s.DoRaw("HEAD", validURL, nil, http.StatusOK)
-
- // unauthorized requests
- s.DoRawNoAuth("HEAD", validURL, nil, http.StatusUnauthorized)
- s.token = "invalid"
- s.DoRaw("HEAD", validURL, nil, http.StatusUnauthorized)
- s.token = s.cachedAdminToken
-
- // wrong enroll secret
- invalidURL := installerURL("wrong-enroll", "pkg", false)
- s.DoRaw("HEAD", invalidURL, nil, http.StatusNotFound)
-
- // non-existent package
- invalidURL = installerURL(enrollSecret, "exe", false)
- s.DoRaw("HEAD", invalidURL, nil, http.StatusNotFound)
-}
-
-func installerURL(secret, kind string, desktop bool) string {
- path := fmt.Sprintf("/api/latest/fleet/download_installer/%s?enroll_secret=%s", kind, secret)
- if desktop {
- path += "&desktop=1"
- }
- return path
-}
-
-func installerPOSTReq(secret, kind, token string, desktop bool) (string, []byte) {
- path := installerURL(secret, kind, desktop)
- d := "0"
- if desktop {
- d = "1"
- }
- formBody := make(url.Values)
- formBody.Set("token", token)
- formBody.Set("enroll_secret", secret)
- formBody.Set("desktop", d)
- return path, []byte(formBody.Encode())
-}
diff --git a/server/service/mail_test.go b/server/service/mail_test.go
index be041e7e6ec6..c83168301cac 100644
--- a/server/service/mail_test.go
+++ b/server/service/mail_test.go
@@ -86,6 +86,18 @@ func TestMailService(t *testing.T) {
return invite, nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
ctx = test.UserContext(ctx, test.UserAdmin)
// (1) Modifying the app config `sender_address` field to trigger a test e-mail send.
diff --git a/server/service/osquery_test.go b/server/service/osquery_test.go
index 0697c7de69ce..ff913ac46f30 100644
--- a/server/service/osquery_test.go
+++ b/server/service/osquery_test.go
@@ -1062,6 +1062,7 @@ func verifyDiscovery(t *testing.T, queries, discovery map[string]string) {
hostDetailQueryPrefix + "orbit_info": {},
hostDetailQueryPrefix + "software_vscode_extensions": {},
hostDetailQueryPrefix + "software_macos_firefox": {},
+ hostDetailQueryPrefix + "battery_windows": {},
}
for name := range queries {
require.NotEmpty(t, discovery[name])
diff --git a/server/service/osquery_utils/queries.go b/server/service/osquery_utils/queries.go
index f13454b2d110..5c93b5d2d680 100644
--- a/server/service/osquery_utils/queries.go
+++ b/server/service/osquery_utils/queries.go
@@ -555,7 +555,7 @@ var extraDetailQueries = map[string]DetailQuery{
DirectIngestFunc: directIngestChromeProfiles,
Discovery: discoveryTable("google_chrome_profiles"),
},
- "battery": {
+ "battery_macos": {
Query: `SELECT serial_number, cycle_count, health FROM battery;`,
Platforms: []string{"darwin"},
DirectIngestFunc: directIngestBattery,
@@ -563,6 +563,12 @@ var extraDetailQueries = map[string]DetailQuery{
// osquery table on darwin (https://osquery.io/schema/5.3.0#battery), it is
// always present.
},
+ "battery_windows": {
+ Query: `SELECT serial_number, cycle_count, designed_capacity, max_capacity FROM battery`,
+ Platforms: []string{"windows"},
+ DirectIngestFunc: directIngestBattery,
+ Discovery: discoveryTable("battery"), // added to Windows in v5.12.1 (https://github.com/osquery/osquery/releases/tag/5.12.1)
+ },
"os_windows": {
// This query is used to populate the `operating_systems` and `host_operating_system`
// tables. Separately, the `hosts` table is populated via the `os_version` and
@@ -1297,23 +1303,70 @@ func directIngestChromeProfiles(ctx context.Context, logger log.Logger, host *fl
func directIngestBattery(ctx context.Context, logger log.Logger, host *fleet.Host, ds fleet.Datastore, rows []map[string]string) error {
mapping := make([]*fleet.HostBattery, 0, len(rows))
for _, row := range rows {
- cycleCount, err := strconv.ParseInt(EmptyToZero(row["cycle_count"]), 10, 64)
+ cycleCount, err := strconv.Atoi(EmptyToZero(row["cycle_count"]))
if err != nil {
return err
}
- mapping = append(mapping, &fleet.HostBattery{
- HostID: host.ID,
- SerialNumber: row["serial_number"],
- CycleCount: int(cycleCount),
- // database type is VARCHAR(40) and since there isn't a
- // canonical list of strings we can get for health, we
- // truncate the value just in case.
- Health: fmt.Sprintf("%.40s", row["health"]),
- })
+
+ switch host.Platform {
+ case "darwin":
+ mapping = append(mapping, &fleet.HostBattery{
+ HostID: host.ID,
+ SerialNumber: row["serial_number"],
+ CycleCount: cycleCount,
+ // database type is VARCHAR(40) and since there isn't a
+ // canonical list of strings we can get for health, we
+ // truncate the value just in case.
+ Health: fmt.Sprintf("%.40s", row["health"]),
+ })
+ case "windows":
+ health, err := generateWindowsBatteryHealth(row["designed_capacity"], row["max_capacity"])
+ if err != nil {
+ level.Error(logger).Log("op", "directIngestBattery", "hostID", host.ID, "err", err)
+ }
+
+ mapping = append(mapping, &fleet.HostBattery{
+ HostID: host.ID,
+ SerialNumber: row["serial_number"],
+ CycleCount: cycleCount,
+ Health: health,
+ })
+ }
}
return ds.ReplaceHostBatteries(ctx, host.ID, mapping)
}
+const (
+ batteryStatusUnknown = "Unknown"
+ batteryStatusDegraded = "Check Battery"
+ batteryStatusGood = "Good"
+ batteryDegradedThreshold = 80
+)
+
+func generateWindowsBatteryHealth(designedCapacity, maxCapacity string) (string, error) {
+ if designedCapacity == "" || maxCapacity == "" {
+ return batteryStatusUnknown, fmt.Errorf("missing battery capacity values, designed: %s, max: %s", designedCapacity, maxCapacity)
+ }
+
+ designed, err := strconv.ParseInt(designedCapacity, 10, 64)
+ if err != nil {
+ return batteryStatusUnknown, err
+ }
+
+ max, err := strconv.ParseInt(maxCapacity, 10, 64)
+ if err != nil {
+ return batteryStatusUnknown, err
+ }
+
+ health := float64(max) / float64(designed) * 100
+
+ if health < batteryDegradedThreshold {
+ return batteryStatusDegraded, nil
+ }
+
+ return batteryStatusGood, nil
+}
+
func directIngestWindowsUpdateHistory(
ctx context.Context,
logger log.Logger,
diff --git a/server/service/osquery_utils/queries_test.go b/server/service/osquery_utils/queries_test.go
index 8a2931447083..3593ff20f1a5 100644
--- a/server/service/osquery_utils/queries_test.go
+++ b/server/service/osquery_utils/queries_test.go
@@ -279,7 +279,8 @@ func TestGetDetailQueries(t *testing.T) {
"mdm_windows",
"munki_info",
"google_chrome_profiles",
- "battery",
+ "battery_macos",
+ "battery_windows",
"os_windows",
"os_unix_like",
"os_chrome",
@@ -296,7 +297,7 @@ func TestGetDetailQueries(t *testing.T) {
sortedKeysCompare(t, queriesNoConfig, baseQueries)
queriesWithoutWinOSVuln := GetDetailQueries(context.Background(), config.FleetConfig{Vulnerabilities: config.VulnerabilitiesConfig{DisableWinOSVulnerabilities: true}}, nil, nil)
- require.Len(t, queriesWithoutWinOSVuln, 25)
+ require.Len(t, queriesWithoutWinOSVuln, 26)
queriesWithUsers := GetDetailQueries(context.Background(), config.FleetConfig{App: config.AppConfig{EnableScheduledQueryStats: true}}, nil, &fleet.Features{EnableHostUsers: true})
qs := append(baseQueries, "users", "users_chrome", "scheduled_query_stats")
@@ -984,7 +985,8 @@ func TestDirectIngestBattery(t *testing.T) {
}
host := fleet.Host{
- ID: 1,
+ ID: 1,
+ Platform: "darwin",
}
err := directIngestBattery(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{
@@ -994,6 +996,37 @@ func TestDirectIngestBattery(t *testing.T) {
require.NoError(t, err)
require.True(t, ds.ReplaceHostBatteriesFuncInvoked)
+
+ ds.ReplaceHostBatteriesFunc = func(ctx context.Context, id uint, mappings []*fleet.HostBattery) error {
+ require.Equal(t, mappings, []*fleet.HostBattery{
+ {HostID: uint(2), SerialNumber: "a", CycleCount: 2, Health: batteryStatusGood},
+ {HostID: uint(2), SerialNumber: "b", CycleCount: 3, Health: batteryStatusDegraded},
+ {HostID: uint(2), SerialNumber: "c", CycleCount: 4, Health: batteryStatusUnknown},
+ {HostID: uint(2), SerialNumber: "d", CycleCount: 5, Health: batteryStatusUnknown},
+ {HostID: uint(2), SerialNumber: "e", CycleCount: 6, Health: batteryStatusUnknown},
+ {HostID: uint(2), SerialNumber: "f", CycleCount: 7, Health: batteryStatusUnknown},
+ })
+ return nil
+ }
+
+ // reset the ds flag
+ ds.ReplaceHostBatteriesFuncInvoked = false
+
+ host = fleet.Host{
+ ID: 2,
+ Platform: "windows",
+ }
+
+ err = directIngestBattery(context.Background(), log.NewNopLogger(), &host, ds, []map[string]string{
+ {"serial_number": "a", "cycle_count": "2", "designed_capacity": "3000", "max_capacity": "2400"}, // max_capacity >= 80%
+ {"serial_number": "b", "cycle_count": "3", "designed_capacity": "3000", "max_capacity": "2399"}, // max_capacity < 50%
+ {"serial_number": "c", "cycle_count": "4", "designed_capacity": "3000", "max_capacity": ""}, // missing max_capacity
+ {"serial_number": "d", "cycle_count": "5", "designed_capacity": "", "max_capacity": ""}, // missing designed_capacity and max_capacity
+ {"serial_number": "e", "cycle_count": "6", "designed_capacity": "", "max_capacity": "2000"}, // missing designed_capacity
+ {"serial_number": "f", "cycle_count": "7", "designed_capacity": "foo", "max_capacity": "bar"}, // invalid designed_capacity and max_capacity
+ })
+ require.NoError(t, err)
+ require.True(t, ds.ReplaceHostBatteriesFuncInvoked)
}
func TestDirectIngestOSWindows(t *testing.T) {
diff --git a/server/service/service_appconfig_test.go b/server/service/service_appconfig_test.go
index b6318c584b39..d83479e5f156 100644
--- a/server/service/service_appconfig_test.go
+++ b/server/service/service_appconfig_test.go
@@ -373,6 +373,18 @@ func TestModifyAppConfigPatches(t *testing.T) {
return nil
}
+ ds.SaveABMTokenFunc = func(ctx context.Context, tok *fleet.ABMToken) error {
+ return nil
+ }
+
+ ds.ListVPPTokensFunc = func(ctx context.Context) ([]*fleet.VPPTokenDB, error) {
+ return []*fleet.VPPTokenDB{}, nil
+ }
+
+ ds.ListABMTokensFunc = func(ctx context.Context) ([]*fleet.ABMToken, error) {
+ return []*fleet.ABMToken{}, nil
+ }
+
configJSON := []byte(`{"org_info": { "org_name": "Acme", "org_logo_url": "somelogo.jpg" }}`)
ctx = test.UserContext(ctx, test.UserAdmin)
diff --git a/server/service/testdata/software-installers/README.md b/server/service/testdata/software-installers/README.md
index b5a59d9daf64..7baa9592be02 100644
--- a/server/service/testdata/software-installers/README.md
+++ b/server/service/testdata/software-installers/README.md
@@ -1,3 +1,4 @@
# testdata
-- `fleet-osquery.msi` is a dummy MSI installer created by `packaging.BuildMSI` with a fake `orbit.exe` that just has `hello world` in it. Its software title is `Fleet osquery` and its version is `1.0.0`.
\ No newline at end of file
+- `fleet-osquery.msi` is a dummy MSI installer created by `packaging.BuildMSI` with a fake `orbit.exe` that just has `hello world` in it. Its software title is `Fleet osquery` and its version is `1.0.0`.
+- `ruby.rpm` was downloaded from https://rpmfind.net/linux/fedora/linux/development/rawhide/Everything/x86_64/os/Packages/r/ruby-3.3.5-15.fc42.x86_64.rpm.
\ No newline at end of file
diff --git a/server/service/testdata/software-installers/ruby.rpm b/server/service/testdata/software-installers/ruby.rpm
new file mode 100644
index 000000000000..e7020796dab0
Binary files /dev/null and b/server/service/testdata/software-installers/ruby.rpm differ
diff --git a/server/test/new_objects.go b/server/test/new_objects.go
index f8a2de578c24..8a285783e5e8 100644
--- a/server/test/new_objects.go
+++ b/server/test/new_objects.go
@@ -176,6 +176,13 @@ func AddBuiltinLabels(t *testing.T, ds fleet.Datastore) {
LabelType: fleet.LabelTypeBuiltIn,
LabelMembershipType: fleet.LabelMembershipTypeManual,
},
+ {
+ Name: "Fedora Linux",
+ Platform: "rhel",
+ Query: "select 1 from os_version where name = 'Fedora Linux';",
+ LabelType: fleet.LabelTypeBuiltIn,
+ LabelMembershipType: fleet.LabelMembershipTypeDynamic,
+ },
}
names := fleet.ReservedLabelNames()
diff --git a/terraform/addons/logging-alb/main.tf b/terraform/addons/logging-alb/main.tf
index 632d04fb6e60..58437a12fbf2 100644
--- a/terraform/addons/logging-alb/main.tf
+++ b/terraform/addons/logging-alb/main.tf
@@ -250,4 +250,6 @@ resource "aws_athena_workgroup" "logs" {
}
}
}
+
+ force_destroy = true
}
diff --git a/terraform/addons/vuln-processing/variables.tf b/terraform/addons/vuln-processing/variables.tf
index 8d296903fdc4..11c5bb870715 100644
--- a/terraform/addons/vuln-processing/variables.tf
+++ b/terraform/addons/vuln-processing/variables.tf
@@ -24,7 +24,7 @@ variable "fleet_config" {
vuln_processing_cpu = optional(number, 2048)
vuln_data_stream_mem = optional(number, 1024)
vuln_data_stream_cpu = optional(number, 512)
- image = optional(string, "fleetdm/fleet:v4.57.0")
+ image = optional(string, "fleetdm/fleet:v4.57.1")
family = optional(string, "fleet-vuln-processing")
sidecars = optional(list(any), [])
extra_environment_variables = optional(map(string), {})
@@ -82,7 +82,7 @@ variable "fleet_config" {
vuln_processing_cpu = 2048
vuln_data_stream_mem = 1024
vuln_data_stream_cpu = 512
- image = "fleetdm/fleet:v4.57.0"
+ image = "fleetdm/fleet:v4.57.1"
family = "fleet-vuln-processing"
sidecars = []
extra_environment_variables = {}
diff --git a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
index 27565cb90fa8..51dc799bc5b7 100644
--- a/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
+++ b/terraform/byo-vpc/byo-db/byo-ecs/variables.tf
@@ -16,7 +16,7 @@ variable "fleet_config" {
mem = optional(number, 4096)
cpu = optional(number, 512)
pid_mode = optional(string, null)
- image = optional(string, "fleetdm/fleet:v4.57.0")
+ image = optional(string, "fleetdm/fleet:v4.57.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -119,7 +119,7 @@ variable "fleet_config" {
mem = 512
cpu = 256
pid_mode = null
- image = "fleetdm/fleet:v4.57.0"
+ image = "fleetdm/fleet:v4.57.1"
family = "fleet"
sidecars = []
depends_on = []
diff --git a/terraform/byo-vpc/byo-db/variables.tf b/terraform/byo-vpc/byo-db/variables.tf
index 041ff9d0f861..b19d74931ec5 100644
--- a/terraform/byo-vpc/byo-db/variables.tf
+++ b/terraform/byo-vpc/byo-db/variables.tf
@@ -77,7 +77,7 @@ variable "fleet_config" {
mem = optional(number, 4096)
cpu = optional(number, 512)
pid_mode = optional(string, null)
- image = optional(string, "fleetdm/fleet:v4.57.0")
+ image = optional(string, "fleetdm/fleet:v4.57.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -205,7 +205,7 @@ variable "fleet_config" {
mem = 512
cpu = 256
pid_mode = null
- image = "fleetdm/fleet:v4.57.0"
+ image = "fleetdm/fleet:v4.57.1"
family = "fleet"
sidecars = []
depends_on = []
diff --git a/terraform/byo-vpc/example/main.tf b/terraform/byo-vpc/example/main.tf
index 3176d07def1f..7f75b975ad42 100644
--- a/terraform/byo-vpc/example/main.tf
+++ b/terraform/byo-vpc/example/main.tf
@@ -17,7 +17,7 @@ provider "aws" {
}
locals {
- fleet_image = "fleetdm/fleet:v4.57.0"
+ fleet_image = "fleetdm/fleet:v4.57.1"
domain_name = "example.com"
}
diff --git a/terraform/byo-vpc/variables.tf b/terraform/byo-vpc/variables.tf
index ce2a81f88c41..2c8fdb1d1742 100644
--- a/terraform/byo-vpc/variables.tf
+++ b/terraform/byo-vpc/variables.tf
@@ -170,7 +170,7 @@ variable "fleet_config" {
mem = optional(number, 4096)
cpu = optional(number, 512)
pid_mode = optional(string, null)
- image = optional(string, "fleetdm/fleet:v4.57.0")
+ image = optional(string, "fleetdm/fleet:v4.57.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -298,7 +298,7 @@ variable "fleet_config" {
mem = 512
cpu = 256
pid_mode = null
- image = "fleetdm/fleet:v4.57.0"
+ image = "fleetdm/fleet:v4.57.1"
family = "fleet"
sidecars = []
depends_on = []
diff --git a/terraform/example/main.tf b/terraform/example/main.tf
index 2b2112517925..bf8f569a36b8 100644
--- a/terraform/example/main.tf
+++ b/terraform/example/main.tf
@@ -63,8 +63,8 @@ module "fleet" {
fleet_config = {
# To avoid pull-rate limiting from dockerhub, consider using our quay.io mirror
- # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.57.0"
- image = "fleetdm/fleet:v4.57.0" # override default to deploy the image you desire
+ # for the Fleet image. e.g. "quay.io/fleetdm/fleet:v4.57.1"
+ image = "fleetdm/fleet:v4.57.1" # override default to deploy the image you desire
# See https://fleetdm.com/docs/deploy/reference-architectures#aws for appropriate scaling
# memory and cpu.
autoscaling = {
diff --git a/terraform/variables.tf b/terraform/variables.tf
index 7dc798cf63d8..de37b0020f25 100644
--- a/terraform/variables.tf
+++ b/terraform/variables.tf
@@ -218,7 +218,7 @@ variable "fleet_config" {
mem = optional(number, 4096)
cpu = optional(number, 512)
pid_mode = optional(string, null)
- image = optional(string, "fleetdm/fleet:v4.57.0")
+ image = optional(string, "fleetdm/fleet:v4.57.1")
family = optional(string, "fleet")
sidecars = optional(list(any), [])
depends_on = optional(list(any), [])
@@ -346,7 +346,7 @@ variable "fleet_config" {
mem = 512
cpu = 256
pid_mode = null
- image = "fleetdm/fleet:v4.57.0"
+ image = "fleetdm/fleet:v4.57.1"
family = "fleet"
sidecars = []
depends_on = []
diff --git a/tools/fleetctl-npm/package.json b/tools/fleetctl-npm/package.json
index 96a4dcd08170..13ac3172e426 100644
--- a/tools/fleetctl-npm/package.json
+++ b/tools/fleetctl-npm/package.json
@@ -1,6 +1,6 @@
{
"name": "fleetctl",
- "version": "v4.57.0",
+ "version": "v4.57.1",
"description": "Installer for the fleetctl CLI tool",
"bin": {
"fleetctl": "./run.js"
diff --git a/tools/seed_data/queries/seed_queries.go b/tools/seed_data/queries/seed_queries.go
new file mode 100644
index 000000000000..857fe4b2b4f7
--- /dev/null
+++ b/tools/seed_data/queries/seed_queries.go
@@ -0,0 +1,74 @@
+package main
+
+import (
+ "database/sql"
+ "fmt"
+ "log"
+ "strings"
+
+ _ "github.com/go-sql-driver/mysql"
+)
+
+const (
+ batchSize = 1000
+ totalRecords = 1000000
+)
+
+func main() {
+ // MySQL connection details from your Docker Compose file
+ user := "fleet"
+ password := "insecure"
+ host := "localhost" // Assuming you are running this script on the same host as Docker
+ port := "3306"
+ database := "fleet"
+
+ // Construct the MySQL DSN (Data Source Name)
+ dsn := fmt.Sprintf("%s:%s@tcp(%s:%s)/%s?charset=utf8mb4&parseTime=True&loc=Local", user, password, host, port, database)
+
+ // Open MySQL connection
+ db, err := sql.Open("mysql", dsn)
+ if err != nil {
+ log.Fatal(err)
+ }
+ defer db.Close()
+
+ // Disable foreign key checks to improve performance
+ _, err = db.Exec("SET FOREIGN_KEY_CHECKS=0")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ // Prepare the insert statement
+ stmtPrefix := "INSERT INTO `queries` (`saved`, `name`, `description`, `query`, `author_id`, `observer_can_run`, `team_id`, `team_id_char`, `platform`, `min_osquery_version`, `schedule_interval`, `automations_enabled`, `logging_type`, `discard_data`) VALUES "
+ stmtSuffix := ";"
+
+ // Insert records in batches
+ for batch := 0; batch < totalRecords/batchSize; batch++ {
+ var valueStrings []string
+ var valueArgs []interface{}
+
+ // Generate batch of 1000 records
+ for i := 0; i < batchSize; i++ {
+ queryID := batch*batchSize + i + 1
+ valueStrings = append(valueStrings, "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
+ valueArgs = append(valueArgs, 0, fmt.Sprintf("query_%d", queryID), "", "SELECT * FROM processes;", 1, 0, nil, "", "", "", 0, 0, "snapshot", 0)
+ }
+
+ // Construct and execute the batch insert
+ stmt := stmtPrefix + strings.Join(valueStrings, ",") + stmtSuffix
+ _, err := db.Exec(stmt, valueArgs...)
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Printf("Inserted batch %d/%d\n", batch+1, totalRecords/batchSize)
+ }
+
+ // Re-enable foreign key checks
+ _, err = db.Exec("SET FOREIGN_KEY_CHECKS=1")
+ if err != nil {
+ log.Fatal(err)
+ }
+
+ fmt.Println("Finished inserting 1 million records.")
+}
diff --git a/tools/seed_data/README.md b/tools/seed_data/vulnerabilities/README.md
similarity index 100%
rename from tools/seed_data/README.md
rename to tools/seed_data/vulnerabilities/README.md
diff --git a/tools/seed_data/seed_vuln_data.go b/tools/seed_data/vulnerabilities/seed_vuln_data.go
similarity index 100%
rename from tools/seed_data/seed_vuln_data.go
rename to tools/seed_data/vulnerabilities/seed_vuln_data.go
diff --git a/tools/seed_data/software-macos.csv b/tools/seed_data/vulnerabilities/software-macos.csv
similarity index 100%
rename from tools/seed_data/software-macos.csv
rename to tools/seed_data/vulnerabilities/software-macos.csv
diff --git a/tools/seed_data/software-ubuntu.csv b/tools/seed_data/vulnerabilities/software-ubuntu.csv
similarity index 100%
rename from tools/seed_data/software-ubuntu.csv
rename to tools/seed_data/vulnerabilities/software-ubuntu.csv
diff --git a/tools/seed_data/software-win.csv b/tools/seed_data/vulnerabilities/software-win.csv
similarity index 100%
rename from tools/seed_data/software-win.csv
rename to tools/seed_data/vulnerabilities/software-win.csv
diff --git a/website/assets/images/icon-hand-wave-20x20@2x.png b/website/assets/images/icon-hand-wave-20x20@2x.png
new file mode 100644
index 000000000000..ab60da621402
Binary files /dev/null and b/website/assets/images/icon-hand-wave-20x20@2x.png differ
diff --git a/website/assets/styles/pages/device-management.less b/website/assets/styles/pages/device-management.less
index 10557dc6196e..551df9ac5800 100644
--- a/website/assets/styles/pages/device-management.less
+++ b/website/assets/styles/pages/device-management.less
@@ -39,6 +39,27 @@
strong {
color: @core-fleet-black;
}
+ [purpose='jnuc-banner'] {
+ height: 44px;
+ background-color: #0587FF;
+ p {
+ color: #FFF;
+ font-size: 14px;
+ font-weight: 700;
+ margin-bottom: 0px;
+ }
+ a {
+ color: #FFF;
+ text-decoration-line: underline;
+ text-underline-offset: 2px;
+
+ }
+ img {
+ display: inline;
+ height: 20px;
+ margin-right: 10px;
+ }
+ }
[purpose='page-container'] {
padding-left: 64px;
padding-right: 64px;
diff --git a/website/views/pages/device-management.ejs b/website/views/pages/device-management.ejs
index 0c077596e59a..409a23381318 100644
--- a/website/views/pages/device-management.ejs
+++ b/website/views/pages/device-management.ejs
@@ -1,4 +1,5 @@