fleet/frontend/services/entities/software.ts
2026-03-13 16:47:09 -04:00

813 lines
23 KiB
TypeScript

import { AxiosProgressEvent } from "axios";
import sendRequest, {
sendRequestWithHeaders,
sendRequestWithProgressAndHeaders,
} from "services";
import endpoints from "utilities/endpoints";
import {
encodeScriptBase64,
SCRIPTS_ENCODED_HEADER,
} from "utilities/scripts_encoding";
import software, {
ISoftwareResponse,
ISoftwareCountResponse,
ISoftwareVersion,
ISoftwareTitle,
ISoftwareTitleDetails,
IFleetMaintainedApp,
IFleetMaintainedAppDetails,
ISoftwarePackage,
SoftwareCategory,
} from "interfaces/software";
import {
ApplePlatform,
CommaSeparatedPlatformString,
} from "interfaces/platform";
import {
buildQueryStringFromParams,
convertParamsToSnakeCase,
getPathWithQueryParams,
} from "utilities/url";
import { IPackageFormData } from "pages/SoftwarePage/components/forms/PackageForm/PackageForm";
import { IEditPackageFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditSoftwareModal/EditSoftwareModal";
import { ISoftwareVppFormData } from "pages/SoftwarePage/components/forms/SoftwareVppForm/SoftwareVppForm";
import { ISoftwareAutoUpdateConfigFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditAutoUpdateConfigModal/EditAutoUpdateConfigModal";
import { ISoftwareDisplayNameFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditIconModal/EditIconModal";
import { IAddFleetMaintainedData } from "pages/SoftwarePage/SoftwareAddPage/SoftwareFleetMaintained/FleetMaintainedAppDetailsPage/FleetMaintainedAppDetailsPage";
import { listNamesFromSelectedLabels } from "components/TargetLabelSelector/TargetLabelSelector";
import { ISoftwareAndroidFormData } from "pages/SoftwarePage/components/forms/SoftwareAndroidForm/SoftwareAndroidForm";
import { ISoftwareConfigurationFormData } from "pages/SoftwarePage/SoftwareTitleDetailsPage/EditConfigurationModal/EditConfigurationModal";
export interface ISoftwareApiParams {
page?: number;
perPage?: number;
orderKey?: string;
orderDirection?: "asc" | "desc";
query?: string;
vulnerable?: boolean;
max_cvss_score?: number;
min_cvss_score?: number;
exploit?: boolean;
availableForInstall?: boolean;
packagesOnly?: boolean;
selfService?: boolean;
teamId?: number;
}
export interface ISoftwareTitlesResponse {
counts_updated_at: string | null;
count: number;
software_titles: ISoftwareTitle[];
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export interface ISoftwareVersionsResponse {
counts_updated_at: string | null;
count: number;
software: ISoftwareVersion[];
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export interface ISoftwareTitleResponse {
software_title: ISoftwareTitleDetails;
}
export interface ISoftwareVersionResponse {
software: ISoftwareVersion;
}
export interface ISoftwareVersionsQueryKey extends ISoftwareApiParams {
// used to trigger software refetches from sibling pages
addedSoftwareToken: string | null;
scope: "software-versions";
}
export interface ISoftwareTitlesQueryKey extends ISoftwareApiParams {
// used to trigger software refetches from sibling pages
addedSoftwareToken?: string | null;
platform?: CommaSeparatedPlatformString;
scope: "software-titles";
}
export interface ISoftwareQueryKey extends ISoftwareApiParams {
scope: "software";
}
export interface ISoftwareCountQueryKey
extends Pick<ISoftwareApiParams, "query" | "vulnerable" | "teamId"> {
scope: "softwareCount";
}
export interface IGetSoftwareTitleQueryParams {
softwareId: number;
teamId?: number;
}
export interface IGetSoftwareTitleQueryKey
extends IGetSoftwareTitleQueryParams {
scope: "softwareById";
}
export interface IGetSoftwareVersionQueryParams {
versionId: number;
teamId?: number;
}
export interface IGetSoftwareVersionQueryKey
extends IGetSoftwareVersionQueryParams {
scope: "softwareVersion";
}
export interface ISoftwareInstallTokenResponse {
token: string;
}
export interface ISoftwareFleetMaintainedAppsQueryParams {
team_id: number;
query?: string;
order_key?: string;
order_direction?: "asc" | "desc";
page?: number;
per_page?: number;
}
export interface ISoftwareFleetMaintainedAppsResponse {
fleet_maintained_apps: IFleetMaintainedApp[];
count: number;
apps_updated_at: string | null;
meta: {
has_next_results: boolean;
has_previous_results: boolean;
};
}
export interface IFleetMaintainedAppResponse {
fleet_maintained_app: IFleetMaintainedAppDetails;
}
interface IAddFleetMaintainedAppPostBody {
fleet_id: number;
fleet_maintained_app_id: number;
pre_install_query?: string;
install_script?: string;
post_install_script?: string;
uninstall_script?: string;
self_service?: boolean;
automatic_install?: boolean;
labels_include_any?: string[];
labels_exclude_any?: string[];
categories: string[];
}
export interface IAddAppStoreAppPostBody {
app_store_id: string;
fleet_id: number;
platform: ApplePlatform | "android";
// True by default for android apps
self_service?: boolean;
// No automatic_install on add Android app
automatic_install?: boolean;
labels_include_any?: string[];
labels_exclude_any?: string[];
categories?: SoftwareCategory[];
}
// 4.77 Edit for Android app is not yet available
export interface IEditAppStoreAppPostBody {
fleet_id: number;
self_service?: boolean;
// No automatic_install on edit VPP or android app
labels_include_any?: string[];
labels_exclude_any?: string[];
categories?: SoftwareCategory[];
display_name?: string;
configuration?: string;
auto_update_enabled?: boolean;
auto_update_window_start?: string;
auto_update_window_end?: string;
}
const ORDER_KEY = "name";
const ORDER_DIRECTION = "asc";
const handleAndroidForm = (
teamId: number,
formData: ISoftwareAndroidFormData
) => {
const { SOFTWARE_APP_STORE_APPS } = endpoints;
const body: IAddAppStoreAppPostBody = {
app_store_id: formData.applicationID,
fleet_id: teamId,
platform: formData.platform,
self_service: formData.selfService,
automatic_install: formData.automaticInstall,
};
if (formData.categories && formData.categories.length > 0) {
body.categories = formData.categories as SoftwareCategory[];
}
if (formData.targetType === "Custom") {
const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets);
if (formData.customTarget === "labelsIncludeAny") {
body.labels_include_any = selectedLabels;
} else {
body.labels_exclude_any = selectedLabels;
}
}
return sendRequest("POST", SOFTWARE_APP_STORE_APPS, body);
};
const handleVppAppForm = (teamId: number, formData: ISoftwareVppFormData) => {
const { SOFTWARE_APP_STORE_APPS } = endpoints;
if (!formData.selectedApp) {
throw new Error("Selected app is required. This should not happen.");
}
const body: IAddAppStoreAppPostBody = {
app_store_id: formData.selectedApp.app_store_id,
fleet_id: teamId,
platform: formData.selectedApp?.platform, // Nested platform
self_service: formData.selfService,
automatic_install: formData.automaticInstall,
};
if (formData.categories && formData.categories.length > 0) {
body.categories = formData.categories as SoftwareCategory[];
}
if (formData.targetType === "Custom") {
const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets);
if (formData.customTarget === "labelsIncludeAny") {
body.labels_include_any = selectedLabels;
} else {
body.labels_exclude_any = selectedLabels;
}
}
return sendRequest("POST", SOFTWARE_APP_STORE_APPS, body);
};
const handleDisplayNameForm = (
data: ISoftwareDisplayNameFormData,
formData: FormData
) => {
formData.append("display_name", data.displayName || "");
};
const handleEditPackageForm = (
data: IEditPackageFormData,
formData: FormData,
orignalPackage: ISoftwarePackage
) => {
data.software && formData.append("software", data.software);
formData.append("self_service", data.selfService.toString());
// Base64 encode script fields to bypass WAF rules that block script patterns
formData.append(
"install_script",
encodeScriptBase64(data.installScript) || ""
);
formData.append(
"pre_install_query",
encodeScriptBase64(data.preInstallQuery || "") || ""
);
formData.append(
"post_install_script",
encodeScriptBase64(data.postInstallScript || "") || ""
);
formData.append(
"uninstall_script",
encodeScriptBase64(data.uninstallScript || "") || ""
);
if (data.categories) {
data.categories.forEach((category) => {
formData.append("categories", category);
});
}
// clear out labels if targetType is "All hosts"
if (data.targetType === "All hosts") {
if (orignalPackage.labels_include_any) {
formData.append("labels_include_any", "");
} else {
formData.append("labels_exclude_any", "");
}
}
// add custom labels if targetType is "Custom"
if (data.targetType === "Custom") {
const selectedLabels = listNamesFromSelectedLabels(data.labelTargets);
let labelKey = "";
if (data.customTarget === "labelsIncludeAny") {
labelKey = "labels_include_any";
} else {
labelKey = "labels_exclude_any";
}
selectedLabels?.forEach((label) => {
formData.append(labelKey, label);
});
}
};
const handleDisplayNameAppStoreAppForm = (
formData: ISoftwareDisplayNameFormData,
body: IEditAppStoreAppPostBody
) => {
body.display_name = formData.displayName || "";
};
const handleConfigurationAppStoreAppForm = (
formData: ISoftwareConfigurationFormData,
body: IEditAppStoreAppPostBody
) => {
body.configuration = formData.configuration || "{}";
};
const handleAutoUpdateConfigAppStoreAppForm = (
formData: ISoftwareAutoUpdateConfigFormData,
body: IEditAppStoreAppPostBody
) => {
body.auto_update_enabled = formData.autoUpdateEnabled;
if (formData.autoUpdateEnabled) {
body.auto_update_window_start = formData.autoUpdateStartTime;
body.auto_update_window_end = formData.autoUpdateEndTime;
}
if (formData.targetType === "Custom") {
const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets);
if (formData.customTarget === "labelsIncludeAny") {
body.labels_include_any = selectedLabels;
} else {
body.labels_exclude_any = selectedLabels;
}
} else {
body.labels_exclude_any = [];
body.labels_include_any = [];
}
};
const handleEditAppStoreAppForm = (
formData: ISoftwareVppFormData,
body: IEditAppStoreAppPostBody
) => {
body.self_service = formData.selfService;
if (formData.categories && formData.categories.length > 0) {
body.categories = formData.categories as SoftwareCategory[];
} else {
body.categories = [];
}
if (formData.targetType === "Custom") {
const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets);
if (formData.customTarget === "labelsIncludeAny") {
body.labels_include_any = selectedLabels;
} else {
body.labels_exclude_any = selectedLabels;
}
} else {
body.labels_exclude_any = [];
body.labels_include_any = [];
}
};
export default {
load: async ({
page,
perPage,
orderKey = ORDER_KEY,
orderDirection: orderDir = ORDER_DIRECTION,
query,
vulnerable,
// availableForInstall, // TODO: Is this supported for the versions endpoint?
teamId,
}: Omit<
ISoftwareApiParams,
"availableForInstall" | "selfService"
>): Promise<ISoftwareResponse> => {
const { SOFTWARE } = endpoints;
const queryParams = {
page,
perPage,
orderKey,
orderDirection: orderDir,
teamId,
query,
vulnerable,
// availableForInstall,
};
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
const { team_id, ...restParams } = snakeCaseParams;
const queryString = buildQueryStringFromParams({
...restParams,
fleet_id: team_id,
});
const path = `${SOFTWARE}?${queryString}`;
try {
return sendRequest("GET", path);
} catch (error) {
throw error;
}
},
getCount: async ({
query,
teamId,
vulnerable,
}: Pick<
ISoftwareApiParams,
"query" | "teamId" | "vulnerable"
>): Promise<ISoftwareCountResponse> => {
const { SOFTWARE } = endpoints;
const path = `${SOFTWARE}/count`;
const queryParams = {
query,
teamId,
vulnerable,
};
const snakeCaseParams = convertParamsToSnakeCase(queryParams);
const { team_id, ...restCountParams } = snakeCaseParams;
const queryString = buildQueryStringFromParams({
...restCountParams,
fleet_id: team_id,
});
return sendRequest("GET", path.concat(`?${queryString}`));
},
getSoftwareTitles: (
params: ISoftwareApiParams
): Promise<ISoftwareTitlesResponse> => {
const { SOFTWARE_TITLES } = endpoints;
const snakeCaseParams = convertParamsToSnakeCase(params);
const { team_id, ...restTitleParams } = snakeCaseParams;
const queryString = buildQueryStringFromParams({
...restTitleParams,
fleet_id: team_id,
});
const path = `${SOFTWARE_TITLES}?${queryString}`;
return sendRequest("GET", path);
},
getSoftwareTitle: ({
softwareId,
teamId,
}: IGetSoftwareTitleQueryParams): Promise<ISoftwareTitleResponse> => {
const endpoint = endpoints.SOFTWARE_TITLE(softwareId);
const queryString = buildQueryStringFromParams({ fleet_id: teamId });
const path =
typeof teamId === "undefined" ? endpoint : `${endpoint}?${queryString}`;
return sendRequest("GET", path);
},
getSoftwareVersions: (params: ISoftwareApiParams) => {
const { SOFTWARE_VERSIONS } = endpoints;
const snakeCaseParams = convertParamsToSnakeCase(params);
const { team_id, ...restVersionParams } = snakeCaseParams;
const queryString = buildQueryStringFromParams({
...restVersionParams,
fleet_id: team_id,
});
const path = `${SOFTWARE_VERSIONS}?${queryString}`;
return sendRequest("GET", path);
},
getSoftwareVersion: ({
versionId,
teamId,
}: IGetSoftwareVersionQueryParams) => {
const endpoint = endpoints.SOFTWARE_VERSION(versionId);
const queryString = buildQueryStringFromParams({ fleet_id: teamId });
const path =
typeof teamId === "undefined" ? endpoint : `${endpoint}?${queryString}`;
return sendRequest("GET", path);
},
addSoftwarePackage: ({
data,
teamId,
timeout,
onUploadProgress,
signal,
}: {
data: IPackageFormData;
teamId?: number;
timeout?: number;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
signal?: AbortSignal;
}) => {
const { SOFTWARE_PACKAGE_ADD } = endpoints;
if (!data.software) {
throw new Error("Software package is required");
}
const formData = new FormData();
formData.append("software", data.software);
formData.append("self_service", data.selfService.toString());
// Base64 encode script fields to bypass WAF rules that block script patterns
data.installScript &&
formData.append(
"install_script",
encodeScriptBase64(data.installScript) || ""
);
data.uninstallScript &&
formData.append(
"uninstall_script",
encodeScriptBase64(data.uninstallScript) || ""
);
data.preInstallQuery &&
formData.append(
"pre_install_query",
encodeScriptBase64(data.preInstallQuery) || ""
);
data.postInstallScript &&
formData.append(
"post_install_script",
encodeScriptBase64(data.postInstallScript) || ""
);
data.automaticInstall &&
formData.append("automatic_install", data.automaticInstall.toString());
teamId && formData.append("fleet_id", teamId.toString());
if (data.categories) {
data.categories.forEach((category) => {
formData.append("categories", category);
});
}
if (data.targetType === "Custom") {
const selectedLabels = listNamesFromSelectedLabels(data.labelTargets);
let labelKey = "";
if (data.customTarget === "labelsIncludeAny") {
labelKey = "labels_include_any";
} else {
labelKey = "labels_exclude_any";
}
selectedLabels?.forEach((label) => {
formData.append(labelKey, label);
});
}
return sendRequestWithProgressAndHeaders({
method: "POST",
path: SOFTWARE_PACKAGE_ADD,
data: formData,
customHeaders: { [SCRIPTS_ENCODED_HEADER]: "base64" },
timeout,
skipParseError: true,
onUploadProgress,
signal,
});
},
editSoftwarePackage: ({
data,
orignalPackage,
softwareId,
teamId,
timeout,
onUploadProgress,
signal,
}: {
data: IEditPackageFormData | ISoftwareDisplayNameFormData;
orignalPackage?: ISoftwarePackage;
softwareId: number;
teamId: number;
timeout?: number;
onUploadProgress?: (progressEvent: AxiosProgressEvent) => void;
signal?: AbortSignal;
}) => {
const { EDIT_SOFTWARE_PACKAGE } = endpoints;
const formData = new FormData();
formData.append("fleet_id", teamId.toString());
if ("displayName" in data) {
// Handles Edit display name form only
handleDisplayNameForm(data, formData);
} else {
// TODO: Confirm if orignalPackage is required
if (!orignalPackage) {
throw new Error("originalPackage is required for EditPackageFormData");
}
// Handles primary Edit Package form
handleEditPackageForm(
data as IEditPackageFormData,
formData,
orignalPackage
);
}
return sendRequestWithProgressAndHeaders({
method: "PATCH",
path: EDIT_SOFTWARE_PACKAGE(softwareId),
data: formData,
customHeaders: { [SCRIPTS_ENCODED_HEADER]: "base64" },
timeout,
skipParseError: true,
onUploadProgress,
signal,
});
},
addAppStoreApp: (
teamId: number,
formData: ISoftwareVppFormData | ISoftwareAndroidFormData
) => {
if ("platform" in formData) {
// Android form data
return handleAndroidForm(teamId, formData as ISoftwareAndroidFormData);
}
// Apple VPP form data
return handleVppAppForm(teamId, formData as ISoftwareVppFormData);
},
editAppStoreApp: (
softwareId: number,
teamId: number,
formData:
| ISoftwareVppFormData
| ISoftwareAndroidFormData
| ISoftwareDisplayNameFormData
| ISoftwareConfigurationFormData
| ISoftwareAutoUpdateConfigFormData
) => {
const { EDIT_SOFTWARE_APP_STORE_APP } = endpoints;
const body: IEditAppStoreAppPostBody = { fleet_id: teamId };
if ("displayName" in formData) {
// Handles Edit display name form only
handleDisplayNameAppStoreAppForm(
formData as ISoftwareDisplayNameFormData,
body
);
} else if ("configuration" in formData) {
// Handles Edit configuration form only
handleConfigurationAppStoreAppForm(
formData as ISoftwareConfigurationFormData,
body
);
} else if ("autoUpdateEnabled" in formData) {
// Handles Edit auto update configuration form only
handleAutoUpdateConfigAppStoreAppForm(
formData as ISoftwareAutoUpdateConfigFormData,
body
);
} else {
// Handles primary Edit AppStoreApp form
// 4.77 Currently, only VPP apps can be edited, not Google Play apps
handleEditAppStoreAppForm(formData as IEditPackageFormData, body);
}
return sendRequest("PATCH", EDIT_SOFTWARE_APP_STORE_APP(softwareId), body);
},
getSoftwareIcon: (softwareId: number, teamId: number) => {
const { SOFTWARE_ICON } = endpoints;
const path = getPathWithQueryParams(SOFTWARE_ICON(softwareId), {
fleet_id: teamId,
});
return sendRequest(
"GET",
path,
undefined,
"blob",
undefined,
undefined,
true
); // returnRaw is true to get headers
},
// This API call is for both:
// "/api/v1/fleet/software/titles/{softwareId}/icon?fleet_id={teamId}"
// "/api/v1/fleet/device/{deviceToken}/software/titles/{softwareId}/icon"
getSoftwareIconFromApiUrl: (apiUrl: string) => {
// sendRequest prepends "/api" to the path, so we need to remove it
// if it's already included in the apiUrl param
const result = apiUrl.replace(/^\/api/, "");
return sendRequest("GET", result, undefined, "blob");
},
deleteSoftwareIcon: (softwareId: number, teamId: number) => {
const { SOFTWARE_ICON } = endpoints;
const path = getPathWithQueryParams(SOFTWARE_ICON(softwareId), {
fleet_id: teamId,
});
return sendRequest("DELETE", path);
},
editSoftwareIcon: (
softwareId: number,
teamId: number,
fileObject: { icon: File }
) => {
const { SOFTWARE_ICON } = endpoints;
const path = getPathWithQueryParams(SOFTWARE_ICON(softwareId), {
fleet_id: teamId,
});
const formData = new FormData();
formData.append("icon", fileObject.icon);
return sendRequest("PUT", path, formData);
},
// Endpoint for deleting packages or VPP
deleteSoftwareInstaller: (softwareId: number, teamId: number) => {
const { SOFTWARE_AVAILABLE_FOR_INSTALL } = endpoints;
const path = `${SOFTWARE_AVAILABLE_FOR_INSTALL(
softwareId
)}?fleet_id=${teamId}`;
return sendRequest("DELETE", path);
},
getSoftwarePackageToken: (
softwareTitleId: number,
teamId: number
): Promise<ISoftwareInstallTokenResponse> => {
const path = `${endpoints.SOFTWARE_PACKAGE_TOKEN(
softwareTitleId
)}?${buildQueryStringFromParams({ alt: "media", fleet_id: teamId })}`;
return sendRequest("POST", path);
},
getSoftwareInstallResult: (installUuid: string) => {
const { SOFTWARE_INSTALL_RESULTS } = endpoints;
const path = SOFTWARE_INSTALL_RESULTS(installUuid);
return sendRequest("GET", path);
},
getFleetMaintainedApps: (
params: ISoftwareFleetMaintainedAppsQueryParams
): Promise<ISoftwareFleetMaintainedAppsResponse> => {
const { SOFTWARE_FLEET_MAINTAINED_APPS } = endpoints;
const { team_id, ...rest } = params;
const queryStr = buildQueryStringFromParams({ ...rest, fleet_id: team_id });
const path = `${SOFTWARE_FLEET_MAINTAINED_APPS}?${queryStr}`;
return sendRequest("GET", path);
},
getFleetMaintainedApp: (
id: number,
teamId?: string
): Promise<IFleetMaintainedAppResponse> => {
const { SOFTWARE_FLEET_MAINTAINED_APP } = endpoints;
const path = getPathWithQueryParams(SOFTWARE_FLEET_MAINTAINED_APP(id), {
fleet_id: teamId,
});
return sendRequest("GET", path);
},
addFleetMaintainedApp: (
teamId: number,
formData: IAddFleetMaintainedData
) => {
const { SOFTWARE_FLEET_MAINTAINED_APPS } = endpoints;
// Base64 encode script fields to bypass WAF rules that block script patterns
const body: IAddFleetMaintainedAppPostBody = {
fleet_id: teamId,
fleet_maintained_app_id: formData.appId,
pre_install_query: encodeScriptBase64(formData.preInstallQuery),
install_script: encodeScriptBase64(formData.installScript),
post_install_script: encodeScriptBase64(formData.postInstallScript),
uninstall_script: encodeScriptBase64(formData.uninstallScript),
self_service: formData.selfService,
automatic_install: formData.automaticInstall,
categories: formData.categories,
};
if (formData.targetType === "Custom") {
const selectedLabels = listNamesFromSelectedLabels(formData.labelTargets);
if (formData.customTarget === "labelsIncludeAny") {
body.labels_include_any = selectedLabels;
} else {
body.labels_exclude_any = selectedLabels;
}
}
return sendRequestWithHeaders(
"POST",
SOFTWARE_FLEET_MAINTAINED_APPS,
body,
{
[SCRIPTS_ENCODED_HEADER]: "base64",
}
);
},
};