diff --git a/cmd/osquery-perf/osquery_perf/stats.go b/cmd/osquery-perf/osquery_perf/stats.go index 05c3157c87a..26beb35c6a4 100644 --- a/cmd/osquery-perf/osquery_perf/stats.go +++ b/cmd/osquery-perf/osquery_perf/stats.go @@ -273,10 +273,15 @@ func (s *Stats) Log() { s.l.Lock() defer s.l.Unlock() + var errorRate float64 + if s.osqueryEnrollments > 0 { + errorRate = float64(s.errors) / float64(s.osqueryEnrollments) + } + log.Printf( "uptime: %s, error rate: %.2f, osquery enrolls: %d, orbit enrolls: %d, mdm enrolls: %d, distributed/reads: %d, distributed/writes: %d, config requests: %d, result log requests: %d, mdm sessions initiated: %d, mdm on-demand syncs: %d, mdm commands received: %d, config errors: %d, distributed/read errors: %d, distributed/write errors: %d, log result errors: %d, orbit errors: %d, desktop errors: %d, mdm errors: %d, mdm scep requests: %d, mdm scep success: %d, mdm scep errors: %d, ddm tokens success: %d, ddm tokens errors: %d, ddm declaration items success: %d, ddm declaration items errors: %d, ddm activation success: %d, ddm activation errors: %d, ddm configuration success: %d, ddm configuration errors: %d, ddm status success: %d, ddm status errors: %d, buffered logs: %d, script execs (errs): %d (%d), software installs (errs): %d (%d)", time.Since(s.StartTime).Round(time.Second), - float64(s.errors)/float64(s.osqueryEnrollments), + errorRate, s.osqueryEnrollments, s.orbitEnrollments, s.mdmEnrollments, diff --git a/ee/cis/macos-14/test/scripts/CIS_2.6.2.sh b/ee/cis/macos-14/test/scripts/CIS_2.6.2.sh index 00fc667cbe8..9a4fa6056fa 100644 --- a/ee/cis/macos-14/test/scripts/CIS_2.6.2.sh +++ b/ee/cis/macos-14/test/scripts/CIS_2.6.2.sh @@ -1,16 +1,12 @@ #!/bin/bash -sudo /usr/bin/defaults write /Library/Application\ -Support/CrashReporter/DiagnosticMessagesHistory.plist AutoSubmit -bool false +sudo /usr/bin/defaults write "/Library/Application Support/CrashReporter/DiagnosticMessagesHistory.plist" AutoSubmit -bool false -sudo /usr/bin/defaults write /Library/Application\ -Support/CrashReporter/DiagnosticMessagesHistory.plist ThirdPartyDataSubmit -bool false +sudo /usr/bin/defaults write "/Library/Application Support/CrashReporter/DiagnosticMessagesHistory.plist" ThirdPartyDataSubmit -bool false -sudo /bin/chmod 644 /Library/Application\ -Support/CrashReporter/DiagnosticMessagesHistory.plist +sudo /bin/chmod 644 "/Library/Application Support/CrashReporter/DiagnosticMessagesHistory.plist" -sudo /usr/sbin/chgrp admin /Library/Application\ -Support/CrashReporter/DiagnosticMessagesHistory.plist +sudo /usr/sbin/chgrp admin "/Library/Application Support/CrashReporter/DiagnosticMessagesHistory.plist" echo "This needs modification" @@ -18,4 +14,4 @@ sudo -u /usr/bin/defaults write /Users//Library/Preferences/com.apple.assistant.support "Siri DataSharing Opt-In Status" -int 2 # Example: -# sudo -u sharonkatz /usr/bin/defaults write /Users/sharonkatz/Library/Preferences/com.apple.assistant.support "Siri Data Sharing Opt-In Status" -int 2 \ No newline at end of file +# sudo -u sharonkatz /usr/bin/defaults write /Users/sharonkatz/Library/Preferences/com.apple.assistant.support "Siri Data Sharing Opt-In Status" -int 2 diff --git a/ee/fleetd-chrome/src/tables/os_version.ts b/ee/fleetd-chrome/src/tables/os_version.ts index f63b0935605..4b39fab70e3 100644 --- a/ee/fleetd-chrome/src/tables/os_version.ts +++ b/ee/fleetd-chrome/src/tables/os_version.ts @@ -88,10 +88,6 @@ export default class TableOSVersion extends Table { column: "codename", error_message: err.message.toString(), }); - warningsArray.push({ - column: "platform_like", - error_message: err.message.toString(), - }); } let arch; diff --git a/ee/maintained-apps/ingesters/winget/external_refs/main.go b/ee/maintained-apps/ingesters/winget/external_refs/main.go index c3ba336abc9..0062358d4ab 100644 --- a/ee/maintained-apps/ingesters/winget/external_refs/main.go +++ b/ee/maintained-apps/ingesters/winget/external_refs/main.go @@ -1,7 +1,7 @@ package externalrefs import ( - "fmt" + "log" maintained_apps "github.com/fleetdm/fleet/v4/ee/maintained-apps" ) @@ -14,15 +14,18 @@ var Funcs = map[string][]func(*maintained_apps.FMAManifestApp) (*maintained_apps // EnrichManifest applies all registered enrichment functions for the given app. // Enrichers are looked up by app.Slug and run sequentially. -// Errors are logged but do not stop the enrichment pipeline. +// Errors are logged and stop the enrichment pipeline for that app, preserving +// the last known-good app value. func EnrichManifest(app *maintained_apps.FMAManifestApp) { if enrichers, ok := Funcs[app.Slug]; ok { for _, enricher := range enrichers { - var err error - app, err = enricher(app) + enriched, err := enricher(app) if err != nil { - fmt.Printf("Error enriching app %s: %v\n", app.UniqueIdentifier, err) + log.Printf("Error enriching app %s: %v\n", app.UniqueIdentifier, err) + break } + app = enriched } } } + diff --git a/ee/maintained-apps/inputs/homebrew/scripts/cleanmymac-uninstall.sh b/ee/maintained-apps/inputs/homebrew/scripts/cleanmymac-uninstall.sh index 5feeec0f936..18657fd9f14 100755 --- a/ee/maintained-apps/inputs/homebrew/scripts/cleanmymac-uninstall.sh +++ b/ee/maintained-apps/inputs/homebrew/scripts/cleanmymac-uninstall.sh @@ -100,14 +100,18 @@ trash() { fi local trash="/Users/$logged_in_user/.Trash" - local file_name="$(basename "${target_file}")" - - if [[ -e "$target_file" ]]; then - echo "removing $target_file." - mv -f "$target_file" "$trash/${file_name}_${timestamp}_${rand}" - else - echo "$target_file doesn't exist." - fi + local file_name + + # Glob-expand target_file (compgen preserves spaces in the path; [[ -e "$x" ]] does not expand *) + while IFS= read -r expanded_file; do + if [[ -e "$expanded_file" ]]; then + file_name="$(basename "${expanded_file}")" + echo "removing $expanded_file." + mv -f "$expanded_file" "$trash/${file_name}_${timestamp}_${rand}" + else + echo "$expanded_file doesn't exist." + fi + done < <(compgen -G "$target_file") } remove_launchctl_service 'com.macpaw.CleanMyMac5.HealthMonitor' @@ -130,3 +134,4 @@ trash $LOGGED_IN_USER '~/Library/LaunchAgents/com.macpaw.CleanMyMac5.Updater.pli trash $LOGGED_IN_USER '~/Library/Logs/com.macpaw.CleanMyMac5' trash $LOGGED_IN_USER '~/Library/Preferences/com.macpaw.CleanMyMac5.plist' trash $LOGGED_IN_USER '~/Library/Saved Application State/com.macpaw.CleanMyMac5.savedState' + diff --git a/ee/maintained-apps/inputs/homebrew/scripts/docker_desktop_install.sh b/ee/maintained-apps/inputs/homebrew/scripts/docker_desktop_install.sh index 9994c6aa659..82147d29906 100644 --- a/ee/maintained-apps/inputs/homebrew/scripts/docker_desktop_install.sh +++ b/ee/maintained-apps/inputs/homebrew/scripts/docker_desktop_install.sh @@ -4,7 +4,7 @@ set -euo pipefail # variables APPDIR="/Applications/" -TMPDIR=$(dirname "$(realpath "$INSTALLER_PATH")") +TMPDIR=$(mktemp -d) MOUNT_POINT="" cleanup() { @@ -15,6 +15,9 @@ cleanup() { fi rmdir "$mp" >/dev/null 2>&1 || true fi + if [[ -n "${TMPDIR:-}" ]]; then + rm -rf "$TMPDIR" >/dev/null 2>&1 || true + fi } trap cleanup EXIT @@ -142,3 +145,4 @@ mkdir -p /usr/local/bin /bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-desktop" "/usr/local/bin/docker-credential-desktop" /bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-ecr-login" "/usr/local/bin/docker-credential-ecr-login" /bin/ln -h -f -s -- "$APPDIR/Docker.app/Contents/Resources/bin/docker-credential-osxkeychain" "/usr/local/bin/docker-credential-osxkeychain" + diff --git a/ee/vulnerability-dashboard/api/helpers/get-vulnerabilities.js b/ee/vulnerability-dashboard/api/helpers/get-vulnerabilities.js index b2e4ce252d1..87ad01fd7b2 100644 --- a/ee/vulnerability-dashboard/api/helpers/get-vulnerabilities.js +++ b/ee/vulnerability-dashboard/api/helpers/get-vulnerabilities.js @@ -139,28 +139,36 @@ module.exports = { } else {// If we're filtering by a specific team and not getting the results for a single vulnerability, we'll build a list of vulnerabilities that affect that team. let hostIdsToFind = _.pluck(await Host.find({teamApid: teamApid}).select(['id']), 'id'); - // TODO use parameterization let firstNativeQueryToSend; + let hostIdsPlaceholders = hostIdsToFind.map((id, i)=>{ + return sails.config.datastores.default.adapter === 'sails-postgresql' ? `$${i + 1}` : '?'; + }).join(','); if(sails.config.datastores.default.adapter === 'sails-postgresql') { - firstNativeQueryToSend = ` + firstNativeQueryToSend = { + sql: ` SELECT * FROM "vulnerability" WHERE "id" IN ( SELECT "vulnerability" FROM "vulnerabilityinstall" - WHERE "host" IN (${hostIdsToFind.join(',')}) - );`; + WHERE "host" IN (${hostIdsPlaceholders}) + );`, + bindings: hostIdsToFind + }; } else if(sails.config.datastores.default.adapter === 'sails-mysql') { - firstNativeQueryToSend = 'SELECT * FROM `vulnerability` '+ - 'WHERE `id` IN ('+ - 'SELECT `vulnerability` FROM `vulnerabilityinstall` '+ - 'WHERE `host` IN ('+ - hostIdsToFind.join(',')+ - ')'+ - ');'; + firstNativeQueryToSend = { + sql: 'SELECT * FROM `vulnerability` '+ + 'WHERE `id` IN ('+ + 'SELECT `vulnerability` FROM `vulnerabilityinstall` '+ + 'WHERE `host` IN ('+ + hostIdsPlaceholders+ + ')'+ + ');', + bindings: hostIdsToFind + }; } - let rawResultFromDatabase = await sails.sendNativeQuery(firstNativeQueryToSend); + let rawResultFromDatabase = await sails.sendNativeQuery(firstNativeQueryToSend.sql, firstNativeQueryToSend.bindings); for(let row of rawResultFromDatabase.rows){ row.createdAt = Number(row.createdAt); @@ -200,17 +208,21 @@ module.exports = { // Build a where clause for the native query we will be sending. let vulnerabilityInstallWhereClause; + let vulnerabilityIdsToFind = _.pluck(vulnerabilities, 'id'); + let vulnerabilityIdsPlaceholders = vulnerabilityIdsToFind.map((id, i)=>{ + return sails.config.datastores.default.adapter === 'sails-postgresql' ? `$${i + 1}` : '?'; + }).join(','); // If we're including resolved install information in this report for a CSV export, we'll send a query without the uninstalledAt condition. if(includeResolvedInstalls){ - vulnerabilityInstallWhereClause = `WHERE vulnerability IN (${_.pluck(vulnerabilities,'id').join(',')})`;// Note this where clause will work with Postgres and MySQL datastores. + vulnerabilityInstallWhereClause = `WHERE vulnerability IN (${vulnerabilityIdsPlaceholders})`;// Note this where clause will work with Postgres and MySQL datastores. } else { // Otherwise, we'll send queries to only get unresolved installs. if(sails.config.datastores.default.adapter === 'sails-postgresql') { // If this app is configured to use a Postgres datastore, we'll need to put double quotes around the uninstalledAt column name. - vulnerabilityInstallWhereClause = `WHERE "uninstalledAt" = 0 AND vulnerability IN (${_.pluck(vulnerabilities,'id').join(',')})`; + vulnerabilityInstallWhereClause = `WHERE "uninstalledAt" = 0 AND vulnerability IN (${vulnerabilityIdsPlaceholders})`; } else if(sails.config.datastores.default.adapter === 'sails-mysql') { - vulnerabilityInstallWhereClause = `WHERE uninstalledAt = 0 AND vulnerability IN (${_.pluck(vulnerabilities,'id').join(',')})`; + vulnerabilityInstallWhereClause = `WHERE uninstalledAt = 0 AND vulnerability IN (${vulnerabilityIdsPlaceholders})`; } } @@ -219,7 +231,7 @@ module.exports = { SELECT * FROM vulnerabilityinstall ${vulnerabilityInstallWhereClause}`; - let selectedInstallsFromEntireOrg = await sails.sendNativeQuery(vulnerabilityInstallNativeQuery); + let selectedInstallsFromEntireOrg = await sails.sendNativeQuery(vulnerabilityInstallNativeQuery, vulnerabilityIdsToFind); let hostsFromEntireOrg = await Host.find(); @@ -305,3 +317,4 @@ module.exports = { }; + diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx index 33fb4d4c7cd..a1fa89eec34 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx @@ -44,7 +44,7 @@ const baseClass = "software-script-details-modal"; export type IPackageInstallDetails = { host_display_name?: string; - install_uuid?: string; // not actually optional + install_uuid: string; }; export const renderContactOption = (url?: string) => ( @@ -201,8 +201,7 @@ export const SoftwareScriptDetailsModal = ({ onRerun, contactUrl, }: ISoftwareInstallDetailsProps) => { - // will always be present - const installUUID = detailsFromProps.install_uuid ?? ""; + const installUUID = detailsFromProps.install_uuid; const [showInstallDetails, setShowInstallDetails] = useState(false); const toggleInstallDetails = () => { @@ -223,6 +222,7 @@ export const SoftwareScriptDetailsModal = ({ { ...DEFAULT_USE_QUERY_OPTIONS, staleTime: 3000, + enabled: !!installUUID, select: (data) => data.results as ISoftwareScriptResult, } ); @@ -263,6 +263,15 @@ export const SoftwareScriptDetailsModal = ({ : undefined; const renderContent = () => { + if (!installUUID) { + return ( + + ); + } + if (isLoading) { return ; } diff --git a/frontend/hooks/useSoftwareInstallerMeta.ts b/frontend/hooks/useSoftwareInstallerMeta.ts index 750491ad203..2c66fc2ee71 100644 --- a/frontend/hooks/useSoftwareInstallerMeta.ts +++ b/frontend/hooks/useSoftwareInstallerMeta.ts @@ -93,11 +93,6 @@ export const useSoftwareInstaller = ( 0 ); - const fmaVersions = - isFleetMaintainedApp && "fleet_maintained_versions" in softwareInstaller - ? softwareInstaller.fleet_maintained_versions - : []; - const isCustomPackage = installerType === "package" && !isFleetMaintainedApp; @@ -141,7 +136,6 @@ export const useSoftwareInstaller = ( isAndroidPlayStoreWebApp, isFleetMaintainedApp, isLatestFmaVersion, - fmaVersions, isCustomPackage, isIosOrIpadosApp, sha256, diff --git a/frontend/interfaces/mdm.ts b/frontend/interfaces/mdm.ts index 22c70b0680a..e335820eaca 100644 --- a/frontend/interfaces/mdm.ts +++ b/frontend/interfaces/mdm.ts @@ -338,7 +338,8 @@ export const isAndroidBYO = (enrollmentStatus: MdmEnrollmentStatus | null) => { return enrollmentStatus === "On (personal)"; }; -/** Android COBO (company-owned, fully managed) enrollment. */ +/** Android COBO (company-owned, fully managed) enrollment. Shares the same + * current/legacy enrollment status semantics as isAutomaticDeviceEnrollment. */ export const isAndroidCOBO = (enrollmentStatus: MdmEnrollmentStatus | null) => { - return enrollmentStatus === "On (automatic)"; + return isAutomaticDeviceEnrollment(enrollmentStatus); }; diff --git a/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx b/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx index d7e849e5e5f..6b19a5a431d 100644 --- a/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx +++ b/frontend/pages/DashboardPage/cards/OperatingSystems/OSTable.tsx @@ -19,7 +19,7 @@ const EmptyOS = (platform: PlatformValueOptions): JSX.Element => ( handlePolicyModal(p.id)} > diff --git a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx index e1d688a9a9b..01933df359e 100644 --- a/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx +++ b/frontend/pages/ManageControlsPage/OSSettings/cards/DiskEncryption/DiskEncryption.tsx @@ -7,6 +7,7 @@ import { ITeamConfig } from "interfaces/team"; import { getErrorReason } from "interfaces/errors"; import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants"; +import { API_NO_TEAM_ID } from "interfaces/team"; import diskEncryptionAPI from "services/entities/disk_encryption"; import teamsAPI, { ILoadTeamResponse } from "services/entities/teams"; @@ -93,7 +94,7 @@ const DiskEncryption = ({ { refetchOnWindowFocus: false, retry: false, - enabled: currentTeamId !== 0, + enabled: currentTeamId !== API_NO_TEAM_ID, select: (res) => res.fleet, onSuccess: (res) => { const enableDiskEncryption = res.mdm?.enable_disk_encryption ?? false; @@ -119,7 +120,7 @@ const DiskEncryption = ({ ); onMutation(); setShowAggregate(diskEncryptionEnabled); - if (currentTeamId === 0) { + if (currentTeamId === API_NO_TEAM_ID) { getUpdatedAppConfig(); } } catch (e) { @@ -148,7 +149,7 @@ const DiskEncryption = ({ } }; - if (currentTeamId === 0 && isLoadingTeam) { + if (currentTeamId === API_NO_TEAM_ID && isLoadingTeam) { setIsLoadingTeam(false); } diff --git a/frontend/pages/ManageControlsPage/SetupExperience/SetupExperience.tsx b/frontend/pages/ManageControlsPage/SetupExperience/SetupExperience.tsx index 347997c7399..ec4aeab4afe 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/SetupExperience.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/SetupExperience.tsx @@ -1,4 +1,4 @@ -import React, { useContext } from "react"; +import React, { useContext, useEffect } from "react"; import { InjectedRouter, Params } from "react-router/lib/Router"; import { AppContext } from "context/app"; @@ -27,6 +27,24 @@ const SetupExperience = ({ const { section, platform: urlPlatformParam } = params; const { isPremiumTier } = useContext(AppContext); + const DEFAULT_SETTINGS_SECTION = SETUP_EXPERIENCE_NAV_ITEMS[0]; + + const currentFormSection = + SETUP_EXPERIENCE_NAV_ITEMS.find((item) => item.urlSection === section) ?? + DEFAULT_SETTINGS_SECTION; + + useEffect(() => { + if ( + currentFormSection.urlSection !== "install-software" && + urlPlatformParam + ) { + router.replace( + currentFormSection.path + queryString // current card doesn't support platforms yet + ); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentFormSection.urlSection, urlPlatformParam, queryString]); + // Not premium shows premium message if (!isPremiumTier) { return ( @@ -36,20 +54,6 @@ const SetupExperience = ({ ); } - const DEFAULT_SETTINGS_SECTION = SETUP_EXPERIENCE_NAV_ITEMS[0]; - - const currentFormSection = - SETUP_EXPERIENCE_NAV_ITEMS.find((item) => item.urlSection === section) ?? - DEFAULT_SETTINGS_SECTION; - - if ( - currentFormSection.urlSection !== "install-software" && - urlPlatformParam - ) { - router.replace( - currentFormSection.path + queryString // current card doesn't support platforms yet - ); - } const CurrentCard = currentFormSection.Card; return ( diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTableConfig.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTableConfig.tsx index f5a7247dd49..548fb5dc374 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTableConfig.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/InstallSoftware/components/InstallSoftwareTable/InstallSoftwareTableConfig.tsx @@ -104,14 +104,12 @@ const generateTableConfig = ( let displayedVersion = title.software_package?.version || title.app_store_app?.version; - if (platform === "linux") { + if (platform === "linux" && displayedVersion) { const packageTypeCopy = getSetupExperienceLinuxPackageCopy( title.source ); if (packageTypeCopy) { - displayedVersion = ( - displayedVersion ?? DEFAULT_EMPTY_CELL_VALUE - ).concat(` (.${packageTypeCopy})`); + displayedVersion = displayedVersion.concat(` (.${packageTypeCopy})`); } } return ; diff --git a/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/UsersForm.tsx b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/UsersForm.tsx index 4ecab1a6be5..dd87b0a0b43 100644 --- a/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/UsersForm.tsx +++ b/frontend/pages/ManageControlsPage/SetupExperience/cards/Users/components/UsersForm/UsersForm.tsx @@ -116,14 +116,17 @@ const UsersForm = ({ }), }); renderFlash("success", "Successfully updated."); + if (isMacMdmEnabledAndConfigured) { + setFormData((prev) => ({ + ...prev, + lockEndUserInfo: canLockEndUserInfo, + })); + } } catch { renderFlash("error", "Couldn't update settings. Please try again."); } setIsUpdating(false); - if (isMacMdmEnabledAndConfigured) { - setFormData((prev) => ({ ...prev, lockEndUserInfo: canLockEndUserInfo })); - } }; return ( diff --git a/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tsx b/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tsx index 1156aaba7f4..fd4dd1fcd58 100644 --- a/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tsx +++ b/frontend/pages/SoftwarePage/SoftwareInventory/SoftwareInventoryTable/SoftwareInventoryTable.tsx @@ -176,10 +176,13 @@ const SoftwareTable = ({ const vulnFilterDetails = getVulnFilterRenderDetails(vulnFilters); const hasVulnFilters = vulnFilterDetails.filterCount > 0; - // Include showVersions — the titles view can have installers even when - // the versions view is empty, so the toggle should stay interactive. - const isTrulyEmpty = - !hasData && !hasQuery && !hasVulnFilters && !showVersions; + // On the versions view, installable software existing elsewhere (titles + // view) should keep the toggle interactive even if the versions view + // itself is empty. On the titles view, there's no separate signal, so + // emptiness alone determines the page-wide-disable state. + const isTrulyEmpty = showVersions + ? !hasData && !hasQuery && !hasVulnFilters && !installableSoftwareExists + : !hasData && !hasQuery && !hasVulnFilters; const controlsDisabled = !isSoftwareEnabled || isTrulyEmpty; const handleShowVersionsToggle = () => { diff --git a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/helpers.tsx b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/helpers.tsx index bbe8d688914..3f736b4e934 100644 --- a/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/helpers.tsx +++ b/frontend/pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/helpers.tsx @@ -67,10 +67,7 @@ const validateWindowLength = ( .map(Number); const startTotalMinutes = startHours * 60 + startMinutes; const endTotalMinutes = endHours * 60 + endMinutes; - return ( - endTotalMinutes < startTotalMinutes || - endTotalMinutes - startTotalMinutes >= 60 - ); + return endTotalMinutes - startTotalMinutes >= 60; }; const FORM_VALIDATIONS: IFormValidations = { @@ -188,3 +185,4 @@ export const validateFormData = ( return formValidation; }; + diff --git a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx index 82042bf2862..838f23fa817 100644 --- a/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx +++ b/frontend/pages/admin/OrgSettingsPage/cards/Advanced/Advanced.tsx @@ -180,7 +180,7 @@ const Advanced = ({ const payload = { server_settings: { live_reporting_disabled: formData.disableLiveQuery, - discard_reports_data: formData.disableQueryReports, + query_reports_disabled: formData.disableQueryReports, scripts_disabled: formData.disableScripts, deferred_save_host: appConfig.server_settings.deferred_save_host, ai_features_disabled: formData.disableAIFeatures, diff --git a/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tsx b/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tsx index 8eec9669f7e..50af6ae597f 100644 --- a/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tsx +++ b/frontend/pages/hosts/components/TransferHostModal/TransferHostModal.tsx @@ -11,7 +11,7 @@ import { ITeam } from "interfaces/team"; interface ITransferHostModal { isGlobalAdmin: boolean; teams: ITeam[]; - onSubmit: (team: ITeam) => void; + onSubmit: (team: ITeam | INoTeamOption) => void; onCancel: () => void; isUpdating: boolean; multipleHosts?: boolean; @@ -73,7 +73,10 @@ const TransferHostModal = ({ ); const onSubmitTransferHost = useCallback(() => { - onSubmit(selectedTeam as ITeam); + if (selectedTeam === undefined) { + return; + } + onSubmit(selectedTeam); }, [onSubmit, selectedTeam]); const createTeamDropdownOptions = (): CustomOptionType[] => { @@ -135,3 +138,4 @@ const TransferHostModal = ({ }; export default TransferHostModal; + diff --git a/frontend/pages/labels/ManageLabelsPage/ManageLabelsPage.tsx b/frontend/pages/labels/ManageLabelsPage/ManageLabelsPage.tsx index 55c00e92d16..8739066a8ff 100644 --- a/frontend/pages/labels/ManageLabelsPage/ManageLabelsPage.tsx +++ b/frontend/pages/labels/ManageLabelsPage/ManageLabelsPage.tsx @@ -116,7 +116,15 @@ const ManageLabelsPage = ({ router }: IManageLabelsPageProps): JSX.Element => { repoURL={repoURL} /> ); - }, [currentUser, error, isLoading, labels, onClickAction]); + }, [ + currentUser, + error, + isLoading, + labels, + onClickAction, + labelsGitOpsManaged, + repoURL, + ]); return ( diff --git a/frontend/pages/policies/edit/components/PolicyErrorsTable/PolicyErrorsTableConfig.tsx b/frontend/pages/policies/edit/components/PolicyErrorsTable/PolicyErrorsTableConfig.tsx index 0123caf4a7d..1dac6605c3e 100644 --- a/frontend/pages/policies/edit/components/PolicyErrorsTable/PolicyErrorsTableConfig.tsx +++ b/frontend/pages/policies/edit/components/PolicyErrorsTable/PolicyErrorsTableConfig.tsx @@ -72,10 +72,9 @@ const generateTableHeaders = (): IDataColumn[] => { const generateDataSet = memoize( (policyHostsErrorsList: ICampaignError[] = []): ICampaignError[] => { - policyHostsErrorsList = policyHostsErrorsList.sort((a, b) => + return [...policyHostsErrorsList].sort((a, b) => sortUtils.caseInsensitiveAsc(a.host_display_name, b.host_display_name) ); - return policyHostsErrorsList; } ); diff --git a/orbit/pkg/keystore/keystore_darwin.go b/orbit/pkg/keystore/keystore_darwin.go index e97a64f8aef..d02d3224637 100644 --- a/orbit/pkg/keystore/keystore_darwin.go +++ b/orbit/pkg/keystore/keystore_darwin.go @@ -120,7 +120,7 @@ func GetSecret() (string, error) { C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecClass), unsafe.Pointer(C.kSecClassGenericPassword)) C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecReturnData), unsafe.Pointer(C.kCFBooleanTrue)) C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecMatchLimit), unsafe.Pointer(C.kSecMatchLimitOne)) - C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecAttrLabel), unsafe.Pointer(serviceStringRef)) + C.CFDictionaryAddValue(query, unsafe.Pointer(C.kSecAttrService), unsafe.Pointer(serviceStringRef)) var data C.CFTypeRef status := C.SecItemCopyMatching(C.CFDictionaryRef(query), &data) //nolint:gocritic // dubSubExpr false positive @@ -171,3 +171,4 @@ func stringToCFString(s string) C.CFStringRef { func releaseCFString(s C.CFStringRef) { C.CFRelease(C.CFTypeRef(s)) } + diff --git a/orbit/pkg/table/diskutil/apfs/apfs_darwin.go b/orbit/pkg/table/diskutil/apfs/apfs_darwin.go index 541722a68c5..15673ecf106 100644 --- a/orbit/pkg/table/diskutil/apfs/apfs_darwin.go +++ b/orbit/pkg/table/diskutil/apfs/apfs_darwin.go @@ -158,7 +158,7 @@ func PhysicalStoresGenerate(ctx context.Context, queryContext table.QueryContext func parseDiskutilPhysicalStores(out []byte) ([]map[string]string, error) { var m CmdResult if _, err := plist.Unmarshal(out, &m); err != nil { - return nil, err + return nil, fmt.Errorf("parse diskutil apfs list -plist output: %w", err) } rows := make([]map[string]string, 0) diff --git a/orbit/pkg/table/pmset/pmset_darwin.go b/orbit/pkg/table/pmset/pmset_darwin.go index 65e27363c63..85149196d2d 100644 --- a/orbit/pkg/table/pmset/pmset_darwin.go +++ b/orbit/pkg/table/pmset/pmset_darwin.go @@ -67,6 +67,9 @@ func parsePMSetOutput(output []byte) map[string]interface{} { curKey := "" for scanner.Scan() { line := scanner.Text() + if len(line) == 0 { + continue + } if line[0] != ' ' { curKey = strings.TrimSpace(line) result[curKey] = make(map[string]string) diff --git a/pkg/download/download.go b/pkg/download/download.go index b15a2cd75b7..86139e1a05e 100644 --- a/pkg/download/download.go +++ b/pkg/download/download.go @@ -142,7 +142,7 @@ func download(client *http.Client, u *url.URL, path string, extract bool) error } if err := os.Rename(tmpFile.Name(), path); err != nil { - return err + return fmt.Errorf("rename temporary file: %w", err) } moved = true diff --git a/server/activity/internal/mysql/new_activity.go b/server/activity/internal/mysql/new_activity.go index 3b1a25d2296..76c99b4e4bc 100644 --- a/server/activity/internal/mysql/new_activity.go +++ b/server/activity/internal/mysql/new_activity.go @@ -113,7 +113,10 @@ func (ds *Datastore) NewActivity( var sb strings.Builder if hostIDs := ah.HostIDs(); len(hostIDs) > 0 { sb.WriteString(insertActHostStmt) - actID, _ := res.LastInsertId() + actID, lastIDErr := res.LastInsertId() + if lastIDErr != nil { + return ctxerr.Wrap(ctx, lastIDErr, "get last insert id for activity") + } for _, hid := range hostIDs { // >>> OPENFRAME(mysql-multitenancy) if stampTeam { diff --git a/server/aws_common/iam_auth.go b/server/aws_common/iam_auth.go index f07c900467a..0abcdba4e70 100644 --- a/server/aws_common/iam_auth.go +++ b/server/aws_common/iam_auth.go @@ -19,8 +19,9 @@ const ( // IAMTokenCache holds a cached token and its generation time type IAMTokenCache struct { - token string - generated time.Time + token string + generated time.Time + expiryTime time.Duration } // TokenGenerator is a function that generates a new IAM authentication token @@ -45,13 +46,9 @@ func NewIAMAuthTokenManager(tokenGen TokenGenerator) *IAMAuthTokenManager { // GetToken retrieves a valid IAM authentication token, using cache when possible func (m *IAMAuthTokenManager) GetToken(ctx context.Context) (string, error) { - // Calculate expiry time with jitter - jitter := time.Duration(rand.Int63n(int64(maxJitter))) //nolint:gosec // jitter doesn't need cryptographic randomness - expiryTime := tokenRefreshTime + jitter - // Check if we have a valid cached token m.cacheMu.RLock() - if m.cache != nil && time.Since(m.cache.generated) < expiryTime { + if m.cache != nil && time.Since(m.cache.generated) < m.cache.expiryTime { token := m.cache.token m.cacheMu.RUnlock() return token, nil @@ -63,7 +60,7 @@ func (m *IAMAuthTokenManager) GetToken(ctx context.Context) (string, error) { defer m.cacheMu.Unlock() // Double-check in case another goroutine generated a token while we were waiting - if m.cache != nil && time.Since(m.cache.generated) < expiryTime { + if m.cache != nil && time.Since(m.cache.generated) < m.cache.expiryTime { return m.cache.token, nil } @@ -72,9 +69,14 @@ func (m *IAMAuthTokenManager) GetToken(ctx context.Context) (string, error) { return "", err } + // Calculate expiry time with jitter, fixed at generation time + jitter := time.Duration(rand.Int63n(int64(maxJitter))) //nolint:gosec // jitter doesn't need cryptographic randomness + expiryTime := tokenRefreshTime + jitter + m.cache = &IAMTokenCache{ - token: token, - generated: time.Now(), + token: token, + generated: time.Now(), + expiryTime: expiryTime, } return token, nil diff --git a/server/datastore/mysql/cron_stats.go b/server/datastore/mysql/cron_stats.go index a761a197275..613bbdc75bf 100644 --- a/server/datastore/mysql/cron_stats.go +++ b/server/datastore/mysql/cron_stats.go @@ -69,7 +69,7 @@ func (ds *Datastore) UpdateCronStats(ctx context.Context, id int, status fleet.C stmt := `UPDATE cron_stats SET status = ?, errors = ? WHERE id = ?` errorsJSON := sql.NullString{} - if len(*cronErrors) > 0 { + if cronErrors != nil && len(*cronErrors) > 0 { b, err := json.Marshal(cronErrors) if err == nil { errorsJSON.String = string(b) diff --git a/server/datastore/mysql/migrations/openframe/20260301000002_AddQueryHostsJoinTable.go b/server/datastore/mysql/migrations/openframe/20260301000002_AddQueryHostsJoinTable.go index 2a746016f61..28c8c0e6792 100644 --- a/server/datastore/mysql/migrations/openframe/20260301000002_AddQueryHostsJoinTable.go +++ b/server/datastore/mysql/migrations/openframe/20260301000002_AddQueryHostsJoinTable.go @@ -30,5 +30,7 @@ CREATE TABLE IF NOT EXISTS query_hosts ( } func Down_20260301000002(tx *sql.Tx) error { - return nil + _, err := tx.Exec("DROP TABLE IF EXISTS query_hosts;") + return err } + diff --git a/server/datastore/mysql/migrations/tables/20210601000008_TeamsEnrollSecrets.go b/server/datastore/mysql/migrations/tables/20210601000008_TeamsEnrollSecrets.go index 81c45857cb1..8b2f70e7741 100644 --- a/server/datastore/mysql/migrations/tables/20210601000008_TeamsEnrollSecrets.go +++ b/server/datastore/mysql/migrations/tables/20210601000008_TeamsEnrollSecrets.go @@ -57,6 +57,7 @@ func Up_20210601000008(tx *sql.Tx) error { if err != nil { return errors.Wrap(err, "remove duplicate secrets") } + defer rows.Close() type sec struct { secret string count int @@ -71,6 +72,9 @@ func Up_20210601000008(tx *sql.Tx) error { } secretsToReduce = append(secretsToReduce, sec{secret: secret, count: c}) } + if err := rows.Err(); err != nil { + return errors.Wrap(err, "iterating duplicated secrets") + } for _, s := range secretsToReduce { // Remove duplicate secrets if _, err := tx.Exec( diff --git a/server/datastore/mysql/migrations/tables/20210818151827_RemoveForeignKeysSchedQStats.go b/server/datastore/mysql/migrations/tables/20210818151827_RemoveForeignKeysSchedQStats.go index c856dc620e0..83a30a367bd 100644 --- a/server/datastore/mysql/migrations/tables/20210818151827_RemoveForeignKeysSchedQStats.go +++ b/server/datastore/mysql/migrations/tables/20210818151827_RemoveForeignKeysSchedQStats.go @@ -41,6 +41,7 @@ func constraintsForTable(tx *sql.Tx, table string, referencedTables map[string]s if err != nil { return nil, errors.Wrapf(err, "getting fk for %s", table) } + defer rows.Close() for rows.Next() { var constraintName string var referencedTable sql.NullString @@ -57,6 +58,9 @@ func constraintsForTable(tx *sql.Tx, table string, referencedTables map[string]s constraints = append(constraints, constraintName) } } + if err := rows.Err(); err != nil { + return nil, errors.Wrapf(err, "iterating fk rows for %s", table) + } return constraints, nil } diff --git a/server/datastore/mysql/migrations/tables/20250701155654_AddEULAHashColumn_test.go b/server/datastore/mysql/migrations/tables/20250701155654_AddEULAHashColumn_test.go index e0cfbcb26cf..6469c5f5475 100644 --- a/server/datastore/mysql/migrations/tables/20250701155654_AddEULAHashColumn_test.go +++ b/server/datastore/mysql/migrations/tables/20250701155654_AddEULAHashColumn_test.go @@ -15,7 +15,7 @@ func TestUp_20250701155654(t *testing.T) { hash := sha256.New() _, _ = hash.Write(eulaBytes) - sha256 := hash.Sum(nil) + expectedHash := hash.Sum(nil) execNoErr(t, db, `INSERT INTO eulas (id, bytes, token, name) VALUES (?, ?, ?, ?)`, @@ -29,5 +29,5 @@ func TestUp_20250701155654(t *testing.T) { err := db.Get(&got, `SELECT sha256 FROM eulas WHERE id = ?`, 1) require.NoError(t, err) - require.True(t, bytes.Equal(got, sha256)) + require.True(t, bytes.Equal(got, expectedHash)) } diff --git a/server/mdm/acme/internal/service/challenge.go b/server/mdm/acme/internal/service/challenge.go index 39933dc6a50..a321b15fbc6 100644 --- a/server/mdm/acme/internal/service/challenge.go +++ b/server/mdm/acme/internal/service/challenge.go @@ -9,6 +9,7 @@ import ( "encoding/base64" "encoding/pem" "fmt" + "sync" "time" "github.com/fleetdm/fleet/v4/server/contexts/ctxerr" @@ -36,6 +37,31 @@ var ( OIDAppleNonce = asn1.ObjectIdentifier{1, 2, 840, 113635, 100, 8, 11, 1} ) +var ( + appleEnterpriseAttestationRootCAPoolOnce sync.Once + appleEnterpriseAttestationRootCAPool *x509.CertPool + appleEnterpriseAttestationRootCAPoolErr error +) + +func getAppleEnterpriseAttestationRootCAPool() (*x509.CertPool, error) { + appleEnterpriseAttestationRootCAPoolOnce.Do(func() { + rootCABlock, _ := pem.Decode([]byte(appleEnterpriseAttestationRootCA)) + if rootCABlock == nil { + appleEnterpriseAttestationRootCAPoolErr = fmt.Errorf("Failed to parse Apple Enterprise Attestation Root CA certificate") + return + } + rootCA, err := x509.ParseCertificate(rootCABlock.Bytes) + if err != nil { + appleEnterpriseAttestationRootCAPoolErr = fmt.Errorf("Failed to parse Apple Enterprise Attestation Root CA certificate: %s", err.Error()) + return + } + pool := x509.NewCertPool() + pool.AddCert(rootCA) + appleEnterpriseAttestationRootCAPool = pool + }) + return appleEnterpriseAttestationRootCAPool, appleEnterpriseAttestationRootCAPoolErr +} + func (s *Service) ValidateChallenge(ctx context.Context, enrollment *types.Enrollment, account *types.Account, challengeID uint, payload string) (*types.ChallengeResponse, error) { ctx, span := tracer.Start(ctx, "acme.service.ValidateChallenge") defer span.End() @@ -128,16 +154,11 @@ func (s *Service) validateDeviceAttestationChallenge(ctx context.Context, enroll func (s *Service) validateAppleDeviceAttestationStatement(ctx context.Context, enrollment *types.Enrollment, challenge *types.Challenge, attStmt types.AppleDeviceAttestationStatement) error { roots := s.TestAppleRootCAs if roots == nil { - roots = x509.NewCertPool() - rootCABlock, _ := pem.Decode([]byte(appleEnterpriseAttestationRootCA)) - if rootCABlock == nil { - return types.BadAttestationStatementError("Failed to parse Apple Enterprise Attestation Root CA certificate") - } - rootCA, err := x509.ParseCertificate(rootCABlock.Bytes) + pool, err := getAppleEnterpriseAttestationRootCAPool() if err != nil { - return types.BadAttestationStatementError(fmt.Sprintf("Failed to parse Apple Enterprise Attestation Root CA certificate: %s", err.Error())) + return types.BadAttestationStatementError(err.Error()) } - roots.AddCert(rootCA) + roots = pool } if len(attStmt.X5C) < 1 { diff --git a/server/mdm/nanomdm/test/e2e/certauth.go b/server/mdm/nanomdm/test/e2e/certauth.go index 142b5303e88..fb15433aa13 100644 --- a/server/mdm/nanomdm/test/e2e/certauth.go +++ b/server/mdm/nanomdm/test/e2e/certauth.go @@ -79,8 +79,8 @@ func certAuthRetro(t *testing.T, ctx context.Context, store storage.CertAuthStor } func expectErr(t *testing.T, have, want error) { + t.Helper() if !errors.Is(have, want) { - t.Helper() t.Errorf("have: %v; want: %v", have, want) } } diff --git a/server/service/client_appconfig_test.go b/server/service/client_appconfig_test.go index 38be22c9ef6..e87525d0f5b 100644 --- a/server/service/client_appconfig_test.go +++ b/server/service/client_appconfig_test.go @@ -64,7 +64,7 @@ func TestValidateOrgLogoFile(t *testing.T) { // fleet.ValidateOrgLogoBytes fires its size check before // image.DecodeConfig, so the body content doesn't need to // decode as a real image. - body := make([]byte, orgLogoMaxFileSize+1) + body := make([]byte, fleet.OrgLogoMaxFileSize+1) err := validateOrgLogoFile(writeTempFile(t, "big.png", body)) require.Error(t, err) assert.ErrorContains(t, err, "100KB or less") diff --git a/tools/fleet-mcp/mcp_tools_policies.go b/tools/fleet-mcp/mcp_tools_policies.go index d0f85ed1490..a6f4549a583 100644 --- a/tools/fleet-mcp/mcp_tools_policies.go +++ b/tools/fleet-mcp/mcp_tools_policies.go @@ -66,6 +66,9 @@ func registerGetPolicyCompliance(s *server.MCPServer, fleetClient *FleetClient) if terr != nil { return mcp.NewToolResultError(fmt.Sprintf("Failed to resolve fleet %q: %v", fleet, terr)), nil } + if len(teamIDs) == 0 { + return mcp.NewToolResultError(fmt.Sprintf("Failed to resolve fleet %q: no matching team found", fleet)), nil + } compliance, err = fleetClient.GetTeamPolicyCompliance(ctx, fmt.Sprintf("%d", teamIDs[0]), policyID) } else { compliance, err = fleetClient.GetPolicyCompliance(ctx, policyID) diff --git a/tools/mdm/assets/main.go b/tools/mdm/assets/main.go index 2fd34c2870a..728aae166a9 100644 --- a/tools/mdm/assets/main.go +++ b/tools/mdm/assets/main.go @@ -72,15 +72,6 @@ func setupSharedFlags() { } func setupDS(privateKey, userName, password, address, name string) *mysql.Datastore { - db, err := sql.Open( - "mysql", - fmt.Sprintf("%s:%s@tcp(%s)/?multiStatements=true&tls=skip-verify", testUsername, testPassword, testAddress), - ) - if err != nil { - log.Fatal("opening MySQL connection:", err) - } - defer db.Close() - mysqlCfg := config.MysqlConfig{ Username: userName, Password: password, diff --git a/tools/terraform/fleetdm_client/fleetdm_client_test.go b/tools/terraform/fleetdm_client/fleetdm_client_test.go index 6d0ad8226d7..b897f4419d3 100644 --- a/tools/terraform/fleetdm_client/fleetdm_client_test.go +++ b/tools/terraform/fleetdm_client/fleetdm_client_test.go @@ -37,6 +37,9 @@ func TestBasic(t *testing.T) { team, err := client.CreateTeam(teamName, defaultDescription) require.NoError(t, err) require.NotNil(t, team) + t.Cleanup(func() { + _ = client.DeleteTeam(team.Team.ID) + }) require.Equal(t, teamName, team.Team.Name) require.Equal(t, defaultDescription, team.Team.Description) aoBytes, err := json.Marshal(team.Team.AgentOptions) diff --git a/website/assets/js/pages/docs/command-details.page.js b/website/assets/js/pages/docs/command-details.page.js index 4d138fd5f33..439c24591bb 100644 --- a/website/assets/js/pages/docs/command-details.page.js +++ b/website/assets/js/pages/docs/command-details.page.js @@ -22,11 +22,11 @@ parasails.registerPage('command-details', { $('[purpose="copy-button"]').on('click', async function() { let code = $(this).closest('[purpose="codeblock"]').find('pre:visible code').text(); if(code) { + navigator.clipboard.writeText(code); $(this).addClass('copied'); - await setTimeout(()=>{ + setTimeout(()=>{ $(this).removeClass('copied'); }, 2000); - navigator.clipboard.writeText(code); } }); },