mirror of
https://github.com/fleetdm/fleet
synced 2026-04-21 21:47:20 +00:00
597 lines
19 KiB
TypeScript
597 lines
19 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";
|
||
// @ts-ignore
|
||
import { stringToClipboard } from "utilities/copy_text";
|
||
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 Checkbox from "components/forms/fields/Checkbox";
|
||
import TooltipWrapper from "components/TooltipWrapper";
|
||
import TabsWrapper from "components/TabsWrapper";
|
||
import InfoBanner from "components/InfoBanner/InfoBanner";
|
||
import CustomLink from "components/CustomLink/CustomLink";
|
||
|
||
import { isValidPemCertificate } from "../../../pages/hosts/ManageHostsPage/helpers";
|
||
|
||
interface IPlatformSubNav {
|
||
name: string;
|
||
type: string;
|
||
}
|
||
|
||
const platformSubNav: IPlatformSubNav[] = [
|
||
{
|
||
name: "macOS",
|
||
type: "pkg",
|
||
},
|
||
{
|
||
name: "Windows",
|
||
type: "msi",
|
||
},
|
||
{
|
||
name: "Linux (RPM)",
|
||
type: "rpm",
|
||
},
|
||
{
|
||
name: "Linux (deb)",
|
||
type: "deb",
|
||
},
|
||
{
|
||
name: "ChromeOS",
|
||
type: "chromeos",
|
||
},
|
||
{
|
||
name: "Advanced",
|
||
type: "advanced",
|
||
},
|
||
];
|
||
|
||
interface IPlatformWrapperProps {
|
||
enrollSecret: string;
|
||
onCancel: () => void;
|
||
certificate: any;
|
||
isFetchingCertificate: boolean;
|
||
fetchCertificateError: any;
|
||
config: IConfig | null;
|
||
}
|
||
|
||
const CHROME_OS_INFO = {
|
||
extensionId: "fleeedmmihkfkeemmipgmhhjemlljidg",
|
||
url: "https://chrome.fleetdm.com/updates.xml",
|
||
policyForExtension: `{
|
||
"fleet_url": {
|
||
"Value": "https://dogfood.fleetdm.com"
|
||
},
|
||
"enroll_secret": {
|
||
"Value": "eeb2e8a7d132d9cbbdd0f024f9419f88"
|
||
}
|
||
}`,
|
||
};
|
||
|
||
const baseClass = "platform-wrapper";
|
||
|
||
const PlatformWrapper = ({
|
||
enrollSecret,
|
||
onCancel,
|
||
certificate,
|
||
isFetchingCertificate,
|
||
fetchCertificateError,
|
||
config,
|
||
}: IPlatformWrapperProps): JSX.Element => {
|
||
const { renderFlash } = useContext(NotificationContext);
|
||
|
||
const [copyMessage, setCopyMessage] = useState<Record<string, string>>({});
|
||
const [includeFleetDesktop, setIncludeFleetDesktop] = useState(true);
|
||
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" ? (
|
||
<p className={`${baseClass}__advanced--heading`}>
|
||
Download your Fleet certificate
|
||
</p>
|
||
) : (
|
||
<p
|
||
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>
|
||
</p>
|
||
)}
|
||
{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} ${
|
||
includeFleetDesktop ? "--fleet-desktop " : ""
|
||
}--fleet-url=${
|
||
config?.server_settings.server_url
|
||
} --enroll-secret=${enrollSecret}`;
|
||
};
|
||
|
||
const renderLabel = (packageType: string, installerString: string) => {
|
||
const onCopyInstaller = (evt: React.MouseEvent) => {
|
||
evt.preventDefault();
|
||
|
||
stringToClipboard(installerString)
|
||
.then(() =>
|
||
setCopyMessage((prev) => ({ ...prev, [packageType]: "Copied!" }))
|
||
)
|
||
.catch(() =>
|
||
setCopyMessage((prev) => ({ ...prev, [packageType]: "Copy failed" }))
|
||
);
|
||
|
||
// Clear message after 1 second
|
||
setTimeout(
|
||
() => setCopyMessage((prev) => ({ ...prev, [packageType]: "" })),
|
||
1000
|
||
);
|
||
|
||
return false;
|
||
};
|
||
|
||
return (
|
||
<>
|
||
{packageType === "plain-osquery" ? (
|
||
<>
|
||
<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>
|
||
</>
|
||
) : (
|
||
<span className={`${baseClass}__cta`}>
|
||
Run this command with the{" "}
|
||
<a
|
||
className={`${baseClass}__command-line-tool`}
|
||
href="https://fleetdm.com/docs/using-fleet/fleetctl-cli"
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
>
|
||
Fleet command-line tool
|
||
</a>{" "}
|
||
installed:
|
||
</span>
|
||
)}{" "}
|
||
<span className={`${baseClass}__name`}>
|
||
<span className="buttons">
|
||
<Button
|
||
variant="unstyled"
|
||
className={`${baseClass}__installer-copy-icon`}
|
||
onClick={onCopyInstaller}
|
||
>
|
||
<Icon name="copy" />
|
||
</Button>
|
||
{copyMessage[packageType] && (
|
||
<span
|
||
className={`${baseClass}__copy-message`}
|
||
>{`${copyMessage[packageType]} `}</span>
|
||
)}
|
||
</span>
|
||
</span>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const renderChromeOSLabel = (label: string, value: string) => {
|
||
const onCopyChromeOSLabel = (evt: React.MouseEvent) => {
|
||
evt.preventDefault();
|
||
|
||
stringToClipboard(value)
|
||
.then(() => setCopyMessage((prev) => ({ ...prev, [label]: "Copied!" })))
|
||
.catch(() =>
|
||
setCopyMessage((prev) => ({
|
||
...prev,
|
||
[label]: "Copy failed",
|
||
}))
|
||
);
|
||
|
||
// Clear message after 1 second
|
||
setTimeout(
|
||
() => setCopyMessage((prev) => ({ ...prev, [label]: "" })),
|
||
1000
|
||
);
|
||
|
||
return false;
|
||
};
|
||
|
||
return (
|
||
<>
|
||
{label}
|
||
<span className="buttons">
|
||
<Button
|
||
variant="unstyled"
|
||
className={`${baseClass}__chromeos-copy-icon`}
|
||
onClick={onCopyChromeOSLabel}
|
||
>
|
||
<Icon name="copy" />
|
||
</Button>
|
||
{copyMessage[label] && (
|
||
<span className={`${baseClass}__copy-message`}>Copied!</span>
|
||
)}
|
||
</span>
|
||
</>
|
||
);
|
||
};
|
||
|
||
const renderTab = (packageType: string) => {
|
||
if (packageType === "chromeos") {
|
||
return (
|
||
<div className={baseClass}>
|
||
<div className={`${baseClass}__chromeos`}>
|
||
<div className={`${baseClass}__chromeos--add-extension`}>
|
||
<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#add-chromebooks-with-the-fleetd-chrome-extension"
|
||
text="adding hosts"
|
||
newTab
|
||
multiline
|
||
/>
|
||
</InfoBanner>
|
||
<div className={`${baseClass}__chromeos--installer`}>
|
||
<InputField
|
||
disabled
|
||
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-extension-id`}
|
||
name="Extension ID"
|
||
label={renderChromeOSLabel(
|
||
"Extension ID",
|
||
CHROME_OS_INFO.extensionId
|
||
)}
|
||
value={CHROME_OS_INFO.extensionId}
|
||
/>
|
||
<InputField
|
||
disabled
|
||
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-url`}
|
||
name="URL"
|
||
label={renderChromeOSLabel("URL", CHROME_OS_INFO.url)}
|
||
value={CHROME_OS_INFO.url}
|
||
/>
|
||
<InputField
|
||
disabled
|
||
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__chromeos-policy-for-extension`}
|
||
name="Policy for extension"
|
||
label={renderChromeOSLabel(
|
||
"Policy for extension",
|
||
CHROME_OS_INFO.policyForExtension
|
||
)}
|
||
type="textarea"
|
||
value={CHROME_OS_INFO.policyForExtension}
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
if (packageType === "advanced") {
|
||
return (
|
||
<div className={baseClass}>
|
||
<div className={`${baseClass}__advanced`}>
|
||
{renderFleetCertificateBlock("tooltip")}
|
||
<div className={`${baseClass}__advanced--installer`}>
|
||
<InputField
|
||
disabled
|
||
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__installer-input-${packageType}`}
|
||
name="installer"
|
||
label={renderLabel(
|
||
packageType,
|
||
renderInstallerString(packageType)
|
||
)}
|
||
type="textarea"
|
||
value={renderInstallerString(packageType)}
|
||
/>
|
||
<p className={`${baseClass}__advanced--instructions`}>
|
||
Distribute your package to add hosts to Fleet.
|
||
</p>
|
||
<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`}>
|
||
<InputField
|
||
disabled
|
||
inputWrapperClass={`${baseClass}__run-osquery-input`}
|
||
name="run-osquery"
|
||
label={renderLabel(
|
||
"plain-osquery",
|
||
"osqueryd --flagfile=flagfile.txt --verbose"
|
||
)}
|
||
type={"text"}
|
||
value={"osqueryd --flagfile=flagfile.txt --verbose"}
|
||
/>
|
||
</div>
|
||
</>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
return (
|
||
<>
|
||
{packageType !== "pkg" && (
|
||
<Checkbox
|
||
name="include-fleet-desktop"
|
||
onChange={(value: boolean) => setIncludeFleetDesktop(value)}
|
||
value={includeFleetDesktop}
|
||
>
|
||
<>
|
||
Include
|
||
<TooltipWrapper
|
||
tipContent={
|
||
"Include Fleet Desktop if your’re adding workstations."
|
||
}
|
||
>
|
||
Fleet Desktop
|
||
</TooltipWrapper>
|
||
</>
|
||
</Checkbox>
|
||
)}
|
||
<InputField
|
||
disabled
|
||
inputWrapperClass={`${baseClass}__installer-input ${baseClass}__installer-input-${packageType}`}
|
||
name="installer"
|
||
label={renderLabel(packageType, renderInstallerString(packageType))}
|
||
type={"textarea"}
|
||
value={renderInstallerString(packageType)}
|
||
/>
|
||
<span>Distribute your package to add hosts to Fleet.</span>
|
||
</>
|
||
);
|
||
};
|
||
|
||
return (
|
||
<div className={baseClass}>
|
||
<TabsWrapper>
|
||
<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}>
|
||
{navItem.name}
|
||
</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}>
|
||
{renderTab(navItem.type)}
|
||
</TabPanel>
|
||
);
|
||
})}
|
||
</Tabs>
|
||
</TabsWrapper>
|
||
<div className="modal-cta-wrap">
|
||
<Button onClick={onCancel} variant="brand">
|
||
Done
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|
||
|
||
export default PlatformWrapper;
|