From 0cc27f9282ec96c1d69bee753e9fa516cf7fc664 Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Tue, 23 Jun 2026 16:14:08 +0200 Subject: [PATCH 1/2] feat: first approach advisory-tab --- .../refs/[assetVersionSlug]/advisory/page.tsx | 52 +++++++++++++++++++ src/hooks/useAssetMenu.ts | 24 ++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/page.tsx diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/page.tsx new file mode 100644 index 000000000..0eed38c54 --- /dev/null +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/page.tsx @@ -0,0 +1,52 @@ +"use client"; + +import AssetTitle from "@/components/common/AssetTitle"; +import Page from "@/components/Page"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { useState, type FunctionComponent } from "react"; +import { useAssetMenu } from "@/hooks/useAssetMenu"; +import useDecodedParams from "@/hooks/useDecodedParams"; +import { browserApiClient } from "@/services/devGuardApi"; +const Index: FunctionComponent = () => { + const assetMenu = useAssetMenu(); + const { organizationSlug, projectSlug, assetSlug, assetVersionSlug } = + useDecodedParams() as { + organizationSlug: string; + projectSlug: string; + assetSlug: string; + assetVersionSlug: string; + }; + const [data, setData] = useState(""); + const handleClick = async () => { + const resp = await browserApiClient( + "/organizations/" + + organizationSlug + + "/projects/" + + projectSlug + + "/assets/" + + assetSlug + + "/refs/" + + assetVersionSlug + + "/advisory/submit", + { + method: "POST", + body: JSON.stringify({ + name: data, + }), + }, + ); + }; + return ( + }> +
+ setData(e.target.value)} /> +
+
+ +
+
+ ); +}; + +export default Index; diff --git a/src/hooks/useAssetMenu.ts b/src/hooks/useAssetMenu.ts index 3418a1314..7aeef63e6 100644 --- a/src/hooks/useAssetMenu.ts +++ b/src/hooks/useAssetMenu.ts @@ -20,7 +20,13 @@ import { ShareIcon, WrenchScrewdriverIcon, } from "@heroicons/react/24/outline"; -import { CodeIcon, BookCheckIcon, ScanText, TextSelect } from "lucide-react"; +import { + CodeIcon, + BookCheckIcon, + ScanText, + TextSelect, + ShieldEllipsis, +} from "lucide-react"; import type { ForwardRefExoticComponent, RefAttributes, SVGProps } from "react"; import { useActiveAsset } from "./useActiveAsset"; import { useActiveAssetVersion } from "./useActiveAssetVersion"; @@ -226,6 +232,22 @@ export const useAssetMenu = () => { isActive: pathname.includes("artifacts"), testId: "nav-asset-artifacts", }, + { + title: "Security Advisory", + href: + "/" + + orgSlug + + "/projects/" + + projectSlug + + "/assets/" + + assetSlug + + "/refs/" + + assetVersionSlug + + "/advisory", + Icon: ShieldEllipsis, + isActive: pathname.includes("advisory"), + testId: "nav-asset-advisory", + }, ]); } else { menu.unshift({ From 0b73e13b8b4a709ee5112977b82ea43a72de484b Mon Sep 17 00:00:00 2001 From: Julian Kepka Date: Fri, 26 Jun 2026 11:50:43 +0200 Subject: [PATCH 2/2] feat: first approach of SA top page and details page --- .../advisory/[advisoryId]/loading.tsx | 7 + .../advisory/[advisoryId]/page.tsx | 133 ++++++++++ .../refs/[assetVersionSlug]/advisory/page.tsx | 234 +++++++++++++++++- src/types/api/api.ts | 18 ++ 4 files changed, 379 insertions(+), 13 deletions(-) create mode 100644 src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/[advisoryId]/loading.tsx create mode 100644 src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/[advisoryId]/page.tsx diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/[advisoryId]/loading.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/[advisoryId]/loading.tsx new file mode 100644 index 000000000..6de409e63 --- /dev/null +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/[advisoryId]/loading.tsx @@ -0,0 +1,7 @@ +import PageSkeleton from "@/components/PageSkeleton"; + +const loading = () => { + return ; +}; + +export default loading; diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/[advisoryId]/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/[advisoryId]/page.tsx new file mode 100644 index 000000000..b723d829c --- /dev/null +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/[advisoryId]/page.tsx @@ -0,0 +1,133 @@ +"use client"; + +import useSWR from "swr"; +import Page from "@/components/Page"; +import { fetcher } from "@/data-fetcher/fetcher"; +import { useAssetMenu } from "@/hooks/useAssetMenu"; +import useDecodedParams from "@/hooks/useDecodedParams"; +import { Skeleton } from "@/components/ui/skeleton"; +import type { SecurityAdvisory } from "@/types/api/api"; +import { getSeverityClassNames } from "@/components/common/Severity"; +import Markdown from "@/components/common/Markdown"; +import { classNames } from "@/utils/common"; +import { Button } from "@/components/ui/button"; + +const Index = () => { + const params = useDecodedParams(); + const { + organizationSlug, + projectSlug, + assetSlug, + assetVersionSlug, + advisoryId, + } = params; + const assetMenu = useAssetMenu(); + const { data: advisory, isLoading } = useSWR( + organizationSlug && + projectSlug && + assetSlug && + assetVersionSlug && + advisoryId + ? `/organizations/${organizationSlug}/projects/${projectSlug}/assets/${assetSlug}/refs/${assetVersionSlug}/advisory/${advisoryId}/` + : null, + fetcher, + ); + + if (isLoading || !advisory) { + return ( + + + + + + + ); + } + + const severityUpper = advisory.severity?.toUpperCase() ?? ""; + + return ( + +
+
+
+

{advisory.title}

+
+ + {advisory.affectedPackages.length > 0 && ( +
+ + + + + + + + + + {advisory.affectedPackages.map((pkg) => ( + + + + + + ))} + +
Package + Affected versions + + Patched versions +
{pkg.packagename} + {pkg.semverStart ? `< ${pkg.semverStart}` : "—"} + + {pkg.semverEnd ?? "—"} +
+
+ )} + + {advisory.description && ( +
+

Description

+ {advisory.description} +
+ )} +
+ +
+
+
+
+ Severity +
+ + {advisory.severity} + +
+ + {advisory.vectorstring && ( +
+
+ Vector +
+ + {advisory.vectorstring} + +
+ )} +
+
+
+
+ ); +}; + +export default Index; diff --git a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/page.tsx b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/page.tsx index 0eed38c54..8b08e755e 100644 --- a/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/page.tsx +++ b/src/app/(loading-group)/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/refs/[assetVersionSlug]/advisory/page.tsx @@ -2,14 +2,67 @@ import AssetTitle from "@/components/common/AssetTitle"; import Page from "@/components/Page"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { useState, type FunctionComponent } from "react"; import { useAssetMenu } from "@/hooks/useAssetMenu"; import useDecodedParams from "@/hooks/useDecodedParams"; +import Section from "@/components/common/Section"; +import { Button } from "@/components/ui/button"; +import AuthGuard from "@/components/AuthGuard"; +import { useRouter, usePathname } from "next/navigation"; +import EmptyParty from "@/components/common/EmptyParty"; +import { Loader2 } from "lucide-react"; +import CustomPagination from "@/components/common/CustomPagination"; +import { + createColumnHelper, + flexRender, + getSortedRowModel, +} from "@tanstack/react-table"; +import type { ColumnDef } from "@tanstack/react-table"; +import type { SecurityAdvisory, Paged } from "@/types/api/api"; +import useSWR from "swr"; +import { fetcher } from "@/data-fetcher/fetcher"; +import useTable from "@/hooks/useTable"; +import SortingCaret from "@/components/common/SortingCaret"; +import { classNames } from "@/utils/common"; +import { Skeleton } from "../../../../../../../../../../components/ui/skeleton"; +import type { FunctionComponent } from "react"; import { browserApiClient } from "@/services/devGuardApi"; +import { useActiveAsset } from "@/hooks/useActiveAsset"; + +const columnHelper = createColumnHelper(); + +const columnsDef: ColumnDef[] = [ + columnHelper.accessor("title", { + header: "Title", + enableSorting: true, + cell: (info) => { + return ( + info.getValue() && ( +
+ {info.getValue()} +
+ ) + ); + }, + }), + + columnHelper.accessor("severity", { + header: "Severity", + enableSorting: true, + cell: (info) => { + return ( + + {info.getValue()} + + ); + }, + }), +]; + const Index: FunctionComponent = () => { - const assetMenu = useAssetMenu(); + const asset = useActiveAsset(); + const assetId = asset?.id; + const router = useRouter(); + const pathname = usePathname(); const { organizationSlug, projectSlug, assetSlug, assetVersionSlug } = useDecodedParams() as { organizationSlug: string; @@ -17,8 +70,28 @@ const Index: FunctionComponent = () => { assetSlug: string; assetVersionSlug: string; }; - const [data, setData] = useState(""); - const handleClick = async () => { + const { + data: advisories, + isLoading, + error, + } = useSWR>( + `/organizations/${organizationSlug}/projects/${projectSlug}/assets/${assetSlug}/refs/${assetVersionSlug}/advisory`, + (url: string) => + fetcher(url).then((res: SecurityAdvisory[] | Paged) => + Array.isArray(res) + ? { data: res, total: res.length, page: 0, pageSize: res.length } + : res, + ), + { keepPreviousData: true }, + ); + const assetMenu = useAssetMenu(); + const { table } = useTable( + { columnsDef, data: advisories?.data || [] }, + { getSortedRowModel: getSortedRowModel(), manualSorting: false }, + ); + if (error) return
Fehler beim Laden
; + + const clickerPOST = async () => { const resp = await browserApiClient( "/organizations/" + organizationSlug + @@ -28,23 +101,158 @@ const Index: FunctionComponent = () => { assetSlug + "/refs/" + assetVersionSlug + - "/advisory/submit", + "/advisory/", { method: "POST", body: JSON.stringify({ - name: data, + title: "Das ist eine 67", + description: + "# Title This is a **bold** statement and *italic* text.- Item 1- Item 2> This is a blockquote.| Name | Age ||------|-----|| John | 30 |", + severity: "Critical", + vectorstring: + "CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:H/VI:H/VA:H/SC:N/SI:N/SA:N", + affectedPackages: [ + { + ecosystem: "go", + packageName: "pkg:gogogo", + semverStart: "0.0.0", + semverEnd: "1.2.0", + }, + { + ecosystem: "afgo", + packageName: "pkg:afafaafgogogo", + semverStart: "0.0.1", + semverEnd: "1.2.0", + }, + ], + assetId: assetId, }), }, ); }; + return ( }> -
- setData(e.target.value)} /> -
-
- +
+
+ + + +
+
+
+ {isLoading && ( + + )} +
+
+ {!advisories?.data?.length ? ( +
+ +
+ ) : ( +
+
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {!table.getRowModel().rows && + isLoading && + Array.from(Array(10).keys()).map((el, i, arr) => ( + + + + + + ))} + {table.getRowModel().rows.map((row, i, arr) => ( + + router?.push(pathname + "/" + row.original.id) + } + className={classNames( + "relative cursor-pointer align-top transition-all", + i === arr.length - 1 ? "" : "border-b", + i % 2 != 0 && "bg-card/50", + "hover:bg-muted", + )} + key={row.original.id} + > + {row.getVisibleCells().map((cell) => ( + + ))} + + ))} + +
+
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} + +
+
+ + + + + +
+ {flexRender( + cell.column.columnDef.cell, + cell.getContext(), + )} +
+
+
+
+ {advisories && } +
+
+
+ )} ); }; diff --git a/src/types/api/api.ts b/src/types/api/api.ts index 4a46cc032..8baa40838 100644 --- a/src/types/api/api.ts +++ b/src/types/api/api.ts @@ -1146,3 +1146,21 @@ export interface InstanceInfoDTO { runtime: InstanceRuntimeInfo; database: InstanceDatabaseInfo; } + +export interface SecurityAdvisory { + id: string; + title: string; + description: string; + severity: string; + vectorstring: string; + assetID: string; + affectedPackages: AdvisoryAffectedPackage[]; +} + +export interface AdvisoryAffectedPackage { + id: string; + ecosystem: string; + packagename: string; + semverStart: string | null; + semverEnd: string | null; +}