Skip to content

Commit

Permalink
Extend posture checks with peer network range check (#344)
Browse files Browse the repository at this point in the history
add support to peer network checks
  • Loading branch information
heisbrot authored Feb 27, 2024
1 parent 02a0b71 commit 8e7bcc0
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 5 deletions.
1 change: 1 addition & 0 deletions .github/workflows/build_and_push.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ name: build and push
on:
push:
branches:
- "feature/**"
- main
tags:
- "**"
Expand Down
6 changes: 6 additions & 0 deletions src/interfaces/PostureCheck.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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" },
Expand Down
219 changes: 219 additions & 0 deletions src/modules/posture-checks/checks/PostureCheckPeerNetworkRange.tsx
Original file line number Diff line number Diff line change
@@ -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>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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
);
};
18 changes: 14 additions & 4 deletions src/modules/posture-checks/modal/PostureCheckModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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,
},
};

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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"}>
Expand Down Expand Up @@ -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
Expand Down
Loading

0 comments on commit 8e7bcc0

Please sign in to comment.