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 ( +
+
{banner}
+
+ ); } 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.