diff --git a/docs/Contributing/reference/audit-logs.md b/docs/Contributing/reference/audit-logs.md index ec754cb806..cbb5350b8c 100644 --- a/docs/Contributing/reference/audit-logs.md +++ b/docs/Contributing/reference/audit-logs.md @@ -1343,6 +1343,7 @@ This activity contains the following fields: - "software_title": Name of the software. - "software_package": Filename of the installer. - "status": Status of the software installation. +- "source": Software source type (e.g., "pkg_packages", "sh_packages", "ps1_packages"). - "policy_id": ID of the policy whose failure triggered the installation. Null if no associated policy. - "policy_name": Name of the policy whose failure triggered installation. Null if no associated policy. @@ -1358,6 +1359,7 @@ This activity contains the following fields: "self_service": true, "install_uuid": "d6cffa75-b5b5-41ef-9230-15073c8a88cf", "status": "pending", + "source": "pkg_packages", "policy_id": 1337, "policy_name": "Ensure 1Password is installed and up to date" } @@ -1374,6 +1376,7 @@ This activity contains the following fields: - "script_execution_id": ID of the software uninstall script. - "self_service": Whether the uninstallation was initiated by the end user from the My device UI. - "status": Status of the software uninstallation. +- "source": Software source type (e.g., "pkg_packages", "sh_packages", "ps1_packages"). #### Example @@ -1384,7 +1387,8 @@ This activity contains the following fields: "software_title": "Falcon.app", "script_execution_id": "ece8d99d-4313-446a-9af2-e152cd1bad1e", "self_service": false, - "status": "uninstalled" + "status": "uninstalled", + "source": "pkg_packages" } ``` diff --git a/ee/server/service/orbit.go b/ee/server/service/orbit.go index 606a905530..2c295802d2 100644 --- a/ee/server/service/orbit.go +++ b/ee/server/service/orbit.go @@ -259,12 +259,23 @@ func (svc *Service) failCancelledSetupExperienceInstalls( // https://github.com/fleetdm/fleet/issues/34288 if r.IsForSoftwarePackage() { softwarePackage := "" + var source *string installerMeta, err := svc.ds.GetSoftwareInstallerMetadataByID(ctx, *r.SoftwareInstallerID) if err != nil && !fleet.IsNotFound(err) { return ctxerr.Wrap(ctx, err, "getting software installer metadata for cancelled setup experience software install") } if installerMeta != nil { softwarePackage = installerMeta.Name + // Get the software title to retrieve the source + if installerMeta.TitleID != nil { + title, err := svc.ds.SoftwareTitleByID(ctx, *installerMeta.TitleID, nil, fleet.TeamFilter{}) + if err != nil && !fleet.IsNotFound(err) { + return ctxerr.Wrap(ctx, err, "getting software title for cancelled setup experience software install") + } + if title != nil { + source = &title.Source + } + } } activity := fleet.ActivityTypeInstalledSoftware{ HostID: hostID, @@ -274,6 +285,7 @@ func (svc *Service) failCancelledSetupExperienceInstalls( InstallUUID: *r.HostSoftwareInstallsExecutionID, Status: "failed", SelfService: false, + Source: source, FromSetupExperience: true, } err = svc.NewActivity(ctx, nil, activity) diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index 3c1a8f7019..d520a11ad4 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -1707,7 +1707,12 @@ func (svc *Service) addScriptPackageMetadata(ctx context.Context, payload *fleet payload.BundleIdentifier = "" payload.PackageIDs = nil payload.Extension = extension - payload.Source = "scripts" + switch extension { + case "sh": + payload.Source = "sh_packages" + case "ps1": + payload.Source = "ps1_packages" + } platform, err := fleet.SoftwareInstallerPlatformFromExtension(extension) if err != nil { @@ -2413,11 +2418,11 @@ func (svc *Service) SelfServiceInstallSoftwareTitle(ctx context.Context, host *f func packageExtensionToPlatform(ext string) string { var requiredPlatform string switch ext { - case ".msi", ".exe": + case ".msi", ".exe", ".ps1": requiredPlatform = "windows" case ".pkg", ".dmg", ".zip": requiredPlatform = "darwin" - case ".deb", ".rpm", ".gz", ".tgz": + case ".deb", ".rpm", ".gz", ".tgz", ".sh": requiredPlatform = "linux" default: return "" diff --git a/ee/server/service/software_installers_test.go b/ee/server/service/software_installers_test.go index 4b6421acf1..6eb1d6ea46 100644 --- a/ee/server/service/software_installers_test.go +++ b/ee/server/service/software_installers_test.go @@ -400,7 +400,7 @@ func TestAddScriptPackageMetadata(t *testing.T) { require.Equal(t, "", payload.Version) require.Equal(t, scriptContents, payload.InstallScript) require.Equal(t, "linux", payload.Platform) - require.Equal(t, "scripts", payload.Source) + require.Equal(t, "sh_packages", payload.Source) require.Empty(t, payload.BundleIdentifier) require.Empty(t, payload.PackageIDs) require.NotEmpty(t, payload.StorageID) @@ -430,7 +430,7 @@ func TestAddScriptPackageMetadata(t *testing.T) { require.Equal(t, "", payload.Version) require.Equal(t, scriptContents, payload.InstallScript) require.Equal(t, "windows", payload.Platform) - require.Equal(t, "scripts", payload.Source) + require.Equal(t, "ps1_packages", payload.Source) require.Empty(t, payload.BundleIdentifier) require.Empty(t, payload.PackageIDs) require.NotEmpty(t, payload.StorageID) diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx index f7e7e4a6e4..229182c981 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tests.tsx @@ -131,7 +131,7 @@ describe("SoftwareScriptDetailsModal - ModalButtons component", () => { expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument(); await user.click(screen.getByRole("button", { name: "Rerun" })); - expect(onRerun).toHaveBeenCalledWith(99); + expect(onRerun).toHaveBeenCalledWith(99, true); expect(onCancel).toHaveBeenCalled(); await user.click(screen.getByRole("button", { name: "Cancel" })); diff --git a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx index 955a69ade7..14fd65ebda 100644 --- a/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx +++ b/frontend/components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal.tsx @@ -144,7 +144,7 @@ interface IModalButtonsProps { deviceAuthToken?: string; installResultStatus?: string; hostSoftwareId?: number; - onRerun?: (id: number) => void; + onRerun?: (id: number, isScriptPackage: boolean) => void; onCancel: () => void; } @@ -159,7 +159,7 @@ export const ModalButtons = ({ const onClickRerun = () => { // on My Device Page, where this is relevant, both will be defined if (onRerun && hostSoftwareId) { - onRerun(hostSoftwareId); + onRerun(hostSoftwareId, true); // isScriptPackage defined for copy changes } onCancel(); }; @@ -190,7 +190,7 @@ interface ISoftwareInstallDetailsProps { hostSoftware?: IHostSoftware; // for software name when not Fleet installed (not present on activity feeds) deviceAuthToken?: string; // My Device Page only onCancel: () => void; - onRerun?: (id: number) => void; // My Device Page only + onRerun?: (id: number, isScriptPackage?: boolean) => void; // My Device Page only contactUrl?: string; // My Device Page only } diff --git a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx index 9a460388b0..3e536effe3 100644 --- a/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx +++ b/frontend/pages/DashboardPage/cards/ActivityFeed/ActivityFeed.tsx @@ -12,6 +12,7 @@ import activitiesAPI, { import { resolveUninstallStatus, SoftwareInstallUninstallStatus, + SCRIPT_PACKAGE_SOURCES, } from "interfaces/software"; import { ActivityType, IActivityDetails } from "interfaces/activity"; @@ -24,6 +25,7 @@ import Pagination from "components/Pagination"; import VppInstallDetailsModal from "components/ActivityDetails/InstallDetails/VppInstallDetailsModal"; import { SoftwareInstallDetailsModal } from "components/ActivityDetails/InstallDetails/SoftwareInstallDetailsModal/SoftwareInstallDetailsModal"; +import SoftwareScriptDetailsModal from "components/ActivityDetails/InstallDetails/SoftwareScriptDetailsModal/SoftwareScriptDetailsModal"; import SoftwareUninstallDetailsModal, { ISWUninstallDetailsParentState, } from "components/ActivityDetails/InstallDetails/SoftwareUninstallDetailsModal/SoftwareUninstallDetailsModal"; @@ -58,6 +60,10 @@ const ActivityFeed = ({ packageInstallDetails, setPackageInstallDetails, ] = useState(null); + const [ + scriptPackageDetails, + setScriptPackageDetails, + ] = useState(null); const [ packageUninstallDetails, setPackageUninstallDetails, @@ -135,7 +141,11 @@ const ActivityFeed = ({ setShowScriptDetailsModal(true); break; case ActivityType.InstalledSoftware: - setPackageInstallDetails({ ...details }); + if (SCRIPT_PACKAGE_SOURCES.includes(details?.source || "")) { + setScriptPackageDetails({ ...details }); + } else { + setPackageInstallDetails({ ...details }); + } break; case ActivityType.UninstalledSoftware: setPackageUninstallDetails({ @@ -256,6 +266,12 @@ const ActivityFeed = ({ onCancel={() => setPackageInstallDetails(null)} /> )} + {scriptPackageDetails && ( + setScriptPackageDetails(null)} + /> + )} {packageUninstallDetails && ( {renderNoEnrollSecretBanner()} {renderTable()} diff --git a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx index 3737251a7f..80f7be18f4 100644 --- a/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx +++ b/frontend/pages/hosts/ManageHostsPage/components/HostsFilterBlock/HostsFilterBlock.tsx @@ -110,6 +110,7 @@ interface IHostsFilterBlockProps { onClickEditLabel: (evt: React.MouseEvent) => void; onClickDeleteLabel: () => void; isLoading?: boolean; + isScriptPackage?: boolean; } /** @@ -164,6 +165,7 @@ const HostsFilterBlock = ({ onClickEditLabel, onClickDeleteLabel, isLoading = false, + isScriptPackage, }: IHostsFilterBlockProps) => { const { currentUser, isOnGlobalTeam } = useContext(AppContext); @@ -522,7 +524,7 @@ const HostsFilterBlock = ({ const renderSoftwareInstallStatusBlock = () => { const OPTIONS = [ - { value: "installed", label: "Installed" }, + { value: "installed", label: isScriptPackage ? "Ran" : "Installed" }, { value: "failed", label: "Failed" }, { value: "pending", label: "Pending" }, ]; diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx index e9386558bc..de7310530e 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostInstallerActionCell/HostInstallerActionCell.tsx @@ -17,6 +17,7 @@ import { IHostAppStoreApp, EnhancedSoftwareInstallUninstallStatus, IHostSoftwareWithUiStatus, + SCRIPT_PACKAGE_SOURCES, } from "interfaces/software"; import { IconNames } from "components/icons"; import { @@ -55,7 +56,10 @@ interface IHostInstallerActionButtonProps { interface IHostInstallerActionCellProps { software: IHostSoftwareWithUiStatus; - onClickInstallAction: (softwareId: number) => void; + onClickInstallAction: ( + softwareId: number, + isSoftwarePackage?: boolean + ) => void; onClickUninstallAction: () => void; onClickOpenInstructionsAction?: () => void; baseClass: string; @@ -287,7 +291,12 @@ export const HostInstallerActionCell = ({ baseClass={baseClass} tooltip={installTooltip} disabled={installDisabled} - onClick={() => onClickInstallAction(id)} + onClick={() => + onClickInstallAction( + id, + SCRIPT_PACKAGE_SOURCES.includes(software.source) + ) + } icon={buttonDisplayConfig.install.icon} text={buttonDisplayConfig.install.text} testId={`${baseClass}__install-button--test`} diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx index 8514e0f36d..a763547ad5 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibrary.tsx @@ -434,20 +434,30 @@ const HostSoftwareLibrary = ({ }, []); const onClickInstallAction = useCallback( - async (softwareId: number) => { + async (softwareId: number, isScriptPackage = false) => { try { await hostAPI.installHostSoftwarePackage(id as number, softwareId); if (isMountedRef.current) { onInstallOrUninstall(); } + + const message = () => { + switch (true) { + case isHostOnline && isScriptPackage: + return "Script is running."; + case isHostOnline && !isScriptPackage: + return "Software is installing."; + case !isHostOnline && isScriptPackage: + return "Script will run when the host comes online."; + default: + return "Software will install when the host comes online."; + } + }; + renderFlash( "success", <> - Software{" "} - {isHostOnline - ? "is installing" - : "will install when the host comes online"} - . To see details, go to Details > Activity. + {message()} To see details, go to Details > Activity. ); } catch (e) { diff --git a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibraryTable/HostSoftwareLibraryTableConfig.tsx b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibraryTable/HostSoftwareLibraryTableConfig.tsx index fa61557e5c..198c61342a 100644 --- a/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibraryTable/HostSoftwareLibraryTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/HostSoftwareLibrary/HostSoftwareLibraryTable/HostSoftwareLibraryTableConfig.tsx @@ -55,7 +55,7 @@ interface IHostSWLibraryTableHeaders { details?: ISWUninstallDetailsParentState ) => void; onSetSelectedVPPInstallDetails: (s: IVPPHostSoftware) => void; - onClickInstallAction: (softwareId: number) => void; + onClickInstallAction: (softwareId: number, isScriptPackage?: boolean) => void; onClickUninstallAction: (softwareId: number) => void; isHostOnline: boolean; hostName: string; diff --git a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx index 962a768376..b1f9cb0619 100644 --- a/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx +++ b/frontend/pages/hosts/details/cards/Software/InstallStatusCell/InstallStatusCell.tsx @@ -373,7 +373,7 @@ const getEmptyCellTooltip = ( return ( <> - {softwareName ? {softwareName} : "Software"} can be + {softwareName ? {softwareName} : "Software"} can be{" "} {isScriptPackage ? "ran" : "installed"} on the host.
Select Actions > Install to install. diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx index 99412120cc..36b6c015df 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/SelfService.tsx @@ -350,7 +350,7 @@ const SoftwareSelfService = ({ }, []); const onClickInstallAction = useCallback( - async (softwareId: number) => { + async (softwareId: number, isScriptPackage = false) => { try { await deviceApi.installSelfServiceSoftware(deviceToken, softwareId); if (isMountedRef.current) { @@ -358,7 +358,10 @@ const SoftwareSelfService = ({ } } catch (error) { // We only show toast message if API returns an error - renderFlash("error", "Couldn't install. Please try again."); + renderFlash( + "error", + `Couldn't ${isScriptPackage ? "run" : "install"}. Please try again.` + ); } }, [deviceToken, onInstallOrUninstall, renderFlash] diff --git a/frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceTableConfig.tsx b/frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceTableConfig.tsx index 28d45b527a..47adadae92 100644 --- a/frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceTableConfig.tsx +++ b/frontend/pages/hosts/details/cards/Software/SelfService/SelfServiceTableConfig.tsx @@ -46,7 +46,7 @@ interface ISelfServiceTableHeaders { onShowUninstallDetails: ( uninstallDetails: ISWUninstallDetailsParentState ) => void; - onClickInstallAction: (softwareId: number) => void; + onClickInstallAction: (softwareId: number, isScriptPackage?: boolean) => void; onClickUninstallAction: (software: IHostSoftwareWithUiStatus) => void; onClickOpenInstructionsAction: (software: IHostSoftwareWithUiStatus) => void; } diff --git a/frontend/services/entities/hosts.ts b/frontend/services/entities/hosts.ts index a3c83d13bc..dc9c48c55a 100644 --- a/frontend/services/entities/hosts.ts +++ b/frontend/services/entities/hosts.ts @@ -13,6 +13,7 @@ import { IHostSoftware, ISoftware, SoftwareAggregateStatus, + SoftwareSource, } from "interfaces/software"; import { DiskEncryptionStatus, @@ -35,8 +36,12 @@ export interface ISortOption { export interface ILoadHostsResponse { hosts: IHost[]; - software: ISoftware | undefined; - software_title: { name: string; version?: string } | null | undefined; // TODO: confirm type + software?: ISoftware; + software_title?: { + name: string; + version?: string; + source?: SoftwareSource; + } | null; munki_issue: IMunkiIssuesAggregate; mobile_device_management_solution: IMdmSolution; } diff --git a/frontend/utilities/software_install_scripts.ts b/frontend/utilities/software_install_scripts.ts index 045f94b70b..af9a63c8db 100644 --- a/frontend/utilities/software_install_scripts.ts +++ b/frontend/utilities/software_install_scripts.ts @@ -26,8 +26,9 @@ const getDefaultInstallScript = (fileName: string): string => { case "rpm": return installRPM; case "exe": - return ""; case "tar.gz": + case "sh": + case "ps1": return ""; default: throw new Error(`unsupported file extension: ${extension}`); diff --git a/frontend/utilities/software_uninstall_scripts.ts b/frontend/utilities/software_uninstall_scripts.ts index 9591a5e6b8..b96c2507f5 100644 --- a/frontend/utilities/software_uninstall_scripts.ts +++ b/frontend/utilities/software_uninstall_scripts.ts @@ -26,8 +26,9 @@ const getDefaultUninstallScript = (fileName: string): string => { case "rpm": return uninstallRPM; case "exe": - return ""; case "tar.gz": + case "sh": + case "ps1": return ""; default: throw new Error(`unsupported file extension: ${extension}`); diff --git a/server/datastore/mysql/activities.go b/server/datastore/mysql/activities.go index 41b34b2c3a..6992897a7d 100644 --- a/server/datastore/mysql/activities.go +++ b/server/datastore/mysql/activities.go @@ -379,6 +379,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint 'install_uuid', ua.execution_id, 'status', 'pending_install', 'self_service', ua.payload->'$.self_service' IS TRUE, + 'source', COALESCE(st.source, ua.payload->>'$.source'), 'policy_id', siua.policy_id, 'policy_name', p.name ) as details, @@ -420,6 +421,7 @@ func (ds *Datastore) ListHostUpcomingActivities(ctx context.Context, hostID uint 'script_execution_id', ua.execution_id, 'status', 'pending_uninstall', 'self_service', COALESCE(ua.payload->'$.self_service', FALSE) IS TRUE, + 'source', COALESCE(st.source, ua.payload->>'$.source'), 'policy_id', siua.policy_id, 'policy_name', p.name ) as details, diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index d08e4e70a2..eaacbb5d5b 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -975,7 +975,7 @@ func (ds *Datastore) InsertSoftwareInstallRequest(ctx context.Context, hostID ui const ( getInstallerStmt = ` SELECT - filename, "version", title_id, COALESCE(st.name, '[deleted title]') title_name + filename, "version", title_id, COALESCE(st.name, '[deleted title]') title_name, st.source FROM software_installers si LEFT JOIN software_titles st @@ -992,6 +992,7 @@ VALUES 'installer_filename', ?, 'version', ?, 'software_title_name', ?, + 'source', ?, 'user', (SELECT JSON_OBJECT('name', name, 'email', email, 'gravatar_url', gravatar_url) FROM users WHERE id = ?) ) )` @@ -1021,6 +1022,7 @@ VALUES Version string `db:"version"` TitleID *uint `db:"title_id"` TitleName *string `db:"title_name"` + Source *string `db:"source"` } if err = sqlx.GetContext(ctx, ds.reader(ctx), &installerDetails, getInstallerStmt, softwareInstallerID); err != nil { if err == sql.ErrNoRows { @@ -1047,6 +1049,7 @@ VALUES installerDetails.Filename, installerDetails.Version, installerDetails.TitleName, + installerDetails.Source, userID, ) if err != nil { @@ -1152,7 +1155,7 @@ func (ds *Datastore) runInstallerUpdateSideEffectsInTransaction(ctx context.Cont func (ds *Datastore) InsertSoftwareUninstallRequest(ctx context.Context, executionID string, hostID uint, softwareInstallerID uint, selfService bool) error { const ( - getInstallerStmt = `SELECT title_id, COALESCE(st.name, '[deleted title]') title_name + getInstallerStmt = `SELECT title_id, COALESCE(st.name, '[deleted title]') title_name, st.source FROM software_installers si LEFT JOIN software_titles st ON si.title_id = st.id WHERE si.id = ?` insertUAStmt = ` @@ -1164,6 +1167,7 @@ VALUES 'installer_filename', '', 'version', 'unknown', 'software_title_name', ?, + 'source', ?, 'user', (SELECT JSON_OBJECT('name', name, 'email', email, 'gravatar_url', gravatar_url) FROM users WHERE id = ?), 'self_service', ? ) @@ -1191,6 +1195,7 @@ VALUES var installerDetails struct { TitleID *uint `db:"title_id"` TitleName *string `db:"title_name"` + Source *string `db:"source"` } if err = sqlx.GetContext(ctx, ds.reader(ctx), &installerDetails, getInstallerStmt, softwareInstallerID); err != nil { if err == sql.ErrNoRows { @@ -1213,6 +1218,7 @@ VALUES false, executionID, installerDetails.TitleName, + installerDetails.Source, userID, selfService, ) @@ -1258,7 +1264,8 @@ SELECT hsi.host_deleted_at, hsi.policy_id, hsi.created_at as created_at, - hsi.updated_at as updated_at + hsi.updated_at as updated_at, + st.source FROM host_software_installs hsi LEFT JOIN software_titles st ON hsi.software_title_id = st.id @@ -1286,7 +1293,8 @@ SELECT NULL AS host_deleted_at, siua.policy_id AS policy_id, ua.created_at as created_at, - ua.updated_at as updated_at + ua.updated_at as updated_at, + st.source FROM upcoming_activities ua INNER JOIN software_install_upcoming_activities siua diff --git a/server/fleet/activities.go b/server/fleet/activities.go index f2987555d7..e13fe33da2 100644 --- a/server/fleet/activities.go +++ b/server/fleet/activities.go @@ -1837,6 +1837,7 @@ type ActivityTypeInstalledSoftware struct { SelfService bool `json:"self_service"` InstallUUID string `json:"install_uuid"` Status string `json:"status"` + Source *string `json:"source,omitempty"` PolicyID *uint `json:"policy_id"` PolicyName *string `json:"policy_name"` FromSetupExperience bool `json:"-"` @@ -1864,6 +1865,7 @@ func (a ActivityTypeInstalledSoftware) Documentation() (activity, details, detai - "software_title": Name of the software. - "software_package": Filename of the installer. - "status": Status of the software installation. +- "source": Software source type (e.g., "pkg_packages", "sh_packages", "ps1_packages"). - "policy_id": ID of the policy whose failure triggered the installation. Null if no associated policy. - "policy_name": Name of the policy whose failure triggered installation. Null if no associated policy. `, `{ @@ -1874,18 +1876,20 @@ func (a ActivityTypeInstalledSoftware) Documentation() (activity, details, detai "self_service": true, "install_uuid": "d6cffa75-b5b5-41ef-9230-15073c8a88cf", "status": "pending", + "source": "pkg_packages", "policy_id": 1337, "policy_name": "Ensure 1Password is installed and up to date" }` } type ActivityTypeUninstalledSoftware struct { - HostID uint `json:"host_id"` - HostDisplayName string `json:"host_display_name"` - SoftwareTitle string `json:"software_title"` - ExecutionID string `json:"script_execution_id"` - SelfService bool `json:"self_service"` - Status string `json:"status"` + HostID uint `json:"host_id"` + HostDisplayName string `json:"host_display_name"` + SoftwareTitle string `json:"software_title"` + ExecutionID string `json:"script_execution_id"` + SelfService bool `json:"self_service"` + Status string `json:"status"` + Source *string `json:"source,omitempty"` } func (a ActivityTypeUninstalledSoftware) ActivityName() string { @@ -1904,13 +1908,15 @@ func (a ActivityTypeUninstalledSoftware) Documentation() (activity, details, det - "software_title": Name of the software. - "script_execution_id": ID of the software uninstall script. - "self_service": Whether the uninstallation was initiated by the end user from the My device UI. -- "status": Status of the software uninstallation.`, `{ +- "status": Status of the software uninstallation. +- "source": Software source type (e.g., "pkg_packages", "sh_packages", "ps1_packages").`, `{ "host_id": 1, "host_display_name": "Anna's MacBook Pro", "software_title": "Falcon.app", "script_execution_id": "ece8d99d-4313-446a-9af2-e152cd1bad1e", "self_service": false, - "status": "uninstalled" + "status": "uninstalled", + "source": "pkg_packages" }` } diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index c5f6c948a3..a7aa77a203 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -378,6 +378,8 @@ type HostSoftwareInstallerResult struct { SoftwareInstallerID *uint `json:"-" db:"software_installer_id"` // SoftwarePackage is the name of the software installer package. SoftwarePackage string `json:"software_package" db:"software_package"` + // Source is the osquery source for this software (e.g., "sh_packages", "ps1_packages"). + Source *string `json:"source" db:"source"` // HostID is the ID of the host. HostID uint `json:"host_id" db:"host_id"` // Status is the status of the software installer package on the host. @@ -584,8 +586,10 @@ func SofwareInstallerSourceFromExtensionAndName(ext, name string) (string, error return "pkg_packages", nil case "tar.gz": return "tgz_packages", nil - case "sh", "ps1": - return "scripts", nil + case "sh": + return "sh_packages", nil + case "ps1": + return "ps1_packages", nil default: return "", fmt.Errorf("unsupported file type: %s", ext) } diff --git a/server/fleet/software_installer_test.go b/server/fleet/software_installer_test.go index 79470ad220..425d80e62b 100644 --- a/server/fleet/software_installer_test.go +++ b/server/fleet/software_installer_test.go @@ -203,10 +203,10 @@ func TestSofwareInstallerSourceFromExtensionAndName(t *testing.T) { {"tar.gz", "archive.tar.gz", "tgz_packages", false}, // New script extensions - {".sh", "script.sh", "scripts", false}, - {"sh", "setup.sh", "scripts", false}, - {".ps1", "script.ps1", "scripts", false}, - {"ps1", "setup.ps1", "scripts", false}, + {".sh", "script.sh", "sh_packages", false}, + {"sh", "setup.sh", "sh_packages", false}, + {".ps1", "script.ps1", "ps1_packages", false}, + {"ps1", "setup.ps1", "ps1_packages", false}, // Unsupported extensions {".zip", "archive.zip", "", true}, diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index 45afc82e89..2e345e47fe 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -14201,6 +14201,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { SoftwarePackage: payload.Filename, InstallUUID: installUUIDs[0], Status: string(fleet.SoftwareInstallFailed), + Source: ptr.String("deb_packages"), } s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) @@ -14225,6 +14226,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { SoftwarePackage: payload2.Filename, InstallUUID: installUUIDs[1], Status: string(fleet.SoftwareInstallFailed), + Source: ptr.String("deb_packages"), } s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) @@ -14255,6 +14257,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { SoftwarePackage: payload3.Filename, InstallUUID: installUUIDs[2], Status: string(fleet.SoftwareInstalled), + Source: ptr.String("deb_packages"), } lastActID := s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) @@ -14291,6 +14294,7 @@ func (s *integrationEnterpriseTestSuite) TestHostSoftwareInstallResult() { SoftwarePackage: payload3.Filename, InstallUUID: installUUIDs[2], Status: string(fleet.SoftwareInstallFailed), + Source: ptr.String("deb_packages"), } s.lastActivityOfTypeMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) @@ -16578,6 +16582,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers "self_service": false, "install_uuid": "%s", "status": "installed", + "source": "apps", "policy_id": %d, "policy_name": "%s" }`, host1Team1.ID, host1Team1.DisplayName(), "DummyApp", "dummy_installer.pkg", host1LastInstall.ExecutionID, policy1Team1.ID, policy1Team1.Name), 0) @@ -16598,6 +16603,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers "self_service": false, "install_uuid": "%s", "status": "%s", + "source": "deb_packages", "policy_id": %d, "policy_name": "%s" }`, host2Team1.ID, host2Team1.DisplayName(), "ruby", "ruby.deb", host2LastInstall.ExecutionID, fleet.SoftwareInstallFailed, policy2Team1.ID, policy2Team1.Name), 0) @@ -16640,6 +16646,7 @@ func (s *integrationEnterpriseTestSuite) TestPolicyAutomationsSoftwareInstallers "self_service": false, "install_uuid": "%s", "status": "%s", + "source": "programs", "policy_id": %f, "policy_name": "%s" }`, host3Team2.ID, host3Team2.DisplayName(), "Fleet osquery", "fleet-osquery.msi", host3LastInstall.ExecutionID, @@ -17588,6 +17595,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareUploadRPM() { SoftwarePackage: payload.Filename, InstallUUID: installUUID, Status: string(fleet.SoftwareInstallFailed), + Source: ptr.String("rpm_packages"), } s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) } @@ -18512,6 +18520,7 @@ func (s *integrationEnterpriseTestSuite) TestSoftwareInstallerOrbitDownloadFailu SoftwarePackage: payload.Filename, InstallUUID: swInstallExecID, Status: string(fleet.SoftwareInstalled), + Source: ptr.String("deb_packages"), } s.lastActivityMatches(wantAct.ActivityName(), string(jsonMustMarshal(t, wantAct)), 0) } @@ -20311,6 +20320,7 @@ func (s *integrationEnterpriseTestSuite) TestSetupExperienceLinuxWithSoftware() "install_uuid": %q, "status": "failed", "self_service": false, + "source": "deb_packages", "policy_name": null, "policy_id": null }`, ubuntuHost.ID, ubuntuHost.DisplayName(), "vim", "vim.deb", executionIDs["vim"]), 0) diff --git a/server/service/integration_mdm_setup_experience_test.go b/server/service/integration_mdm_setup_experience_test.go index dcb279167d..c42611f576 100644 --- a/server/service/integration_mdm_setup_experience_test.go +++ b/server/service/integration_mdm_setup_experience_test.go @@ -395,6 +395,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu "self_service": false, "software_title": "%s", "software_package": "%s", + "source": "apps", "host_display_name": "%s" } `, enrolledHost.ID, installUUID, getSoftwareTitleResp.SoftwareTitle.Name, getSoftwareTitleResp.SoftwareTitle.SoftwarePackage.Name, enrolledHost.DisplayName()) @@ -484,6 +485,7 @@ func (s *integrationMDMTestSuite) TestSetupExperienceFlowWithSoftwareAndScriptAu "self_service": false, "install_uuid": "%s", "status": "installed", + "source": "apps", "policy_id": null, "policy_name": null } diff --git a/server/service/orbit.go b/server/service/orbit.go index 183aa71708..cc270ca8ab 100644 --- a/server/service/orbit.go +++ b/server/service/orbit.go @@ -1398,6 +1398,7 @@ func (svc *Service) SaveHostSoftwareInstallResult(ctx context.Context, result *f SoftwarePackage: hsi.SoftwarePackage, InstallUUID: failedExecID, Status: string(result.Status()), + Source: hsi.Source, SelfService: hsi.SelfService, PolicyID: nil, PolicyName: nil, @@ -1480,6 +1481,7 @@ func (svc *Service) SaveHostSoftwareInstallResult(ctx context.Context, result *f SoftwarePackage: hsi.SoftwarePackage, InstallUUID: result.InstallUUID, Status: string(status), + Source: hsi.Source, SelfService: hsi.SelfService, PolicyID: hsi.PolicyID, PolicyName: policyName,