diff --git a/.github/workflows/dogfood-deploy.yml b/.github/workflows/dogfood-deploy.yml
index bae6a032e97d..f9872e23a813 100644
--- a/.github/workflows/dogfood-deploy.yml
+++ b/.github/workflows/dogfood-deploy.yml
@@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
DOCKER_IMAGE:
- description: 'The full name of the docker image to be deployed. (e.g. fleetdm/fleet:v4.30.0). Note: do not use fleetdm/fleet:main directly. Use the short hash instead. If pull-rate limited, try using the quay.io/fleetdm/fleet mirror.'
+ description: "The full name of the docker image to be deployed. (e.g. fleetdm/fleet:v4.30.0). Note: do not use fleetdm/fleet:main directly. Use the short hash instead. If pull-rate limited, try using the quay.io/fleetdm/fleet mirror."
required: true
# This allows a subsequently queued workflow run to interrupt previous runs
@@ -26,7 +26,8 @@ env:
TF_WORKSPACE: fleet
TF_VAR_fleet_image: ${{ github.event.inputs.DOCKER_IMAGE || 'fleetdm/fleet:main' }}
TF_VAR_fleet_license: ${{ secrets.DOGFOOD_LICENSE_KEY }}
- TF_VAR_slack_webhook: ${{ secrets.SLACK_G_HELP_P1_WEBHOOK_URL }}
+ TF_VAR_slack_p1_webhook: ${{ secrets.SLACK_G_HELP_P1_WEBHOOK_URL }}
+ TF_VAR_slack_p2_webhook: ${{ secrets.SLACK_G_HELP_P2_WEBHOOK_URL }}
TF_VAR_fleet_sentry_dsn: ${{ secrets.DOGFOOD_SENTRY_DSN }}
TF_VAR_elastic_url: ${{ secrets.ELASTIC_APM_SERVER_URL }}
TF_VAR_elastic_token: ${{ secrets.ELASTIC_APM_SECRET_TOKEN }}
@@ -64,7 +65,7 @@ jobs:
- uses: hashicorp/setup-terraform@633666f66e0061ca3b725c73b2ec20cd13a8fdd1 # v2.0.3
with:
- terraform_version: 1.6.3
+ terraform_version: 1.10.2
terraform_wrapper: false
- name: Terraform Init
id: init
diff --git a/CODEOWNERS b/CODEOWNERS
index e4d1312134c0..c8512f309c58 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -65,9 +65,9 @@ go.mod @fleetdm/go
#
# (see website/config/custom.js for DRIs of other paths not listed here)
##############################################################################################
-/docs @eashaw
-/docs/REST\ API/rest-api.md @iansltx # « REST API reference documentation
-/docs/Contributing/API-for-contributors.md @iansltx # « Advanced / contributors-only API reference documentation
+/docs @rachaelshaw
+/docs/REST\ API/rest-api.md @rachaelshaw # « REST API reference documentation
+/docs/Contributing/API-for-contributors.md @rachaelshaw # « Advanced / contributors-only API reference documentation
/schema @eashaw # « Data tables (osquery/fleetd schema) documentation
/render.yaml @edwardsb
diff --git a/articles/enforce-os-updates.md b/articles/enforce-os-updates.md
index 4976b5cdc038..ac7df5bba3df 100644
--- a/articles/enforce-os-updates.md
+++ b/articles/enforce-os-updates.md
@@ -34,7 +34,9 @@ OS version enforcement options are declared within the [controls](https://fleetd
### macOS
-When a minimum version is enforced, end users see a native macOS notification (DDM) once per day. Users can choose to update ahead of the deadline or schedule it for that night. 24 hours before the deadline, the notification appears hourly and ignores Do Not Disturb. One hour before the deadline, the notification appears every 30 minutes and then every 10 minutes.
+When a minimum version is enforced, end users see a native macOS notification (DDM) once per day. Users can choose to update ahead of the deadline or schedule it for that night. 24 hours before the deadline, the notification appears hourly and ignores Do Not Disturb. One hour before the deadline, the notification appears every 30 minutes and then every 10 minutes.
+
+> Certain user preferences may suppress macOS update notifications. To prevent users from being surprised by a forced update or unexpected restart, consider communicating OS update deadlines through additional channels.
If the host was turned off when the deadline passed, the update will be scheduled an hour after it’s turned on.
diff --git a/articles/role-based-access.md b/articles/role-based-access.md
index 125bbffaed9a..08356c86f9cd 100644
--- a/articles/role-based-access.md
+++ b/articles/role-based-access.md
@@ -34,7 +34,7 @@ GitOps is an API-only and write-only role that can be used on CI/CD pipelines.
## User permissions
| **Action** | Observer | Observer+* | Maintainer | Admin | GitOps* |
-| ------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ---------- | ---------- | ----- | ------- |
+| ------------------------------------------------------------------------------------------------------------------------------------------ | :------: | :--------: | :--------: | :---: | :-----: |
| View all [activity](https://fleetdm.com/docs/using-fleet/rest-api#activities) | ✅ | ✅ | ✅ | ✅ | |
| Manage [activity automations](https://fleetdm.com/docs/using-fleet/audit-logs) | | | | ✅ | ✅ |
| View all hosts | ✅ | ✅ | ✅ | ✅ | |
@@ -123,7 +123,7 @@ Users can be assigned to multiple teams in Fleet.
Users with access to multiple teams can be assigned different roles for each team. For example, a user can be given access to the "Workstations" team and assigned the "Observer" role. This same user can be given access to the "Servers" team and assigned the "Maintainer" role.
| **Action** | Team observer | Team observer+ | Team maintainer | Team admin | Team GitOps |
-| -------------------------------------------------------------------------------------------------------------------------------- | ------------- | -------------- | --------------- | ---------- | ----------- |
+| -------------------------------------------------------------------------------------------------------------------------------- | :-----------: | :------------: | :-------------: | :--------: | :---------: |
| View hosts | ✅ | ✅ | ✅ | ✅ | |
| View a host by identifier | ✅ | ✅ | ✅ | ✅ | ✅ |
| Filter hosts using [labels](https://fleetdm.com/docs/using-fleet/rest-api#labels) | ✅ | ✅ | ✅ | ✅ | |
diff --git a/changes/23512-clarify-expected-behavior-of-host-counts b/changes/23512-clarify-expected-behavior-of-host-counts
new file mode 100644
index 000000000000..98e0b64acb81
--- /dev/null
+++ b/changes/23512-clarify-expected-behavior-of-host-counts
@@ -0,0 +1,2 @@
+- Clarify expected behavior of policy host counts, dashboard controls software count, and controls
+ os updates versions count.
diff --git a/changes/23823-cloudfront-cdn b/changes/23823-cloudfront-cdn
new file mode 100644
index 000000000000..21db000a6965
--- /dev/null
+++ b/changes/23823-cloudfront-cdn
@@ -0,0 +1,4 @@
+Allow delivery of bootstrap packages and software installers using signed URLs from CloudFront CDN. To enable, configure server settings:
+- s3_software_installers_cloudfront_url
+- s3_software_installers_cloudfront_url_signing_public_key_id
+- s3_software_installers_cloudfront_url_signing_private_key
diff --git a/changes/24366-success-email-message b/changes/24366-success-email-message
new file mode 100644
index 000000000000..9212848a409a
--- /dev/null
+++ b/changes/24366-success-email-message
@@ -0,0 +1 @@
+- Improve readability of success message on email update by never including the sender address.
diff --git a/changes/24653-live-query-from-edit-affects-performance-stats b/changes/24653-live-query-from-edit-affects-performance-stats
new file mode 100644
index 000000000000..68becee779c9
--- /dev/null
+++ b/changes/24653-live-query-from-edit-affects-performance-stats
@@ -0,0 +1,2 @@
+- When running a live query from the edit query form, consider the results of the run in calculating
+ an existing query's performance impact if the user didn't change the query from the stored version.
diff --git a/changes/24795-host-count b/changes/24795-host-count
new file mode 100644
index 000000000000..20dd5dcf4c09
--- /dev/null
+++ b/changes/24795-host-count
@@ -0,0 +1 @@
+- Fleet UI: Added timestamp for software, OS, and vulnerability detail pages for host count last update time
diff --git a/changes/24804-deleted-profiles b/changes/24804-deleted-profiles
new file mode 100644
index 000000000000..2e1e1e32945a
--- /dev/null
+++ b/changes/24804-deleted-profiles
@@ -0,0 +1 @@
+Fixed issue where deleted Apple config profiles were installing on devices because devices were offline when the profile was added.
diff --git a/changes/25004-fleetctl-packge-cli-instructions b/changes/25004-fleetctl-packge-cli-instructions
new file mode 100644
index 000000000000..dc1fa6fa2bee
--- /dev/null
+++ b/changes/25004-fleetctl-packge-cli-instructions
@@ -0,0 +1 @@
+- Display command line installation instructions when a package is generated
diff --git a/changes/25144-uninstall-after-mdm-action b/changes/25144-uninstall-after-mdm-action
new file mode 100644
index 000000000000..136a2ff7776e
--- /dev/null
+++ b/changes/25144-uninstall-after-mdm-action
@@ -0,0 +1 @@
+* Fixed reporting of software uninstall results after a host has been locked/unlocked
diff --git a/cmd/fleet/main.go b/cmd/fleet/main.go
index b39cbc2ee942..c60fc5a55ac0 100644
--- a/cmd/fleet/main.go
+++ b/cmd/fleet/main.go
@@ -104,24 +104,25 @@ func applyDevFlags(cfg *config.FleetConfig) {
cfg.Prometheus.BasicAuth.Password = "insecure"
}
- cfg.S3 = config.S3Config{
- CarvesBucket: "carves-dev",
- CarvesRegion: "minio",
- CarvesPrefix: "dev-prefix",
- CarvesEndpointURL: "localhost:9000",
- CarvesAccessKeyID: "minio",
- CarvesSecretAccessKey: "minio123!",
- CarvesDisableSSL: true,
- CarvesForceS3PathStyle: true,
-
- SoftwareInstallersBucket: "software-installers-dev",
- SoftwareInstallersRegion: "minio",
- SoftwareInstallersPrefix: "dev-prefix",
- SoftwareInstallersEndpointURL: "localhost:9000",
- SoftwareInstallersAccessKeyID: "minio",
- SoftwareInstallersSecretAccessKey: "minio123!",
- SoftwareInstallersDisableSSL: true,
- SoftwareInstallersForceS3PathStyle: true,
+ cfg.S3.CarvesBucket = "carves-dev"
+ cfg.S3.CarvesRegion = "minio"
+ cfg.S3.CarvesPrefix = "dev-prefix"
+ cfg.S3.CarvesEndpointURL = "localhost:9000"
+ cfg.S3.CarvesAccessKeyID = "minio"
+ cfg.S3.CarvesSecretAccessKey = "minio123!"
+ cfg.S3.CarvesDisableSSL = true
+ cfg.S3.CarvesForceS3PathStyle = true
+
+ // Allow the software installers bucket to be overridden in dev mode
+ if cfg.S3.SoftwareInstallersBucket == "" {
+ cfg.S3.SoftwareInstallersBucket = "software-installers-dev"
+ cfg.S3.SoftwareInstallersRegion = "minio"
+ cfg.S3.SoftwareInstallersPrefix = "dev-prefix"
+ cfg.S3.SoftwareInstallersEndpointURL = "localhost:9000"
+ cfg.S3.SoftwareInstallersAccessKeyID = "minio"
+ cfg.S3.SoftwareInstallersSecretAccessKey = "minio123!"
+ cfg.S3.SoftwareInstallersDisableSSL = true
+ cfg.S3.SoftwareInstallersForceS3PathStyle = true
}
cfg.Packaging.S3 = config.S3Config{
diff --git a/cmd/fleet/serve.go b/cmd/fleet/serve.go
index 2fdab8223f7e..9c227735a4b5 100644
--- a/cmd/fleet/serve.go
+++ b/cmd/fleet/serve.go
@@ -779,6 +779,8 @@ the way that the Fleet server works.
}
bootstrapPackageStore = bstore
level.Info(logger).Log("msg", "using S3 bootstrap package store", "bucket", config.S3.SoftwareInstallersBucket)
+
+ config.S3.ValidateCloudfrontURL(initFatal)
} else {
installerDir := os.TempDir()
if dir := os.Getenv("FLEET_SOFTWARE_INSTALLER_STORE_DIR"); dir != "" {
diff --git a/cmd/fleetctl/package.go b/cmd/fleetctl/package.go
index 4fe19648705e..271203390693 100644
--- a/cmd/fleetctl/package.go
+++ b/cmd/fleetctl/package.go
@@ -378,13 +378,31 @@ func packageCommand() *cli.Command {
}
path, _ = filepath.Abs(path)
+ pathBase := filepath.Base(path)
+ var installInstructions = "double-click the installer"
+ var deviceType string
+ switch c.String("type") {
+ case "pkg":
+ installInstructions += fmt.Sprintf(" or run the command `sudo installer -pkg \"%s\" -target /`", pathBase)
+ deviceType = "macOS"
+ case "deb":
+ installInstructions += fmt.Sprintf(" or run the command `sudo apt install \"%s\"`", pathBase)
+ deviceType = "Debian-based Linux"
+ case "rpm":
+ installInstructions += fmt.Sprintf(" or run the command `sudo dnf install \"%s\"`", pathBase)
+ deviceType = "RPM-based Linux"
+ case "msi":
+ installInstructions += fmt.Sprintf(" or run the command `msiexec /i \"%s\"` as administrator", pathBase)
+ deviceType = "Windows"
+ }
+
fmt.Printf(`
Success! You generated fleetd at %s
-To add this device to Fleet, double-click to install fleetd.
+To add a new %s device to Fleet, %s.
To add other devices to Fleet, distribute fleetd using Chef, Ansible, Jamf, or Puppet. Learn how: https://fleetdm.com/learn-more-about/enrolling-hosts
-`, path)
+`, path, deviceType, installInstructions)
if !disableOpenFolder {
open.Start(filepath.Dir(path)) //nolint:errcheck
}
diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go
index 81b539c5b1f4..4bdba884a577 100644
--- a/ee/server/service/software_installers.go
+++ b/ee/server/service/software_installers.go
@@ -438,10 +438,41 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
payload.SelfService = &existingInstaller.SelfService
}
+ // Get the hosts that are NOT in label scope currently (before the update happens)
+ var hostsNotInScope map[uint]struct{}
+ if dirty["Labels"] {
+ hostsNotInScope, err = svc.ds.GetExcludedHostIDMapForSoftwareInstaller(ctx, payload.InstallerID)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "getting hosts not in scope for installer")
+ }
+ }
+
if err := svc.ds.SaveInstallerUpdates(ctx, payload); err != nil {
return nil, ctxerr.Wrap(ctx, err, "saving installer updates")
}
+ if dirty["Labels"] {
+ // Get the hosts that are now IN label scope (after the update)
+ hostsInScope, err := svc.ds.GetIncludedHostIDMapForSoftwareInstaller(ctx, payload.InstallerID)
+ if err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "getting hosts in scope for installer")
+ }
+
+ var hostsToClear []uint
+ for id := range hostsInScope {
+ if _, ok := hostsNotInScope[id]; ok {
+ // it was not in scope but now it is, so we should clear policy status
+ hostsToClear = append(hostsToClear, id)
+ }
+ }
+
+ // We clear the policy status here because otherwise the policy automation machinery
+ // won't pick this up and the software won't install.
+ if err := svc.ds.ClearAutoInstallPolicyStatusForHosts(ctx, payload.InstallerID, hostsToClear); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "failed to clear auto install policy status for host")
+ }
+ }
+
// if we're updating anything other than self-service, we cancel pending installs/uninstalls,
// and if we're updating the package we reset counts. This is run in its own transaction internally
// for consistency, but independent of the installer update query as the main update should stick
@@ -484,7 +515,8 @@ func (svc *Service) UpdateSoftwareInstaller(ctx context.Context, payload *fleet.
}
func (svc *Service) validateEmbeddedSecretsOnScript(ctx context.Context, scriptName string, script *string,
- argErr *fleet.InvalidArgumentError) *fleet.InvalidArgumentError {
+ argErr *fleet.InvalidArgumentError,
+) *fleet.InvalidArgumentError {
if script != nil {
if errScript := svc.ds.ValidateEmbeddedSecrets(ctx, []string{*script}); errScript != nil {
if argErr != nil {
diff --git a/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.stories.tsx b/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.stories.tsx
new file mode 100644
index 000000000000..2516d5311217
--- /dev/null
+++ b/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.stories.tsx
@@ -0,0 +1,23 @@
+import { Meta, StoryObj } from "@storybook/react";
+
+import LastUpdatedHostCount from "./LastUpdatedHostCount";
+
+const meta: Meta = {
+ title: "Components/LastUpdatedHostCount",
+ component: LastUpdatedHostCount,
+ args: {
+ hostCount: 40,
+ },
+};
+
+export default meta;
+
+type Story = StoryObj;
+
+export const Basic: Story = {};
+
+export const WithLastUpdatedAt: Story = {
+ args: {
+ lastUpdatedAt: "2021-01-01T00:00:00Z",
+ },
+};
diff --git a/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.tests.tsx b/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.tests.tsx
new file mode 100644
index 000000000000..ecde3d189679
--- /dev/null
+++ b/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.tests.tsx
@@ -0,0 +1,37 @@
+import React from "react";
+import { fireEvent, render, screen } from "@testing-library/react";
+
+import LastUpdatedHostCount from ".";
+
+describe("Last updated host count", () => {
+ it("renders host count and updated text", () => {
+ const currentDate = new Date();
+ currentDate.setDate(currentDate.getDate() - 2);
+ const twoDaysAgo = currentDate.toISOString();
+
+ render( );
+
+ const hostCount = screen.getByText(/40/i);
+ const updateText = screen.getByText("Updated 2 days ago");
+
+ expect(hostCount).toBeInTheDocument();
+ expect(updateText).toBeInTheDocument();
+ });
+ it("renders never if missing timestamp", () => {
+ render( );
+
+ const text = screen.getByText("Updated never");
+
+ expect(text).toBeInTheDocument();
+ });
+
+ it("renders tooltip on hover", async () => {
+ render( );
+
+ await fireEvent.mouseEnter(screen.getByText("Updated never"));
+
+ expect(
+ screen.getByText(/last time host data was updated/i)
+ ).toBeInTheDocument();
+ });
+});
diff --git a/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.tsx b/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.tsx
new file mode 100644
index 000000000000..8cd29f6d16a1
--- /dev/null
+++ b/frontend/components/LastUpdatedHostCount/LastUpdatedHostCount.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+import LastUpdatedText from "components/LastUpdatedText";
+
+const baseClass = "last-updated-host-count";
+
+interface ILastUpdatedHostCount {
+ hostCount?: string | number;
+ lastUpdatedAt?: string;
+}
+
+const LastUpdatedHostCount = ({
+ hostCount,
+ lastUpdatedAt,
+}: ILastUpdatedHostCount): JSX.Element => {
+ const tooltipContent = (
+ <>
+ The last time host data was updated.
+ Click View all hosts to see the most
+ up-to-date host count.
+ >
+ );
+
+ return (
+
+ <>{hostCount}>
+
+
+ );
+};
+
+export default LastUpdatedHostCount;
diff --git a/frontend/components/LastUpdatedHostCount/_styles.scss b/frontend/components/LastUpdatedHostCount/_styles.scss
new file mode 100644
index 000000000000..9dd4eb3b3344
--- /dev/null
+++ b/frontend/components/LastUpdatedHostCount/_styles.scss
@@ -0,0 +1,6 @@
+.last-updated-host-count {
+ display: flex;
+ align-items: baseline;
+ gap: $pad-small;
+ font-size: $x-small;
+}
diff --git a/frontend/components/LastUpdatedHostCount/index.ts b/frontend/components/LastUpdatedHostCount/index.ts
new file mode 100644
index 000000000000..d4955ba278ed
--- /dev/null
+++ b/frontend/components/LastUpdatedHostCount/index.ts
@@ -0,0 +1 @@
+export { default } from "./LastUpdatedHostCount";
diff --git a/frontend/components/LastUpdatedText/_styles.scss b/frontend/components/LastUpdatedText/_styles.scss
index 1198fd096bb6..3094d4753bbf 100644
--- a/frontend/components/LastUpdatedText/_styles.scss
+++ b/frontend/components/LastUpdatedText/_styles.scss
@@ -1,5 +1,6 @@
.component__last-updated-text {
font-size: $xx-small;
+ font-weight: $regular;
color: $ui-fleet-black-75;
.tooltip {
diff --git a/frontend/components/MainContent/MainContent.tsx b/frontend/components/MainContent/MainContent.tsx
index 6733a437cb7f..cc9157cb6d66 100644
--- a/frontend/components/MainContent/MainContent.tsx
+++ b/frontend/components/MainContent/MainContent.tsx
@@ -1,12 +1,9 @@
import React, { ReactNode, useContext } from "react";
import classnames from "classnames";
-import { formatDistanceToNow } from "date-fns";
-import { hasLicenseExpired, willExpireWithinXDays } from "utilities/helpers";
+import { hasLicenseExpired } from "utilities/helpers";
-import SandboxExpiryMessage from "components/Sandbox/SandboxExpiryMessage";
import AppleBMTermsMessage from "components/MDM/AppleBMTermsMessage";
-import SandboxGate from "components/Sandbox/SandboxGate";
import { AppContext } from "context/app";
import LicenseExpirationBanner from "components/LicenseExpirationBanner";
import ApplePNCertRenewalMessage from "components/MDM/ApplePNCertRenewalMessage";
@@ -33,10 +30,8 @@ const MainContent = ({
}: IMainContentProps): JSX.Element => {
const classes = classnames(baseClass, className);
const {
- sandboxExpiry,
config,
isPremiumTier,
- noSandboxHosts,
isApplePnsExpired,
isAppleBmExpired,
isVppExpired,
@@ -46,11 +41,6 @@ const MainContent = ({
willVppExpire,
} = useContext(AppContext);
- const sandboxExpiryTime =
- sandboxExpiry === undefined
- ? "..."
- : formatDistanceToNow(new Date(sandboxExpiry));
-
const renderAppWideBanner = () => {
const isFleetLicenseExpired = hasLicenseExpired(
config?.license.expiration || ""
@@ -73,24 +63,18 @@ const MainContent = ({
}
if (banner) {
- return {banner}
;
+ return (
+
+ );
}
return null;
};
return (
-
- {renderAppWideBanner()}
- (
-
- )}
- />
-
+ {renderAppWideBanner()}
{children}
);
diff --git a/frontend/components/PlatformSelector/PlatformSelector.tsx b/frontend/components/PlatformSelector/PlatformSelector.tsx
index 592193a6d0ab..6f87dd236841 100644
--- a/frontend/components/PlatformSelector/PlatformSelector.tsx
+++ b/frontend/components/PlatformSelector/PlatformSelector.tsx
@@ -72,8 +72,8 @@ export const PlatformSelector = ({
Your policy will only run on the selected platform(s). Additionally, if
- install software automation is enabled, it will run only on hosts
- defined in the software scope.
+ install software automation is enabled, it will only be installed on
+ hosts defined in the software scope.
);
diff --git a/frontend/interfaces/software.ts b/frontend/interfaces/software.ts
index 5f8797f27fb3..99470e4585df 100644
--- a/frontend/interfaces/software.ts
+++ b/frontend/interfaces/software.ts
@@ -82,7 +82,7 @@ export interface ISoftwarePackage {
pending_uninstall: number;
failed_uninstall: number;
};
- automatic_install_policies?: ISoftwarePackagePolicy[];
+ automatic_install_policies?: ISoftwarePackagePolicy[] | null;
install_during_setup?: boolean;
labels_include_any: ILabelSoftwareTitle[] | null;
labels_exclude_any: ILabelSoftwareTitle[] | null;
diff --git a/frontend/pages/DashboardPage/DashboardPage.tsx b/frontend/pages/DashboardPage/DashboardPage.tsx
index 056acc965c17..51f846941fc2 100644
--- a/frontend/pages/DashboardPage/DashboardPage.tsx
+++ b/frontend/pages/DashboardPage/DashboardPage.tsx
@@ -322,7 +322,15 @@ const DashboardPage = ({ router, location }: IDashboardProps): JSX.Element => {
setSoftwareTitleDetail(
+ Fleet periodically queries all hosts to
+
+ retrieve software. Click to view
+
+ hosts for the most up-to-date lists.
+ >
+ }
/>
);
setShowSoftwareCard(true);
diff --git a/frontend/pages/DashboardPage/_styles.scss b/frontend/pages/DashboardPage/_styles.scss
index cf62942732dc..cc84a830f556 100644
--- a/frontend/pages/DashboardPage/_styles.scss
+++ b/frontend/pages/DashboardPage/_styles.scss
@@ -24,10 +24,7 @@
}
&__header {
- height: 38px;
- display: flex;
- align-items: center;
- justify-content: space-between;
+ @include normalize-team-header;
margin-bottom: $pad-large;
.Select-control {
diff --git a/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx b/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx
index 7e1cc7f085d5..0b76c3189d0e 100644
--- a/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx
+++ b/frontend/pages/ManageControlsPage/OSUpdates/components/CurrentVersionSection/CurrentVersionSection.tsx
@@ -81,7 +81,17 @@ const CurrentVersionSection = ({
return (
+ Fleet periodically queries all hosts to
+
+ retrieve operating systems. Click to
+
+ view hosts for the most up-to-date
+
+ lists.
+ >
+ }
/>
);
};
diff --git a/frontend/pages/ManageControlsPage/_styles.scss b/frontend/pages/ManageControlsPage/_styles.scss
index 8895cc204d45..a193f7fe3c9e 100644
--- a/frontend/pages/ManageControlsPage/_styles.scss
+++ b/frontend/pages/ManageControlsPage/_styles.scss
@@ -1,9 +1,6 @@
.manage-controls-page {
&__header-wrap {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 38px;
+ @include normalize-team-header;
.button-wrap {
display: flex;
diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx
index 20aaa4c4c95e..426fddd61daf 100644
--- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetAppDetailsForm/FleetAppDetailsForm.tsx
@@ -128,7 +128,7 @@ export const InstallTypeSection = ({
color="yellow"
cta={
diff --git a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx
index 8516ff248378..0f2967547bfa 100644
--- a/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage.tsx
@@ -2,6 +2,7 @@ import React, { useContext, useState } from "react";
import { Location } from "history";
import { useQuery } from "react-query";
import { InjectedRouter } from "react-router";
+import { useErrorHandler } from "react-error-boundary";
import PATHS from "router/paths";
import { buildQueryStringFromParams } from "utilities/url";
@@ -105,6 +106,7 @@ const FleetMaintainedAppDetailsPage = ({
}
const { renderFlash } = useContext(NotificationContext);
+ const handlePageError = useErrorHandler();
const { isPremiumTier } = useContext(AppContext);
const { selectedOsqueryTable, setSelectedOsqueryTable } = useContext(
QueryContext
@@ -125,7 +127,9 @@ const FleetMaintainedAppDetailsPage = ({
{
...DEFAULT_USE_QUERY_OPTIONS,
enabled: isPremiumTier,
+ retry: false,
select: (res) => res.fleet_maintained_app,
+ onError: (error) => handlePageError(error),
}
);
diff --git a/frontend/pages/SoftwarePage/SoftwareOSDetailsPage/SoftwareOSDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareOSDetailsPage/SoftwareOSDetailsPage.tsx
index 39a69bb01fd2..889f232cb9d7 100644
--- a/frontend/pages/SoftwarePage/SoftwareOSDetailsPage/SoftwareOSDetailsPage.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareOSDetailsPage/SoftwareOSDetailsPage.tsx
@@ -72,13 +72,13 @@ const SoftwareOSDetailsPage = ({
});
const {
- data: osVersionDetails,
+ data: { os_version: osVersionDetails, counts_updated_at } = {},
isLoading,
isError: isOsVersionError,
} = useQuery<
IOSVersionResponse,
AxiosError,
- IOperatingSystemVersion,
+ IOSVersionResponse,
IGetOsVersionQueryKey[]
>(
[
@@ -93,7 +93,10 @@ const SoftwareOSDetailsPage = ({
...DEFAULT_USE_QUERY_OPTIONS,
retry: false,
enabled: !!osVersionIdFromURL,
- select: (data) => data.os_version,
+ select: (data) => ({
+ os_version: data.os_version,
+ counts_updated_at: data.counts_updated_at,
+ }),
onError: (error) => {
if (!ignoreAxiosError(error, [403, 404])) {
handlePageError(error);
@@ -154,7 +157,7 @@ const SoftwareOSDetailsPage = ({
onTeamChange={onTeamChange}
/>
)}
- {isOsVersionError ? (
+ {isOsVersionError || !osVersionDetails ? (
{warningText}
Installs or uninstalls currently running on a host will still
- complete, but results won’t appear in Fleet.
+ complete, but results won't appear in Fleet.
You cannot undo this action.
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx
index 98f2ee9bc69e..a7c69196906d 100644
--- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal.tsx
@@ -24,6 +24,7 @@ import { IPackageFormData } from "pages/SoftwarePage/components/PackageForm/Pack
import {
generateSelectedLabels,
getCustomTarget,
+ getInstallType,
getTargetType,
} from "pages/SoftwarePage/components/PackageForm/helpers";
@@ -70,11 +71,7 @@ const EditSoftwareModal = ({
});
const [uploadProgress, setUploadProgress] = useState(0);
- const {
- data: labels,
- isLoading: isLoadingLabels,
- isError: isErrorLabels,
- } = useQuery
(
+ const { data: labels } = useQuery(
["custom_labels"],
() => labelsAPI.summary().then((res) => getCustomLabels(res.labels)),
{
@@ -178,6 +175,7 @@ const EditSoftwareModal = ({
postInstallScript: software.post_install_script || "",
uninstallScript: software.uninstall_script || "",
selfService: software.self_service || false,
+ installType: getInstallType(software),
targetType: getTargetType(software),
customTarget: getCustomTarget(software),
labelTargets: generateSelectedLabels(software),
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx
index 5c312a63172f..7f2fbed6e96f 100644
--- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/SoftwareTitleDetailsPage.tsx
@@ -178,6 +178,7 @@ const SoftwareTitleDetailsPage = ({
type={formatSoftwareType(softwareTitle)}
versions={softwareTitle.versions?.length ?? 0}
hosts={softwareTitle.hosts_count}
+ countsUpdatedAt={softwareTitle.counts_updated_at}
queryParams={{
software_title_id: softwareId,
team_id: teamIdForApi,
diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss
index 1e428eb76da7..4c68ad56d779 100644
--- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss
+++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/_styles.scss
@@ -4,6 +4,10 @@
flex-direction: column;
gap: $pad-medium;
+ .team-dropdown-wrapper {
+ @include normalize-team-header;
+ }
+
h2 {
font-size: $small;
}
diff --git a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss
index 60a88a8254e1..a532be04bafe 100644
--- a/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss
+++ b/frontend/pages/SoftwarePage/SoftwareVersionDetailsPage/_styles.scss
@@ -4,6 +4,10 @@
flex-direction: column;
gap: $pad-medium;
+ .team-dropdown-wrapper {
+ @include normalize-team-header;
+ }
+
h2 {
font-size: $small;
}
diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx
index 42dc104b9fe5..c7cbef556e34 100644
--- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx
+++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/SoftwareVulnSummary/SoftwareVulnSummary.tsx
@@ -11,6 +11,7 @@ import TooltipWrapper from "components/TooltipWrapper";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import ProbabilityOfExploit from "components/ProbabilityOfExploit";
import { HumanTimeDiffWithDateTip } from "components/HumanTimeDiffWithDateTip";
+import LastUpdatedHostCount from "components/LastUpdatedHostCount";
const baseClass = "software-vuln-summary";
@@ -35,6 +36,7 @@ const SoftwareVulnSummary = ({
cve_published,
created_at,
hosts_count,
+ hosts_count_updated_at,
} = vuln;
return (
@@ -126,7 +128,15 @@ const SoftwareVulnSummary = ({
/>
}
/>
-
+
+ }
+ />
);
diff --git a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss
index 7bee8edd59d5..e14267bb1080 100644
--- a/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss
+++ b/frontend/pages/SoftwarePage/SoftwareVulnerabilityDetailsPage/_styles.scss
@@ -2,6 +2,10 @@
background-color: $ui-off-white;
@include page;
+ .team-dropdown-wrapper {
+ @include normalize-team-header;
+ }
+
.card {
display: flex;
flex-direction: column;
diff --git a/frontend/pages/SoftwarePage/_styles.scss b/frontend/pages/SoftwarePage/_styles.scss
index 2704458768fb..5154d532e4fa 100644
--- a/frontend/pages/SoftwarePage/_styles.scss
+++ b/frontend/pages/SoftwarePage/_styles.scss
@@ -1,9 +1,6 @@
.software-page {
&__header-wrap {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 38px;
+ @include normalize-team-header;
.button-wrap {
display: flex;
diff --git a/frontend/pages/SoftwarePage/components/PackageForm/helpers.tsx b/frontend/pages/SoftwarePage/components/PackageForm/helpers.tsx
index b1b87262f698..8b18db4308eb 100644
--- a/frontend/pages/SoftwarePage/components/PackageForm/helpers.tsx
+++ b/frontend/pages/SoftwarePage/components/PackageForm/helpers.tsx
@@ -120,6 +120,10 @@ export const CUSTOM_TARGET_OPTIONS: IDropdownOption[] = [
},
];
+export const getInstallType = (softwarePackage: ISoftwarePackage) => {
+ return softwarePackage.automatic_install_policies ? "automatic" : "manual";
+};
+
export const getTargetType = (softwarePackage: ISoftwarePackage) => {
if (!softwarePackage) return "All hosts";
diff --git a/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx
index 2dd8c25dc114..52222a5e7e2e 100644
--- a/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx
+++ b/frontend/pages/SoftwarePage/components/SoftwareDetailsSummary/SoftwareDetailsSummary.tsx
@@ -10,6 +10,7 @@ import { QueryParams } from "utilities/url";
import ViewAllHostsLink from "components/ViewAllHostsLink";
import DataSet from "components/DataSet";
+import LastUpdatedHostCount from "components/LastUpdatedHostCount";
import SoftwareIcon from "../icons/SoftwareIcon";
@@ -19,6 +20,7 @@ interface ISoftwareDetailsSummaryProps {
title: string;
type?: string;
hosts: number;
+ countsUpdatedAt?: string;
/** The query param that will be added when user clicks on "View all hosts" link */
queryParams: QueryParams;
name?: string;
@@ -31,6 +33,7 @@ const SoftwareDetailsSummary = ({
title,
type,
hosts,
+ countsUpdatedAt,
queryParams,
name,
source,
@@ -46,7 +49,15 @@ const SoftwareDetailsSummary = ({
{!!type && }
{!!versions && }
-
+
+ }
+ />
diff --git a/frontend/pages/SoftwarePage/helpers.ts b/frontend/pages/SoftwarePage/helpers.ts
index 48901f0efc52..a323982309b5 100644
--- a/frontend/pages/SoftwarePage/helpers.ts
+++ b/frontend/pages/SoftwarePage/helpers.ts
@@ -20,8 +20,11 @@ export const generateSecretErrMsg = (err: unknown) => {
}
if (errorType === "profile") {
- return reason
- .split(":")[1]
+ // for profiles we can get two different error messages. One contains a colon
+ // and the other doesn't. We need to handle both cases.
+ const message = reason.split(":").pop() ?? "";
+
+ return message
.replace(/Secret variables?/i, "Variable")
.replace("missing from database", "doesn't exist.");
}
diff --git a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx
index fbf674e60fa4..ed375c7cc64c 100644
--- a/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx
+++ b/frontend/pages/admin/UserManagementPage/components/UsersTable/UsersTable.tsx
@@ -300,10 +300,7 @@ const UsersTable = ({ router }: IUsersTableProps): JSX.Element => {
let userUpdatedFlashMessage = `Successfully edited ${formData.name}`;
if (userData?.email !== formData.email) {
- const senderAddressMessage = config?.smtp_settings?.sender_address
- ? ` from ${config?.smtp_settings?.sender_address}`
- : "";
- userUpdatedFlashMessage += `: A confirmation email was sent${senderAddressMessage} to ${formData.email}`;
+ userUpdatedFlashMessage += `. A confirmation email was sent to ${formData.email}.`;
}
const userUpdatedEmailError =
"A user with this email address already exists";
diff --git a/frontend/pages/errors/Fleet404/_styles.scss b/frontend/pages/errors/Fleet404/_styles.scss
index 5b67d8ee7659..28eff66bfc08 100644
--- a/frontend/pages/errors/Fleet404/_styles.scss
+++ b/frontend/pages/errors/Fleet404/_styles.scss
@@ -5,7 +5,6 @@
}
button {
- margin: $pad-medium;
width: 197px;
font-size: $small;
@@ -17,6 +16,10 @@
}
&__button-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: $pad-medium;
margin-bottom: $pad-large;
}
diff --git a/frontend/pages/hosts/ManageHostsPage/_styles.scss b/frontend/pages/hosts/ManageHostsPage/_styles.scss
index 7052d86396fe..1376eb5be69a 100644
--- a/frontend/pages/hosts/ManageHostsPage/_styles.scss
+++ b/frontend/pages/hosts/ManageHostsPage/_styles.scss
@@ -1,10 +1,7 @@
.manage-hosts {
.header-wrap {
- display: flex;
- align-items: center;
- justify-content: space-between;
+ @include normalize-team-header;
margin-bottom: $pad-medium;
- height: 38px;
.button-wrap {
display: flex;
diff --git a/frontend/pages/hosts/ManageHostsPage/components/DeleteLabelModal/DeleteLabelModal.tsx b/frontend/pages/hosts/ManageHostsPage/components/DeleteLabelModal/DeleteLabelModal.tsx
index 8780026f1d1d..7bd4f7753186 100644
--- a/frontend/pages/hosts/ManageHostsPage/components/DeleteLabelModal/DeleteLabelModal.tsx
+++ b/frontend/pages/hosts/ManageHostsPage/components/DeleteLabelModal/DeleteLabelModal.tsx
@@ -26,11 +26,13 @@ const DeleteLabelModal = ({
<>
If a configuration profile uses this label as a custom target, the
- profile will break: it won't be applied to new hosts.
+ profile will break. After deleting the label, remove broken profiles
+ and upload new profiles in their place.
- To apply the profile to new hosts, you'll have to delete it and
- upload a new profile.
+ If software uses this label as a custom target, the label will not be
+ able to be deleted. Please remove the label from the software target
+ first before deleting.
{
if (hasStatusKey(error) && error.status === 422) {
return getErrorReason(error).includes("built-in")
? "Built-in labels can't be modified or deleted."
- : "Couldn't delete. Software uses this label as a custom target. Please delete the software and try again.";
+ : "Couldn't delete. Software uses this label as a custom target. Remove the label from the software target and try again.";
}
return "Could not delete label. Please try again.";
diff --git a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
index dd0d122aae58..3b2ce1cb427b 100644
--- a/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/ManagePoliciesPage.tsx
@@ -4,6 +4,7 @@ import { useQuery } from "react-query";
import { InjectedRouter } from "react-router/lib/Router";
import PATHS from "router/paths";
import { isEqual } from "lodash";
+import { formatDistanceToNowStrict } from "date-fns";
import { getNextLocationPath, wait } from "utilities/helpers";
@@ -44,6 +45,7 @@ import Spinner from "components/Spinner";
import TeamsDropdown from "components/TeamsDropdown";
import TableDataError from "components/DataError";
import MainContent from "components/MainContent";
+import LastUpdatedText from "components/LastUpdatedText";
import PoliciesTable from "./components/PoliciesTable";
import OtherWorkflowsModal from "./components/OtherWorkflowsModal";
@@ -776,22 +778,43 @@ const ManagePolicyPage = ({
}
}
- const renderPoliciesCount = (count?: number) => {
+ const renderPoliciesCountAndLastUpdated = (
+ count?: number,
+ policies?: IPolicyStats[]
+ ) => {
// Hide count if fetching count || there are errors OR there are no policy results with no a search filter
const isFetchingCount = !isAllTeamsSelected
? isFetchingTeamCountMergeInherited
: isFetchingGlobalCount;
- const hideCount =
+ const hide =
isFetchingCount ||
policiesErrors ||
(!policyResults && searchQuery === "");
- if (hideCount) {
+ if (hide) {
return null;
}
+ // Figure the time since the host counts were updated by finding first policy item with host_count_updated_at.
+ const updatedAt =
+ policies?.find((p) => !!p.host_count_updated_at)?.host_count_updated_at ||
+ "";
- return ;
+ return (
+ <>
+
+
+ Counts are updated hourly. Click host
+
+ counts for the most up-to-date count.
+ >
+ }
+ />
+ >
+ );
};
const renderMainTable = () => {
@@ -815,7 +838,12 @@ const ManagePolicyPage = ({
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
isPremiumTier={isPremiumTier}
- renderPoliciesCount={() => renderPoliciesCount(globalPoliciesCount)}
+ renderPoliciesCount={() =>
+ renderPoliciesCountAndLastUpdated(
+ globalPoliciesCount,
+ globalPolicies
+ )
+ }
searchQuery={searchQuery}
sortHeader={sortHeader}
sortDirection={sortDirection}
@@ -844,7 +872,10 @@ const ManagePolicyPage = ({
currentTeam={currentTeamSummary}
currentAutomatedPolicies={currentAutomatedPolicies}
renderPoliciesCount={() =>
- renderPoliciesCount(teamPoliciesCountMergeInherited)
+ renderPoliciesCountAndLastUpdated(
+ teamPoliciesCountMergeInherited,
+ teamPolicies
+ )
}
isPremiumTier={isPremiumTier}
searchQuery={searchQuery}
diff --git a/frontend/pages/policies/ManagePoliciesPage/_styles.scss b/frontend/pages/policies/ManagePoliciesPage/_styles.scss
index 4edf0c5432d9..632e60e819be 100644
--- a/frontend/pages/policies/ManagePoliciesPage/_styles.scss
+++ b/frontend/pages/policies/ManagePoliciesPage/_styles.scss
@@ -1,9 +1,6 @@
.manage-policies-page {
&__header-wrap {
- display: flex;
- align-items: center;
- justify-content: space-between;
- height: 38px;
+ @include normalize-team-header;
.button-wrap {
display: flex;
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PassingColumnHeader/PassingColumnHeader.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PassingColumnHeader/PassingColumnHeader.tsx
index c3910fd4c0a1..7f9f85ca3c3a 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/PassingColumnHeader/PassingColumnHeader.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/PassingColumnHeader/PassingColumnHeader.tsx
@@ -1,37 +1,20 @@
import Icon from "components/Icon";
import React from "react";
-import TooltipWrapper from "components/TooltipWrapper";
interface IPassingColumnHeaderProps {
isPassing: boolean;
- timeSinceHostCountUpdate: string;
}
const baseClass = "passing-column-header";
-const PassingColumnHeader = ({
- isPassing,
- timeSinceHostCountUpdate,
-}: IPassingColumnHeaderProps) => {
+const PassingColumnHeader = ({ isPassing }: IPassingColumnHeaderProps) => {
const iconName = isPassing ? "success" : "error";
const columnText = isPassing ? "Yes" : "No";
- const updateText = timeSinceHostCountUpdate
- ? `Host count updated ${timeSinceHostCountUpdate}.`
- : "";
return (
-
- {updateText}
- Counts are updated hourly.
- >
- }
- >
- {columnText}
-
+ {columnText}
);
};
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx
index 6db439a07392..88dc32998330 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTable.tsx
@@ -97,7 +97,6 @@ const PoliciesTable = ({
selectedTeamId: currentTeam?.id,
hasPermissionAndPoliciesToDelete,
},
- policiesList,
isPremiumTier
)}
data={generateDataSet(
diff --git a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx
index 76e785d7416d..6f09884cb92c 100644
--- a/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx
+++ b/frontend/pages/policies/ManagePoliciesPage/components/PoliciesTable/PoliciesTableConfig.tsx
@@ -2,11 +2,7 @@
// disable this rule as it was throwing an error in Header and Cell component
// definitions for the selection row for some reason when we dont really need it.
import React from "react";
-import {
- formatDistanceToNowStrict,
- millisecondsToHours,
- millisecondsToMinutes,
-} from "date-fns";
+import { millisecondsToHours, millisecondsToMinutes } from "date-fns";
import { Tooltip as ReactTooltip5 } from "react-tooltip-5";
// @ts-ignore
import Checkbox from "components/forms/fields/Checkbox";
@@ -91,27 +87,10 @@ const generateTableHeaders = (
hasPermissionAndPoliciesToDelete?: boolean;
tableType?: string;
},
- policiesList: IPolicyStats[] = [],
isPremiumTier?: boolean
): IDataColumn[] => {
const { selectedTeamId, hasPermissionAndPoliciesToDelete } = options;
const viewingTeamPolicies = selectedTeamId !== -1;
- // Figure the time since the host counts were updated.
- // First, find first policy item with host_count_updated_at.
- const updatedAt =
- policiesList.find((p) => !!p.host_count_updated_at)
- ?.host_count_updated_at || "";
- let timeSinceHostCountUpdate = "";
- if (updatedAt) {
- try {
- timeSinceHostCountUpdate = formatDistanceToNowStrict(
- new Date(updatedAt),
- { addSuffix: true }
- );
- } catch (e) {
- // Do nothing.
- }
- }
const tableHeaders: IDataColumn[] = [
{
@@ -170,12 +149,7 @@ const generateTableHeaders = (
title: "Yes",
Header: (cellProps) => (
- }
+ value={ }
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
@@ -221,12 +195,7 @@ const generateTableHeaders = (
title: "No",
Header: (cellProps) => (
- }
+ value={ }
isSortedDesc={cellProps.column.isSortedDesc}
/>
),
diff --git a/frontend/pages/queries/ManageQueriesPage/_styles.scss b/frontend/pages/queries/ManageQueriesPage/_styles.scss
index ea724a00c250..570e79406baf 100644
--- a/frontend/pages/queries/ManageQueriesPage/_styles.scss
+++ b/frontend/pages/queries/ManageQueriesPage/_styles.scss
@@ -1,9 +1,6 @@
.manage-queries-page {
&__header-wrap {
- height: 38px;
- display: flex;
- align-items: center;
- justify-content: space-between;
+ @include normalize-team-header;
}
&__header {
diff --git a/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx
index 2decb1260b79..41170b7ed4d1 100644
--- a/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx
+++ b/frontend/pages/queries/edit/components/EditQueryForm/EditQueryForm.tsx
@@ -184,6 +184,7 @@ const EditQueryForm = ({
const [isEditingDescription, setIsEditingDescription] = useState(false);
const [isSaveAsNewLoading, setIsSaveAsNewLoading] = useState(false);
const [showAdvancedOptions, setShowAdvancedOptions] = useState(false);
+ const [queryWasChanged, setQueryWasChanged] = useState(false);
const platformCompatibility = usePlatformCompatibility();
const { setCompatiblePlatforms } = platformCompatibility;
@@ -233,6 +234,7 @@ const EditQueryForm = ({
};
const onChangeQuery = (sqlString: string) => {
+ setQueryWasChanged(true);
setLastEditedQueryBody(sqlString);
};
@@ -882,7 +884,17 @@ const EditQueryForm = ({
className={`${baseClass}__run`}
variant="blue-green"
onClick={() => {
- setEditingExistingQuery(true); // Persists edited query data through live query flow
+ // calling `setEditingExistingQuery` here prevents
+ // inclusion of `query_id` in the subsequent `run` API call, which prevents counting
+ // this live run in performance impact. Since we DO want to count this run in those
+ // stats if the query is the same as the saved one, only set below IF the query
+ // has been changed.
+ // TODO - product: should host details > action > query >
+ // go to the host details page instead of the edit query page, where the user has
+ // the choice to edit the query or run it live directly?
+ if (queryWasChanged) {
+ setEditingExistingQuery(true); // Persists edited query data through live query flow
+ }
router.push(
PATHS.LIVE_QUERY(queryIdForEdit) +
TAGGED_TEMPLATES.queryByHostRoute(hostId, currentTeamId)
diff --git a/frontend/services/entities/operating_systems.ts b/frontend/services/entities/operating_systems.ts
index 2aa36227bd83..df843067cf22 100644
--- a/frontend/services/entities/operating_systems.ts
+++ b/frontend/services/entities/operating_systems.ts
@@ -48,6 +48,7 @@ export interface IGetOsVersionQueryKey extends IGetOsVersionOptions {
}
export interface IOSVersionResponse {
+ counts_updated_at?: string;
os_version: IOperatingSystemVersion;
}
diff --git a/frontend/styles/var/mixins.scss b/frontend/styles/var/mixins.scss
index d55db4c93327..08d7107d7f75 100644
--- a/frontend/styles/var/mixins.scss
+++ b/frontend/styles/var/mixins.scss
@@ -25,6 +25,14 @@ $max-width: 2560px;
}
}
+// Used to normalize styling of team header wrapper on various pages
+@mixin normalize-team-header {
+ display: flex;
+ align-items: center;
+ height: 38px;
+ justify-content: space-between;
+}
+
// Used to keep the settings description sticky under the main nav and sub nav.
// TODO: figure out how to calculate these values with variables. Will be tedious to change otherwise
@mixin sticky-settings-description {
diff --git a/handbook/demand/README.md b/handbook/demand/README.md
index 826eedeca542..1c2e145e8246 100644
--- a/handbook/demand/README.md
+++ b/handbook/demand/README.md
@@ -99,14 +99,10 @@ After an account is marked "[Research-ready](https://fleetdm.lightning.force.com
The Head of Marketing is the DRI for deploying Fleet's outward-facing content. The content schedule is settled significantly in advance to provide ample time for strategy and planning. Use the following steps to settle content strategy:
-1. Using the [content calendar](https://docs.google.com/spreadsheets/d/1KUMsb5OkAsCBQHGkGnNoj__UCPJ7Vbhk1LaEWGEARsg/edit?gid=1931288160#gid=1931288160), propose the content that Fleet will produce in the current quarter, and the strategy behind that content, including:
- - Content type and title (e.g. "Article: Fleet takes bacon to new heights with flying pigs release").
- - Create date: The date by which the DRI will start crafting the content.
+1. Using the [content calendar](https://docs.google.com/spreadsheets/d/1Hso0LxqwrRVINCyW_n436bNHmoqhoLhC8bcbvLPOs9A/edit?gid=809357993#gid=809357993), propose the content that Fleet will produce in the current quarter, and the strategy behind that content, including:
- Release date: The date by which the content will be complete and finalized.
- - Primary buying situation: The intended audience.
- DRI: Person(s) responsible for the project management of this content.
- - Author: Person(s) responsible for the creation of this content.
- - Related event?: Related community or Fleet event, if any.
+ - Title (e.g. "Article: Fleet takes bacon to new heights with flying pigs release").
2. Attend a 30m meeting with Fleet's Client Platform Engineer & Community Advocate, CTO, and CEO to review and settle the proposed content.
diff --git a/handbook/product-design/product-design.rituals.yml b/handbook/product-design/product-design.rituals.yml
index f3ea2b9602f7..992deabd93a4 100644
--- a/handbook/product-design/product-design.rituals.yml
+++ b/handbook/product-design/product-design.rituals.yml
@@ -44,7 +44,7 @@
task: "Quarterly roadmap blog post"
startedOn: "2024-11-05"
frequency: "Monthly"
- description: "Every quarter, Head of Product Design (HPD) writes a short blog post with the theme and an embedded YouTube video in which HPD walks through this."
+ description: "Every quarter, Head of Product Design (HPD) meets with the CEO to run through the current quarter objectives and the 3 biggest open opportunities in the product and the solutions we're envisioning for next quarter. Then, the HPD writes a short blog with the objectives, opportunities, and an embedded YouTube video in which HPD walks through this."
moreInfoUrl: ""
dri: "noahtalerman"
autoIssue:
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/.terraform.lock.hcl b/infrastructure/dogfood/terraform/aws-tf-module/.terraform.lock.hcl
index caa67b71decb..0460a0d425a4 100644
--- a/infrastructure/dogfood/terraform/aws-tf-module/.terraform.lock.hcl
+++ b/infrastructure/dogfood/terraform/aws-tf-module/.terraform.lock.hcl
@@ -2,149 +2,142 @@
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/archive" {
- version = "2.4.0"
+ version = "2.7.0"
hashes = [
- "h1:EtN1lnoHoov3rASpgGmh6zZ/W6aRCTgKC7iMwvFY1yc=",
- "h1:cJokkjeH1jfpG4QEHdRx0t2j8rr52H33A7C/oX73Ok4=",
- "zh:18e408596dd53048f7fc8229098d0e3ad940b92036a24287eff63e2caec72594",
- "zh:392d4216ecd1a1fd933d23f4486b642a8480f934c13e2cae3c13b6b6a7e34a7b",
- "zh:655dd1fa5ca753a4ace21d0de3792d96fff429445717f2ce31c125d19c38f3ff",
- "zh:70dae36c176aa2b258331ad366a471176417a94dd3b4985a911b8be9ff842b00",
+ "h1:1niS9AcwxN8CrWemnJS2Xf6vM72+48Xh3xFSS3DFWQo=",
+ "zh:04e23bebca7f665a19a032343aeecd230028a3822e546e6f618f24c47ff87f67",
+ "zh:5bb38114238e25c45bf85f5c9f627a2d0c4b98fe44a0837e37d48574385f8dad",
+ "zh:64584bc1db4c390abd81c76de438d93acf967c8a33e9b923d68da6ed749d55bd",
+ "zh:697695ab9cce351adf91a1823bdd72ce6f0d219138f5124ef7645cedf8f59a1f",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:7d8c8e3925f1e21daf73f85983894fbe8868e326910e6df3720265bc657b9c9c",
- "zh:a032ec0f0aee27a789726e348e8ad20778c3a1c9190ef25e7cff602c8d175f44",
- "zh:b8e50de62ba185745b0fe9713755079ad0e9f7ac8638d204de6762cc36870410",
- "zh:c8ad0c7697a3d444df21ff97f3473a8604c8639be64afe3f31b8ec7ad7571e18",
- "zh:df736c5a2a7c3a82c5493665f659437a22f0baf8c2d157e45f4dd7ca40e739fc",
- "zh:e8ffbf578a0977074f6d08aa8734e36c726e53dc79894cfc4f25fadc4f45f1df",
- "zh:efea57ff23b141551f92b2699024d356c7ffd1a4ad62931da7ed7a386aef7f1f",
+ "zh:7edefb1d1e2fead8fd155f7b50a2cb49f2f3fed154ac3ef5f991ccaff93d6120",
+ "zh:807fb15b75910bf14795f2ad1a2d41b069f9ef52c242131b2964c8527312e235",
+ "zh:821d9148d261df1d1a8e5a4812df2a6a3ffaf0d2070dad3c785382e489069239",
+ "zh:a7d92251118fb723048c482154a6ac6368aad583d28d15fffc6f5dafd9507463",
+ "zh:b627d4cef192b3c12ddaf9cb2c4f98c10d0129883c8c2a9c0049983f9de7030d",
+ "zh:dfb70306fcc0ad1d512ab7c24765703783cc286062d4849de4fbe23526f5dc8e",
+ "zh:f21de276f857b7e51fa2593d8fef05a7faafb0a7b62db14ac58a03ce1be7d881",
]
}
provider "registry.terraform.io/hashicorp/aws" {
- version = "5.26.0"
+ version = "5.82.2"
constraints = ">= 2.67.0, >= 3.0.0, >= 4.6.0, >= 4.8.0, >= 4.9.0, >= 4.18.0, >= 4.27.0, >= 4.30.0, >= 4.40.0, >= 5.0.0, ~> 5.0"
hashes = [
- "h1:McIRw8larBNW5TeXxyiWd8fD55oj1szEbMOuSQOSDBs=",
- "h1:UkBMGEScvNP+9JDzKXGrgj931LngYpIB8TBBUY+mvdg=",
- "zh:11a4062491e574c8e96b6bc7ced67b5e9338ccfa068223fc9042f9e1e7eda47a",
- "zh:4331f85aeb22223ab656d04b48337a033f44f02f685c8def604c4f8f4687d10f",
- "zh:915d6c996390736709f7ac7582cd41418463cfc07696218af6fea4a282df744a",
- "zh:9306c306dbb2e1597037c54d20b1bd5f22a9cdcdb2b2b7bad657c8230bea2298",
- "zh:93371860b9df369243219606711bfd3cfbd263db67838c06d5d5848cf47b6ede",
- "zh:98338c17764a7b9322ddb6efd3af84e6890a4a0687f846eefdfb0fa03cec892d",
+ "h1:ce6Dw2y4PpuqAPtnQ0dO270dRTmwEARqnfffrE1VYJ8=",
+ "zh:0262fc96012fb7e173e1b7beadd46dfc25b1dc7eaef95b90e936fc454724f1c8",
+ "zh:397413613d27f4f54d16efcbf4f0a43c059bd8d827fe34287522ae182a992f9b",
+ "zh:436c0c5d56e1da4f0a4c13129e12a0b519d12ab116aed52029b183f9806866f3",
+ "zh:4d942d173a2553d8d532a333a0482a090f4e82a2238acf135578f163b6e68470",
+ "zh:624aebc549bfbce06cc2ecfd8631932eb874ac7c10eb8466ce5b9a2fbdfdc724",
"zh:9b12af85486a96aedd8d7984b0ff811a4b42e3d88dad1a3fb4c0b580d04fa425",
- "zh:a28c9d77a5be25bac42d99418365757e4eb65a2c7c6788828263772cf2774869",
- "zh:bd9c4648a090622d6b8c3c91dad513eec81e54db3dfe940ab6d155e5f37735e5",
- "zh:bde63db136cccdeb282489e2ec2b3f9a7566edc9df27911a296352ab00832261",
- "zh:ccd33f9490ce3f2d89efab995abf3b30e75579585f6a8a5b1f756246903d3518",
- "zh:d73d1c461eb9d22833251f6533fc214cf014bc1d3165c5bfaa8ca29cd295ffb2",
- "zh:db4ffb7eec5d0e1d0dbd0d65e1a3eaa6173a3337058105aec41fd0b2af5a2b46",
- "zh:eb36b933419e9f6563330f3b7d53d4f1b09e62d27f7786d5dc6c4a2d0f6de182",
- "zh:ec85ce1976e43f7d7fa10fa191c0a85e97326a3cb22387c0ed8b74d426ec94fd",
+ "zh:9e632dee2dfdf01b371cca7854b1ec63ceefa75790e619b0642b34d5514c6733",
+ "zh:a07567acb115b60a3df8f6048d12735b9b3bcf85ec92a62f77852e13d5a3c096",
+ "zh:ab7002df1a1be6432ac0eb1b9f6f0dd3db90973cd5b1b0b33d2dae54553dfbd7",
+ "zh:bc1ff65e2016b018b3e84db7249b2cd0433cb5c81dc81f9f6158f2197d6b9fde",
+ "zh:bcad84b1d767f87af6e1ba3dc97fdb8f2ad5de9224f192f1412b09aba798c0a8",
+ "zh:cf917dceaa0f9d55d9ff181b5dcc4d1e10af21b6671811b315ae2a6eda866a2a",
+ "zh:d8e90ecfb3216f3cc13ccde5a16da64307abb6e22453aed2ac3067bbf689313b",
+ "zh:d9054e0e40705df729682ad34c20db8695d57f182c65963abd151c6aba1ab0d3",
+ "zh:ecf3a4f3c57eb7e89f71b8559e2a71e4cdf94eea0118ec4f2cb37e4f4d71a069",
]
}
provider "registry.terraform.io/hashicorp/external" {
- version = "2.3.2"
+ version = "2.3.4"
constraints = ">= 1.0.0"
hashes = [
- "h1:7F6FVQh7OcCgIH3YEJg1SJDSb1CU4qrCtGuI2EBHnL8=",
- "h1:cy50n4q+Ir4GYppAfuYhQbBJVxMZbJUlIvM6FVK2axs=",
- "zh:020bf652739ecd841d696e6c1b85ce7dd803e9177136df8fb03aa08b87365389",
- "zh:0c7ea5a1cbf2e01a8627b8a84df69c93683f39fe947b288e958e72b9d12a827f",
- "zh:25a68604c7d6aa736d6e99225051279eaac3a7cf4cab33b00ff7eae7096166f6",
- "zh:34f46d82ca34604f6522de3b36eda19b7ad3be1e38947afc6ac31656eab58c8a",
- "zh:6959f8f2f3de93e61e0abb90dbec41e28a66daec1607c46f43976bd6da50bcfd",
+ "h1:cCabxnWQ5fX1lS7ZqgUzsvWmKZw9FA7NRxAZ94vcTcc=",
+ "zh:037fd82cd86227359bc010672cd174235e2d337601d4686f526d0f53c87447cb",
+ "zh:0ea1db63d6173d01f2fa8eb8989f0809a55135a0d8d424b08ba5dabad73095fa",
+ "zh:17a4d0a306566f2e45778fbac48744b6fd9c958aaa359e79f144c6358cb93af0",
+ "zh:298e5408ab17fd2e90d2cd6d406c6d02344fe610de5b7dae943a58b958e76691",
+ "zh:38ecfd29ee0785fd93164812dcbe0664ebbe5417473f3b2658087ca5a0286ecb",
+ "zh:59f6a6f31acf66f4ea3667a555a70eba5d406c6e6d93c2c641b81d63261eeace",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:a81e5d65a343da9caa6f1d17ae0aced9faecb36b4f8554bd445dbd4f8be21ab6",
- "zh:b1d3f1557214d652c9120862ce27e9a7b61cb5aec5537a28240a5a37bf0b1413",
- "zh:b71588d006471ae2d4a7eca2c51d69fd7c5dec9b088315599b794e2ad0cc5e90",
- "zh:cfdaae4028b644dff3530c77b49d31f7e6f4c4e2a9e5c8ac6a88e383c80c9e9c",
- "zh:dbde15154c2eb38a5f54d0e7646bc67510004179696f3cc2bc1d877cecacf83b",
- "zh:fb681b363f83fb5f64dfa6afbf32d100d0facd2a766cf3493b8ddb0398e1b0f7",
+ "zh:ad0279dfd09d713db0c18469f585e58d04748ca72d9ada83883492e0dd13bd58",
+ "zh:c69f66fd21f5e2c8ecf7ca68d9091c40f19ad913aef21e3ce23836e91b8cbb5f",
+ "zh:d4a56f8c48aa86fc8e0c233d56850f5783f322d6336f3bf1916e293246b6b5d4",
+ "zh:f2b394ebd4af33f343835517e80fc876f79361f4688220833bc3c77655dd2202",
+ "zh:f31982f29f12834e5d21e010856eddd19d59cd8f449adf470655bfd19354377e",
]
}
provider "registry.terraform.io/hashicorp/local" {
- version = "2.4.0"
+ version = "2.5.2"
constraints = ">= 1.0.0"
hashes = [
- "h1:R97FTYETo88sT2VHfMgkPU3lzCsZLunPftjSI5vfKe8=",
- "h1:ZUEYUmm2t4vxwzxy1BvN1wL6SDWrDxfH7pxtzX8c6d0=",
- "zh:53604cd29cb92538668fe09565c739358dc53ca56f9f11312b9d7de81e48fab9",
- "zh:66a46e9c508716a1c98efbf793092f03d50049fa4a83cd6b2251e9a06aca2acf",
- "zh:70a6f6a852dd83768d0778ce9817d81d4b3f073fab8fa570bff92dcb0824f732",
+ "h1:IyFbOIO6mhikFNL/2h1iZJ6kyN3U00jgkpCLUCThAfE=",
+ "zh:136299545178ce281c56f36965bf91c35407c11897f7082b3b983d86cb79b511",
+ "zh:3b4486858aa9cb8163378722b642c57c529b6c64bfbfc9461d940a84cd66ebea",
+ "zh:4855ee628ead847741aa4f4fc9bed50cfdbf197f2912775dd9fe7bc43fa077c0",
+ "zh:4b8cd2583d1edcac4011caafe8afb7a95e8110a607a1d5fb87d921178074a69b",
+ "zh:52084ddaff8c8cd3f9e7bcb7ce4dc1eab00602912c96da43c29b4762dc376038",
+ "zh:71562d330d3f92d79b2952ffdda0dad167e952e46200c767dd30c6af8d7c0ed3",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:82a803f2f484c8b766e2e9c32343e9c89b91997b9f8d2697f9f3837f62926b35",
- "zh:9708a4e40d6cc4b8afd1352e5186e6e1502f6ae599867c120967aebe9d90ed04",
- "zh:973f65ce0d67c585f4ec250c1e634c9b22d9c4288b484ee2a871d7fa1e317406",
- "zh:c8fa0f98f9316e4cfef082aa9b785ba16e36ff754d6aba8b456dab9500e671c6",
- "zh:cfa5342a5f5188b20db246c73ac823918c189468e1382cb3c48a9c0c08fc5bf7",
- "zh:e0e2b477c7e899c63b06b38cd8684a893d834d6d0b5e9b033cedc06dd7ffe9e2",
- "zh:f62d7d05ea1ee566f732505200ab38d94315a4add27947a60afa29860822d3fc",
- "zh:fa7ce69dde358e172bd719014ad637634bbdabc49363104f4fca759b4b73f2ce",
+ "zh:805f81ade06ff68fa8b908d31892eaed5c180ae031c77ad35f82cb7a74b97cf4",
+ "zh:8b6b3ebeaaa8e38dd04e56996abe80db9be6f4c1df75ac3cccc77642899bd464",
+ "zh:ad07750576b99248037b897de71113cc19b1a8d0bc235eb99173cc83d0de3b1b",
+ "zh:b9f1c3bfadb74068f5c205292badb0661e17ac05eb23bfe8bd809691e4583d0e",
+ "zh:cc4cbcd67414fefb111c1bf7ab0bc4beb8c0b553d01719ad17de9a047adff4d1",
]
}
provider "registry.terraform.io/hashicorp/null" {
- version = "3.2.2"
+ version = "3.2.3"
constraints = ">= 2.0.0"
hashes = [
- "h1:IMVAUHKoydFrlPrl9OzasDnw/8ntZFerCC9iXw1rXQY=",
- "h1:zT1ZbegaAYHwQa+QwIFugArWikRJI9dqohj8xb0GY88=",
- "zh:3248aae6a2198f3ec8394218d05bd5e42be59f43a3a7c0b71c66ec0df08b69e7",
- "zh:32b1aaa1c3013d33c245493f4a65465eab9436b454d250102729321a44c8ab9a",
- "zh:38eff7e470acb48f66380a73a5c7cdd76cc9b9c9ba9a7249c7991488abe22fe3",
- "zh:4c2f1faee67af104f5f9e711c4574ff4d298afaa8a420680b0cb55d7bbc65606",
- "zh:544b33b757c0b954dbb87db83a5ad921edd61f02f1dc86c6186a5ea86465b546",
- "zh:696cf785090e1e8cf1587499516b0494f47413b43cb99877ad97f5d0de3dc539",
- "zh:6e301f34757b5d265ae44467d95306d61bef5e41930be1365f5a8dcf80f59452",
+ "h1:I0Um8UkrMUb81Fxq/dxbr3HLP2cecTH2WMJiwKSrwQY=",
+ "zh:22d062e5278d872fe7aed834f5577ba0a5afe34a3bdac2b81f828d8d3e6706d2",
+ "zh:23dead00493ad863729495dc212fd6c29b8293e707b055ce5ba21ee453ce552d",
+ "zh:28299accf21763ca1ca144d8f660688d7c2ad0b105b7202554ca60b02a3856d3",
+ "zh:55c9e8a9ac25a7652df8c51a8a9a422bd67d784061b1de2dc9fe6c3cb4e77f2f",
+ "zh:756586535d11698a216291c06b9ed8a5cc6a4ec43eee1ee09ecd5c6a9e297ac1",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:913a929070c819e59e94bb37a2a253c228f83921136ff4a7aa1a178c7cce5422",
- "zh:aa9015926cd152425dbf86d1abdbc74bfe0e1ba3d26b3db35051d7b9ca9f72ae",
- "zh:bb04798b016e1e1d49bcc76d62c53b56c88c63d6f2dfe38821afef17c416a0e1",
- "zh:c23084e1b23577de22603cff752e59128d83cfecc2e6819edadd8cf7a10af11e",
+ "zh:9d5eea62fdb587eeb96a8c4d782459f4e6b73baeece4d04b4a40e44faaee9301",
+ "zh:a6355f596a3fb8fc85c2fb054ab14e722991533f87f928e7169a486462c74670",
+ "zh:b5a65a789cff4ada58a5baffc76cb9767dc26ec6b45c00d2ec8b1b027f6db4ed",
+ "zh:db5ab669cf11d0e9f81dc380a6fdfcac437aea3d69109c7aef1a5426639d2d65",
+ "zh:de655d251c470197bcbb5ac45d289595295acb8f829f6c781d4a75c8c8b7c7dd",
+ "zh:f5c68199f2e6076bce92a12230434782bf768103a427e9bb9abee99b116af7b5",
]
}
provider "registry.terraform.io/hashicorp/random" {
- version = "3.5.1"
+ version = "3.6.3"
constraints = ">= 2.2.0"
hashes = [
- "h1:IL9mSatmwov+e0+++YX2V6uel+dV6bn+fC/cnGDK3Ck=",
- "h1:VSnd9ZIPyfKHOObuQCaKfnjIHRtR7qTw19Rz8tJxm+k=",
- "zh:04e3fbd610cb52c1017d282531364b9c53ef72b6bc533acb2a90671957324a64",
- "zh:119197103301ebaf7efb91df8f0b6e0dd31e6ff943d231af35ee1831c599188d",
- "zh:4d2b219d09abf3b1bb4df93d399ed156cadd61f44ad3baf5cf2954df2fba0831",
- "zh:6130bdde527587bbe2dcaa7150363e96dbc5250ea20154176d82bc69df5d4ce3",
- "zh:6cc326cd4000f724d3086ee05587e7710f032f94fc9af35e96a386a1c6f2214f",
+ "h1:zG9uFP8l9u+yGZZvi5Te7PV62j50azpgwPunq2vTm1E=",
+ "zh:04ceb65210251339f07cd4611885d242cd4d0c7306e86dda9785396807c00451",
+ "zh:448f56199f3e99ff75d5c0afacae867ee795e4dfda6cb5f8e3b2a72ec3583dd8",
+ "zh:4b4c11ccfba7319e901df2dac836b1ae8f12185e37249e8d870ee10bb87a13fe",
+ "zh:4fa45c44c0de582c2edb8a2e054f55124520c16a39b2dfc0355929063b6395b1",
+ "zh:588508280501a06259e023b0695f6a18149a3816d259655c424d068982cbdd36",
+ "zh:737c4d99a87d2a4d1ac0a54a73d2cb62974ccb2edbd234f333abd079a32ebc9e",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:b6d88e1d28cf2dfa24e9fdcc3efc77adcdc1c3c3b5c7ce503a423efbdd6de57b",
- "zh:ba74c592622ecbcef9dc2a4d81ed321c4e44cddf7da799faa324da9bf52a22b2",
- "zh:c7c5cde98fe4ef1143bd1b3ec5dc04baf0d4cc3ca2c5c7d40d17c0e9b2076865",
- "zh:dac4bad52c940cd0dfc27893507c1e92393846b024c5a9db159a93c534a3da03",
- "zh:de8febe2a2acd9ac454b844a4106ed295ae9520ef54dc8ed2faf29f12716b602",
- "zh:eab0d0495e7e711cca367f7d4df6e322e6c562fc52151ec931176115b83ed014",
+ "zh:a357ab512e5ebc6d1fda1382503109766e21bbfdfaa9ccda43d313c122069b30",
+ "zh:c51bfb15e7d52cc1a2eaec2a903ac2aff15d162c172b1b4c17675190e8147615",
+ "zh:e0951ee6fa9df90433728b96381fb867e3db98f66f735e0c3e24f8f16903f0ad",
+ "zh:e3cdcb4e73740621dabd82ee6a37d6cfce7fee2a03d8074df65086760f5cf556",
+ "zh:eff58323099f1bd9a0bec7cb04f717e7f1b2774c7d612bf7581797e1622613a0",
]
}
provider "registry.terraform.io/hashicorp/tls" {
- version = "4.0.4"
+ version = "4.0.6"
hashes = [
- "h1:GZcFizg5ZT2VrpwvxGBHQ/hO9r6g0vYdQqx3bFD3anY=",
- "h1:pe9vq86dZZKCm+8k1RhzARwENslF3SXb9ErHbQfgjXU=",
- "zh:23671ed83e1fcf79745534841e10291bbf34046b27d6e68a5d0aab77206f4a55",
- "zh:45292421211ffd9e8e3eb3655677700e3c5047f71d8f7650d2ce30242335f848",
- "zh:59fedb519f4433c0fdb1d58b27c210b27415fddd0cd73c5312530b4309c088be",
- "zh:5a8eec2409a9ff7cd0758a9d818c74bcba92a240e6c5e54b99df68fff312bbd5",
- "zh:5e6a4b39f3171f53292ab88058a59e64825f2b842760a4869e64dc1dc093d1fe",
- "zh:810547d0bf9311d21c81cc306126d3547e7bd3f194fc295836acf164b9f8424e",
- "zh:824a5f3617624243bed0259d7dd37d76017097dc3193dac669be342b90b2ab48",
- "zh:9361ccc7048be5dcbc2fafe2d8216939765b3160bd52734f7a9fd917a39ecbd8",
- "zh:aa02ea625aaf672e649296bce7580f62d724268189fe9ad7c1b36bb0fa12fa60",
- "zh:c71b4cd40d6ec7815dfeefd57d88bc592c0c42f5e5858dcc88245d371b4b8b1e",
- "zh:dabcd52f36b43d250a3d71ad7abfa07b5622c69068d989e60b79b2bb4f220316",
+ "h1:n3M50qfWfRSpQV9Pwcvuse03pEizqrmYEryxKky4so4=",
+ "zh:10de0d8af02f2e578101688fd334da3849f56ea91b0d9bd5b1f7a243417fdda8",
+ "zh:37fc01f8b2bc9d5b055dc3e78bfd1beb7c42cfb776a4c81106e19c8911366297",
+ "zh:4578ca03d1dd0b7f572d96bd03f744be24c726bfd282173d54b100fd221608bb",
+ "zh:6c475491d1250050765a91a493ef330adc24689e8837a0f07da5a0e1269e11c1",
+ "zh:81bde94d53cdababa5b376bbc6947668be4c45ab655de7aa2e8e4736dfd52509",
+ "zh:abdce260840b7b050c4e401d4f75c7a199fafe58a8b213947a258f75ac18b3e8",
+ "zh:b754cebfc5184873840f16a642a7c9ef78c34dc246a8ae29e056c79939963c7a",
+ "zh:c928b66086078f9917aef0eec15982f2e337914c5c4dbc31dd4741403db7eb18",
+ "zh:cded27bee5f24de6f2ee0cfd1df46a7f88e84aaffc2ecbf3ff7094160f193d50",
+ "zh:d65eb3867e8f69aaf1b8bb53bd637c99c6b649ba3db16ded50fa9a01076d1a27",
+ "zh:ecb0c8b528c7a619fa71852bb3fb5c151d47576c5aab2bf3af4db52588722eeb",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
diff --git a/infrastructure/dogfood/terraform/aws-tf-module/main.tf b/infrastructure/dogfood/terraform/aws-tf-module/main.tf
index 2e717e692bdd..1cdb6b8c345a 100644
--- a/infrastructure/dogfood/terraform/aws-tf-module/main.tf
+++ b/infrastructure/dogfood/terraform/aws-tf-module/main.tf
@@ -369,17 +369,33 @@ module "osquery-carve" {
}
module "monitoring" {
- source = "github.com/fleetdm/fleet//terraform/addons/monitoring?ref=tf-mod-addon-monitoring-v1.1.3"
- customer_prefix = local.customer
- fleet_ecs_service_name = module.main.byo-vpc.byo-db.byo-ecs.service.name
- fleet_min_containers = module.main.byo-vpc.byo-db.byo-ecs.service.desired_count
- alb_name = module.main.byo-vpc.byo-db.alb.lb_dns_name
- alb_target_group_name = module.main.byo-vpc.byo-db.alb.target_group_names[0]
- alb_target_group_arn_suffix = module.main.byo-vpc.byo-db.alb.target_group_arn_suffixes[0]
- alb_arn_suffix = module.main.byo-vpc.byo-db.alb.lb_arn_suffix
+ source = "github.com/fleetdm/fleet//terraform/addons/monitoring?ref=tf-mod-addon-monitoring-v1.5.0"
+ customer_prefix = local.customer
+ fleet_ecs_service_name = module.main.byo-vpc.byo-db.byo-ecs.service.name
+ albs = [
+ {
+ name = module.main.byo-vpc.byo-db.alb.lb_dns_name,
+ target_group_name = module.main.byo-vpc.byo-db.alb.target_group_names[0]
+ target_group_arn_suffix = module.main.byo-vpc.byo-db.alb.target_group_arn_suffixes[0]
+ arn_suffix = module.main.byo-vpc.byo-db.alb.lb_arn_suffix
+ ecs_service_name = module.main.byo-vpc.byo-db.byo-ecs.service.name
+ min_containers = module.main.byo-vpc.byo-db.byo-ecs.appautoscaling_target.min_capacity
+ alert_thresholds = {
+ HTTPCode_ELB_5XX_Count = {
+ period = 3600
+ threshold = 2
+ },
+ HTTPCode_Target_5XX_Count = {
+ period = 120
+ threshold = 0
+ }
+ }
+ }
+ ]
sns_topic_arns_map = {
- alb_httpcode_5xx = [module.notify_slack.slack_topic_arn]
- cron_monitoring = [module.notify_slack.slack_topic_arn]
+ alb_httpcode_5xx = [module.notify_slack.slack_topic_arn]
+ cron_monitoring = [module.notify_slack.slack_topic_arn]
+ cron_job_failure_monitoring = [module.notify_slack_p2.slack_topic_arn]
}
mysql_cluster_members = module.main.byo-vpc.rds.cluster_members
# The cloudposse module seems to have a nested list here.
@@ -452,7 +468,11 @@ resource "aws_kms_key" "ecr" {
enable_key_rotation = true
}
-variable "slack_webhook" {
+variable "slack_p1_webhook" {
+ type = string
+}
+
+variable "slack_p2_webhook" {
type = string
}
@@ -460,13 +480,25 @@ module "notify_slack" {
source = "terraform-aws-modules/notify-slack/aws"
version = "5.5.0"
- sns_topic_name = "fleet-dogfood"
+ sns_topic_name = "fleet-dogfood-p1-alerts"
- slack_webhook_url = var.slack_webhook
+ slack_webhook_url = var.slack_p1_webhook
slack_channel = "#help-p1"
slack_username = "monitoring"
}
+module "notify_slack_p2" {
+ source = "terraform-aws-modules/notify-slack/aws"
+ version = "5.5.0"
+
+ lambda_function_name = "notify_slack_p2"
+ sns_topic_name = "fleet-dogfood-p2-alerts"
+
+ slack_webhook_url = var.slack_p2_webhook
+ slack_channel = "#help-p2"
+ slack_username = "monitoring"
+}
+
module "ses" {
source = "github.com/fleetdm/fleet//terraform/addons/ses?ref=tf-mod-addon-ses-v1.0.0"
zone_id = aws_route53_zone.main.zone_id
diff --git a/it-and-security/teams/workstations-canary.yml b/it-and-security/teams/workstations-canary.yml
index 58e6b1067ccc..fea33ba1fdbd 100644
--- a/it-and-security/teams/workstations-canary.yml
+++ b/it-and-security/teams/workstations-canary.yml
@@ -96,7 +96,7 @@ controls:
enable_end_user_authentication: false
macos_setup_assistant: null
macos_updates:
- deadline: "2025-01-03"
+ deadline: "2025-01-07"
minimum_version: "15.2"
windows_settings:
custom_settings:
diff --git a/it-and-security/teams/workstations.yml b/it-and-security/teams/workstations.yml
index cc6d9361ed43..e327ae748bcf 100644
--- a/it-and-security/teams/workstations.yml
+++ b/it-and-security/teams/workstations.yml
@@ -66,7 +66,7 @@ controls:
- package_path: ../lib/macos/software/zoom.yml # Zoom for macOS
- app_store_id: '803453959' # Slack Desktop
macos_updates:
- deadline: "2025-01-03"
+ deadline: "2025-01-07"
minimum_version: "15.2"
windows_settings:
custom_settings: null
diff --git a/server/config/config.go b/server/config/config.go
index bdfe0898dcf9..cdb932bd5fff 100644
--- a/server/config/config.go
+++ b/server/config/config.go
@@ -316,16 +316,48 @@ type S3Config struct {
CarvesDisableSSL bool `yaml:"carves_disable_ssl"`
CarvesForceS3PathStyle bool `yaml:"carves_force_s3_path_style"`
- SoftwareInstallersBucket string `yaml:"software_installers_bucket"`
- SoftwareInstallersPrefix string `yaml:"software_installers_prefix"`
- SoftwareInstallersRegion string `yaml:"software_installers_region"`
- SoftwareInstallersEndpointURL string `yaml:"software_installers_endpoint_url"`
- SoftwareInstallersAccessKeyID string `yaml:"software_installers_access_key_id"`
- SoftwareInstallersSecretAccessKey string `yaml:"software_installers_secret_access_key"`
- SoftwareInstallersStsAssumeRoleArn string `yaml:"software_installers_sts_assume_role_arn"`
- SoftwareInstallersStsExternalID string `yaml:"software_installers_sts_external_id"`
- SoftwareInstallersDisableSSL bool `yaml:"software_installers_disable_ssl"`
- SoftwareInstallersForceS3PathStyle bool `yaml:"software_installers_force_s3_path_style"`
+ SoftwareInstallersBucket string `yaml:"software_installers_bucket"`
+ SoftwareInstallersPrefix string `yaml:"software_installers_prefix"`
+ SoftwareInstallersRegion string `yaml:"software_installers_region"`
+ SoftwareInstallersEndpointURL string `yaml:"software_installers_endpoint_url"`
+ SoftwareInstallersAccessKeyID string `yaml:"software_installers_access_key_id"`
+ SoftwareInstallersSecretAccessKey string `yaml:"software_installers_secret_access_key"`
+ SoftwareInstallersStsAssumeRoleArn string `yaml:"software_installers_sts_assume_role_arn"`
+ SoftwareInstallersStsExternalID string `yaml:"software_installers_sts_external_id"`
+ SoftwareInstallersDisableSSL bool `yaml:"software_installers_disable_ssl"`
+ SoftwareInstallersForceS3PathStyle bool `yaml:"software_installers_force_s3_path_style"`
+ SoftwareInstallersCloudfrontURL string `yaml:"software_installers_cloudfront_url"`
+ SoftwareInstallersCloudfrontURLSigningPublicKeyID string `yaml:"software_installers_cloudfront_url_signing_public_key_id"`
+ SoftwareInstallersCloudfrontURLSigningPrivateKey string `yaml:"software_installers_cloudfront_url_signing_private_key"`
+}
+
+func (s S3Config) ValidateCloudfrontURL(initFatal func(err error, msg string)) {
+ if s.SoftwareInstallersCloudfrontURL != "" {
+ cloudfrontURL, err := url.Parse(s.SoftwareInstallersCloudfrontURL)
+ if err != nil {
+ initFatal(err, "S3 software installers cloudfront URL")
+ return
+ }
+ if cloudfrontURL.Scheme != "https" {
+ initFatal(errors.New("cloudfront url scheme must be https"), "S3 software installers cloudfront URL")
+ return
+ }
+ if s.SoftwareInstallersCloudfrontURLSigningPrivateKey != "" && s.SoftwareInstallersCloudfrontURLSigningPublicKeyID == "" ||
+ s.SoftwareInstallersCloudfrontURLSigningPrivateKey == "" && s.SoftwareInstallersCloudfrontURLSigningPublicKeyID != "" {
+ initFatal(errors.New("Couldn't configure. Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set for URL signing."),
+ "S3 software installers cloudfront URL")
+ return
+ }
+ if s.SoftwareInstallersCloudfrontURLSigningPrivateKey == "" && s.SoftwareInstallersCloudfrontURLSigningPublicKeyID == "" {
+ initFatal(errors.New("Couldn't configure. Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set when CloudFront distribution URL is set."),
+ "S3 software installers cloudfront URL")
+ return
+ }
+ } else if s.SoftwareInstallersCloudfrontURLSigningPrivateKey != "" || s.SoftwareInstallersCloudfrontURLSigningPublicKeyID != "" {
+ initFatal(errors.New("Couldn't configure. `s3_software_installers_cloudfront_url` must be set to use `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key`."),
+ "S3 software installers cloudfront URL")
+ return
+ }
}
func (s S3Config) BucketsAndPrefixesMatch() bool {
@@ -1197,6 +1229,9 @@ func (man Manager) addConfigs() {
man.addConfigString("s3.software_installers_sts_external_id", "", "Optional unique identifier that can be used by the principal assuming the role to assert its identity.")
man.addConfigBool("s3.software_installers_disable_ssl", false, "Disable SSL (typically for local testing)")
man.addConfigBool("s3.software_installers_force_s3_path_style", false, "Set this to true to force path-style addressing, i.e., `http://s3.amazonaws.com/BUCKET/KEY`")
+ man.addConfigString("s3.software_installers_cloudfront_url", "", "CloudFront URL for software installers")
+ man.addConfigString("s3.software_installers_cloudfront_url_signing_public_key_id", "", "CloudFront public key ID for URL signing")
+ man.addConfigString("s3.software_installers_cloudfront_url_signing_private_key", "", "CloudFront private key for URL signing")
// PubSub
man.addConfigString("pubsub.project", "", "Google Cloud Project to use")
@@ -1622,16 +1657,19 @@ func (man Manager) loadS3Config() S3Config {
DisableSSL: man.getConfigBool("s3.disable_ssl"),
ForceS3PathStyle: man.getConfigBool("s3.force_s3_path_style"),
- SoftwareInstallersBucket: man.getConfigString("s3.software_installers_bucket"),
- SoftwareInstallersPrefix: man.getConfigString("s3.software_installers_prefix"),
- SoftwareInstallersRegion: man.getConfigString("s3.software_installers_region"),
- SoftwareInstallersEndpointURL: man.getConfigString("s3.software_installers_endpoint_url"),
- SoftwareInstallersAccessKeyID: man.getConfigString("s3.software_installers_access_key_id"),
- SoftwareInstallersSecretAccessKey: man.getConfigString("s3.software_installers_secret_access_key"),
- SoftwareInstallersStsAssumeRoleArn: man.getConfigString("s3.software_installers_sts_assume_role_arn"),
- SoftwareInstallersStsExternalID: man.getConfigString("s3.software_installers_sts_external_id"),
- SoftwareInstallersDisableSSL: man.getConfigBool("s3.software_installers_disable_ssl"),
- SoftwareInstallersForceS3PathStyle: man.getConfigBool("s3.software_installers_force_s3_path_style"),
+ SoftwareInstallersBucket: man.getConfigString("s3.software_installers_bucket"),
+ SoftwareInstallersPrefix: man.getConfigString("s3.software_installers_prefix"),
+ SoftwareInstallersRegion: man.getConfigString("s3.software_installers_region"),
+ SoftwareInstallersEndpointURL: man.getConfigString("s3.software_installers_endpoint_url"),
+ SoftwareInstallersAccessKeyID: man.getConfigString("s3.software_installers_access_key_id"),
+ SoftwareInstallersSecretAccessKey: man.getConfigString("s3.software_installers_secret_access_key"),
+ SoftwareInstallersStsAssumeRoleArn: man.getConfigString("s3.software_installers_sts_assume_role_arn"),
+ SoftwareInstallersStsExternalID: man.getConfigString("s3.software_installers_sts_external_id"),
+ SoftwareInstallersDisableSSL: man.getConfigBool("s3.software_installers_disable_ssl"),
+ SoftwareInstallersForceS3PathStyle: man.getConfigBool("s3.software_installers_force_s3_path_style"),
+ SoftwareInstallersCloudfrontURL: man.getConfigString("s3.software_installers_cloudfront_url"),
+ SoftwareInstallersCloudfrontURLSigningPublicKeyID: man.getConfigString("s3.software_installers_cloudfront_url_signing_public_key_id"),
+ SoftwareInstallersCloudfrontURLSigningPrivateKey: man.getConfigString("s3.software_installers_cloudfront_url_signing_private_key"),
}
}
diff --git a/server/config/config_test.go b/server/config/config_test.go
index c2df31584041..95aa2cd1ac0c 100644
--- a/server/config/config_test.go
+++ b/server/config/config_test.go
@@ -695,3 +695,44 @@ e+Z1cALnWREYhEPv4JrR5U0VvqeIdExDD6Ida61yvd7oc59pn0kpfKjozPJr6FsU
// prevent static analysis tools from raising issues due to detection of private key
// in code.
func testingKey(s string) string { return strings.ReplaceAll(s, "TESTING KEY", "PRIVATE KEY") }
+
+func TestValidateCloudfrontURL(t *testing.T) {
+ t.Parallel()
+ cases := []struct {
+ name string
+ url string
+ publicKey string
+ privateKey string
+ errMatches string
+ }{
+ {"happy path", "https://example.com", "public", "private", ""},
+ {"bad URL", "bozo!://example.com", "public", "private", "parse"},
+ {"non-HTTPS URL", "http://example.com", "public", "private", "cloudfront url scheme must be https"},
+ {"missing URL", "", "public", "private", "`s3_software_installers_cloudfront_url` must be set"},
+ {"missing public key", "https://example.com", "", "private",
+ "Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set"},
+ {"missing private key", "https://example.com", "public", "",
+ "Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set"},
+ {"missing keys", "https://example.com", "", "",
+ "Both `s3_software_installers_cloudfront_url_signing_public_key_id` and `s3_software_installers_cloudfront_url_signing_private_key` must be set"},
+ }
+
+ for _, c := range cases {
+ t.Run(c.name, func(t *testing.T) {
+ s3 := S3Config{
+ SoftwareInstallersCloudfrontURL: c.url,
+ SoftwareInstallersCloudfrontURLSigningPublicKeyID: c.publicKey,
+ SoftwareInstallersCloudfrontURLSigningPrivateKey: c.privateKey,
+ }
+ initFatal := func(err error, msg string) {
+ if c.errMatches != "" {
+ require.Error(t, err)
+ require.Regexp(t, c.errMatches, err.Error())
+ } else {
+ t.Errorf("unexpected error: %v", err)
+ }
+ }
+ s3.ValidateCloudfrontURL(initFatal)
+ })
+ }
+}
diff --git a/server/datastore/mysql/apple_mdm.go b/server/datastore/mysql/apple_mdm.go
index f49f215ef460..8991eac117f1 100644
--- a/server/datastore/mysql/apple_mdm.go
+++ b/server/datastore/mysql/apple_mdm.go
@@ -2719,7 +2719,8 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
detail,
command_uuid,
checksum,
- secrets_updated_at
+ secrets_updated_at,
+ ignore_error
)
VALUES %s
ON DUPLICATE KEY UPDATE
@@ -2728,6 +2729,7 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
detail = VALUES(detail),
checksum = VALUES(checksum),
secrets_updated_at = VALUES(secrets_updated_at),
+ ignore_error = VALUES(ignore_error),
profile_identifier = VALUES(profile_identifier),
profile_name = VALUES(profile_name),
command_uuid = VALUES(command_uuid)`,
@@ -2747,9 +2749,9 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
}
generateValueArgs := func(p *fleet.MDMAppleBulkUpsertHostProfilePayload) (string, []any) {
- valuePart := "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?),"
+ valuePart := "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?),"
args := []any{p.ProfileUUID, p.ProfileIdentifier, p.ProfileName, p.HostUUID, p.Status, p.OperationType, p.Detail, p.CommandUUID,
- p.Checksum, p.SecretsUpdatedAt}
+ p.Checksum, p.SecretsUpdatedAt, p.IgnoreError}
return valuePart, args
}
@@ -2767,14 +2769,25 @@ func (ds *Datastore) BulkUpsertMDMAppleHostProfiles(ctx context.Context, payload
}
func (ds *Datastore) UpdateOrDeleteHostMDMAppleProfile(ctx context.Context, profile *fleet.HostMDMAppleProfile) error {
- if profile.OperationType == fleet.MDMOperationTypeRemove &&
- profile.Status != nil &&
- (*profile.Status == fleet.MDMDeliveryVerifying || *profile.Status == fleet.MDMDeliveryVerified) {
- _, err := ds.writer(ctx).ExecContext(ctx, `
+ if profile.OperationType == fleet.MDMOperationTypeRemove && profile.Status != nil {
+ var ignoreError bool
+ if *profile.Status == fleet.MDMDeliveryFailed {
+ // Check whether we should ignore the error.
+ err := sqlx.GetContext(ctx, ds.reader(ctx), &ignoreError, `
+ SELECT ignore_error FROM host_mdm_apple_profiles WHERE host_uuid = ? AND command_uuid = ?`,
+ profile.HostUUID, profile.CommandUUID)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "get ignore error")
+ }
+ }
+ if ignoreError ||
+ (*profile.Status == fleet.MDMDeliveryVerifying || *profile.Status == fleet.MDMDeliveryVerified) {
+ _, err := ds.writer(ctx).ExecContext(ctx, `
DELETE FROM host_mdm_apple_profiles
WHERE host_uuid = ? AND command_uuid = ?
`, profile.HostUUID, profile.CommandUUID)
- return err
+ return err
+ }
}
detail := profile.Detail
diff --git a/server/datastore/mysql/common_mysql/batch.go b/server/datastore/mysql/common_mysql/batch.go
new file mode 100644
index 000000000000..ea24476005f9
--- /dev/null
+++ b/server/datastore/mysql/common_mysql/batch.go
@@ -0,0 +1,26 @@
+package common_mysql
+
+// BatchProcessSimple is a simple utility function to batch process a slice of payloads.
+// Provide a slice of payloads, a batch size, and a function to execute on each batch.
+func BatchProcessSimple[T any](
+ payloads []T,
+ batchSize int,
+ executeBatch func(payloadsInThisBatch []T) error,
+) error {
+ if len(payloads) == 0 || batchSize <= 0 || executeBatch == nil {
+ return nil
+ }
+
+ for i := 0; i < len(payloads); i += batchSize {
+ start := i
+ end := i + batchSize
+ if end > len(payloads) {
+ end = len(payloads)
+ }
+ if err := executeBatch(payloads[start:end]); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/server/datastore/mysql/common_mysql/batch_test.go b/server/datastore/mysql/common_mysql/batch_test.go
new file mode 100644
index 000000000000..5f561e44de8f
--- /dev/null
+++ b/server/datastore/mysql/common_mysql/batch_test.go
@@ -0,0 +1,52 @@
+package common_mysql
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestBatchProcessSimple(t *testing.T) {
+ payloads := []int{1, 2, 3, 4, 5}
+ executeBatch := func(payloadsInThisBatch []int) error {
+ t.Fatal("executeBatch should not be called")
+ return nil
+ }
+
+ // No payloads
+ err := BatchProcessSimple(nil, 10, executeBatch)
+ assert.NoError(t, err)
+
+ // No batch size
+ err = BatchProcessSimple(payloads, 0, executeBatch)
+ assert.NoError(t, err)
+
+ // No executeBatch
+ err = BatchProcessSimple(payloads, 10, nil)
+ assert.NoError(t, err)
+
+ // Large batch size -- all payloads executed in one batch
+ executeBatch = func(payloadsInThisBatch []int) error {
+ assert.Equal(t, payloads, payloadsInThisBatch)
+ return nil
+ }
+ err = BatchProcessSimple(payloads, 10, executeBatch)
+ assert.NoError(t, err)
+
+ // Small batch size
+ numCalls := 0
+ executeBatch = func(payloadsInThisBatch []int) error {
+ numCalls++
+ switch numCalls {
+ case 1:
+ assert.Equal(t, []int{1, 2, 3}, payloadsInThisBatch)
+ case 2:
+ assert.Equal(t, []int{4, 5}, payloadsInThisBatch)
+ default:
+ t.Errorf("Unexpected number of calls to executeBatch: %d", numCalls)
+ }
+ return nil
+ }
+ err = BatchProcessSimple(payloads, 3, executeBatch)
+ assert.NoError(t, err)
+}
diff --git a/server/datastore/mysql/migrations/tables/20250102121439_AddIgnoreErrorToHostProfiles.go b/server/datastore/mysql/migrations/tables/20250102121439_AddIgnoreErrorToHostProfiles.go
new file mode 100644
index 000000000000..3e7638127f9e
--- /dev/null
+++ b/server/datastore/mysql/migrations/tables/20250102121439_AddIgnoreErrorToHostProfiles.go
@@ -0,0 +1,23 @@
+package tables
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+func init() {
+ MigrationClient.AddMigration(Up_20250102121439, Down_20250102121439)
+}
+
+func Up_20250102121439(tx *sql.Tx) error {
+ _, err := tx.Exec(`ALTER TABLE host_mdm_apple_profiles
+ ADD COLUMN ignore_error TINYINT(1) NOT NULL DEFAULT 0`)
+ if err != nil {
+ return fmt.Errorf("failed to add ignore_error to host_mdm_apple_profiles table: %w", err)
+ }
+ return nil
+}
+
+func Down_20250102121439(_ *sql.Tx) error {
+ return nil
+}
diff --git a/server/datastore/mysql/policies.go b/server/datastore/mysql/policies.go
index 61b2187e6af9..8f25fd467768 100644
--- a/server/datastore/mysql/policies.go
+++ b/server/datastore/mysql/policies.go
@@ -416,6 +416,34 @@ func (ds *Datastore) RecordPolicyQueryExecutions(ctx context.Context, host *flee
return nil
}
+func (ds *Datastore) ClearAutoInstallPolicyStatusForHosts(ctx context.Context, installerID uint, hostIDs []uint) error {
+ if len(hostIDs) == 0 {
+ return nil
+ }
+
+ stmt := `
+UPDATE
+ policies p
+ JOIN policy_membership pm ON pm.policy_id = p.id
+SET
+ passes = NULL
+WHERE
+ p.software_installer_id = ?
+ AND pm.host_id IN (?)
+ `
+
+ stmt, args, err := sqlx.In(stmt, installerID, hostIDs)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "building in statement for clearing auto install policy status")
+ }
+
+ if _, err := ds.writer(ctx).ExecContext(ctx, stmt, args...); err != nil {
+ return ctxerr.Wrap(ctx, err, "clearing auto install policy status")
+ }
+
+ return nil
+}
+
func (ds *Datastore) ListGlobalPolicies(ctx context.Context, opts fleet.ListOptions) ([]*fleet.Policy, error) {
return listPoliciesDB(ctx, ds.reader(ctx), nil, opts)
}
diff --git a/server/datastore/mysql/policies_test.go b/server/datastore/mysql/policies_test.go
index 5ce3c95a7604..7059ff736af6 100644
--- a/server/datastore/mysql/policies_test.go
+++ b/server/datastore/mysql/policies_test.go
@@ -72,6 +72,7 @@ func TestPolicies(t *testing.T) {
{"TestPoliciesTeamPoliciesWithScript", testTeamPoliciesWithScript},
{"TeamPoliciesNoTeam", testTeamPoliciesNoTeam},
{"TestPoliciesBySoftwareTitleID", testPoliciesBySoftwareTitleID},
+ {"TestClearAutoInstallPolicyStatusForHost", testClearAutoInstallPolicyStatusForHost},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
@@ -5371,3 +5372,83 @@ func testPoliciesBySoftwareTitleID(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Len(t, policies, 0)
}
+
+func testClearAutoInstallPolicyStatusForHost(t *testing.T, ds *Datastore) {
+ ctx := context.Background()
+
+ user1 := test.NewUser(t, ds, "Alice", "alice@example.com", true)
+ team1, err := ds.NewTeam(ctx, &fleet.Team{Name: "team1" + t.Name()})
+ require.NoError(t, err)
+
+ // create a regular policy
+ policy1 := newTestPolicy(t, ds, user1, "policy 1"+t.Name(), "darwin", &team1.ID)
+
+ // create an automatic install policy
+ policy2 := newTestPolicy(t, ds, user1, "policy 2"+t.Name(), "darwin", &team1.ID)
+ installer, err := fleet.NewTempFileReader(strings.NewReader("hello"), t.TempDir)
+ require.NoError(t, err)
+
+ installer1ID, _, err := ds.MatchOrCreateSoftwareInstaller(context.Background(), &fleet.UploadSoftwareInstallerPayload{
+ InstallScript: "hello",
+ PreInstallQuery: "SELECT 1",
+ PostInstallScript: "world",
+ InstallerFile: installer,
+ StorageID: "storage1",
+ Filename: "file1",
+ Title: "file1",
+ Version: "1.0",
+ Source: "apps",
+ UserID: user1.ID,
+ TeamID: &team1.ID,
+ ValidatedLabels: &fleet.LabelIdentsWithScope{},
+ })
+ require.NoError(t, err)
+ policy2.SoftwareInstallerID = ptr.Uint(installer1ID)
+ err = ds.SavePolicy(context.Background(), policy2, false, false)
+ require.NoError(t, err)
+
+ // create a host
+ host, err := ds.NewHost(ctx, &fleet.Host{
+ OsqueryHostID: ptr.String(uuid.New().String()),
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now(),
+ NodeKey: ptr.String(uuid.New().String()),
+ UUID: uuid.New().String(),
+ Hostname: "host" + t.Name(),
+ TeamID: &team1.ID,
+ Platform: "darwin",
+ })
+ require.NoError(t, err)
+
+ // record a policy run for both policies
+ err = ds.RecordPolicyQueryExecutions(ctx, host, map[uint]*bool{
+ policy1.ID: ptr.Bool(true),
+ policy2.ID: ptr.Bool(false), // software isn't installed on host, so Fleet should install it
+ }, time.Now(), false)
+ require.NoError(t, err)
+
+ hostPolicies, err := ds.ListPoliciesForHost(ctx, host)
+ require.NoError(t, err)
+ require.Len(t, hostPolicies, 2)
+ sort.Slice(hostPolicies, func(i, j int) bool {
+ return hostPolicies[i].ID < hostPolicies[j].ID
+ })
+ require.Equal(t, hostPolicies[0].Response, "pass")
+ require.Equal(t, hostPolicies[1].Response, "fail")
+
+ // clear status for automatic install policy
+ err = ds.ClearAutoInstallPolicyStatusForHosts(ctx, installer1ID, []uint{host.ID})
+ require.NoError(t, err)
+
+ // the status should be NULL for the automatic install policy but not the "regular" one
+ hostPolicies, err = ds.ListPoliciesForHost(ctx, host)
+ require.NoError(t, err)
+ require.Len(t, hostPolicies, 2)
+ sort.Slice(hostPolicies, func(i, j int) bool {
+ return hostPolicies[i].ID < hostPolicies[j].ID
+ })
+ require.Equal(t, hostPolicies[0].Response, "pass")
+ require.Empty(t, hostPolicies[1].Response)
+}
diff --git a/server/datastore/mysql/schema.sql b/server/datastore/mysql/schema.sql
index 8c4c8927f60e..4588000e1f9d 100644
--- a/server/datastore/mysql/schema.sql
+++ b/server/datastore/mysql/schema.sql
@@ -447,6 +447,7 @@ CREATE TABLE `host_mdm_apple_profiles` (
`created_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6),
`updated_at` timestamp(6) NOT NULL DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
`secrets_updated_at` datetime(6) DEFAULT NULL,
+ `ignore_error` tinyint(1) NOT NULL DEFAULT '0',
PRIMARY KEY (`host_uuid`,`profile_uuid`),
KEY `status` (`status`),
KEY `operation_type` (`operation_type`),
@@ -1110,9 +1111,9 @@ CREATE TABLE `migration_status_tables` (
`is_applied` tinyint(1) NOT NULL,
`tstamp` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
-) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=345 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+) /*!50100 TABLESPACE `innodb_system` */ ENGINE=InnoDB AUTO_INCREMENT=346 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,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,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,20241002104104,1,'2020-01-01 01:01:01'),(314,20241002104105,1,'2020-01-01 01:01:01'),(315,20241002104106,1,'2020-01-01 01:01:01'),(316,20241002210000,1,'2020-01-01 01:01:01'),(317,20241003145349,1,'2020-01-01 01:01:01'),(318,20241004005000,1,'2020-01-01 01:01:01'),(319,20241008083925,1,'2020-01-01 01:01:01'),(320,20241009090010,1,'2020-01-01 01:01:01'),(321,20241017163402,1,'2020-01-01 01:01:01'),(322,20241021224359,1,'2020-01-01 01:01:01'),(323,20241022140321,1,'2020-01-01 01:01:01'),(324,20241025111236,1,'2020-01-01 01:01:01'),(325,20241025112748,1,'2020-01-01 01:01:01'),(326,20241025141855,1,'2020-01-01 01:01:01'),(327,20241110152839,1,'2020-01-01 01:01:01'),(328,20241110152840,1,'2020-01-01 01:01:01'),(329,20241110152841,1,'2020-01-01 01:01:01'),(330,20241116233322,1,'2020-01-01 01:01:01'),(331,20241122171434,1,'2020-01-01 01:01:01'),(332,20241125150614,1,'2020-01-01 01:01:01'),(333,20241203125346,1,'2020-01-01 01:01:01'),(334,20241203130032,1,'2020-01-01 01:01:01'),(335,20241205122800,1,'2020-01-01 01:01:01'),(336,20241209164540,1,'2020-01-01 01:01:01'),(337,20241210140021,1,'2020-01-01 01:01:01'),(338,20241219180042,1,'2020-01-01 01:01:01'),(339,20241220100000,1,'2020-01-01 01:01:01'),(340,20241220114903,1,'2020-01-01 01:01:01'),(341,20241220114904,1,'2020-01-01 01:01:01'),(342,20241224000000,1,'2020-01-01 01:01:01'),(343,20241230000000,1,'2020-01-01 01:01:01'),(344,20241231112624,1,'2020-01-01 01:01:01'),(345,20250102121439,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` (
diff --git a/server/datastore/mysql/scripts.go b/server/datastore/mysql/scripts.go
index 8def2509ddd4..53d162d524cf 100644
--- a/server/datastore/mysql/scripts.go
+++ b/server/datastore/mysql/scripts.go
@@ -129,6 +129,12 @@ func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *f
execution_id = ?`
const hostMDMActionsStmt = `
+ SELECT 'uninstall' AS action
+ FROM
+ host_software_installs
+ WHERE
+ execution_id = :execution_id AND host_id = :host_id
+ UNION -- host_mdm_actions query (and thus row in union) must be last to avoid #25144
SELECT
CASE
WHEN lock_ref = :execution_id THEN 'lock_ref'
@@ -140,12 +146,6 @@ func (ds *Datastore) SetHostScriptExecutionResult(ctx context.Context, result *f
host_mdm_actions
WHERE
host_id = :host_id
- UNION
- SELECT 'uninstall' AS action
- FROM
- host_software_installs
- WHERE
- execution_id = :execution_id AND host_id = :host_id
`
output := truncateScriptResult(result.Output)
diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go
index 0abc88d950b6..dd7a0aa091dc 100644
--- a/server/datastore/mysql/software_installers.go
+++ b/server/datastore/mysql/software_installers.go
@@ -1755,3 +1755,109 @@ func (ds *Datastore) IsSoftwareInstallerLabelScoped(ctx context.Context, install
return res, nil
}
+
+const labelScopedFilter = `
+SELECT
+ 1
+FROM (
+ -- no labels
+ SELECT
+ 0 AS count_installer_labels,
+ 0 AS count_host_labels,
+ 0 AS count_host_updated_after_labels
+ WHERE NOT EXISTS ( SELECT 1 FROM software_installer_labels sil WHERE sil.software_installer_id = ?)
+
+ UNION
+
+ -- include any
+ SELECT
+ COUNT(*) AS count_installer_labels,
+ COUNT(lm.label_id) AS count_host_labels,
+ 0 AS count_host_updated_after_labels
+ FROM
+ software_installer_labels sil
+ LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id
+ AND lm.host_id = h.id
+ WHERE
+ sil.software_installer_id = ?
+ AND sil.exclude = 0
+ HAVING
+ count_installer_labels > 0
+ AND count_host_labels > 0
+
+ UNION
+
+ -- exclude any, ignore software that depends on labels created
+ -- _after_ the label_updated_at timestamp of the host (because
+ -- we don't have results for that label yet, the host may or may
+ -- not be a member).
+ SELECT
+ COUNT(*) AS count_installer_labels,
+ COUNT(lm.label_id) AS count_host_labels,
+ SUM(
+ CASE WHEN lbl.created_at IS NOT NULL
+ AND(
+ SELECT
+ label_updated_at FROM hosts
+ WHERE
+ id = 1) >= lbl.created_at THEN
+ 1
+ ELSE
+ 0
+ END) AS count_host_updated_after_labels
+ FROM
+ software_installer_labels sil
+ LEFT OUTER JOIN labels lbl ON lbl.id = sil.label_id
+ LEFT OUTER JOIN label_membership lm ON lm.label_id = sil.label_id
+ AND lm.host_id = h.id
+WHERE
+ sil.software_installer_id = ?
+ AND sil.exclude = 1
+HAVING
+ count_installer_labels > 0
+ AND count_installer_labels = count_host_updated_after_labels
+ AND count_host_labels = 0) t`
+
+func (ds *Datastore) GetIncludedHostIDMapForSoftwareInstaller(ctx context.Context, installerID uint) (map[uint]struct{}, error) {
+ stmt := fmt.Sprintf(`SELECT
+ h.id
+FROM
+ hosts h
+WHERE
+ EXISTS (%s)
+`, labelScopedFilter)
+
+ var hostIDs []uint
+ if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hostIDs, stmt, installerID, installerID, installerID); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "listing hosts included in software installer scope")
+ }
+
+ res := make(map[uint]struct{}, len(hostIDs))
+ for _, id := range hostIDs {
+ res[id] = struct{}{}
+ }
+
+ return res, nil
+}
+
+func (ds *Datastore) GetExcludedHostIDMapForSoftwareInstaller(ctx context.Context, installerID uint) (map[uint]struct{}, error) {
+ stmt := fmt.Sprintf(`SELECT
+ h.id
+FROM
+ hosts h
+WHERE
+ NOT EXISTS (%s)
+`, labelScopedFilter)
+
+ var hostIDs []uint
+ if err := sqlx.SelectContext(ctx, ds.reader(ctx), &hostIDs, stmt, installerID, installerID, installerID); err != nil {
+ return nil, ctxerr.Wrap(ctx, err, "listing hosts excluded from software installer scope")
+ }
+
+ res := make(map[uint]struct{}, len(hostIDs))
+ for _, id := range hostIDs {
+ res[id] = struct{}{}
+ }
+
+ return res, nil
+}
diff --git a/server/datastore/mysql/software_test.go b/server/datastore/mysql/software_test.go
index cbba49884b63..d654a9e0ad4f 100644
--- a/server/datastore/mysql/software_test.go
+++ b/server/datastore/mysql/software_test.go
@@ -5321,6 +5321,10 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.True(t, scoped)
+ hostsInScope, err := ds.GetIncludedHostIDMapForSoftwareInstaller(ctx, installerID1)
+ require.NoError(t, err)
+ require.Contains(t, hostsInScope, host.ID)
+
label1, err := ds.NewLabel(ctx, &fleet.Label{Name: "label1" + t.Name()})
require.NoError(t, err)
@@ -5343,6 +5347,10 @@ func testListHostSoftwareWithLabelScoping(t *testing.T, ds *Datastore) {
require.NoError(t, err)
require.Empty(t, software)
+ hostsNotInScope, err := ds.GetExcludedHostIDMapForSoftwareInstaller(ctx, installerID1)
+ require.NoError(t, err)
+ require.Contains(t, hostsNotInScope, host.ID)
+
// installer1 should be out of scope since the label is "exclude any"
scoped, err = ds.IsSoftwareInstallerLabelScoped(ctx, installerID1, host.ID)
require.NoError(t, err)
diff --git a/server/fleet/apple_mdm.go b/server/fleet/apple_mdm.go
index 3a23b4cbe18c..940f6152b25d 100644
--- a/server/fleet/apple_mdm.go
+++ b/server/fleet/apple_mdm.go
@@ -326,14 +326,27 @@ type MDMAppleProfilePayload struct {
OperationType MDMOperationType `db:"operation_type"`
Detail string `db:"detail"`
CommandUUID string `db:"command_uuid"`
+ IgnoreError bool `db:"ignore_error"`
}
// DidNotInstallOnHost indicates whether this profile was not installed on the host (and
// therefore is not, as far as Fleet knows, currently on the host).
+// The profile in Pending status could be on the host, but Fleet has not received an Acknowledged status yet.
func (p *MDMAppleProfilePayload) DidNotInstallOnHost() bool {
return p.Status != nil && (*p.Status == MDMDeliveryFailed || *p.Status == MDMDeliveryPending) && p.OperationType == MDMOperationTypeInstall
}
+// FailedInstallOnHost indicates whether this profile failed to install on the host.
+func (p *MDMAppleProfilePayload) FailedInstallOnHost() bool {
+ return p.Status != nil && *p.Status == MDMDeliveryFailed && p.OperationType == MDMOperationTypeInstall
+}
+
+// PendingInstallOnHost indicates whether this profile is pending to install on the host.
+// The profile in Pending status could be on the host, but Fleet has not received an Acknowledged status yet.
+func (p *MDMAppleProfilePayload) PendingInstallOnHost() bool {
+ return p.Status != nil && *p.Status == MDMDeliveryPending && p.OperationType == MDMOperationTypeInstall
+}
+
type MDMAppleBulkUpsertHostProfilePayload struct {
ProfileUUID string
ProfileIdentifier string
@@ -345,6 +358,7 @@ type MDMAppleBulkUpsertHostProfilePayload struct {
Detail string
Checksum []byte
SecretsUpdatedAt *time.Time
+ IgnoreError bool
}
// MDMAppleFileVaultSummary reports the number of macOS hosts being managed with Apples disk
diff --git a/server/fleet/datastore.go b/server/fleet/datastore.go
index 489145fae596..8d9f173f55fa 100644
--- a/server/fleet/datastore.go
+++ b/server/fleet/datastore.go
@@ -1706,6 +1706,18 @@ type Datastore interface {
// Software installers
//
+ // GetIncludedHostIDMapForSoftwareInstaller gets the set of hosts that are targeted/in scope for the
+ // given software installer, based label membership.
+ GetIncludedHostIDMapForSoftwareInstaller(ctx context.Context, installerID uint) (map[uint]struct{}, error)
+
+ // GetExcludedHostIDMapForSoftwareInstaller gets the set of hosts that are NOT targeted/in scope for the
+ // given software installer, based label membership.
+ GetExcludedHostIDMapForSoftwareInstaller(ctx context.Context, installerID uint) (map[uint]struct{}, error)
+
+ // ClearAutoInstallPolicyStatusForHosts clears out the status of the policy related to the given
+ // software installer for all the given hosts.
+ ClearAutoInstallPolicyStatusForHosts(ctx context.Context, installerID uint, hostIDs []uint) error
+
// GetSoftwareInstallDetails returns details required to fetch and
// run software installers
GetSoftwareInstallDetails(ctx context.Context, executionId string) (*SoftwareInstallDetails, error)
diff --git a/server/mdm/apple/commander.go b/server/mdm/apple/commander.go
index 4f1a279d7e49..15914ac393b7 100644
--- a/server/mdm/apple/commander.go
+++ b/server/mdm/apple/commander.go
@@ -425,6 +425,11 @@ func (svc *MDMAppleCommander) SendNotifications(ctx context.Context, hostUUIDs [
return nil
}
+// BulkDeleteHostUserCommandsWithoutResults calls the storage method with the same name.
+func (svc *MDMAppleCommander) BulkDeleteHostUserCommandsWithoutResults(ctx context.Context, commandToIDs map[string][]string) error {
+ return svc.storage.BulkDeleteHostUserCommandsWithoutResults(ctx, commandToIDs)
+}
+
// APNSDeliveryError records an error and the associated host UUIDs in which it
// occurred.
type APNSDeliveryError struct {
diff --git a/server/mdm/maintainedapps/testdata/adobe-acrobat-reader.json b/server/mdm/maintainedapps/testdata/adobe-acrobat-reader.json
index 719d8386f7ca..2f1e0693bd90 100644
--- a/server/mdm/maintainedapps/testdata/adobe-acrobat-reader.json
+++ b/server/mdm/maintainedapps/testdata/adobe-acrobat-reader.json
@@ -6,7 +6,7 @@
"name": [
"Adobe Acrobat Reader"
],
- "desc": "View, print, and comment on PDF documents",
+ "desc": "Adobe Acrobat Reader is the industry-standard tool for viewing, printing, and commenting on PDF documents.",
"homepage": "https://www.adobe.com/acrobat/pdf-reader.html",
"url": "https://ardownload2.adobe.com/pub/adobe/reader/mac/AcrobatDC/2400221005/AcroRdrDC_2400221005_MUI.dmg",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/box-drive.json b/server/mdm/maintainedapps/testdata/box-drive.json
index 543d1959ea1f..732737da148e 100644
--- a/server/mdm/maintainedapps/testdata/box-drive.json
+++ b/server/mdm/maintainedapps/testdata/box-drive.json
@@ -6,7 +6,7 @@
"name": [
"Box Drive"
],
- "desc": "Client for the Box cloud storage service",
+ "desc": "Box Drive is the desktop client for Box Cloud, enabling seamless access to your files without taking up local storage.",
"homepage": "https://www.box.com/drive",
"url": "https://e3.boxcdn.net/desktop/releases/mac/BoxDrive-2.40.345.pkg",
"url_specs": {
diff --git a/server/mdm/maintainedapps/testdata/brave-browser.json b/server/mdm/maintainedapps/testdata/brave-browser.json
index dfe62913e855..b7a713fe2393 100644
--- a/server/mdm/maintainedapps/testdata/brave-browser.json
+++ b/server/mdm/maintainedapps/testdata/brave-browser.json
@@ -6,7 +6,7 @@
"name": [
"Brave"
],
- "desc": "Web browser focusing on privacy",
+ "desc": "Brave is a web browser designed with privacy, blocking ads and trackers by default while maintaining fast speed.",
"homepage": "https://brave.com/",
"url": "https://updates-cdn.bravesoftware.com/sparkle/Brave-Browser/stable-arm64/169.162/Brave-Browser-arm64.dmg",
"url_specs": {
diff --git a/server/mdm/maintainedapps/testdata/cloudflare-warp.json b/server/mdm/maintainedapps/testdata/cloudflare-warp.json
index 2981f0bc8911..88ab4f797735 100644
--- a/server/mdm/maintainedapps/testdata/cloudflare-warp.json
+++ b/server/mdm/maintainedapps/testdata/cloudflare-warp.json
@@ -6,7 +6,7 @@
"name": [
"Cloudflare WARP"
],
- "desc": "Free app that makes your Internet safer",
+ "desc": "Cloudflare WARP enhances internet safety and performance by encrypting your data and optimizing connections for privacy.",
"homepage": "https://cloudflarewarp.com/",
"url": "https://1111-releases.cloudflareclient.com/mac/Cloudflare_WARP_2024.6.474.0.pkg",
"url_specs": {
diff --git a/server/mdm/maintainedapps/testdata/docker.json b/server/mdm/maintainedapps/testdata/docker.json
index 15aec7b0b8be..41d9cd262cc8 100644
--- a/server/mdm/maintainedapps/testdata/docker.json
+++ b/server/mdm/maintainedapps/testdata/docker.json
@@ -8,7 +8,7 @@
"Docker Community Edition",
"Docker CE"
],
- "desc": "App to build and share containerised applications and microservices",
+ "desc": "Docker Desktop provides a seamless environment for building, sharing, and running containerized applications and microservices.",
"homepage": "https://www.docker.com/products/docker-desktop",
"url": "https://desktop.docker.com/mac/main/arm64/165256/Docker.dmg",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/figma.json b/server/mdm/maintainedapps/testdata/figma.json
index d8af9b3e4a8f..fbc5029e8760 100644
--- a/server/mdm/maintainedapps/testdata/figma.json
+++ b/server/mdm/maintainedapps/testdata/figma.json
@@ -6,7 +6,7 @@
"name": [
"Figma"
],
- "desc": "Collaborative team software",
+ "desc": "Figma is a collaborative design tool for teams to create, prototype, and share designs in real time.",
"homepage": "https://www.figma.com/",
"url": "https://desktop.figma.com/mac-arm/Figma-124.3.2.zip",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/firefox.json b/server/mdm/maintainedapps/testdata/firefox.json
index ed766c177114..724d7a92ce38 100644
--- a/server/mdm/maintainedapps/testdata/firefox.json
+++ b/server/mdm/maintainedapps/testdata/firefox.json
@@ -6,7 +6,7 @@
"name": [
"Mozilla Firefox"
],
- "desc": "Web browser",
+ "desc": "Firefox is a powerful, open-source web browser built for speed, privacy, and customization.",
"homepage": "https://www.mozilla.org/firefox/",
"url": "https://download-installer.cdn.mozilla.net/pub/firefox/releases/130.0/mac/en-US/Firefox%20130.0.dmg",
"url_specs": {
diff --git a/server/mdm/maintainedapps/testdata/google-chrome.json b/server/mdm/maintainedapps/testdata/google-chrome.json
index aaae6a12910b..9424d36113fb 100644
--- a/server/mdm/maintainedapps/testdata/google-chrome.json
+++ b/server/mdm/maintainedapps/testdata/google-chrome.json
@@ -6,7 +6,7 @@
"name": [
"Google Chrome"
],
- "desc": "Web browser",
+ "desc": "Google Chrome is a fast, reliable web browser built for performance and compatibility across platforms.",
"homepage": "https://www.google.com/chrome/",
"url": "https://dl.google.com/chrome/mac/universal/stable/GGRO/googlechrome.dmg",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/microsoft-edge.json b/server/mdm/maintainedapps/testdata/microsoft-edge.json
index e7b1827cc59f..5d566d1344c3 100644
--- a/server/mdm/maintainedapps/testdata/microsoft-edge.json
+++ b/server/mdm/maintainedapps/testdata/microsoft-edge.json
@@ -6,7 +6,7 @@
"name": [
"Microsoft Edge"
],
- "desc": "Web browser",
+ "desc": "Microsoft Edge is a secure, fast, and modern web browser built for productivity and compatibility.",
"homepage": "https://www.microsoft.com/en-us/edge?form=",
"url": "https://msedge.sf.dl.delivery.mp.microsoft.com/filestreamingservice/files/be6d4c8f-ec75-405e-a5f7-fec66b2898a2/MicrosoftEdge-128.0.2739.67.pkg",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/microsoft-excel.json b/server/mdm/maintainedapps/testdata/microsoft-excel.json
index d62356f284a8..6344a69a8f78 100644
--- a/server/mdm/maintainedapps/testdata/microsoft-excel.json
+++ b/server/mdm/maintainedapps/testdata/microsoft-excel.json
@@ -6,7 +6,7 @@
"name": [
"Microsoft Excel"
],
- "desc": "Spreadsheet software",
+ "desc": "Microsoft Excel is the industry-standard spreadsheet software, perfect for data analysis, reporting, and visualization.",
"homepage": "https://www.microsoft.com/en-US/microsoft-365/excel",
"url": "https://officecdnmac.microsoft.com/pr/C1297A47-86C4-4C1F-97FA-950631F94777/MacAutoupdate/Microsoft_Excel_16.88.24081116_Installer.pkg",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/microsoft-teams.json b/server/mdm/maintainedapps/testdata/microsoft-teams.json
index 07af90bdc742..6dd6d1e88853 100644
--- a/server/mdm/maintainedapps/testdata/microsoft-teams.json
+++ b/server/mdm/maintainedapps/testdata/microsoft-teams.json
@@ -6,7 +6,7 @@
"name": [
"Microsoft Teams"
],
- "desc": "Meet, chat, call, and collaborate in just one place",
+ "desc": "Microsoft Teams is an all-in-one collaboration platform for meetings, chats, calls, and document sharing.",
"homepage": "https://www.microsoft.com/en/microsoft-teams/group-chat-software/",
"url": "https://statics.teams.cdn.office.net/production-osx/24215.1002.3039.5089/MicrosoftTeams.pkg",
"url_specs": {
diff --git a/server/mdm/maintainedapps/testdata/microsoft-word.json b/server/mdm/maintainedapps/testdata/microsoft-word.json
index 9b639ed38a73..b361fe352e5b 100644
--- a/server/mdm/maintainedapps/testdata/microsoft-word.json
+++ b/server/mdm/maintainedapps/testdata/microsoft-word.json
@@ -6,7 +6,7 @@
"name": [
"Microsoft Word"
],
- "desc": "Word processor",
+ "desc": "Microsoft Word is the industry-standard word processor for creating, editing, and sharing professional documents.",
"homepage": "https://www.microsoft.com/en-US/microsoft-365/word",
"url": "https://officecdnmac.microsoft.com/pr/C1297A47-86C4-4C1F-97FA-950631F94777/MacAutoupdate/Microsoft_Word_16.88.24081116_Installer.pkg",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/notion.json b/server/mdm/maintainedapps/testdata/notion.json
index 3dcb88dff861..5cefa2b6a360 100644
--- a/server/mdm/maintainedapps/testdata/notion.json
+++ b/server/mdm/maintainedapps/testdata/notion.json
@@ -6,7 +6,7 @@
"name": [
"Notion"
],
- "desc": "App to write, plan, collaborate, and get organised",
+ "desc": "Notion is an all-in-one workspace for writing, planning, collaborating, and organizing.",
"homepage": "https://www.notion.so/",
"url": "https://desktop-release.notion-static.com/Notion-3.14.0-arm64.dmg",
"url_specs": {
diff --git a/server/mdm/maintainedapps/testdata/postman.json b/server/mdm/maintainedapps/testdata/postman.json
index e6ca9722ad81..36e266210046 100644
--- a/server/mdm/maintainedapps/testdata/postman.json
+++ b/server/mdm/maintainedapps/testdata/postman.json
@@ -6,7 +6,7 @@
"name": [
"Postman"
],
- "desc": "Collaboration platform for API development",
+ "desc": "Postman is a collaboration platform for API development that simplifies building, testing, and sharing APIs.",
"homepage": "https://www.postman.com/",
"url": "https://dl.pstmn.io/download/version/11.12.0/osx_arm64",
"url_specs": {
diff --git a/server/mdm/maintainedapps/testdata/slack.json b/server/mdm/maintainedapps/testdata/slack.json
index eaca79517a9c..7215c0842896 100644
--- a/server/mdm/maintainedapps/testdata/slack.json
+++ b/server/mdm/maintainedapps/testdata/slack.json
@@ -6,7 +6,7 @@
"name": [
"Slack"
],
- "desc": "Team communication and collaboration software",
+ "desc": "Slack is a team communication and collaboration software for modern workplaces",
"homepage": "https://slack.com/",
"url": "https://downloads.slack-edge.com/desktop-releases/mac/arm64/4.40.126/Slack-4.40.126-macOS.dmg",
"url_specs": {
diff --git a/server/mdm/maintainedapps/testdata/teamviewer.json b/server/mdm/maintainedapps/testdata/teamviewer.json
index a8e6a12dc99d..75dd9d777f43 100644
--- a/server/mdm/maintainedapps/testdata/teamviewer.json
+++ b/server/mdm/maintainedapps/testdata/teamviewer.json
@@ -6,7 +6,7 @@
"name": [
"TeamViewer"
],
- "desc": "Remote access and connectivity software focused on security",
+ "desc": "TeamViewer is a versatile remote access and connectivity platform trusted for secure remote desktop control, support, and collaboration.",
"homepage": "https://www.teamviewer.com/",
"url": "https://dl.teamviewer.com/download/version_15x/update/15.57.5/TeamViewer.pkg",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/visual-studio-code.json b/server/mdm/maintainedapps/testdata/visual-studio-code.json
index 398e6abb04d3..0e76eef10667 100644
--- a/server/mdm/maintainedapps/testdata/visual-studio-code.json
+++ b/server/mdm/maintainedapps/testdata/visual-studio-code.json
@@ -7,7 +7,7 @@
"Microsoft Visual Studio Code",
"VS Code"
],
- "desc": "Open-source code editor",
+ "desc": "Microsoft Visual Studio Code (VS Code) is an open-source, lightweight, and powerful code editor.",
"homepage": "https://code.visualstudio.com/",
"url": "https://update.code.visualstudio.com/1.93.0/darwin-arm64/stable",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/whatsapp.json b/server/mdm/maintainedapps/testdata/whatsapp.json
index 97cbecbb810a..f12e4e286952 100644
--- a/server/mdm/maintainedapps/testdata/whatsapp.json
+++ b/server/mdm/maintainedapps/testdata/whatsapp.json
@@ -6,7 +6,7 @@
"name": [
"WhatsApp"
],
- "desc": "Native desktop client for WhatsApp",
+ "desc": "WhatsApp's native desktop client for seamless messaging and calling on macOS.",
"homepage": "https://www.whatsapp.com/",
"url": "https://web.whatsapp.com/desktop/mac_native/release/?version=2.24.16.80&extension=zip&configuration=Release&branch=relbranch",
"url_specs": {},
diff --git a/server/mdm/maintainedapps/testdata/zoom-for-it-admins.json b/server/mdm/maintainedapps/testdata/zoom-for-it-admins.json
index 4f6953a08dfc..82c1096a99e1 100644
--- a/server/mdm/maintainedapps/testdata/zoom-for-it-admins.json
+++ b/server/mdm/maintainedapps/testdata/zoom-for-it-admins.json
@@ -4,7 +4,7 @@
"old_tokens": [],
"tap": "homebrew/cask",
"name": ["Zoom for IT Admins"],
- "desc": "Video communication and virtual meeting platform",
+ "desc": "Zoom is a leading video communication platform for meetings, webinars, and collaboration.",
"homepage": "https://www.zoom.us/",
"url": "https://cdn.zoom.us/prod/6.2.11.43613/ZoomInstallerIT.pkg",
"url_specs": {},
diff --git a/server/mdm/nanomdm/storage/allmulti/allmulti.go b/server/mdm/nanomdm/storage/allmulti/allmulti.go
index 4ca7d6300d1c..9bc017202761 100644
--- a/server/mdm/nanomdm/storage/allmulti/allmulti.go
+++ b/server/mdm/nanomdm/storage/allmulti/allmulti.go
@@ -104,3 +104,10 @@ func (ms *MultiAllStorage) ExpandEmbeddedSecrets(ctx context.Context, document s
})
return doc.(string), err
}
+
+func (ms *MultiAllStorage) BulkDeleteHostUserCommandsWithoutResults(ctx context.Context, commandToIDs map[string][]string) error {
+ _, err := ms.execStores(ctx, func(s storage.AllStorage) (interface{}, error) {
+ return nil, s.BulkDeleteHostUserCommandsWithoutResults(ctx, commandToIDs)
+ })
+ return err
+}
diff --git a/server/mdm/nanomdm/storage/file/file.go b/server/mdm/nanomdm/storage/file/file.go
index c816a34e730f..00bc069f59c0 100644
--- a/server/mdm/nanomdm/storage/file/file.go
+++ b/server/mdm/nanomdm/storage/file/file.go
@@ -245,3 +245,8 @@ func (s *FileStorage) ExpandEmbeddedSecrets(_ context.Context, document string)
// NOT IMPLEMENTED
return document, nil
}
+
+func (s *FileStorage) BulkDeleteHostUserCommandsWithoutResults(_ context.Context, _ map[string][]string) error {
+ // NOT IMPLEMENTED
+ return nil
+}
diff --git a/server/mdm/nanomdm/storage/mysql/queue.go b/server/mdm/nanomdm/storage/mysql/queue.go
index 5e84b30bb940..ead83efb6c79 100644
--- a/server/mdm/nanomdm/storage/mysql/queue.go
+++ b/server/mdm/nanomdm/storage/mysql/queue.go
@@ -7,6 +7,7 @@ import (
"fmt"
"strings"
+ "github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
"github.com/fleetdm/fleet/v4/server/datastore/mysql/common_mysql"
"github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/google/uuid"
@@ -260,3 +261,54 @@ WHERE
)
return err
}
+
+// BulkDeleteHostUserCommandsWithoutResults deletes all commands without results for the given host/user IDs.
+// This is used to clean up the queue when a profile is deleted from Fleet.
+func (m *MySQLStorage) BulkDeleteHostUserCommandsWithoutResults(ctx context.Context, commandToIDs map[string][]string) error {
+ if len(commandToIDs) == 0 {
+ return nil
+ }
+ return common_mysql.WithRetryTxx(ctx, sqlx.NewDb(m.db, ""), func(tx sqlx.ExtContext) error {
+ return m.bulkDeleteHostUserCommandsWithoutResults(ctx, tx, commandToIDs)
+ }, loggerWrapper{m.logger})
+}
+
+func (m *MySQLStorage) bulkDeleteHostUserCommandsWithoutResults(ctx context.Context, tx sqlx.ExtContext,
+ commandToIDs map[string][]string) error {
+ stmt := `
+DELETE
+ eq
+FROM
+ nano_enrollment_queue AS eq
+ LEFT JOIN nano_command_results AS cr
+ ON cr.command_uuid = eq.command_uuid AND cr.id = eq.id
+WHERE
+ cr.command_uuid IS NULL AND eq.command_uuid = ? AND eq.id IN (?);`
+
+ // We process each commandUUID one at a time, in batches of hostUserIDs.
+ // This is because the number of hostUserIDs can be large, and number of unique commands is normally small.
+ // If we have a use case where each host has a unique command, we can create a separate method for that use case.
+ for commandUUID, hostUserIDs := range commandToIDs {
+ if len(hostUserIDs) == 0 {
+ continue
+ }
+
+ batchSize := 10000
+ err := common_mysql.BatchProcessSimple(hostUserIDs, batchSize, func(hostUserIDsToProcess []string) error {
+ expanded, args, err := sqlx.In(stmt, commandUUID, hostUserIDsToProcess)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "expanding bulk delete nano commands")
+ }
+ _, err = tx.ExecContext(ctx, expanded, args...)
+ if err != nil {
+ return ctxerr.Wrap(ctx, err, "bulk delete nano commands")
+ }
+ return nil
+ })
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/server/mdm/nanomdm/storage/storage.go b/server/mdm/nanomdm/storage/storage.go
index 89efc1fb4b11..759c50b72827 100644
--- a/server/mdm/nanomdm/storage/storage.go
+++ b/server/mdm/nanomdm/storage/storage.go
@@ -27,6 +27,8 @@ type CommandAndReportResultsStore interface {
StoreCommandReport(r *mdm.Request, report *mdm.CommandResults) error
RetrieveNextCommand(r *mdm.Request, skipNotNow bool) (*mdm.CommandWithSubtype, error)
ClearQueue(r *mdm.Request) error
+ // BulkDeleteHostUserCommandsWithoutResults deletes all commands without results for the given host/user IDs.
+ BulkDeleteHostUserCommandsWithoutResults(ctx context.Context, commandToId map[string][]string) error
}
type BootstrapTokenStore interface {
diff --git a/server/mock/datastore_mock.go b/server/mock/datastore_mock.go
index f56e54e43081..b686b20370ad 100644
--- a/server/mock/datastore_mock.go
+++ b/server/mock/datastore_mock.go
@@ -1077,6 +1077,12 @@ type WipeHostViaWindowsMDMFunc func(ctx context.Context, host *fleet.Host, cmd *
type UpdateHostLockWipeStatusFromAppleMDMResultFunc func(ctx context.Context, hostUUID string, cmdUUID string, requestType string, succeeded bool) error
+type GetIncludedHostIDMapForSoftwareInstallerFunc func(ctx context.Context, installerID uint) (map[uint]struct{}, error)
+
+type GetExcludedHostIDMapForSoftwareInstallerFunc func(ctx context.Context, installerID uint) (map[uint]struct{}, error)
+
+type ClearAutoInstallPolicyStatusForHostsFunc func(ctx context.Context, installerID uint, hostIDs []uint) error
+
type GetSoftwareInstallDetailsFunc func(ctx context.Context, executionId string) (*fleet.SoftwareInstallDetails, error)
type ListPendingSoftwareInstallsFunc func(ctx context.Context, hostID uint) ([]string, error)
@@ -2772,6 +2778,15 @@ type DataStore struct {
UpdateHostLockWipeStatusFromAppleMDMResultFunc UpdateHostLockWipeStatusFromAppleMDMResultFunc
UpdateHostLockWipeStatusFromAppleMDMResultFuncInvoked bool
+ GetIncludedHostIDMapForSoftwareInstallerFunc GetIncludedHostIDMapForSoftwareInstallerFunc
+ GetIncludedHostIDMapForSoftwareInstallerFuncInvoked bool
+
+ GetExcludedHostIDMapForSoftwareInstallerFunc GetExcludedHostIDMapForSoftwareInstallerFunc
+ GetExcludedHostIDMapForSoftwareInstallerFuncInvoked bool
+
+ ClearAutoInstallPolicyStatusForHostsFunc ClearAutoInstallPolicyStatusForHostsFunc
+ ClearAutoInstallPolicyStatusForHostsFuncInvoked bool
+
GetSoftwareInstallDetailsFunc GetSoftwareInstallDetailsFunc
GetSoftwareInstallDetailsFuncInvoked bool
@@ -6636,6 +6651,27 @@ func (s *DataStore) UpdateHostLockWipeStatusFromAppleMDMResult(ctx context.Conte
return s.UpdateHostLockWipeStatusFromAppleMDMResultFunc(ctx, hostUUID, cmdUUID, requestType, succeeded)
}
+func (s *DataStore) GetIncludedHostIDMapForSoftwareInstaller(ctx context.Context, installerID uint) (map[uint]struct{}, error) {
+ s.mu.Lock()
+ s.GetIncludedHostIDMapForSoftwareInstallerFuncInvoked = true
+ s.mu.Unlock()
+ return s.GetIncludedHostIDMapForSoftwareInstallerFunc(ctx, installerID)
+}
+
+func (s *DataStore) GetExcludedHostIDMapForSoftwareInstaller(ctx context.Context, installerID uint) (map[uint]struct{}, error) {
+ s.mu.Lock()
+ s.GetExcludedHostIDMapForSoftwareInstallerFuncInvoked = true
+ s.mu.Unlock()
+ return s.GetExcludedHostIDMapForSoftwareInstallerFunc(ctx, installerID)
+}
+
+func (s *DataStore) ClearAutoInstallPolicyStatusForHosts(ctx context.Context, installerID uint, hostIDs []uint) error {
+ s.mu.Lock()
+ s.ClearAutoInstallPolicyStatusForHostsFuncInvoked = true
+ s.mu.Unlock()
+ return s.ClearAutoInstallPolicyStatusForHostsFunc(ctx, installerID, hostIDs)
+}
+
func (s *DataStore) GetSoftwareInstallDetails(ctx context.Context, executionId string) (*fleet.SoftwareInstallDetails, error) {
s.mu.Lock()
s.GetSoftwareInstallDetailsFuncInvoked = true
diff --git a/server/mock/mdm/datastore_mdm_mock.go b/server/mock/mdm/datastore_mdm_mock.go
index cbed04c82540..cd094f0f9b0e 100644
--- a/server/mock/mdm/datastore_mdm_mock.go
+++ b/server/mock/mdm/datastore_mdm_mock.go
@@ -29,6 +29,8 @@ type RetrieveNextCommandFunc func(r *mdm.Request, skipNotNow bool) (*mdm.Command
type ClearQueueFunc func(r *mdm.Request) error
+type BulkDeleteHostUserCommandsWithoutResultsFunc func(ctx context.Context, commandToId map[string][]string) error
+
type StoreBootstrapTokenFunc func(r *mdm.Request, msg *mdm.SetBootstrapToken) error
type RetrieveBootstrapTokenFunc func(r *mdm.Request, msg *mdm.GetBootstrapToken) (*mdm.BootstrapToken, error)
@@ -89,6 +91,9 @@ type MDMAppleStore struct {
ClearQueueFunc ClearQueueFunc
ClearQueueFuncInvoked bool
+ BulkDeleteHostUserCommandsWithoutResultsFunc BulkDeleteHostUserCommandsWithoutResultsFunc
+ BulkDeleteHostUserCommandsWithoutResultsFuncInvoked bool
+
StoreBootstrapTokenFunc StoreBootstrapTokenFunc
StoreBootstrapTokenFuncInvoked bool
@@ -198,6 +203,13 @@ func (fs *MDMAppleStore) ClearQueue(r *mdm.Request) error {
return fs.ClearQueueFunc(r)
}
+func (fs *MDMAppleStore) BulkDeleteHostUserCommandsWithoutResults(ctx context.Context, commandToId map[string][]string) error {
+ fs.mu.Lock()
+ fs.BulkDeleteHostUserCommandsWithoutResultsFuncInvoked = true
+ fs.mu.Unlock()
+ return fs.BulkDeleteHostUserCommandsWithoutResultsFunc(ctx, commandToId)
+}
+
func (fs *MDMAppleStore) StoreBootstrapToken(r *mdm.Request, msg *mdm.SetBootstrapToken) error {
fs.mu.Lock()
fs.StoreBootstrapTokenFuncInvoked = true
diff --git a/server/service/apple_mdm.go b/server/service/apple_mdm.go
index 7394a82aeb93..50758be0cd87 100644
--- a/server/service/apple_mdm.go
+++ b/server/service/apple_mdm.go
@@ -3464,17 +3464,25 @@ func ReconcileAppleProfiles(
}
for _, p := range toRemove {
+ // Exclude profiles that are also marked for installation.
if _, ok := profileIntersection.GetMatchingProfileInDesiredState(p); ok {
hostProfilesToCleanup = append(hostProfilesToCleanup, p)
continue
}
- if p.DidNotInstallOnHost() {
- // then we shouldn't send an additional remove command since it wasn't installed on the
- // host.
+ if p.FailedInstallOnHost() {
+ // then we shouldn't send an additional remove command since it failed to install on the host.
hostProfilesToCleanup = append(hostProfilesToCleanup, p)
continue
}
+ if p.PendingInstallOnHost() {
+ // The profile most likely did not install on host. However, it is possible that the profile
+ // is currently being installed. So, we clean up the profile from the database, but also send
+ // a remove command to the host.
+ hostProfilesToCleanup = append(hostProfilesToCleanup, p)
+ // IgnoreError is set since the removal command is likely to fail.
+ p.IgnoreError = true
+ }
target := removeTargets[p.ProfileUUID]
if target == nil {
@@ -3496,6 +3504,7 @@ func ReconcileAppleProfiles(
ProfileName: p.ProfileName,
Checksum: p.Checksum,
SecretsUpdatedAt: p.SecretsUpdatedAt,
+ IgnoreError: p.IgnoreError,
})
}
@@ -3504,6 +3513,16 @@ func ReconcileAppleProfiles(
// `InstallProfile` for the same identifier, which can cause race
// conditions. It's better to "update" the profile by sending a single
// `InstallProfile` command.
+ //
+ // Create a map of command UUIDs to host IDs
+ commandUUIDToHostIDsCleanupMap := make(map[string][]string)
+ for _, hp := range hostProfilesToCleanup {
+ commandUUIDToHostIDsCleanupMap[hp.CommandUUID] = append(commandUUIDToHostIDsCleanupMap[hp.CommandUUID], hp.HostUUID)
+ }
+ // We need to delete commands from the nano queue so they don't get sent to device.
+ if err := commander.BulkDeleteHostUserCommandsWithoutResults(ctx, commandUUIDToHostIDsCleanupMap); err != nil {
+ return ctxerr.Wrap(ctx, err, "deleting nano commands without results")
+ }
if err := ds.BulkDeleteMDMAppleHostsConfigProfiles(ctx, hostProfilesToCleanup); err != nil {
return ctxerr.Wrap(ctx, err, "deleting profiles that didn't change")
}
diff --git a/server/service/apple_mdm_test.go b/server/service/apple_mdm_test.go
index ac9780d8863b..09e167d38cab 100644
--- a/server/service/apple_mdm_test.go
+++ b/server/service/apple_mdm_test.go
@@ -2293,6 +2293,10 @@ func TestMDMAppleReconcileAppleProfiles(t *testing.T) {
require.Empty(t, payload)
return nil
}
+ mdmStorage.BulkDeleteHostUserCommandsWithoutResultsFunc = func(ctx context.Context, commandToIDs map[string][]string) error {
+ require.Empty(t, commandToIDs)
+ return nil
+ }
var enqueueFailForOp fleet.MDMOperationType
var mu sync.Mutex
diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go
index 76acce2cebd2..8d102a974687 100644
--- a/server/service/integration_enterprise_test.go
+++ b/server/service/integration_enterprise_test.go
@@ -12500,6 +12500,21 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerHostRequests() {
"install_script_output": "ok"
}`, *h.OrbitNodeKey, installUUID)), http.StatusNoContent)
+ // simulate a lock/unlock; this creates the host_mdm_actions table, which reproduces #25144
+ s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/lock", h.ID), nil, http.StatusNoContent)
+ status, err := s.ds.GetHostLockWipeStatus(context.Background(), h)
+ require.NoError(t, err)
+ var orbitScriptResp orbitPostScriptResultResponse
+ s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
+ json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *h.OrbitNodeKey, status.LockScript.ExecutionID)),
+ http.StatusOK, &orbitScriptResp)
+ s.Do("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/unlock", h.ID), nil, http.StatusNoContent)
+ status, err = s.ds.GetHostLockWipeStatus(context.Background(), h)
+ require.NoError(t, err)
+ s.DoJSON("POST", "/api/fleet/orbit/scripts/result",
+ json.RawMessage(fmt.Sprintf(`{"orbit_node_key": %q, "execution_id": %q, "exit_code": 0, "output": "ok"}`, *h.OrbitNodeKey, status.UnlockScript.ExecutionID)),
+ http.StatusOK, &orbitScriptResp)
+
// Do uninstall
s.DoJSON("POST", fmt.Sprintf("/api/latest/fleet/hosts/%d/software/%d/uninstall", h.ID, titleID), nil, http.StatusAccepted, &resp)
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d/software", h.ID), nil, http.StatusOK, &getHostSoftwareResp)
@@ -15291,6 +15306,215 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers
require.NotNil(t, host1LastInstall)
}
+func (s *integrationEnterpriseTestSuite) TestPolicyAutomationLabelScopingRetrigger() {
+ t := s.T()
+ ctx := context.Background()
+ host, err := s.ds.NewHost(context.Background(), &fleet.Host{
+ DetailUpdatedAt: time.Now(),
+ LabelUpdatedAt: time.Now(),
+ PolicyUpdatedAt: time.Now(),
+ SeenTime: time.Now().Add(-1 * time.Minute),
+ OsqueryHostID: ptr.String(t.Name()),
+ NodeKey: ptr.String(t.Name()),
+ UUID: uuid.New().String(),
+ Hostname: fmt.Sprintf("%sfoo.local", t.Name()),
+ Platform: "linux",
+ })
+ require.NoError(t, err)
+ orbitKey := setOrbitEnrollment(t, host, s.ds)
+ host.OrbitNodeKey = &orbitKey
+
+ // Create a few labels
+ var newLabelResp createLabelResponse
+ s.DoJSON("POST", "/api/v1/fleet/labels", fleet.LabelPayload{
+ Name: uuid.NewString(),
+ Query: "SELECT 1",
+ }, http.StatusOK, &newLabelResp)
+ lbl1 := newLabelResp.Label
+
+ newLabelResp = createLabelResponse{}
+ s.DoJSON("POST", "/api/v1/fleet/labels", fleet.LabelPayload{
+ Name: uuid.NewString(),
+ Query: "SELECT 2",
+ }, http.StatusOK, &newLabelResp)
+ lbl2 := newLabelResp.Label
+
+ newLabelResp = createLabelResponse{}
+ s.DoJSON("POST", "/api/v1/fleet/labels", fleet.LabelPayload{
+ Name: uuid.NewString(),
+ Query: "SELECT 3",
+ }, http.StatusOK, &newLabelResp)
+ lbl3 := newLabelResp.Label
+
+ // Add label1 and label2 to the host
+ err = s.ds.RecordLabelQueryExecutions(context.Background(), host, map[uint]*bool{lbl1.ID: ptr.Bool(true), lbl2.ID: ptr.Bool(true)}, time.Now(), false)
+ require.NoError(t, err)
+
+ // upload software. Add label1 and label3 as "exclude any" labels.
+ rubyPayload := &fleet.UploadSoftwareInstallerPayload{
+ InstallScript: "some deb install script",
+ Filename: "ruby.deb",
+ TeamID: nil,
+ LabelsIncludeAny: []string{lbl1.Name, lbl3.Name},
+ Platform: "linux",
+ }
+ s.uploadSoftwareInstaller(t, rubyPayload, http.StatusOK, "")
+
+ // Get software title ID of the uploaded installer.
+ resp := listSoftwareTitlesResponse{}
+ s.DoJSON(
+ "GET", "/api/latest/fleet/software/titles",
+ listSoftwareTitlesRequest{},
+ http.StatusOK, &resp,
+ "query", "ruby",
+ "team_id", "0",
+ )
+ require.Len(t, resp.SoftwareTitles, 1)
+ require.NotNil(t, resp.SoftwareTitles[0].SoftwarePackage)
+ rubyDebTitleID := resp.SoftwareTitles[0].ID
+
+ var rubyDetail getSoftwareTitleResponse
+ s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", rubyDebTitleID), nil, http.StatusOK, &rubyDetail)
+ require.NotNil(t, rubyDetail.SoftwareTitle)
+ require.NotNil(t, rubyDetail.SoftwareTitle.SoftwarePackage)
+ rubyInstallerID := rubyDetail.SoftwareTitle.SoftwarePackage.InstallerID
+
+ policy1, err := s.ds.NewTeamPolicy(ctx, 0, nil, fleet.PolicyPayload{
+ Name: "policy1",
+ Query: "SELECT 1;",
+ Platform: "linux",
+ })
+ require.NoError(t, err)
+
+ mtplr := modifyTeamPolicyResponse{}
+ s.DoJSON("PATCH", fmt.Sprintf("/api/latest/fleet/teams/0/policies/%d", policy1.ID), modifyTeamPolicyRequest{
+ ModifyPolicyPayload: fleet.ModifyPolicyPayload{
+ SoftwareTitleID: optjson.Any[uint]{Set: true, Valid: true, Value: rubyDebTitleID},
+ },
+ }, http.StatusOK, &mtplr)
+
+ // No install attempt yet
+ host1LastInstall, err := s.ds.GetHostLastInstallData(ctx, host.ID, rubyInstallerID)
+ require.NoError(t, err)
+ require.Nil(t, host1LastInstall)
+
+ // Send back a failed result for the policy.
+ distributedResp := submitDistributedQueryResultsResponse{}
+ s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ host,
+ map[uint]*bool{
+ policy1.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+ err = s.ds.UpdateHostPolicyCounts(ctx)
+ require.NoError(t, err)
+ policy1, err = s.ds.Policy(ctx, policy1.ID)
+ require.NoError(t, err)
+
+ // Because the installer is in scope, the policy is failing
+ require.Equal(t, uint(0), policy1.PassingHostCount)
+ require.Equal(t, uint(1), policy1.FailingHostCount)
+
+ // We've triggered an installation attempt
+ host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, rubyInstallerID)
+ require.NoError(t, err)
+ require.NotNil(t, host1LastInstall)
+
+ // Update the installer's labels to "exclude any". This de-scopes the software.
+ s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
+ InstallScript: ptr.String("some install script"),
+ PreInstallQuery: ptr.String("some pre install query"),
+ PostInstallScript: ptr.String("some post install script"),
+ Filename: "ruby.deb",
+ TitleID: rubyDebTitleID,
+ TeamID: nil,
+ LabelsExcludeAny: []string{lbl1.Name, lbl3.Name},
+ }, http.StatusOK, "")
+
+ // The update should clear out the installation attempt
+ host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, rubyInstallerID)
+ require.NoError(t, err)
+ require.Nil(t, host1LastInstall)
+
+ // Update the installer's labels to be "include any" again. The software is now back in scope.
+ s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
+ InstallScript: ptr.String("some install script"),
+ PreInstallQuery: ptr.String("some pre install query"),
+ PostInstallScript: ptr.String("some post install script"),
+ Filename: "ruby.deb",
+ TitleID: rubyDebTitleID,
+ TeamID: nil,
+ LabelsIncludeAny: []string{lbl1.Name, lbl3.Name},
+ }, http.StatusOK, "")
+
+ // Simulate a failure of the policy
+ s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ host,
+ map[uint]*bool{
+ policy1.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+ err = s.ds.UpdateHostPolicyCounts(ctx)
+ require.NoError(t, err)
+ policy1, err = s.ds.Policy(ctx, policy1.ID)
+ require.NoError(t, err)
+ // Because the installer is in scope, the policy should be failing again
+ require.Equal(t, uint(0), policy1.PassingHostCount)
+ require.Equal(t, uint(1), policy1.FailingHostCount)
+
+ // We have an installation attempt again; the policy automation has been re-triggered
+ host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, rubyInstallerID)
+ require.NoError(t, err)
+ require.NotNil(t, host1LastInstall)
+
+ // Update the include any labels. The host doesn't have label2, so this means that the software
+ // moved out of scope.
+ s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
+ InstallScript: ptr.String("some install script"),
+ PreInstallQuery: ptr.String("some pre install query"),
+ PostInstallScript: ptr.String("some post install script"),
+ Filename: "ruby.deb",
+ TitleID: rubyDebTitleID,
+ TeamID: nil,
+ LabelsIncludeAny: []string{lbl2.Name},
+ }, http.StatusOK, "")
+
+ host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, rubyInstallerID)
+ require.NoError(t, err)
+ require.Nil(t, host1LastInstall)
+
+ // Update to exclude any with label 2. This moves the software back into scope. The policy
+ // automation should re-trigger.
+ s.updateSoftwareInstaller(t, &fleet.UpdateSoftwareInstallerPayload{
+ InstallScript: ptr.String("some install script"),
+ PreInstallQuery: ptr.String("some pre install query"),
+ PostInstallScript: ptr.String("some post install script"),
+ Filename: "ruby.deb",
+ TitleID: rubyDebTitleID,
+ TeamID: nil,
+ LabelsExcludeAny: []string{lbl2.Name},
+ }, http.StatusOK, "")
+
+ s.DoJSONWithoutAuth("POST", "/api/osquery/distributed/write", genDistributedReqWithPolicyResults(
+ host,
+ map[uint]*bool{
+ policy1.ID: ptr.Bool(false),
+ },
+ ), http.StatusOK, &distributedResp)
+ err = s.ds.UpdateHostPolicyCounts(ctx)
+ require.NoError(t, err)
+ policy1, err = s.ds.Policy(ctx, policy1.ID)
+ require.NoError(t, err)
+ // Because the installer is in scope, the policy should be failing again.
+ require.Equal(t, uint(0), policy1.PassingHostCount)
+ require.Equal(t, uint(1), policy1.FailingHostCount)
+
+ // We have an installation attempt again.
+ host1LastInstall, err = s.ds.GetHostLastInstallData(ctx, host.ID, rubyInstallerID)
+ require.NoError(t, err)
+ require.Nil(t, host1LastInstall)
+}
+
func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsScripts() {
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 9fd9f0b3b368..9e9c0f0c6241 100644
--- a/server/service/integration_mdm_profiles_test.go
+++ b/server/service/integration_mdm_profiles_test.go
@@ -26,9 +26,11 @@ import (
microsoft_mdm "github.com/fleetdm/fleet/v4/server/mdm/microsoft"
"github.com/fleetdm/fleet/v4/server/mdm/microsoft/syncml"
"github.com/fleetdm/fleet/v4/server/mdm/nanodep/godep"
+ "github.com/fleetdm/fleet/v4/server/mdm/nanomdm/mdm"
"github.com/fleetdm/fleet/v4/server/ptr"
"github.com/fleetdm/fleet/v4/server/test"
"github.com/google/uuid"
+ "github.com/groob/plist"
"github.com/jmoiron/sqlx"
"github.com/smallstep/pkcs7"
"github.com/stretchr/testify/assert"
@@ -1347,6 +1349,10 @@ func (s *integrationMDMTestSuite) TestPuppetMatchPreassignProfiles() {
{Identifier: "i4", OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetdConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
{Identifier: mobileconfig.FleetCARootConfigPayloadIdentifier, OperationType: fleet.MDMOperationTypeInstall, Status: &fleet.MDMDeliveryPending},
+ // Profiles from previous team being deleted
+ {Identifier: "i2", OperationType: fleet.MDMOperationTypeRemove, Status: &fleet.MDMDeliveryPending},
+ {Identifier: mobileconfig.FleetFileVaultPayloadIdentifier, OperationType: fleet.MDMOperationTypeRemove,
+ Status: &fleet.MDMDeliveryPending},
},
})
@@ -5538,3 +5544,158 @@ func (s *integrationMDMTestSuite) TestWindowsConfigSecretVariablesUpload() {
s.testSecretVariablesUpload(newProfileBytes, getProfileContents, "xml", "windows")
}
+
+func (s *integrationMDMTestSuite) TestAppleProfileDeletion() {
+ t := s.T()
+ ctx := context.Background()
+
+ err := s.ds.ApplyEnrollSecrets(ctx, nil, []*fleet.EnrollSecret{{Secret: t.Name()}})
+ require.NoError(t, err)
+
+ globalProfiles := [][]byte{
+ mobileconfigForTest("N1", "I1"),
+ }
+ wantGlobalProfiles := globalProfiles
+ wantGlobalProfiles = append(
+ wantGlobalProfiles,
+ setupExpectedFleetdProfile(t, s.server.URL, t.Name(), nil),
+ )
+
+ // add global profiles
+ s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
+
+ // Create a host and then enroll to MDM.
+ host, mdmDevice := createHostThenEnrollMDM(s.ds, s.server.URL, t)
+ // Add IdP email to host
+ mysql.ExecAdhocSQL(t, s.ds, func(e sqlx.ExtContext) error {
+ _, err := e.ExecContext(ctx, `INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?)`, "idp@example.com", host.ID,
+ fleet.DeviceMappingMDMIdpAccounts)
+ return err
+ })
+
+ // trigger a profile sync
+ s.awaitTriggerProfileSchedule(t)
+ installs, removes := checkNextPayloads(t, mdmDevice, false)
+ // verify that we received all profiles
+ s.signedProfilesMatch(
+ append(wantGlobalProfiles, setupExpectedCAProfile(t, s.ds)),
+ installs,
+ )
+ require.Empty(t, removes)
+
+ // Add a profile with a Fleet variable. We are also testing that removal of a profile with a Fleet variable works.
+ // A unique command is created for each host when this Fleet variable is used.
+ globalProfilesPlusOne := [][]byte{
+ globalProfiles[0],
+ mobileconfigForTest("N2", "$FLEET_VAR_"+FleetVarHostEndUserEmailIDP),
+ }
+ s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfilesPlusOne},
+ http.StatusNoContent)
+ // trigger a profile sync
+ s.awaitTriggerProfileSchedule(t)
+
+ // Make sure profile was uploaded
+ profiles, err := s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
+ require.NoError(t, err)
+ assert.Len(t, profiles, 4)
+
+ // Delete a profile before it is sent to device
+ s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
+ // trigger a profile sync
+ s.awaitTriggerProfileSchedule(t)
+ sendErrorOnRemoveProfile := func(device *mdmtest.TestAppleMDMClient) {
+ // The host grabs the removal command from Fleet
+ cmd, err := device.Idle()
+ require.NoError(t, err)
+ assert.Equal(t, "RemoveProfile", cmd.Command.RequestType)
+ // Since profile is not on the device, it returns an error.
+ errChain := []mdm.ErrorChain{
+ {
+ ErrorCode: 89,
+ ErrorDomain: "FooErrorDomain",
+ LocalizedDescription: "The profile not found",
+ },
+ }
+ cmd, err = device.Err(cmd.CommandUUID, errChain)
+ require.NoError(t, err)
+ assert.Nil(t, cmd)
+ }
+ sendErrorOnRemoveProfile(mdmDevice)
+
+ // Make sure deleted profile no longer shows up
+ profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
+ require.NoError(t, err)
+ assert.Len(t, profiles, 3)
+
+ // Add a profile again
+ s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfilesPlusOne},
+ http.StatusNoContent)
+ // trigger a profile sync
+ s.awaitTriggerProfileSchedule(t)
+
+ // The host grabs the profile from Fleet
+ cmd, err := mdmDevice.Idle()
+ require.NoError(t, err)
+ assert.Equal(t, "InstallProfile", cmd.Command.RequestType)
+ // Verify that the Fleet variable was replaced with the IdP email
+ type Command struct {
+ Command struct {
+ Payload []byte
+ }
+ }
+ var p Command
+ err = plist.Unmarshal(cmd.Raw, &p)
+ require.NoError(t, err)
+ assert.NotContains(t, string(p.Command.Payload), "$FLEET_VAR_"+FleetVarHostEndUserEmailIDP)
+ assert.Contains(t, string(p.Command.Payload), "idp@example.com")
+
+ // While the host is installing the profile, we delete it.
+ s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
+ // trigger a profile sync
+ s.awaitTriggerProfileSchedule(t)
+
+ // Host acknowledges installing the profile and grabs the remove command
+ cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ assert.Equal(t, "RemoveProfile", cmd.Command.RequestType)
+ cmd, err = mdmDevice.Acknowledge(cmd.CommandUUID)
+ require.NoError(t, err)
+ assert.Nil(t, cmd)
+
+ // Add another device
+ host2, mdmDevice2 := createHostThenEnrollMDM(s.ds, s.server.URL, t)
+ // Add IdP email to host
+ mysql.ExecAdhocSQL(t, s.ds, func(e sqlx.ExtContext) error {
+ _, err := e.ExecContext(ctx, `INSERT INTO host_emails (email, host_id, source) VALUES (?, ?, ?)`, "idp2@example.com", host2.ID,
+ fleet.DeviceMappingMDMIdpAccounts)
+ return err
+ })
+
+ // trigger a profile sync
+ s.awaitTriggerProfileSchedule(t)
+ installs, removes = checkNextPayloads(t, mdmDevice2, false)
+ assert.Len(t, installs, 3)
+ assert.Empty(t, removes)
+
+ // Add a profile again
+ s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfilesPlusOne},
+ http.StatusNoContent)
+ // trigger a profile sync
+ s.awaitTriggerProfileSchedule(t)
+ // Delete a profile before it is sent to both devices
+ s.Do("POST", "/api/v1/fleet/mdm/apple/profiles/batch", batchSetMDMAppleProfilesRequest{Profiles: globalProfiles}, http.StatusNoContent)
+ // trigger a profile sync
+ s.awaitTriggerProfileSchedule(t)
+ // The host grabs the removal command from Fleet
+ sendErrorOnRemoveProfile(mdmDevice)
+ sendErrorOnRemoveProfile(mdmDevice2)
+
+ // Make sure deleted profile no longer shows up on either host
+ profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host.UUID)
+ require.NoError(t, err)
+ assert.Len(t, profiles, 3)
+ profiles, err = s.ds.GetHostMDMAppleProfiles(ctx, host2.UUID)
+ require.NoError(t, err)
+ assert.Len(t, profiles, 3)
+
+}
diff --git a/server/service/integration_mdm_test.go b/server/service/integration_mdm_test.go
index fc321093df8e..46c0e36406b3 100644
--- a/server/service/integration_mdm_test.go
+++ b/server/service/integration_mdm_test.go
@@ -9751,9 +9751,13 @@ func (s *integrationMDMTestSuite) TestRemoveFailedProfiles() {
getHostResp = getHostResponse{}
s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/hosts/%d", host.ID), nil, http.StatusOK, &getHostResp)
require.NotNil(t, getHostResp.Host.MDM.Profiles)
- require.Len(t, *getHostResp.Host.MDM.Profiles, 2)
+ // Since Fleet doesn't know for sure whether profile was installed or not, it sends a remove command just in case.
+ require.Len(t, *getHostResp.Host.MDM.Profiles, 3)
for _, hm := range *getHostResp.Host.MDM.Profiles {
require.Equal(t, fleet.MDMDeliveryPending, *hm.Status)
+ if hm.Name == "N3" {
+ assert.Equal(t, fleet.MDMOperationTypeRemove, hm.OperationType)
+ }
}
}
diff --git a/server/service/testing_client.go b/server/service/testing_client.go
index b3e0c8966b5a..6734c061289c 100644
--- a/server/service/testing_client.go
+++ b/server/service/testing_client.go
@@ -692,6 +692,16 @@ func (ts *withServer) updateSoftwareInstaller(
require.NoError(t, w.WriteField("self_service", "false"))
}
}
+ if payload.LabelsIncludeAny != nil {
+ for _, l := range payload.LabelsIncludeAny {
+ require.NoError(t, w.WriteField("labels_include_any", l))
+ }
+ }
+ if payload.LabelsExcludeAny != nil {
+ for _, l := range payload.LabelsExcludeAny {
+ require.NoError(t, w.WriteField("labels_exclude_any", l))
+ }
+ }
w.Close()
diff --git a/website/api/helpers/strings/to-html.js b/website/api/helpers/strings/to-html.js
index 5182528b986d..60f8cd39ff35 100644
--- a/website/api/helpers/strings/to-html.js
+++ b/website/api/helpers/strings/to-html.js
@@ -107,13 +107,13 @@ module.exports = {
if(infostring === 'mermaid') {
return `${_.escape(code)}
`;
} else if(infostring === 'js') {// Interpret `js` as `javascript`
- return `${_.escape(code)}
`;
+ return `${_.escape(code)}
`;
} else if(infostring === 'bash' || infostring === 'sh') {// Interpret `sh` and `bash` as `bash`
- return `${_.escape(code)}
`;
+ return `${_.escape(code)}
`;
} else if(infostring !== '') {// leaving the code language as-is if the infoString is anything else.
- return `${_.escape(code)}
`;
+ return `${_.escape(code)}
`;
} else {// When unspecified, default to `text`
- return `${_.escape(code)}
`;
+ return `${_.escape(code)}
`;
}
};
diff --git a/website/scripts/build-static-content.js b/website/scripts/build-static-content.js
index 104264662a4f..d11cc4274f64 100644
--- a/website/scripts/build-static-content.js
+++ b/website/scripts/build-static-content.js
@@ -259,10 +259,11 @@ module.exports = {
if(mdString.match(/(-|\d\.)\s.*\n\n+])\n+(>)/g, '$1$2'); // « Removes any newlines that might exist before the closing `>` when the compontent is added to markdown files.
// [?] Looking for code that used to be here related to syntax highlighting? Please see https://github.com/fleetdm/fleet/pull/14124/files -mikermcneil, 2023-09-25
let htmlString = await sails.helpers.strings.toHtml(mdString);