Skip to content

Commit 79164e9

Browse files
authored
Add process posture check (#378)
* Add process posture check * Add support for separate linux and mac paths
1 parent 5caeab1 commit 79164e9

File tree

8 files changed

+504
-10
lines changed

8 files changed

+504
-10
lines changed

src/components/Input.tsx

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export interface InputProps
1313
icon?: React.ReactNode;
1414
error?: string;
1515
errorTooltip?: boolean;
16+
errorTooltipPosition?: "top" | "top-right";
1617
}
1718

1819
const inputVariants = cva("", {
@@ -49,6 +50,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
4950
maxWidthClass = "",
5051
error,
5152
errorTooltip = false,
53+
errorTooltipPosition = "top",
5254
...props
5355
},
5456
ref,
@@ -105,9 +107,12 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
105107
</div>
106108
{error && errorTooltip && (
107109
<div
108-
className={
109-
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center"
110-
}
110+
className={cn(
111+
errorTooltipPosition == "top" &&
112+
"absolute right-0 top-2 h-[0px] w-full flex items-center pr-3 justify-center",
113+
errorTooltipPosition == "top-right" &&
114+
"absolute -right-6 top-2 h-[0px] w-full flex items-center pr-3 justify-end",
115+
)}
111116
>
112117
<FullTooltip
113118
content={
@@ -120,7 +125,7 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
120125
</div>
121126
}
122127
interactive={false}
123-
align={"center"}
128+
align={errorTooltipPosition == "top" ? "center" : "end"}
124129
side={"top"}
125130
keepOpen={true}
126131
>

src/interfaces/PostureCheck.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface PostureCheck {
1010
os_version_check?: OperatingSystemVersionCheck;
1111
geo_location_check?: GeoLocationCheck;
1212
peer_network_range_check?: PeerNetworkRangeCheck;
13+
process_check?: ProcessCheck;
1314
};
1415
policies?: Policy[];
1516
active?: boolean;
@@ -53,6 +54,17 @@ export interface PeerNetworkRangeCheck {
5354
action: "allow" | "deny";
5455
}
5556

57+
export interface ProcessCheck {
58+
processes: Process[];
59+
}
60+
61+
export interface Process {
62+
id: string;
63+
linux_path?: string;
64+
mac_path?: string;
65+
windows_path?: string;
66+
}
67+
5668
export const windowsKernelVersions: SelectOption[] = [
5769
{ value: "5.0", label: "Windows 2000" },
5870
{ value: "5.1", label: "Windows XP" },
Lines changed: 310 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,310 @@
1+
import Button from "@components/Button";
2+
import HelpText from "@components/HelpText";
3+
import InlineLink from "@components/InlineLink";
4+
import { Input } from "@components/Input";
5+
import { Label } from "@components/Label";
6+
import { ModalClose, ModalFooter } from "@components/modal/Modal";
7+
import Paragraph from "@components/Paragraph";
8+
import { cn, validator } from "@utils/helpers";
9+
import { isEmpty, uniqueId } from "lodash";
10+
import {
11+
ExternalLinkIcon,
12+
MinusCircleIcon,
13+
PlusCircle,
14+
ServerCogIcon,
15+
TerminalIcon,
16+
} from "lucide-react";
17+
import * as React from "react";
18+
import { useMemo, useState } from "react";
19+
import AppleIcon from "@/assets/icons/AppleIcon";
20+
import WindowsIcon from "@/assets/icons/WindowsIcon";
21+
import { Process, ProcessCheck } from "@/interfaces/PostureCheck";
22+
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
23+
24+
type Props = {
25+
value?: ProcessCheck;
26+
onChange: (value: ProcessCheck | undefined) => void;
27+
};
28+
29+
export const PostureCheckProcess = ({ value, onChange }: Props) => {
30+
const [open, setOpen] = useState(false);
31+
32+
return (
33+
<PostureCheckCard
34+
open={open}
35+
setOpen={setOpen}
36+
key={open ? 1 : 0}
37+
active={value?.processes && value?.processes?.length > 0}
38+
title={"Process"}
39+
description={
40+
"Restrict access in your network based on running processes of a peer."
41+
}
42+
icon={<ServerCogIcon size={18} />}
43+
iconClass={"bg-gradient-to-tr from-nb-gray-500 to-nb-gray-300"}
44+
modalWidthClass={"max-w-xl"}
45+
onReset={() => onChange(undefined)}
46+
>
47+
<CheckContent
48+
value={value}
49+
onChange={(v) => {
50+
onChange(v);
51+
setOpen(false);
52+
}}
53+
/>
54+
</PostureCheckCard>
55+
);
56+
};
57+
58+
const CheckContent = ({ value, onChange }: Props) => {
59+
const [processes, setProcesses] = useState<Process[]>(
60+
value?.processes
61+
? value.processes.map((p) => {
62+
return {
63+
id: uniqueId("process"),
64+
linux_path: p?.linux_path || "",
65+
mac_path: p?.mac_path || "",
66+
windows_path: p?.windows_path || "",
67+
};
68+
})
69+
: [
70+
{
71+
id: uniqueId("process"),
72+
linux_path: "",
73+
mac_path: "",
74+
windows_path: "",
75+
},
76+
],
77+
);
78+
79+
const handleProcessChange = (
80+
id: string,
81+
linux_path: string,
82+
mac_path: string,
83+
windows_path: string,
84+
) => {
85+
const newProcesses = processes.map((p) =>
86+
p.id === id ? { ...p, linux_path, mac_path, windows_path } : p,
87+
);
88+
setProcesses(newProcesses);
89+
};
90+
91+
const removeProcess = (id: string) => {
92+
const newProcesses = processes.filter((p) => p.id !== id);
93+
setProcesses(newProcesses);
94+
};
95+
96+
const addProcess = () => {
97+
setProcesses([
98+
...processes,
99+
{
100+
id: uniqueId("process"),
101+
linux_path: "",
102+
mac_path: "",
103+
windows_path: "",
104+
},
105+
]);
106+
};
107+
108+
const pathErrors = useMemo(() => {
109+
if (processes && processes.length > 0) {
110+
return processes.map((p) => {
111+
return {
112+
id: p.id,
113+
errorMacPath: p?.mac_path
114+
? validator.isValidUnixFilePath(p?.mac_path || "")
115+
? ""
116+
: "Please enter a valid macOS file path"
117+
: "",
118+
errorLinuxPath: p?.linux_path
119+
? validator.isValidUnixFilePath(p?.linux_path || "")
120+
? ""
121+
: "Please enter a valid Unix file path"
122+
: "",
123+
errorWindowsPath: p?.windows_path
124+
? validator.isValidWindowsFilePath(p?.windows_path || "")
125+
? ""
126+
: "Please enter a valid Windows file path"
127+
: "",
128+
};
129+
});
130+
} else {
131+
return [];
132+
}
133+
}, [processes]);
134+
135+
const hasErrorsOrIsEmpty = useMemo(() => {
136+
if (processes.length === 0) return true;
137+
const hasOnlyEmptyPaths = processes.some(
138+
(p) => p.linux_path === "" && p.mac_path === "" && p.windows_path === "",
139+
);
140+
const hasPathErrors = pathErrors.some(
141+
(e) =>
142+
e.errorLinuxPath !== "" ||
143+
e.errorMacPath !== "" ||
144+
e.errorWindowsPath !== "",
145+
);
146+
return hasOnlyEmptyPaths || hasPathErrors;
147+
}, [processes, pathErrors]);
148+
149+
return (
150+
<>
151+
<div className={"flex flex-col px-8 gap-2 pb-6"}>
152+
<div className={"flex justify-between items-start gap-10 mt-2"}>
153+
<div>
154+
<Label>Processes</Label>
155+
<HelpText className={""}>
156+
Add the path of an executable file of the process. You can define
157+
a path for Linux, macOS and Windows. Peers will only be allowed to
158+
connect if the process is running on their system.
159+
</HelpText>
160+
</div>
161+
</div>
162+
{processes.length > 0 && (
163+
<div className={"mb-2 flex flex-col gap-4 w-full "}>
164+
{processes.map((p) => {
165+
return (
166+
<div key={p.id} className={"flex gap-2 items-center"}>
167+
<div className={"w-full flex flex-col gap-1.5"}>
168+
<Input
169+
customPrefix={<TerminalIcon size={16} />}
170+
placeholder={"/usr/local/bin/netbird"}
171+
value={p.linux_path}
172+
error={
173+
pathErrors.find((e) => e.id === p.id)?.errorLinuxPath
174+
}
175+
errorTooltip={true}
176+
errorTooltipPosition={"top-right"}
177+
className={"w-full"}
178+
onChange={(e) =>
179+
handleProcessChange(
180+
p.id,
181+
e.target.value,
182+
p?.mac_path || "",
183+
p?.windows_path || "",
184+
)
185+
}
186+
/>
187+
<Input
188+
customPrefix={
189+
<AppleIcon
190+
size={16}
191+
className={cn(
192+
pathErrors.find((e) => e.id === p.id)
193+
?.errorMacPath && "fill-red-500",
194+
)}
195+
/>
196+
}
197+
placeholder={
198+
"/Applications/NetBird.app/Contents/MacOS/netbird"
199+
}
200+
value={p.mac_path}
201+
error={
202+
pathErrors.find((e) => e.id === p.id)?.errorMacPath
203+
}
204+
errorTooltip={true}
205+
errorTooltipPosition={"top-right"}
206+
className={"w-full"}
207+
onChange={(e) =>
208+
handleProcessChange(
209+
p.id,
210+
p?.linux_path || "",
211+
e.target.value,
212+
p?.windows_path || "",
213+
)
214+
}
215+
/>
216+
<Input
217+
customPrefix={
218+
<WindowsIcon
219+
size={16}
220+
className={cn(
221+
pathErrors.find((e) => e.id === p.id)
222+
?.errorWindowsPath && "fill-red-500",
223+
)}
224+
/>
225+
}
226+
placeholder={`C:\\ProgramData\\NetBird\\netbird.exe`}
227+
value={p.windows_path}
228+
errorTooltip={true}
229+
errorTooltipPosition={"top-right"}
230+
error={
231+
pathErrors.find((e) => e.id === p.id)?.errorWindowsPath
232+
}
233+
className={"w-full"}
234+
onChange={(e) =>
235+
handleProcessChange(
236+
p.id,
237+
p?.linux_path || "",
238+
p?.mac_path || "",
239+
e.target.value,
240+
)
241+
}
242+
/>
243+
</div>
244+
245+
<Button
246+
className={"h-[42px]"}
247+
variant={"default-outline"}
248+
onClick={() => removeProcess(p.id)}
249+
>
250+
<MinusCircleIcon size={15} />
251+
</Button>
252+
</div>
253+
);
254+
})}
255+
</div>
256+
)}
257+
<Button
258+
variant={"dotted"}
259+
size={"sm"}
260+
onClick={addProcess}
261+
className={"mt-1"}
262+
>
263+
<PlusCircle size={16} />
264+
Add Process
265+
</Button>
266+
</div>
267+
<ModalFooter className={"items-center"}>
268+
<div className={"w-full"}>
269+
<Paragraph className={"text-sm mt-auto"}>
270+
Learn more about
271+
<InlineLink
272+
href={
273+
"https://docs.netbird.io/how-to/manage-posture-checks#process-check"
274+
}
275+
target={"_blank"}
276+
>
277+
Process Check
278+
<ExternalLinkIcon size={12} />
279+
</InlineLink>
280+
</Paragraph>
281+
</div>
282+
<div className={"flex gap-3 w-full justify-end"}>
283+
<ModalClose asChild={true}>
284+
<Button variant={"secondary"}>Cancel</Button>
285+
</ModalClose>
286+
<Button
287+
variant={"primary"}
288+
disabled={hasErrorsOrIsEmpty}
289+
onClick={() => {
290+
if (isEmpty(processes)) {
291+
onChange(undefined);
292+
} else {
293+
onChange({
294+
processes: processes.filter(
295+
(p) =>
296+
p.linux_path !== "" ||
297+
p.mac_path !== "" ||
298+
p.windows_path !== "",
299+
),
300+
});
301+
}
302+
}}
303+
>
304+
Save
305+
</Button>
306+
</div>
307+
</ModalFooter>
308+
</>
309+
);
310+
};

0 commit comments

Comments
 (0)