mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
550 lines
17 KiB
TypeScript
550 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 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="text-icon"
|
|
className={`${baseClass}__fleet-certificate-download`}
|
|
onClick={onDownloadCertificate}
|
|
>
|
|
Download
|
|
<Icon name="download" color="core-fleet-blue" size="small" />
|
|
</Button>
|
|
</p>
|
|
) : (
|
|
<p className={`${baseClass}__certificate-error`}>
|
|
<em>Fleet failed to load your certificate.</em>
|
|
<span>
|
|
If you'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`}>
|
|
Run this command with the{" "}
|
|
<a
|
|
className={`${baseClass}__command-line-tool`}
|
|
href="https://fleetdm.com/learn-more-about/installing-fleetctl"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
Fleet command-line tool
|
|
</a>
|
|
:
|
|
</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 = (
|
|
<>
|
|
For CentOS, Red Hat, and Fedora Linux, use <code>--type=rpm</code>.
|
|
For ARM, use <code>--arch=arm64</code>
|
|
</>
|
|
);
|
|
} else if (packageType === "msi") {
|
|
packageTypeHelpText = (
|
|
<>
|
|
For ARM, use <code>--arch=arm64</code>
|
|
</>
|
|
);
|
|
} else if (packageType === "pkg") {
|
|
packageTypeHelpText = "Install this package to add hosts to Fleet.";
|
|
} 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{" "}
|
|
<CustomLink
|
|
url="https://fleetdm.com/docs/using-fleet/adding-hosts#enroll-chromebooks"
|
|
text="adding hosts"
|
|
newTab
|
|
multiline
|
|
/>
|
|
</InfoBanner>
|
|
</div>
|
|
<InputField
|
|
readOnly
|
|
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-extension-id`}
|
|
name="Extension ID"
|
|
enableCopy
|
|
copyButtonPosition="inside"
|
|
label="Extension ID"
|
|
value={CHROME_OS_INFO.extensionId}
|
|
/>
|
|
<InputField
|
|
readOnly
|
|
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-url`}
|
|
name="Installation URL"
|
|
enableCopy
|
|
copyButtonPosition="inside"
|
|
label="Installation URL"
|
|
value={CHROME_OS_INFO.installationUrl}
|
|
/>
|
|
<InputField
|
|
readOnly
|
|
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-policy-for-extension`}
|
|
name="Policy for extension"
|
|
enableCopy
|
|
copyButtonPosition="inside"
|
|
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
|
|
copyButtonPosition="inside"
|
|
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,{" "}
|
|
<Button
|
|
variant="text-link"
|
|
onClick={() => setSelectedTabIndex(4)}
|
|
>
|
|
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="text-icon" onClick={onDownloadEnrollSecret}>
|
|
Download
|
|
<Icon
|
|
name="download"
|
|
color="core-fleet-blue"
|
|
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="text-icon" onClick={onDownloadFlagfile}>
|
|
Download
|
|
<Icon
|
|
name="download"
|
|
color="core-fleet-blue"
|
|
size="small"
|
|
/>
|
|
</Button>
|
|
)}
|
|
</p>
|
|
</div>
|
|
<div className={`${baseClass}__advanced--osqueryd`}>
|
|
<p className={`${baseClass}__advanced--heading`}>
|
|
With{" "}
|
|
<a
|
|
href="https://www.osquery.io/downloads"
|
|
target="_blank"
|
|
rel="noopener noreferrer"
|
|
>
|
|
osquery
|
|
</a>{" "}
|
|
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
|
|
copyButtonPosition="inside"
|
|
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
|
|
copyButtonPosition="inside"
|
|
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}>Done</Button>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default PlatformWrapper;
|