fleet/frontend/components/AddHostsModal/PlatformWrapper/PlatformWrapper.tsx
Rachael Shaw 40df80f848
Update "Add hosts" modal copy (#41517)
Follow-up to https://github.com/fleetdm/fleet/pull/41055

---------

Co-authored-by: Jacob Shandling <jacob@shandling.dev>
2026-03-24 15:35:26 -05:00

542 lines
17 KiB
TypeScript

import React, { useContext, useState } from "react";
import { Tab, Tabs, TabList, TabPanel } from "react-tabs";
import FileSaver from "file-saver";
import { NotificationContext } from "context/notification";
import { IConfig } from "interfaces/config";
import { LEARN_MORE_ABOUT_BASE_LINK } from "utilities/constants";
import Button from "components/buttons/Button";
import Icon from "components/Icon/Icon";
import RevealButton from "components/buttons/RevealButton";
// @ts-ignore
import InputField from "components/forms/fields/InputField";
import TooltipWrapper from "components/TooltipWrapper";
import TabNav from "components/TabNav";
import InfoBanner from "components/InfoBanner/InfoBanner";
import CustomLink from "components/CustomLink/CustomLink";
import Radio from "components/forms/fields/Radio";
import TabText from "components/TabText";
import { isValidPemCertificate } from "../../../pages/hosts/ManageHostsPage/helpers";
import IosIpadosPanel from "./IosIpadosPanel";
import AndroidPanel from "./AndroidPanel";
interface IPlatformSubNav {
name: string;
type: string;
}
const platformSubNav: IPlatformSubNav[] = [
{
name: "macOS",
type: "pkg",
},
{
name: "Windows",
type: "msi",
},
{
name: "Linux",
type: "deb",
},
{
name: "ChromeOS",
type: "chromeos",
},
{
name: "iOS & iPadOS",
type: "ios-ipados",
},
{
name: "Android",
type: "android",
},
{
name: "Advanced",
type: "advanced",
},
];
interface IPlatformWrapperProps {
enrollSecret: string;
onCancel: () => void;
certificate: any;
isFetchingCertificate: boolean;
fetchCertificateError: any;
config: IConfig | null;
}
const baseClass = "platform-wrapper";
const PlatformWrapper = ({
enrollSecret,
onCancel,
certificate,
isFetchingCertificate,
fetchCertificateError,
config,
}: IPlatformWrapperProps): JSX.Element => {
const { renderFlash } = useContext(NotificationContext);
const [hostType, setHostType] = useState<"workstation" | "server">(
"workstation"
);
const [showPlainOsquery, setShowPlainOsquery] = useState(false);
const [selectedTabIndex, setSelectedTabIndex] = useState(0); // External link requires control in state
let tlsHostname = config?.server_settings.server_url || "";
try {
const serverUrl = new URL(config?.server_settings.server_url || "");
tlsHostname = serverUrl.hostname;
if (serverUrl.port) {
tlsHostname += `:${serverUrl.port}`;
}
} catch (e) {
if (!(e instanceof TypeError)) {
throw e;
}
}
const flagfileContent = `# Server
--tls_hostname=${tlsHostname}
--tls_server_certs=fleet.pem
# Enrollment
--host_identifier=instance
--enroll_secret_path=secret.txt
--enroll_tls_endpoint=/api/osquery/enroll
# Configuration
--config_plugin=tls
--config_tls_endpoint=/api/v1/osquery/config
--config_refresh=10
# Live query
--disable_distributed=false
--distributed_plugin=tls
--distributed_interval=10
--distributed_tls_max_attempts=3
--distributed_tls_read_endpoint=/api/v1/osquery/distributed/read
--distributed_tls_write_endpoint=/api/v1/osquery/distributed/write
# Logging
--logger_plugin=tls
--logger_tls_endpoint=/api/v1/osquery/log
--logger_tls_period=10
# File carving
--disable_carver=false
--carver_start_endpoint=/api/v1/osquery/carve/begin
--carver_continue_endpoint=/api/v1/osquery/carve/block
--carver_block_size=8000000`;
const onDownloadEnrollSecret = (evt: React.MouseEvent) => {
evt.preventDefault();
const filename = "secret.txt";
const file = new global.window.File([enrollSecret], filename);
FileSaver.saveAs(file);
return false;
};
const onDownloadFlagfile = (evt: React.MouseEvent) => {
evt.preventDefault();
const filename = "flagfile.txt";
const file = new global.window.File([flagfileContent], filename);
FileSaver.saveAs(file);
return false;
};
const onDownloadCertificate = (evt: React.MouseEvent) => {
evt.preventDefault();
if (certificate && isValidPemCertificate(certificate)) {
const filename = "fleet.pem";
const file = new global.window.File([certificate], filename, {
type: "application/x-pem-file",
});
FileSaver.saveAs(file);
} else {
renderFlash(
"error",
"Your certificate could not be downloaded. Please check your Fleet configuration."
);
}
return false;
};
const renderFleetCertificateBlock = (type: "plain" | "tooltip") => {
return (
<div className={`${baseClass}__advanced--fleet-certificate`}>
{type === "plain" ? (
<div className={`${baseClass}__advanced--heading`}>
Download your Fleet certificate
</div>
) : (
<div
className={`${baseClass}__advanced--heading download-certificate--tooltip`}
>
Download your{" "}
<TooltipWrapper tipContent="A Fleet certificate is required if Fleet is running with a self signed or otherwise untrusted certificate.">
Fleet certificate:
</TooltipWrapper>
</div>
)}
{isFetchingCertificate && (
<p className={`${baseClass}__certificate-loading`}>
Loading your certificate
</p>
)}
{!isFetchingCertificate &&
(certificate ? (
<p>
{type === "plain" && (
<>
Prove the TLS certificate used by the Fleet server to enable
secure connections from osquery:
<br />
</>
)}
<Button
variant="inverse"
className={`${baseClass}__fleet-certificate-download`}
onClick={onDownloadCertificate}
>
Download
<Icon name="download" size="small" />
</Button>
</p>
) : (
<p className={`${baseClass}__certificate-error`}>
<em>Fleet failed to load your certificate.</em>
<span>
If you&apos;re able to access Fleet at a private or secure
(HTTPS) IP address, please log into Fleet at this address to
load your certificate.
</span>
</p>
))}
</div>
);
};
const renderInstallerString = (packageType: string) => {
return packageType === "advanced"
? `fleetctl package --type=YOUR_TYPE --fleet-url=${config?.server_settings.server_url} --enroll-secret=${enrollSecret} --fleet-certificate=PATH_TO_YOUR_CERTIFICATE/fleet.pem`
: `fleetctl package --type=${packageType} ${
config && !config.server_settings.scripts_disabled
? "--enable-scripts "
: ""
}${hostType === "workstation" ? "--fleet-desktop " : ""}--fleet-url=${
config?.server_settings.server_url
} --enroll-secret=${enrollSecret}`;
};
const renderLabel = (packageType: string) => {
return (
<>
{packageType !== "plain-osquery" && (
<span className={`${baseClass}__cta`}>
Use this command to generate Fleet&apos;s agent.{" "}
<CustomLink
className={`${baseClass}__command-line-tool`}
url={`${LEARN_MORE_ABOUT_BASE_LINK}/generate-fleets-agent`}
text="Learn how"
newTab
/>
</span>
)}
</>
);
};
const renderPanel = (packageType: string) => {
const CHROME_OS_INFO = {
extensionId: "fleeedmmihkfkeemmipgmhhjemlljidg",
installationUrl: "https://chrome.fleetdm.com/updates.xml",
policyForExtension: `{
"fleet_url": {
"Value": "${config?.server_settings.server_url}"
},
"enroll_secret": {
"Value": "${enrollSecret}"
}
}`,
};
let packageTypeHelpText;
if (packageType === "deb") {
packageTypeHelpText = (
<>
Run this on your computer, then deploy the generated package to your
hosts. For CentOS, Red Hat, and Fedora Linux, use{" "}
<code>--type=rpm</code>. For Arch Linux, use{" "}
<code>--type=pkg.tar.zst</code>. For ARM, use{" "}
<code>--arch=arm64</code>
</>
);
} else if (packageType === "msi") {
packageTypeHelpText = (
<>
Run this on your computer, then deploy the generated package to your
hosts. For ARM, use <code>--arch=arm64</code>
</>
);
} else if (packageType === "pkg") {
packageTypeHelpText =
"Run this on your computer, then deploy the generated package to your hosts.";
} else {
packageTypeHelpText = "";
}
if (packageType === "chromeos") {
return (
<>
<div className={`${baseClass}__chromeos--info`}>
<p className={`${baseClass}__chromeos--heading`}>
In Google Admin:
</p>
<p>
Add the extension for the relevant users & browsers using the
information below.
</p>
<InfoBanner className={`${baseClass}__chromeos--instructions`}>
For a step-by-step guide, see the documentation page for&nbsp;
<CustomLink
url="https://fleetdm.com/docs/using-fleet/adding-hosts#enroll-chromebooks"
text="adding hosts"
newTab
multiline
variant="banner-link"
/>
</InfoBanner>
</div>
<InputField
readOnly
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-extension-id`}
name="Extension ID"
enableCopy
label="Extension ID"
value={CHROME_OS_INFO.extensionId}
/>
<InputField
readOnly
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-url`}
name="Installation URL"
enableCopy
label="Installation URL"
value={CHROME_OS_INFO.installationUrl}
/>
<InputField
readOnly
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-policy-for-extension`}
name="Policy for extension"
enableCopy
label="Policy for extension"
type="textarea"
value={CHROME_OS_INFO.policyForExtension}
/>
</>
);
}
if (packageType === "ios-ipados") {
return <IosIpadosPanel enrollSecret={enrollSecret} />;
}
if (packageType === "android") {
return <AndroidPanel enrollSecret={enrollSecret} />;
}
if (packageType === "advanced") {
return (
<>
{renderFleetCertificateBlock("tooltip")}
<div className={`${baseClass}__advanced--installer`}>
<InputField
readOnly
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__installer-input-${packageType}`}
name="installer"
enableCopy
label={renderLabel(packageType)}
type="textarea"
value={renderInstallerString(packageType)}
helpText="Distribute your package to add hosts to Fleet."
/>
</div>
<div>
<InfoBanner className={`${baseClass}__chrome--instructions`}>
This works for macOS, Windows, and Linux hosts. To add
Chromebooks,&nbsp;
<Button
variant="text-link-dark"
onClick={() => setSelectedTabIndex(3)}
>
click here
</Button>
.
</InfoBanner>
</div>
<RevealButton
className={baseClass}
isShowing={showPlainOsquery}
hideText="Plain osquery"
showText="Plain osquery"
caretPosition="after"
onClick={() => setShowPlainOsquery((prev) => !prev)}
/>
{showPlainOsquery && (
<>
<div className={`${baseClass}__advanced--enroll-secrets`}>
<p className={`${baseClass}__advanced--heading`}>
Download your enroll secret:
</p>
<p>
Osquery uses an enroll secret to authenticate with the Fleet
server.
<br />
<Button variant="inverse" onClick={onDownloadEnrollSecret}>
Download
<Icon
name="download"
color="ui-fleet-black-75"
size="small"
/>
</Button>
</p>
</div>
{renderFleetCertificateBlock("plain")}
<div className={`${baseClass}__advanced--flagfile`}>
<p className={`${baseClass}__advanced--heading`}>
Download your flagfile:
</p>
<p>
If using the enroll secret and server certificate downloaded
above, use the generated flagfile. In some configurations,
modifications may need to be made.
<br />
{fetchCertificateError ? (
<span className={`${baseClass}__error`}>
{fetchCertificateError}
</span>
) : (
<Button variant="inverse" onClick={onDownloadFlagfile}>
Download
<Icon name="download" size="small" />
</Button>
)}
</p>
</div>
<div className={`${baseClass}__advanced--osqueryd`}>
<p className={`${baseClass}__advanced--heading`}>
With{" "}
<CustomLink
url="https://www.osquery.io/downloads"
text="osquery"
newTab
/>{" "}
installed:
</p>
<p className={`${baseClass}__advanced--text`}>
Run osquery from the directory containing the above files (may
require sudo or Run as Administrator privileges):
</p>
<InputField
readOnly
inputWrapperClass={`${baseClass}__run-osquery-input`}
name="run-osquery"
enableCopy
label={renderLabel("plain-osquery")}
type="text"
value="osqueryd --flagfile=flagfile.txt --verbose"
/>
</div>
</>
)}
</>
);
}
return (
// "form" className applies the global form styling
<div className="form">
{packageType !== "pkg" && (
// Windows & Linux
<div className="form-field">
<div className="form-field__label">Type</div>
<Radio
className={`${baseClass}__radio-input`}
label="Workstation"
id="workstation-host"
checked={hostType === "workstation"}
value="workstation"
name="host-typ"
onChange={() => setHostType("workstation")}
/>
<Radio
className={`${baseClass}__radio-input`}
label="Server"
id="server-host"
checked={hostType === "server"}
value="server"
name="host-type"
onChange={() => setHostType("server")}
/>
</div>
)}
<InputField
readOnly
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__installer-input-${packageType}`}
name="installer"
enableCopy
label={renderLabel(packageType)}
type="textarea"
value={renderInstallerString(packageType)}
helpText={packageTypeHelpText}
/>
</div>
);
};
return (
<div className={baseClass}>
<TabNav>
<Tabs
onSelect={(index) => setSelectedTabIndex(index)}
selectedIndex={selectedTabIndex}
>
<TabList>
{platformSubNav.map((navItem) => {
// Bolding text when the tab is active causes a layout shift
// so we add a hidden pseudo element with the same text string
return (
<Tab key={navItem.name} data-text={navItem.name}>
<TabText>{navItem.name}</TabText>
</Tab>
);
})}
</TabList>
{platformSubNav.map((navItem) => {
// Bolding text when the tab is active causes a layout shift
// so we add a hidden pseudo element with the same text string
return (
<TabPanel className={`${baseClass}__info`} key={navItem.type}>
<div className={`${baseClass} form`}>
{renderPanel(navItem.type)}
</div>
</TabPanel>
);
})}
</Tabs>
</TabNav>
<div className="modal-cta-wrap">
<Button onClick={onCancel}>Close</Button>
</div>
</div>
);
};
export default PlatformWrapper;