mirror of
https://github.com/netbirdio/dashboard.git
synced 2026-01-26 01:21:04 +00:00
Extend posture checks with peer network range check (#344)
add support to peer network checks
This commit is contained in:
1
.github/workflows/build_and_push.yml
vendored
1
.github/workflows/build_and_push.yml
vendored
@@ -2,6 +2,7 @@ name: build and push
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "feature/**"
|
||||
- main
|
||||
tags:
|
||||
- "**"
|
||||
|
||||
@@ -9,6 +9,7 @@ export interface PostureCheck {
|
||||
nb_version_check?: NetBirdVersionCheck;
|
||||
os_version_check?: OperatingSystemVersionCheck;
|
||||
geo_location_check?: GeoLocationCheck;
|
||||
peer_network_range_check?: PeerNetworkRangeCheck;
|
||||
};
|
||||
policies?: Policy[];
|
||||
active?: boolean;
|
||||
@@ -47,6 +48,11 @@ export interface GeoLocation {
|
||||
city_name: string;
|
||||
}
|
||||
|
||||
export interface PeerNetworkRangeCheck {
|
||||
ranges: string[];
|
||||
action: "allow" | "deny";
|
||||
}
|
||||
|
||||
export const windowsKernelVersions: SelectOption[] = [
|
||||
{ value: "5.0", label: "Windows 2000" },
|
||||
{ value: "5.1", label: "Windows XP" },
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
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 { RadioGroup, RadioGroupItem } from "@components/RadioGroup";
|
||||
import cidr from "ip-cidr";
|
||||
import { isEmpty, uniqueId } from "lodash";
|
||||
import {
|
||||
ExternalLinkIcon,
|
||||
MinusCircleIcon,
|
||||
NetworkIcon,
|
||||
PlusCircle,
|
||||
ShieldCheck,
|
||||
ShieldXIcon,
|
||||
} from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { useMemo, useState } from "react";
|
||||
import { PeerNetworkRangeCheck } from "@/interfaces/PostureCheck";
|
||||
import { PostureCheckCard } from "@/modules/posture-checks/ui/PostureCheckCard";
|
||||
|
||||
type Props = {
|
||||
value?: PeerNetworkRangeCheck;
|
||||
onChange: (value: PeerNetworkRangeCheck | undefined) => void;
|
||||
};
|
||||
|
||||
export const PostureCheckPeerNetworkRange = ({ value, onChange }: Props) => {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<PostureCheckCard
|
||||
open={open}
|
||||
setOpen={setOpen}
|
||||
key={open ? 1 : 0}
|
||||
icon={<NetworkIcon size={16} />}
|
||||
title={"Peer Network Range"}
|
||||
modalWidthClass={"max-w-xl"}
|
||||
description={
|
||||
"Restrict access by allowing or blocking peer network ranges."
|
||||
}
|
||||
iconClass={"bg-gradient-to-tr from-blue-500 to-blue-400"}
|
||||
active={value !== undefined}
|
||||
onReset={() => onChange(undefined)}
|
||||
>
|
||||
<CheckContent
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
onChange(v);
|
||||
setOpen(false);
|
||||
}}
|
||||
/>
|
||||
</PostureCheckCard>
|
||||
);
|
||||
};
|
||||
|
||||
interface NetworkRange {
|
||||
id: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
const CheckContent = ({ value, onChange }: Props) => {
|
||||
const [allowOrDeny, setAllowOrDeny] = useState<string>(
|
||||
value?.action ? value.action : "allow",
|
||||
);
|
||||
|
||||
const [networkRanges, setNetworkRanges] = useState<NetworkRange[]>(
|
||||
value?.ranges
|
||||
? value.ranges.map((r) => {
|
||||
return {
|
||||
id: uniqueId("range"),
|
||||
value: r,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
);
|
||||
|
||||
const handleNetworkRangeChange = (id: string, value: string) => {
|
||||
const newRanges = networkRanges.map((r) =>
|
||||
r.id === id ? { ...r, value } : r,
|
||||
);
|
||||
setNetworkRanges(newRanges);
|
||||
};
|
||||
|
||||
const removeNetworkRange = (id: string) => {
|
||||
const newRanges = networkRanges.filter((r) => r.id !== id);
|
||||
setNetworkRanges(newRanges);
|
||||
};
|
||||
|
||||
const addNetworkRange = () => {
|
||||
setNetworkRanges([...networkRanges, { id: uniqueId("range"), value: "" }]);
|
||||
};
|
||||
|
||||
const validateNetworkRange = (networkRange: string) => {
|
||||
if (networkRange == "") return "";
|
||||
const validCIDR = cidr.isValidAddress(networkRange);
|
||||
if (!validCIDR) return "Please enter a valid CIDR, e.g., 192.168.1.0/24";
|
||||
return "";
|
||||
};
|
||||
|
||||
const cidrErrors = useMemo(() => {
|
||||
if (networkRanges && networkRanges.length > 0) {
|
||||
return networkRanges.map((r) => {
|
||||
return {
|
||||
id: r.id,
|
||||
error: validateNetworkRange(r.value),
|
||||
};
|
||||
});
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [networkRanges]);
|
||||
|
||||
const hasErrorsOrIsEmpty = useMemo(() => {
|
||||
if (networkRanges.length === 0) return true;
|
||||
return cidrErrors.some((e) => e.error !== "");
|
||||
}, [networkRanges, cidrErrors]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={"flex flex-col px-8 gap-2 pb-6"}>
|
||||
<div className={"flex justify-between items-start gap-10 mt-2"}>
|
||||
<div>
|
||||
<Label>Allow or Block Ranges</Label>
|
||||
<HelpText className={""}>
|
||||
Choose whether you want to allow or block specific peer network
|
||||
ranges
|
||||
</HelpText>
|
||||
</div>
|
||||
<RadioGroup value={allowOrDeny} onChange={setAllowOrDeny}>
|
||||
<RadioGroupItem value={"allow"} variant={"green"}>
|
||||
<ShieldCheck size={16} />
|
||||
Allow
|
||||
</RadioGroupItem>
|
||||
<RadioGroupItem value={"deny"} variant={"red"}>
|
||||
<ShieldXIcon size={16} />
|
||||
Block
|
||||
</RadioGroupItem>
|
||||
</RadioGroup>
|
||||
</div>
|
||||
{networkRanges.length > 0 && (
|
||||
<div className={"mb-2 flex flex-col gap-2 w-full "}>
|
||||
{networkRanges.map((ipRange) => {
|
||||
return (
|
||||
<div key={ipRange.id} className={"flex gap-2"}>
|
||||
<div className={"w-full"}>
|
||||
<Input
|
||||
customPrefix={<NetworkIcon size={16} />}
|
||||
placeholder={"e.g., 172.16.0.0/16"}
|
||||
value={ipRange.value}
|
||||
error={cidrErrors.find((e) => e.id === ipRange.id)?.error}
|
||||
errorTooltip={false}
|
||||
className={"font-mono !text-[13px] w-full"}
|
||||
onChange={(e) =>
|
||||
handleNetworkRangeChange(ipRange.id, e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className={"h-[42px]"}
|
||||
variant={"default-outline"}
|
||||
onClick={() => removeNetworkRange(ipRange.id)}
|
||||
>
|
||||
<MinusCircleIcon size={15} />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Button variant={"dotted"} size={"sm"} onClick={addNetworkRange}>
|
||||
<PlusCircle size={16} />
|
||||
Add Network Range
|
||||
</Button>
|
||||
</div>
|
||||
<ModalFooter className={"items-center"}>
|
||||
<div className={"w-full"}>
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/manage-posture-checks#peer-network-range-check"
|
||||
}
|
||||
target={"_blank"}
|
||||
>
|
||||
Peer Network Range Check
|
||||
<ExternalLinkIcon size={12} />
|
||||
</InlineLink>
|
||||
</Paragraph>
|
||||
</div>
|
||||
<div className={"flex gap-3 w-full justify-end"}>
|
||||
<ModalClose asChild={true}>
|
||||
<Button variant={"secondary"}>Cancel</Button>
|
||||
</ModalClose>
|
||||
<Button
|
||||
variant={"primary"}
|
||||
disabled={hasErrorsOrIsEmpty}
|
||||
onClick={() => {
|
||||
if (isEmpty(networkRanges)) {
|
||||
onChange(undefined);
|
||||
} else {
|
||||
onChange({
|
||||
action: allowOrDeny as "allow" | "deny",
|
||||
ranges: networkRanges
|
||||
.map((r) => r.value)
|
||||
.filter((r) => r !== ""),
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,67 @@
|
||||
import Badge from "@components/Badge";
|
||||
import FullTooltip from "@components/FullTooltip";
|
||||
import { ScrollArea } from "@components/ScrollArea";
|
||||
import { NetworkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import { PeerNetworkRangeCheck } from "@/interfaces/PostureCheck";
|
||||
|
||||
type Props = {
|
||||
check?: PeerNetworkRangeCheck;
|
||||
children?: React.ReactNode;
|
||||
};
|
||||
export const PeerNetworkRangeTooltip = ({ check, children }: Props) => {
|
||||
return check ? (
|
||||
<FullTooltip
|
||||
className={"w-full"}
|
||||
interactive={true}
|
||||
contentClassName={"p-0"}
|
||||
content={
|
||||
<div
|
||||
className={"text-neutral-300 text-sm max-w-xs flex flex-col gap-1"}
|
||||
>
|
||||
<div className={"px-4 pt-3"}>
|
||||
{check.action == "allow" ? (
|
||||
<span>
|
||||
<span className={"text-green-500 font-semibold"}>
|
||||
Allow only
|
||||
</span>{" "}
|
||||
the following peer network ranges
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<span className={"text-red-500 font-semibold"}>Block</span> the
|
||||
following peer network ranges
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<ScrollArea
|
||||
className={"max-h-[275px] overflow-y-auto flex flex-col px-4"}
|
||||
>
|
||||
<div className={"flex flex-col gap-1.5 mt-1 text-xs mb-3.5"}>
|
||||
{check.ranges.map((ipRange, index) => {
|
||||
return (
|
||||
<Badge
|
||||
variant={"gray"}
|
||||
useHover={false}
|
||||
key={index}
|
||||
className={
|
||||
"justify-start font-medium font-mono text-[11px]"
|
||||
}
|
||||
>
|
||||
<NetworkIcon size={10} />
|
||||
{ipRange}
|
||||
</Badge>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</FullTooltip>
|
||||
) : (
|
||||
children
|
||||
);
|
||||
};
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
import { PostureCheckGeoLocation } from "@/modules/posture-checks/checks/PostureCheckGeoLocation";
|
||||
import { PostureCheckNetBirdVersion } from "@/modules/posture-checks/checks/PostureCheckNetBirdVersion";
|
||||
import { PostureCheckOperatingSystem } from "@/modules/posture-checks/checks/PostureCheckOperatingSystem";
|
||||
import { PostureCheckPeerNetworkRange } from "@/modules/posture-checks/checks/PostureCheckPeerNetworkRange";
|
||||
|
||||
type Props = {
|
||||
open: boolean;
|
||||
@@ -54,6 +55,9 @@ export default function PostureCheckModal({
|
||||
const [osVersionCheck, setOsVersionCheck] = useState(
|
||||
postureCheck?.checks.os_version_check || undefined,
|
||||
);
|
||||
const [peerNetworkRangeCheck, setPeerNetworkRangeCheck] = useState(
|
||||
postureCheck?.checks.peer_network_range_check || undefined,
|
||||
);
|
||||
|
||||
const validateOSCheck = (osCheck?: OperatingSystemVersionCheck) => {
|
||||
if (!osCheck) return;
|
||||
@@ -93,6 +97,7 @@ export default function PostureCheckModal({
|
||||
nb_version_check: nbVersionCheck,
|
||||
geo_location_check: validateLocationCheck(geoLocationCheck),
|
||||
os_version_check: validateOSCheck(osVersionCheck),
|
||||
peer_network_range_check: peerNetworkRangeCheck,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -125,7 +130,10 @@ export default function PostureCheckModal({
|
||||
};
|
||||
|
||||
const isAtLeastOneCheckEnabled =
|
||||
!!nbVersionCheck || !!geoLocationCheck || !!osVersionCheck;
|
||||
!!nbVersionCheck ||
|
||||
!!geoLocationCheck ||
|
||||
!!osVersionCheck ||
|
||||
!!peerNetworkRangeCheck;
|
||||
const canCreate = !isEmpty(name) && isAtLeastOneCheckEnabled;
|
||||
|
||||
const [tab, setTab] = useState("checks");
|
||||
@@ -180,6 +188,10 @@ export default function PostureCheckModal({
|
||||
value={osVersionCheck}
|
||||
onChange={setOsVersionCheck}
|
||||
/>
|
||||
<PostureCheckPeerNetworkRange
|
||||
value={peerNetworkRangeCheck}
|
||||
onChange={setPeerNetworkRangeCheck}
|
||||
/>
|
||||
</>
|
||||
</TabsContent>
|
||||
<TabsContent value={"general"} className={"pb-8 px-8"}>
|
||||
@@ -221,9 +233,7 @@ export default function PostureCheckModal({
|
||||
<Paragraph className={"text-sm mt-auto"}>
|
||||
Learn more about
|
||||
<InlineLink
|
||||
href={
|
||||
"https://docs.netbird.io/how-to/manage-posture-checks"
|
||||
}
|
||||
href={"https://docs.netbird.io/how-to/manage-posture-checks"}
|
||||
target={"_blank"}
|
||||
>
|
||||
Posture Checks
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { cn } from "@utils/helpers";
|
||||
import { Disc3Icon, FlagIcon } from "lucide-react";
|
||||
import { Disc3Icon, FlagIcon, NetworkIcon } from "lucide-react";
|
||||
import * as React from "react";
|
||||
import NetBirdIcon from "@/assets/icons/NetBirdIcon";
|
||||
import { PostureCheck } from "@/interfaces/PostureCheck";
|
||||
import { GeoLocationTooltip } from "@/modules/posture-checks/checks/tooltips/GeoLocationTooltip";
|
||||
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";
|
||||
|
||||
type Props = {
|
||||
check: PostureCheck;
|
||||
@@ -56,6 +57,20 @@ export const PostureCheckChecksCell = ({ check }: Props) => {
|
||||
</div>
|
||||
</OperatingSystemTooltip>
|
||||
)}
|
||||
|
||||
{check.checks.peer_network_range_check && (
|
||||
<PeerNetworkRangeTooltip
|
||||
check={check.checks.peer_network_range_check}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
"bg-gradient-to-tr from-blue-500 to-blue-400 h-8 w-8 rounded-full flex items-center justify-center relative z-[8] hover:scale-[1.1] transition-all",
|
||||
)}
|
||||
>
|
||||
<NetworkIcon size={14} />
|
||||
</div>
|
||||
</PeerNetworkRangeTooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user