Skip to content

Commit

Permalink
adds link from dependency graph to risk assessment
Browse files Browse the repository at this point in the history
  • Loading branch information
timbastin committed Jul 16, 2024
1 parent 75aeb49 commit a03a86a
Show file tree
Hide file tree
Showing 5 changed files with 114 additions and 72 deletions.
34 changes: 21 additions & 13 deletions src/components/DependencyGraph.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
import { useActiveAsset } from "@/hooks/useActiveAsset";
import { AffectedPackage, DependencyTreeNode } from "@/types/api/api";
import { AffectedPackage, DependencyTreeNode, FlawDTO } from "@/types/api/api";
import { ViewDependencyTreeNode } from "@/types/view/assetTypes";
import dagre, { graphlib } from "@dagrejs/dagre";
import {
Expand All @@ -30,11 +30,12 @@ import {
useNodesState,
} from "@xyflow/react";

import { DependencyGraphNode, riskToBgColor } from "./DependencyGraphNode";
import { DependencyGraphNode } from "./DependencyGraphNode";
import { useRouter } from "next/router";

// or if you just want basic styles
import "@xyflow/react/dist/base.css";
import { riskToSeverity, severityToColor } from "./common/Severity";

const nodeWidth = 300;
const nodeHeight = 100;
Expand Down Expand Up @@ -82,7 +83,7 @@ const recursiveFlatten = (

const getLayoutedElements = (
tree: ViewDependencyTreeNode,
affectedPackages: Array<AffectedPackage> = [],
flaws: Array<FlawDTO> = [],
direction = "LR",
): [
Array<{
Expand All @@ -99,12 +100,12 @@ const getLayoutedElements = (
const dagreGraph = new dagre.graphlib.Graph();
dagreGraph.setDefaultEdgeLabel(() => ({}));
// build a map of all affected packages
const affectedMap = affectedPackages.reduce(
const flawMap = flaws.reduce(
(acc, cur) => {
acc[cur.PurlWithVersion] = cur;
acc[cur.componentPurlOrCpe!] = cur;
return acc;
},
{} as { [key: string]: AffectedPackage },
{} as { [key: string]: FlawDTO },
);

const riskMap = recursiveFlatten(tree).reduce(
Expand Down Expand Up @@ -141,10 +142,11 @@ const getLayoutedElements = (
data: {
label: el,
risk: riskMap[el],
affectedPackage: affectedMap[el],
flaw: flawMap[el],
},
};
});

const edges = dagreGraph.edges().map((el) => {
const source = el.v;
const target = el.w;
Expand All @@ -154,10 +156,12 @@ const getLayoutedElements = (
target: source,
source: target,
// type: "smoothstep",
animated: true,
animated: false,
style: {
stroke:
riskMap[target] > 0 ? riskToBgColor(riskMap[target]) : "#a1a1aa",
riskMap[target] > 0
? severityToColor(riskToSeverity(riskMap[target]))
: "#a1a1aa",
},
};
});
Expand All @@ -171,9 +175,9 @@ const nodeTypes = {
const DependencyGraph: FunctionComponent<{
width: number;
height: number;
affectedPackages: Array<AffectedPackage>;
flaws: Array<FlawDTO>;
graph: { root: ViewDependencyTreeNode };
}> = ({ graph, width, height, affectedPackages }) => {
}> = ({ graph, width, height, flaws }) => {
const asset = useActiveAsset();
const router = useRouter();

Expand All @@ -182,11 +186,11 @@ const DependencyGraph: FunctionComponent<{
const [initialNodes, initialEdges, rootNode] = useMemo(() => {
graph.root.name = asset?.name ?? "";

const [nodes, edges] = getLayoutedElements(graph.root, affectedPackages);
const [nodes, edges] = getLayoutedElements(graph.root, flaws);
// get the root node - we use it for the initial position of the viewport
const rootNode = nodes.find((n) => n.data.label === graph.root.name)!;
return [nodes, edges, rootNode];
}, [graph, asset?.name, affectedPackages]);
}, [graph, asset?.name, flaws]);

const [nodes, setNodes, onNodesChange] = useNodesState(initialNodes);
const [edges, setEdges, onEdgesChange] = useEdgesState(initialEdges);
Expand Down Expand Up @@ -237,6 +241,10 @@ const DependencyGraph: FunctionComponent<{
nodeTypes={nodeTypes}
nodesConnectable={false}
edges={edges}
edgesFocusable={false}
defaultEdgeOptions={{
selectable: false,
}}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onConnect={onConnect}
Expand Down
87 changes: 60 additions & 27 deletions src/components/DependencyGraphNode.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,61 +13,94 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

import { AffectedPackage } from "@/types/api/api";
import { FunctionComponent } from "react";
import { FlawDTO } from "@/types/api/api";
import { Handle, Position } from "@xyflow/react";
import tinycolor from "tinycolor2";
import { FunctionComponent } from "react";

import { classNames } from "@/utils/common";
import { useRouter } from "next/router";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "./ui/dropdown-menu";
import FlawState from "./common/FlawState";
import Link from "next/link";
import Severity, { riskToSeverity, severityToColor } from "./common/Severity";

export interface DependencyGraphNodeProps {
data: {
label: string;
affectedPackage: AffectedPackage;
flaw: FlawDTO;
risk: number;
};
}

const riskToTextColor = (risk: number) => {
const red = new tinycolor("red");
return red.lighten((1 - risk) * 50).isLight() ? "black" : "white";
};

export const riskToBgColor = (risk: number) => {
const red = new tinycolor("red");
const color = red.lighten((1 - risk) * 50).toString("hex");

return color;
if (risk > 4) {
return "black";
}
return "white";
};

export const DependencyGraphNode: FunctionComponent<
DependencyGraphNodeProps
> = (props) => {
const color = riskToBgColor(props.data.risk);
const color = severityToColor(riskToSeverity(props.data.risk));
const shouldFocus = useRouter().query.pkg === props.data.label;
return (
const router = useRouter();
const Node = (
<div
style={{
maxWidth: 200,
color:
props.data.affectedPackage === undefined
? "black"
: riskToTextColor(props.data.risk),
backgroundColor:
props.data.affectedPackage !== undefined ? color : "white",
maxWidth: 300,
borderColor: props.data.flaw !== undefined ? color : undefined,
//backgroundColor: props.data.flaw !== undefined ? color : "white",
}}
className={classNames(
"relative rounded border bg-white p-3 text-xs",

"relative rounded border bg-card p-3 text-xs text-card-foreground",
shouldFocus ? "border-2 border-primary" : "",
)}
>
<Handle type="target" position={Position.Right} />
<div>
<Handle
className="rounded-full"
type="target"
position={Position.Right}
/>
<div className="flex flex-col items-start justify-center gap-2">
<label htmlFor="text">{props.data.label}</label>
</div>
<Handle type="source" position={Position.Left} id="a" />
<Handle
className="rounded-full"
type="source"
position={Position.Left}
id="a"
/>
</div>
);

if (!props.data.flaw) {
return Node;
}
return (
<DropdownMenu>
<DropdownMenuTrigger>{Node}</DropdownMenuTrigger>
<DropdownMenuContent className="text-xs">
<div className="p-2">{props.data.flaw.cveId}</div>
<DropdownMenuSeparator />
<DropdownMenuItem>
<Link
className="!text-foreground hover:no-underline"
href={
router.asPath.split("?")[0] + `/../flaws/${props.data.flaw.id}`
}
>
Go to risk assessment
</Link>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
);
};
19 changes: 17 additions & 2 deletions src/components/common/Severity.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { classNames } from "@/utils/common";

const getClassNames = (severity: string) => {
export const getClassNames = (severity: string) => {
switch (severity) {
case "CRITICAL":
return "text-red-500 border border-red-500";
Expand All @@ -15,7 +15,22 @@ const getClassNames = (severity: string) => {
}
};

const riskToSeverity = (risk: number) => {
export const severityToColor = (severity: string) => {
switch (severity) {
case "CRITICAL":
return "#ef4444";
case "HIGH":
return "#f97316";
case "MEDIUM":
return "#facc15";
case "LOW":
return "#22c55e";
default:
return "gray";
}
};

export const riskToSeverity = (risk: number) => {
if (risk >= 9) return "CRITICAL";
if (risk >= 7) return "HIGH";
if (risk >= 4) return "MEDIUM";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import {
Select,
SelectContent,
SelectItem,
SelectValue,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Switch } from "@/components/ui/switch";
import { HEADER_HEIGHT, SIDEBAR_WIDTH } from "@/const/viewConstants";
Expand All @@ -38,7 +38,7 @@ import { useActiveProject } from "@/hooks/useActiveProject";
import { useAssetMenu } from "@/hooks/useAssetMenu";
import useDimensions from "@/hooks/useDimensions";
import { getApiClientFromContext } from "@/services/devGuardApi";
import { AffectedPackage, DependencyTreeNode } from "@/types/api/api";
import { DependencyTreeNode, FlawDTO } from "@/types/api/api";
import { ViewDependencyTreeNode } from "@/types/view/assetTypes";

import Link from "next/link";
Expand All @@ -48,8 +48,8 @@ import { FunctionComponent } from "react";
const DependencyGraphPage: FunctionComponent<{
graph: { root: ViewDependencyTreeNode };
versions: string[];
affectedPackages: Array<AffectedPackage>;
}> = ({ graph, affectedPackages, versions }) => {
flaws: Array<FlawDTO>;
}> = ({ graph, flaws, versions }) => {
const activeOrg = useActiveOrg();
const project = useActiveProject();
const asset = useActiveAsset();
Expand Down Expand Up @@ -179,7 +179,7 @@ const DependencyGraphPage: FunctionComponent<{
>
<div className="h-screen w-full rounded-lg border bg-white dark:bg-black">
<DependencyGraph
affectedPackages={affectedPackages}
flaws={flaws}
width={dimensions.width - SIDEBAR_WIDTH}
height={dimensions.height - HEADER_HEIGHT - 85}
graph={graph}
Expand All @@ -192,31 +192,16 @@ const DependencyGraphPage: FunctionComponent<{

export default DependencyGraphPage;

const severityToRisk = (severity: string): number => {
switch (severity) {
case "CRITICAL":
return 1;
case "HIGH":
return 0.7;
case "MEDIUM":
return 0.5;
case "LOW":
return 0.3;
default:
return 0;
}
};

const RISK_INHERITANCE_FACTOR = 0.33;
const recursiveAddRisk = (
node: ViewDependencyTreeNode,
affected: Array<AffectedPackage>,
flaws: Array<FlawDTO>,
) => {
const affectedPackage = affected.find((p) => p.PurlWithVersion === node.name);
const flaw = flaws.find((p) => p.componentPurlOrCpe === node.name);

// if there are no children, the risk is the risk of the affected package
if (affectedPackage) {
node.risk = severityToRisk(affectedPackage.CVE.severity);
if (flaw) {
node.risk = flaw.rawRiskAssessment;
// update the parent node with the risk of this node
let parent = node.parent;
let i = 0;
Expand All @@ -228,7 +213,7 @@ const recursiveAddRisk = (
parent = parent.parent;
}
}
node.children.forEach((child) => recursiveAddRisk(child, affected));
node.children.forEach((child) => recursiveAddRisk(child, flaws));

return node;
};
Expand Down Expand Up @@ -282,7 +267,7 @@ export const getServerSideProps = middleware(
// check for version query parameter
const version = context.query.version as string | undefined;

const [resp, affectedResp, versionsResp] = await Promise.all([
const [resp, flawResp, versionsResp] = await Promise.all([
apiClient(
uri + "dependency-graph" + (version ? "?version=" + version : " "),
),
Expand All @@ -294,15 +279,15 @@ export const getServerSideProps = middleware(

// fetch a personal access token from the user

const [graph, affected, versions] = await Promise.all([
const [graph, flaws, versions] = await Promise.all([
resp.json() as Promise<{ root: DependencyTreeNode }>,
affectedResp.json() as Promise<Array<AffectedPackage>>,
flawResp.json() as Promise<Array<FlawDTO>>,
versionsResp.json() as Promise<Array<string>>,
]);

const converted = convertGraph(graph.root);

recursiveAddRisk(converted, affected);
recursiveAddRisk(converted, flaws);
// we cannot return a circular data structure - remove the parent again
recursiveRemoveParent(converted);

Expand All @@ -314,7 +299,7 @@ export const getServerSideProps = middleware(
return {
props: {
graph: { root: converted },
affectedPackages: affected,
flaws,
versions,
},
};
Expand Down
1 change: 1 addition & 0 deletions src/types/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export interface FlawDTO {
createdAt: string;
updatedAt: string;
cveId: string | null;
componentPurlOrCpe: string | null;

state:
| "open"
Expand Down

0 comments on commit a03a86a

Please sign in to comment.