diff --git a/src/components/Input.tsx b/src/components/Input.tsx index 31649ca..e2e5230 100644 --- a/src/components/Input.tsx +++ b/src/components/Input.tsx @@ -13,6 +13,7 @@ export interface InputProps icon?: React.ReactNode; error?: string; errorTooltip?: boolean; + errorTooltipPosition?: "top" | "top-right"; } const inputVariants = cva("", { @@ -49,6 +50,7 @@ const Input = React.forwardRef( maxWidthClass = "", error, errorTooltip = false, + errorTooltipPosition = "top", ...props }, ref, @@ -105,9 +107,12 @@ const Input = React.forwardRef( {error && errorTooltip && (
(
} interactive={false} - align={"center"} + align={errorTooltipPosition == "top" ? "center" : "end"} side={"top"} keepOpen={true} > diff --git a/src/interfaces/PostureCheck.ts b/src/interfaces/PostureCheck.ts index ad11098..6a44181 100644 --- a/src/interfaces/PostureCheck.ts +++ b/src/interfaces/PostureCheck.ts @@ -10,6 +10,7 @@ export interface PostureCheck { os_version_check?: OperatingSystemVersionCheck; geo_location_check?: GeoLocationCheck; peer_network_range_check?: PeerNetworkRangeCheck; + process_check?: ProcessCheck; }; policies?: Policy[]; active?: boolean; @@ -53,6 +54,17 @@ export interface PeerNetworkRangeCheck { action: "allow" | "deny"; } +export interface ProcessCheck { + processes: Process[]; +} + +export interface Process { + id: string; + linux_path?: string; + mac_path?: string; + windows_path?: string; +} + export const windowsKernelVersions: SelectOption[] = [ { value: "5.0", label: "Windows 2000" }, { value: "5.1", label: "Windows XP" }, diff --git a/src/modules/posture-checks/checks/PostureCheckProcess.tsx b/src/modules/posture-checks/checks/PostureCheckProcess.tsx new file mode 100644 index 0000000..34fee5b --- /dev/null +++ b/src/modules/posture-checks/checks/PostureCheckProcess.tsx @@ -0,0 +1,310 @@ +import Button from "@components/Button"; +import HelpText from "@components/HelpText"; +import InlineLink from "@components/InlineLink"; +import { Input } from "@components/Input"; +import { Label } from "@components/Label"; +import { ModalClose, ModalFooter } from "@components/modal/Modal"; +import Paragraph from "@components/Paragraph"; +import { cn, validator } from "@utils/helpers"; +import { isEmpty, uniqueId } from "lodash"; +import { + ExternalLinkIcon, + MinusCircleIcon, + PlusCircle, + ServerCogIcon, + TerminalIcon, +} from "lucide-react"; +import * as React from "react"; +import { useMemo, useState } from "react"; +import AppleIcon from "@/assets/icons/AppleIcon"; +import WindowsIcon from "@/assets/icons/WindowsIcon"; +import { Process, ProcessCheck } from "@/interfaces/PostureCheck"; +import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard"; + +type Props = { + value?: ProcessCheck; + onChange: (value: ProcessCheck | undefined) => void; +}; + +export const PostureCheckProcess = ({ value, onChange }: Props) => { + const [open, setOpen] = useState(false); + + return ( + 0} + title={"Process"} + description={ + "Restrict access in your network based on running processes of a peer." + } + icon={} + iconClass={"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300"} + modalWidthClass={"max-w-xl"} + onReset={() => onChange(undefined)} + > + { + onChange(v); + setOpen(false); + }} + /> + + ); +}; + +const CheckContent = ({ value, onChange }: Props) => { + const [processes, setProcesses] = useState( + value?.processes + ? value.processes.map((p) => { + return { + id: uniqueId("process"), + linux_path: p?.linux_path || "", + mac_path: p?.mac_path || "", + windows_path: p?.windows_path || "", + }; + }) + : [ + { + id: uniqueId("process"), + linux_path: "", + mac_path: "", + windows_path: "", + }, + ], + ); + + const handleProcessChange = ( + id: string, + linux_path: string, + mac_path: string, + windows_path: string, + ) => { + const newProcesses = processes.map((p) => + p.id === id ? { ...p, linux_path, mac_path, windows_path } : p, + ); + setProcesses(newProcesses); + }; + + const removeProcess = (id: string) => { + const newProcesses = processes.filter((p) => p.id !== id); + setProcesses(newProcesses); + }; + + const addProcess = () => { + setProcesses([ + ...processes, + { + id: uniqueId("process"), + linux_path: "", + mac_path: "", + windows_path: "", + }, + ]); + }; + + const pathErrors = useMemo(() => { + if (processes && processes.length > 0) { + return processes.map((p) => { + return { + id: p.id, + errorMacPath: p?.mac_path + ? validator.isValidUnixFilePath(p?.mac_path || "") + ? "" + : "Please enter a valid macOS file path" + : "", + errorLinuxPath: p?.linux_path + ? validator.isValidUnixFilePath(p?.linux_path || "") + ? "" + : "Please enter a valid Unix file path" + : "", + errorWindowsPath: p?.windows_path + ? validator.isValidWindowsFilePath(p?.windows_path || "") + ? "" + : "Please enter a valid Windows file path" + : "", + }; + }); + } else { + return []; + } + }, [processes]); + + const hasErrorsOrIsEmpty = useMemo(() => { + if (processes.length === 0) return true; + const hasOnlyEmptyPaths = processes.some( + (p) => p.linux_path === "" && p.mac_path === "" && p.windows_path === "", + ); + const hasPathErrors = pathErrors.some( + (e) => + e.errorLinuxPath !== "" || + e.errorMacPath !== "" || + e.errorWindowsPath !== "", + ); + return hasOnlyEmptyPaths || hasPathErrors; + }, [processes, pathErrors]); + + return ( + <> +
+
+
+ + + Add the path of an executable file of the process. You can define + a path for Linux, macOS and Windows. Peers will only be allowed to + connect if the process is running on their system. + +
+
+ {processes.length > 0 && ( +
+ {processes.map((p) => { + return ( +
+
+ } + placeholder={"/usr/local/bin/netbird"} + value={p.linux_path} + error={ + pathErrors.find((e) => e.id === p.id)?.errorLinuxPath + } + errorTooltip={true} + errorTooltipPosition={"top-right"} + className={"w-full"} + onChange={(e) => + handleProcessChange( + p.id, + e.target.value, + p?.mac_path || "", + p?.windows_path || "", + ) + } + /> + e.id === p.id) + ?.errorMacPath && "fill-red-500", + )} + /> + } + placeholder={ + "/Applications/NetBird.app/Contents/MacOS/netbird" + } + value={p.mac_path} + error={ + pathErrors.find((e) => e.id === p.id)?.errorMacPath + } + errorTooltip={true} + errorTooltipPosition={"top-right"} + className={"w-full"} + onChange={(e) => + handleProcessChange( + p.id, + p?.linux_path || "", + e.target.value, + p?.windows_path || "", + ) + } + /> + e.id === p.id) + ?.errorWindowsPath && "fill-red-500", + )} + /> + } + placeholder={`C:\\ProgramData\\NetBird\\netbird.exe`} + value={p.windows_path} + errorTooltip={true} + errorTooltipPosition={"top-right"} + error={ + pathErrors.find((e) => e.id === p.id)?.errorWindowsPath + } + className={"w-full"} + onChange={(e) => + handleProcessChange( + p.id, + p?.linux_path || "", + p?.mac_path || "", + e.target.value, + ) + } + /> +
+ + +
+ ); + })} +
+ )} + +
+ +
+ + Learn more about + + Process Check + + + +
+
+ + + + +
+
+ + ); +}; diff --git a/src/modules/posture-checks/checks/tooltips/ProcessTooltip.tsx b/src/modules/posture-checks/checks/tooltips/ProcessTooltip.tsx new file mode 100644 index 0000000..dc46bc5 --- /dev/null +++ b/src/modules/posture-checks/checks/tooltips/ProcessTooltip.tsx @@ -0,0 +1,112 @@ +import Badge from "@components/Badge"; +import FullTooltip from "@components/FullTooltip"; +import { ScrollArea } from "@components/ScrollArea"; +import { tryGetProcessNameFromPath } from "@utils/helpers"; +import { TerminalIcon } from "lucide-react"; +import * as React from "react"; +import AppleIcon from "@/assets/icons/AppleIcon"; +import WindowsIcon from "@/assets/icons/WindowsIcon"; +import { ProcessCheck } from "@/interfaces/PostureCheck"; + +type Props = { + check?: ProcessCheck; + children?: React.ReactNode; +}; +export const ProcessTooltip = ({ check, children }: Props) => { + return check ? ( + +
+ + Allow only{" "} + peers which are running the following processes + +
+ + +
+ {check.processes.map((p, index) => { + return ( +
+ {p?.linux_path && ( + + + + + + {tryGetProcessNameFromPath(p?.linux_path) || + "Unknown path"} + + + )} + + {p?.mac_path && ( + + + + + + {tryGetProcessNameFromPath(p?.mac_path) || + "Unknown path"} + + + )} + + {p?.windows_path && ( + + + + + + {tryGetProcessNameFromPath(p?.windows_path) || + "Unknown path"} + + + )} +
+ ); + })} +
+
+ + } + > + {children} +
+ ) : ( + children + ); +}; diff --git a/src/modules/posture-checks/modal/PostureCheckModal.tsx b/src/modules/posture-checks/modal/PostureCheckModal.tsx index 2459427..b3b7467 100644 --- a/src/modules/posture-checks/modal/PostureCheckModal.tsx +++ b/src/modules/posture-checks/modal/PostureCheckModal.tsx @@ -24,6 +24,7 @@ import { PostureCheckGeoLocation } from "@/modules/posture-checks/checks/Posture import { PostureCheckNetBirdVersion } from "@/modules/posture-checks/checks/PostureCheckNetBirdVersion"; import { PostureCheckOperatingSystem } from "@/modules/posture-checks/checks/PostureCheckOperatingSystem"; import { PostureCheckPeerNetworkRange } from "@/modules/posture-checks/checks/PostureCheckPeerNetworkRange"; +import { PostureCheckProcess } from "@/modules/posture-checks/checks/PostureCheckProcess"; type Props = { open: boolean; @@ -58,6 +59,9 @@ export default function PostureCheckModal({ const [peerNetworkRangeCheck, setPeerNetworkRangeCheck] = useState( postureCheck?.checks.peer_network_range_check || undefined, ); + const [processCheck, setProcessCheck] = useState( + postureCheck?.checks.process_check || undefined, + ); const validateOSCheck = (osCheck?: OperatingSystemVersionCheck) => { if (!osCheck) return; @@ -98,6 +102,7 @@ export default function PostureCheckModal({ geo_location_check: validateLocationCheck(geoLocationCheck), os_version_check: validateOSCheck(osVersionCheck), peer_network_range_check: peerNetworkRangeCheck, + process_check: processCheck, }, }; @@ -133,7 +138,8 @@ export default function PostureCheckModal({ !!nbVersionCheck || !!geoLocationCheck || !!osVersionCheck || - !!peerNetworkRangeCheck; + !!peerNetworkRangeCheck || + !!processCheck; const canCreate = !isEmpty(name) && isAtLeastOneCheckEnabled; const [tab, setTab] = useState("checks"); @@ -187,13 +193,17 @@ export default function PostureCheckModal({ value={geoLocationCheck} onChange={setGeoLocationCheckCheck} /> + - diff --git a/src/modules/posture-checks/table/cells/PostureCheckChecksCell.tsx b/src/modules/posture-checks/table/cells/PostureCheckChecksCell.tsx index 19a58fb..fc79bd0 100644 --- a/src/modules/posture-checks/table/cells/PostureCheckChecksCell.tsx +++ b/src/modules/posture-checks/table/cells/PostureCheckChecksCell.tsx @@ -1,5 +1,5 @@ import { cn } from "@utils/helpers"; -import { Disc3Icon, FlagIcon, NetworkIcon } from "lucide-react"; +import { Disc3Icon, FlagIcon, NetworkIcon, ServerCogIcon } from "lucide-react"; import * as React from "react"; import NetBirdIcon from "@/assets/icons/NetBirdIcon"; import { PostureCheck } from "@/interfaces/PostureCheck"; @@ -7,6 +7,7 @@ import { GeoLocationTooltip } from "@/modules/posture-checks/checks/tooltips/Geo import { NetBirdVersionTooltip } from "@/modules/posture-checks/checks/tooltips/NetBirdVersionTooltip"; import { OperatingSystemTooltip } from "@/modules/posture-checks/checks/tooltips/OperatingSystemTooltip"; import { PeerNetworkRangeTooltip } from "@/modules/posture-checks/checks/tooltips/PeerNetworkRangeTooltip"; +import { ProcessTooltip } from "@/modules/posture-checks/checks/tooltips/ProcessTooltip"; type Props = { check: PostureCheck; @@ -71,6 +72,18 @@ export const PostureCheckChecksCell = ({ check }: Props) => { )} + + {check.checks.process_check && ( + +
+ +
+
+ )} diff --git a/src/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell.tsx b/src/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell.tsx index 15df3e3..f484931 100644 --- a/src/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell.tsx +++ b/src/modules/posture-checks/table/cells/PostureCheckPolicyUsageCell.tsx @@ -49,7 +49,10 @@ export const PostureCheckPolicyUsageCell = ({ check }: Props) => { interactive={false} > router.push("/access-control")} + onClick={(e) => { + e.stopPropagation(); + router.push("/access-control"); + }} variant={"gray"} useHover={!!(check.policies && check.policies?.length > 0)} className={cn( diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 45510bf..11cd039 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -74,8 +74,37 @@ export const validator = { /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?$/; return semverRegex.test(version); }, + isValidUnixFilePath: (path: string) => { + const endsWithSlash = path.endsWith("/"); + const unixPathRegex = /^\/(?:[^/]+\/)*[^/]+$/; + const isValid = unixPathRegex.test(path); + return isValid && !endsWithSlash; + }, + isValidWindowsFilePath: (path: string) => { + const endsWithBackSlash = path.endsWith("\\"); + const windowsPathRegex = + /^[a-zA-Z]:\\(?:[^\\/:*?"<>|\r\n]+\\)*[^\\/:*?"<>|\r\n]*$/; + const isValid = windowsPathRegex.test(path); + return isValid && !endsWithBackSlash; + }, }; export function isInt(n: number) { return n % 1 === 0; } + +export function tryGetProcessNameFromPath(path: string) { + try { + const canSplitByForwardSlash = path.includes("/"); + const canSplitByBackSlash = path.includes("\\"); + const byForwardSlash = canSplitByForwardSlash + ? path.split("/").pop() + : undefined; + const byBackSlash = canSplitByBackSlash + ? path.split("\\").pop() + : undefined; + return byForwardSlash || byBackSlash || path; + } catch (e) { + return path; + } +}