From c70d3e669a19dc49ef6928070025a86aa014a307 Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Wed, 25 Dec 2024 14:05:34 +0100 Subject: [PATCH 1/9] adds ability to create child projects --- src/components/common/AssetTitle.tsx | 13 +- src/components/common/EmptyList.tsx | 10 +- src/components/common/EmptyOverview.tsx | 10 +- src/components/common/ProjectTitle.tsx | 69 ++++++++ src/hooks/useProjectMenu.ts | 5 +- src/pages/[organizationSlug]/index.tsx | 14 +- src/pages/[organizationSlug]/projects.tsx | 3 +- .../projects/[projectSlug]/assets.tsx | 161 +++++++++++++----- .../assets/[assetSlug]/index.tsx | 17 +- .../assets/[assetSlug]/risk-handling.tsx | 15 +- .../projects/[projectSlug]/compliance.tsx | 35 +--- .../projects/[projectSlug]/index.tsx | 85 ++------- .../projects/[projectSlug]/settings.tsx | 31 +--- src/types/api/api.ts | 3 + 14 files changed, 257 insertions(+), 214 deletions(-) create mode 100644 src/components/common/ProjectTitle.tsx diff --git a/src/components/common/AssetTitle.tsx b/src/components/common/AssetTitle.tsx index aa56a99..f289d1d 100644 --- a/src/components/common/AssetTitle.tsx +++ b/src/components/common/AssetTitle.tsx @@ -4,10 +4,11 @@ import { useActiveProject } from "@/hooks/useActiveProject"; import Link from "next/link"; import React from "react"; import { Badge } from "../ui/badge"; +import { ProjectElement } from "./ProjectTitle"; const AssetTitle = () => { const activeOrg = useActiveOrg(); - const project = useActiveProject(); + const project = useActiveProject()!; const asset = useActiveAsset(); return ( @@ -22,15 +23,7 @@ const AssetTitle = () => { / - - {project?.name} - - Project - - + / void; - buttonTitle: string; + Button: JSX.Element; } const EmptyList: FunctionComponent = ({ title, description, - buttonTitle, - onClick, + Button, }) => { return (
@@ -47,9 +45,7 @@ const EmptyList: FunctionComponent = ({

{description}

-
- -
+
{Button}
); diff --git a/src/components/common/EmptyOverview.tsx b/src/components/common/EmptyOverview.tsx index 882ff0c..694cad6 100644 --- a/src/components/common/EmptyOverview.tsx +++ b/src/components/common/EmptyOverview.tsx @@ -37,8 +37,7 @@ import { TrendingUp } from "lucide-react"; interface Props { title: string; description: string; - onClick: () => void; - buttonTitle: string; + Button: JSX.Element; } const chartData = () => [ @@ -100,8 +99,7 @@ function ExampleBar() { const EmptyOverview: FunctionComponent = ({ title, description, - buttonTitle, - onClick, + Button, }) => { return (
@@ -127,9 +125,7 @@ const EmptyOverview: FunctionComponent = ({

{description}

-
- -
+
{Button}
diff --git a/src/components/common/ProjectTitle.tsx b/src/components/common/ProjectTitle.tsx new file mode 100644 index 0000000..47d9c58 --- /dev/null +++ b/src/components/common/ProjectTitle.tsx @@ -0,0 +1,69 @@ +import Link from "next/link"; +import React from "react"; +import { useActiveProject } from "../../hooks/useActiveProject"; +import { useActiveOrg } from "../../hooks/useActiveOrg"; +import { Badge } from "../ui/badge"; +import { ProjectDTO } from "../../types/api/api"; + +export const ProjectElement = ({ + project, + activeOrg, +}: { + project: ProjectDTO; + activeOrg: { slug: string; name: string }; +}) => { + if (project.parent != null) { + return ( + <> + + / + + {project.name} + + {project.parentId != null ? "Subproject" : "Project"} + + + + ); + } + return ( + + {project.name} + + {project.parentId != null ? "Subproject" : "Project"} + + + ); +}; + +const ProjectTitle = () => { + const activeOrg = useActiveOrg()!; + const project = useActiveProject()!; + + return ( + + + {activeOrg.name}{" "} + + Organization + + + / + + + ); +}; + +export default ProjectTitle; diff --git a/src/hooks/useProjectMenu.ts b/src/hooks/useProjectMenu.ts index 0276c7c..bb91b94 100644 --- a/src/hooks/useProjectMenu.ts +++ b/src/hooks/useProjectMenu.ts @@ -18,12 +18,9 @@ import { CogIcon, ListBulletIcon, ScaleIcon, - UsersIcon, } from "@heroicons/react/24/outline"; import { useRouter } from "next/router"; import { useCurrentUser } from "./useCurrentUser"; -import { title } from "process"; -import { Icon } from "lucide-react"; export const useProjectMenu = () => { const router = useRouter(); @@ -45,7 +42,7 @@ export const useProjectMenu = () => { }, { - title: "Assets", + title: "Subprojects & Assets", href: "/" + orgSlug + "/projects/" + projectSlug + "/assets", Icon: ListBulletIcon, }, diff --git a/src/pages/[organizationSlug]/index.tsx b/src/pages/[organizationSlug]/index.tsx index 8586ae0..2355ad0 100644 --- a/src/pages/[organizationSlug]/index.tsx +++ b/src/pages/[organizationSlug]/index.tsx @@ -52,6 +52,7 @@ import Link from "next/link"; import { withContentTree } from "@/decorators/withContentTree"; import EmptyOverview from "@/components/common/EmptyOverview"; import { padRiskHistory } from "@/utils/server"; +import { Button } from "../../components/ui/button"; interface Props { organization: OrganizationDTO & { @@ -112,10 +113,15 @@ const Home: FunctionComponent = ({ { - window.location.href = `/${organization.slug}/projects/`; - }} + Button={ + + } /> ); diff --git a/src/pages/[organizationSlug]/projects.tsx b/src/pages/[organizationSlug]/projects.tsx index b02352e..bce5536 100644 --- a/src/pages/[organizationSlug]/projects.tsx +++ b/src/pages/[organizationSlug]/projects.tsx @@ -147,8 +147,7 @@ const Home: FunctionComponent = ({ projects }) => { setOpen(true)} + Button={} /> ) : (
; }; + subprojects: Array }>; } const formSchema = z.object({ @@ -67,7 +71,7 @@ const formSchema = z.object({ availabilityRequirement: z.string(), }); -const Index: FunctionComponent = ({ project }) => { +const Index: FunctionComponent = ({ project, subprojects }) => { const [showModal, setShowModal] = useState(false); const router = useRouter(); @@ -81,8 +85,33 @@ const Index: FunctionComponent = ({ project }) => { }, }); + const projectForm = useForm({ + defaultValues: { + parentId: project.id, + }, + }); + + const [showProjectModal, setShowProjectModal] = useState(false); + const projectMenu = useProjectMenu(); + const handleCreateProject = async (data: ProjectDTO) => { + const resp = await browserApiClient( + "/organizations/" + activeOrg.slug + "/projects/", + { + method: "POST", + body: JSON.stringify(data), + }, + ); + if (resp.ok) { + const res: ProjectDTO = await resp.json(); + // navigate to the new application + router.push(`/${activeOrg.slug}/projects/${res.slug}`); + } else { + toast("Error", { description: "Could not create project" }); + } + }; + const handleCreateAsset = async (data: AssetDTO) => { const resp = await browserApiClient( "/organizations/" + @@ -114,35 +143,7 @@ const Index: FunctionComponent = ({ project }) => { Button={} title={project.name} Menu={projectMenu} - Title={ - - - {activeOrg.name}{" "} - - Organization - - - / - - {project.name} - - Project - - - - } + Title={} > {project.assets.length === 0 ? ( = ({ project }) => { description="You can understand assets as a software project. An asset is a github repository, a gitlab repository, a bundle of source code files, ..." - onClick={() => setShowModal(true)} - buttonTitle="Create new Asset" + Button={ +
+ + + +
+ } /> ) : (
setShowModal(true)}>New Asset +
+ + +
} primaryHeadline description={"Assets managed by the " + project.name + " project"} forceVertical - title="Assets" + title="Subprojects & Assets" > + {subprojects.map((subproject) => ( + + + {subproject.description} + Subproject + + } + Button={ + + + + + + + Edit + + + + } + /> + + ))} {project.assets.map((asset) => ( = ({ project }) => {
)} + + + + Create new Project + + A project groups multiple software projects (repositories) inside + a single enitity. Something like: frontend and backend + + +
+
+ + + + + + + +
+
@@ -259,21 +341,23 @@ const Index: FunctionComponent = ({ project }) => { }; export const getServerSideProps = middleware( - async (context: GetServerSidePropsContext) => { + async (context: GetServerSidePropsContext, { project }) => { // fetch the project - const { organizationSlug, projectSlug } = context.params!; + const { organizationSlug } = context.params!; const apiClient = getApiClientFromContext(context); + const resp = await apiClient( - "/organizations/" + organizationSlug + "/projects/" + projectSlug + "/", + "/organizations/" + organizationSlug + "/projects?parentId=" + project.id, ); - const project = await resp.json(); + const subprojects = await resp.json(); return { props: { initialZustandState: { project, }, + subprojects, project, }, }; @@ -283,6 +367,7 @@ export const getServerSideProps = middleware( organizations: withOrgs, organization: withOrganization, contentTree: withContentTree, + project: withProject, }, ); diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/index.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/index.tsx index 18990c1..f9128b4 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/index.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/index.tsx @@ -51,6 +51,7 @@ import { CollapsibleContent } from "@radix-ui/react-collapsible"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; +import { Button } from "../../../../../../components/ui/button"; interface Props { componentRisk: ComponentRisk; @@ -103,12 +104,18 @@ const Index: FunctionComponent = ({ - router.push( - `/${activeOrg.slug}/projects/${project?.slug}/assets/${asset?.slug}/security-control-center`, - ) + Button={ + } - buttonTitle="Run a scan" /> ); diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx index 6ba7a16..4b57464 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx @@ -38,6 +38,7 @@ import { Loader2 } from "lucide-react"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { withContentTree } from "@/decorators/withContentTree"; import AssetTitle from "@/components/common/AssetTitle"; +import { buttonVariants } from "../../../../../../components/ui/button"; interface Props { flaws: Paged; @@ -232,11 +233,15 @@ const Index: FunctionComponent = (props) => { - router.push( - `/${activeOrg?.slug}/projects/${project?.slug}/assets/${asset?.slug}/security-control-center`, - ) + Button={ + + Start identifying risks + } /> ) : ( diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/compliance.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/compliance.tsx index 0d10714..00f616c 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/compliance.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/compliance.tsx @@ -36,6 +36,7 @@ import { useMemo } from "react"; import { useActiveProject } from "@/hooks/useActiveProject"; import { withProject } from "@/decorators/withProject"; import { useProjectMenu } from "@/hooks/useProjectMenu"; +import ProjectTitle from "../../../../components/common/ProjectTitle"; export default function Compliance({ flaws, @@ -53,39 +54,7 @@ export default function Compliance({ [contentTree], ); return ( - - - {activeOrg.name}{" "} - - Organization - - - / - - {project.name} - - Project - - - - } - Menu={projectMenu} - > + } Menu={projectMenu}>
= ({ if (riskHistory.length === 0) { return ( - - - {activeOrg.name}{" "} - - Organization - - - / - - {project.name} - - Project - - - - } - > + }> { + router.push( + `/${activeOrg.slug}/projects/${project.slug}/assets`, + ); + }} + > + Create new asset + + } description="Create an asset and start scanning it to see the data here." - buttonTitle="Create new asset" - onClick={() => { - router.push(`/${activeOrg.slug}/projects/${project.slug}/assets`); - }} /> ); } return ( - - - {activeOrg.name}{" "} - - Organization - - - / - - {project.name} - - Project - - - - } - > + }>

Overview

diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx index 6ac332a..4ccfe69 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx @@ -25,6 +25,7 @@ import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { withContentTree } from "@/decorators/withContentTree"; +import ProjectTitle from "../../../../components/common/ProjectTitle"; interface Props { project: ProjectDTO; @@ -72,35 +73,7 @@ const Index: FunctionComponent = () => { - - {activeOrg.name}{" "} - - Organization - - - / - - {project?.name} - - Project - - - - } + Title={} >
diff --git a/src/types/api/api.ts b/src/types/api/api.ts index 3f37c40..b885c1a 100644 --- a/src/types/api/api.ts +++ b/src/types/api/api.ts @@ -99,6 +99,9 @@ export interface ProjectDTO { id: string; isPublic: boolean; + + parentId: string | null; + parent: ProjectDTO | null; } export interface EnvDTO { From 592555bcf98e6409a2aa7c21a372de50a6d59cca Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Wed, 25 Dec 2024 15:33:30 +0100 Subject: [PATCH 2/9] adds project id to project settings --- src/components/common/CopyInput.tsx | 45 +++++++++++++++++++ .../projects/[projectSlug]/settings.tsx | 12 ++++- 2 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 src/components/common/CopyInput.tsx diff --git a/src/components/common/CopyInput.tsx b/src/components/common/CopyInput.tsx new file mode 100644 index 0000000..943c217 --- /dev/null +++ b/src/components/common/CopyInput.tsx @@ -0,0 +1,45 @@ +// Copyright (C) 2024 Tim Bastin, l3montree UG (haftungsbeschränkt) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { FunctionComponent } from "react"; +import { toast } from "sonner"; +import { Input } from "../ui/input"; + +interface Props { + value: string; +} +const CopyInput: FunctionComponent = (props) => { + const handleCopy = () => { + navigator.clipboard.writeText(props.value); + toast("Copied to clipboard", { + description: "The value has been copied to your clipboard.", + }); + }; + return ( +
+ +
+ +
+
+ ); +}; + +export default CopyInput; diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx index 4ccfe69..2992747 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx @@ -26,6 +26,11 @@ import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { withContentTree } from "@/decorators/withContentTree"; import ProjectTitle from "../../../../components/common/ProjectTitle"; +import Section from "../../../../components/common/Section"; +import { Input } from "../../../../components/ui/input"; +import CopyCode from "../../../../components/common/CopyCode"; +import CopyInput from "../../../../components/common/CopyInput"; +import { Label } from "../../../../components/ui/label"; interface Props { project: ProjectDTO; @@ -79,10 +84,15 @@ const Index: FunctionComponent = () => {

Project Settings

+ +
+ + +
+
-
From 4b8553fc54ba0a272558b26c97de2174434a2478 Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Thu, 26 Dec 2024 14:44:31 +0100 Subject: [PATCH 3/9] tries to simplify devsecops pipeline setup --- .../projects/[projectSlug]/assets.tsx | 12 +- .../[assetSlug]/security-control-center.tsx | 174 ++++++++++++++++-- 2 files changed, 174 insertions(+), 12 deletions(-) diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/assets.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/assets.tsx index 6c60a48..fe1bda3 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/assets.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/assets.tsx @@ -4,7 +4,7 @@ import { FunctionComponent, useState } from "react"; import { useForm } from "react-hook-form"; import Page from "../../../../components/Page"; import ListItem from "../../../../components/common/ListItem"; - +import Image from "next/image"; import { Button, buttonVariants } from "@/components/ui/button"; import { Dialog, @@ -153,6 +153,16 @@ const Index: FunctionComponent = ({ project, subprojects }) => { files, ..." Button={
+ +
+ )} +
+
+

+ To use the DevGuard-Scanner in your CI/CD pipeline, you + need to store the token in your CI/CD variables. The + token is used to authenticate the scanner with the + DevGuard API:{" "} + + GitLab Documentation + + {", "} + + GitHub Documentation + +

+
+
+ + + + {" "} + GitLab logo{" "} + GitLab + + + GitHub logo{" "} + GitHub + + + + + + + + + +
+ + + + +
+ + + + + + + +
+
+

Operations{" "} From 5f7636481fdd38952ed8bb17609b566fd845a854 Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Thu, 26 Dec 2024 18:12:52 +0100 Subject: [PATCH 4/9] adds kubernetes namespace badge, fixes empty list showing if only subprojects exist --- .../InTotoProvenanceDialog.tsx | 34 +--------- .../risk-identification/PatSection.tsx | 49 ++++++++++++++ src/hooks/usePersonalAccessToken.ts | 4 ++ .../projects/[projectSlug]/assets.tsx | 67 ++++++++++++++++++- .../assets/[assetSlug]/risk-handling.tsx | 11 +-- src/types/api/api.ts | 2 + 6 files changed, 129 insertions(+), 38 deletions(-) create mode 100644 src/components/risk-identification/PatSection.tsx diff --git a/src/components/risk-identification/InTotoProvenanceDialog.tsx b/src/components/risk-identification/InTotoProvenanceDialog.tsx index 596d848..ef901f6 100644 --- a/src/components/risk-identification/InTotoProvenanceDialog.tsx +++ b/src/components/risk-identification/InTotoProvenanceDialog.tsx @@ -20,6 +20,7 @@ import usePersonalAccessToken from "@/hooks/usePersonalAccessToken"; import { PatWithPrivKey } from "@/types/api/api"; import Section from "../common/Section"; import { Button } from "../ui/button"; +import PatSection from "./PatSection"; interface Props { open: boolean; @@ -67,38 +68,7 @@ const InTotoProvenanceDialog: FunctionComponent = ({
-
- {pat && ( -
-
-
- -
- - - Make sure to copy the token. You won't be able to see - it ever again! - -
-
- )} - {!pat && ( -
- -
- )} -
+

diff --git a/src/components/risk-identification/PatSection.tsx b/src/components/risk-identification/PatSection.tsx new file mode 100644 index 0000000..0bd0281 --- /dev/null +++ b/src/components/risk-identification/PatSection.tsx @@ -0,0 +1,49 @@ +import { PatWithPrivKey } from "../../types/api/api"; +import CopyCode from "../common/CopyCode"; +import Section from "../common/Section"; +import { Button } from "../ui/button"; + +const PatSection = ({ + pat, + onCreatePat, +}: { + pat?: PatWithPrivKey; + onCreatePat: (data: { description: string }) => void; +}) => { + return ( +
+ {pat && ( +
+
+
+ +
+ + + Make sure to copy the token. You won't be able to see it ever + again! + +
+
+ )} + {!pat && ( +
+ +
+ )} +
+ ); +}; + +export default PatSection; diff --git a/src/hooks/usePersonalAccessToken.ts b/src/hooks/usePersonalAccessToken.ts index 7aa24fd..2c7e20c 100644 --- a/src/hooks/usePersonalAccessToken.ts +++ b/src/hooks/usePersonalAccessToken.ts @@ -42,5 +42,9 @@ export default function usePersonalAccessToken( personalAccessTokens, onDeletePat: handleDeletePat, onCreatePat: handleCreatePat, + pat: + personalAccessTokens.length > 0 + ? (personalAccessTokens[0] as PatWithPrivKey) + : undefined, }; } diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/assets.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/assets.tsx index fe1bda3..fbbf4a4 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/assets.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/assets.tsx @@ -52,6 +52,11 @@ import { withContentTree } from "@/decorators/withContentTree"; import { ProjectForm } from "../../../../components/project/ProjectForm"; import { withProject } from "../../../../decorators/withProject"; import ProjectTitle from "../../../../components/common/ProjectTitle"; +import Steps from "../../../../components/risk-identification/Steps"; +import CopyCode from "../../../../components/common/CopyCode"; +import PatSection from "../../../../components/risk-identification/PatSection"; +import usePersonalAccessToken from "../../../../hooks/usePersonalAccessToken"; +import { config } from "../../../../config"; interface Props { project: ProjectDTO & { @@ -91,7 +96,10 @@ const Index: FunctionComponent = ({ project, subprojects }) => { }, }); + const { pat, onCreatePat } = usePersonalAccessToken(); + const [showProjectModal, setShowProjectModal] = useState(false); + const [showK8sModal, setShowK8sModal] = useState(false); const projectMenu = useProjectMenu(); @@ -145,7 +153,7 @@ const Index: FunctionComponent = ({ project, subprojects }) => { Menu={projectMenu} Title={} > - {project.assets.length === 0 ? ( + {project.assets.length === 0 && subprojects.length === 0 ? ( -

)} + + + + + Kubernetes logo + Connect a Kubernetes Cluster + + + Connect a Kubernetes cluster to DevGuard to automatically index + your assets. + + +
+ +
+ +
+
+ "} --apiUrl=${config.publicDevGuardApiUrl}`} + language="shell" + /> +
+
+
+
+ diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx index 4b57464..13a2a04 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx @@ -23,7 +23,7 @@ import Page from "../../../../../../components/Page"; import { withOrgs } from "../../../../../../decorators/withOrgs"; import { withSession } from "../../../../../../decorators/withSession"; import { getApiClientFromContext } from "../../../../../../services/devGuardApi"; -import { beautifyPurl } from "../../../../../../utils/common"; +import { beautifyPurl, classNames } from "../../../../../../utils/common"; import CustomPagination from "@/components/common/CustomPagination"; import EcosystemImage from "@/components/common/EcosystemImage"; @@ -235,9 +235,12 @@ const Index: FunctionComponent = (props) => { description="Risk identification is the process of determining what risks exist in the asset and what their characteristics are. This process is done by identifying, assessing, and prioritizing risks." Button={ Start identifying risks diff --git a/src/types/api/api.ts b/src/types/api/api.ts index b885c1a..d3c740a 100644 --- a/src/types/api/api.ts +++ b/src/types/api/api.ts @@ -102,6 +102,8 @@ export interface ProjectDTO { parentId: string | null; parent: ProjectDTO | null; + + type: "default" | "kubernetesNamespace"; } export interface EnvDTO { From 679946a653702a50140655a29ac86baafa03be26 Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Thu, 26 Dec 2024 19:12:47 +0100 Subject: [PATCH 5/9] improves project badges --- src/components/common/ProjectTitle.tsx | 48 +++++++++++++++---- src/pages/[organizationSlug]/projects.tsx | 10 +++- .../projects/[projectSlug]/assets.tsx | 20 +++++++- src/types/api/api.ts | 2 +- 4 files changed, 68 insertions(+), 12 deletions(-) diff --git a/src/components/common/ProjectTitle.tsx b/src/components/common/ProjectTitle.tsx index 47d9c58..933a267 100644 --- a/src/components/common/ProjectTitle.tsx +++ b/src/components/common/ProjectTitle.tsx @@ -4,6 +4,43 @@ import { useActiveProject } from "../../hooks/useActiveProject"; import { useActiveOrg } from "../../hooks/useActiveOrg"; import { Badge } from "../ui/badge"; import { ProjectDTO } from "../../types/api/api"; +import Image from "next/image"; + +export const ProjectBadge = ({ type }: { type: ProjectDTO["type"] }) => { + if (type === "kubernetesNamespace") { + return ( + + Kubernetes + Kubernetes Namespace + + ); + } else if (type === "kubernetesCluster") { + return ( + + Kubernetes + Kubernetes Cluster + + ); + } else { + return ( + + {type === "default" ? "Project" : "Subproject"} + + ); + } +}; export const ProjectElement = ({ project, @@ -22,12 +59,7 @@ export const ProjectElement = ({ href={`/${activeOrg.slug}/projects/${project.slug}/assets`} > {project.name} - - {project.parentId != null ? "Subproject" : "Project"} - + ); @@ -38,9 +70,7 @@ export const ProjectElement = ({ href={`/${activeOrg.slug}/projects/${project.slug}/assets`} > {project.name} - - {project.parentId != null ? "Subproject" : "Project"} - + ); }; diff --git a/src/pages/[organizationSlug]/projects.tsx b/src/pages/[organizationSlug]/projects.tsx index bce5536..81d184c 100644 --- a/src/pages/[organizationSlug]/projects.tsx +++ b/src/pages/[organizationSlug]/projects.tsx @@ -60,6 +60,7 @@ import { withOrganization } from "@/decorators/withOrganization"; import { EllipsisVerticalIcon } from "@heroicons/react/24/outline"; import Link from "next/link"; import { withContentTree } from "@/decorators/withContentTree"; +import { ProjectBadge } from "../../components/common/ProjectTitle"; interface Props { projects: Array; @@ -167,7 +168,14 @@ const Home: FunctionComponent = ({ projects }) => { + {project.description} + {project.type !== "default" && ( + + )} + + } Button={ = ({ project, subprojects }) => { Button={
- +
} primaryHeadline @@ -269,6 +275,18 @@ const Index: FunctionComponent = ({ project, subprojects }) => {
{asset.description}{" "}
+ {project.type === "kubernetesNamespace" && ( + + Kubernetes logo + Kubernetes Workload + + )} {asset.lastSecretScan && ( Secret-Scanning )} diff --git a/src/types/api/api.ts b/src/types/api/api.ts index d3c740a..1c656cf 100644 --- a/src/types/api/api.ts +++ b/src/types/api/api.ts @@ -103,7 +103,7 @@ export interface ProjectDTO { parentId: string | null; parent: ProjectDTO | null; - type: "default" | "kubernetesNamespace"; + type: "default" | "kubernetesNamespace" | "kubernetesCluster"; } export interface EnvDTO { From 3be9ef6935fbdc6312704024fe9da1dba2a9729e Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Fri, 27 Dec 2024 10:15:51 +0100 Subject: [PATCH 6/9] allowing a project to get connected to a repository --- src/components/ConnectToRepoSection.tsx | 183 ++++++++++++++++ src/hooks/useRepositorySearch.ts | 43 ++++ .../assets/[assetSlug]/settings.tsx | 207 +----------------- .../projects/[projectSlug]/settings.tsx | 43 +++- src/types/api/api.ts | 3 + 5 files changed, 274 insertions(+), 205 deletions(-) create mode 100644 src/components/ConnectToRepoSection.tsx create mode 100644 src/hooks/useRepositorySearch.ts diff --git a/src/components/ConnectToRepoSection.tsx b/src/components/ConnectToRepoSection.tsx new file mode 100644 index 0000000..213af7d --- /dev/null +++ b/src/components/ConnectToRepoSection.tsx @@ -0,0 +1,183 @@ +import React, { FunctionComponent, useState } from "react"; +import Section from "./common/Section"; +import ListItem from "./common/ListItem"; +import { Button, buttonVariants } from "./ui/button"; +import { Combobox } from "./common/Combobox"; +import useRepositorySearch from "../hooks/useRepositorySearch"; +import Image from "next/image"; +import { useActiveOrg } from "../hooks/useActiveOrg"; +import Link from "next/link"; +import { cn } from "../lib/utils"; + +interface Props { + repositoryName?: string; + repositoryId?: string; + repositories: Array<{ value: string; label: string }> | null; // will be null, if repos could not be loaded - probably due to a missing github app installation + onUpdate: ( + data: Partial<{ repositoryName: string; repositoryId: string }>, + ) => Promise; +} +const ConnectToRepoSection: FunctionComponent = ({ + repositoryName, + repositoryId, + repositories, + onUpdate, +}) => { + const activeOrg = useActiveOrg(); + const { repos, searchLoading, handleSearchRepos } = + useRepositorySearch(repositories); + const [editRepo, setEditRepo] = useState(!Boolean(repositoryId)); + + const [selectedRepo, setSelectedRepo] = useState<{ + id: string; + name: string; + } | null>(repositoryId ? { id: repositoryId!, name: repositoryName! } : null); + + return ( +
+ {Boolean(repositoryId) && repositories && !editRepo ? ( +
+ + + + + + + {repositoryName} + + } + description={ + "This asset is connected to a " + + (repositoryId?.startsWith("github:") ? "GitHub" : "GitLab") + + " repository " + } + Button={ + <> + + + + } + /> +
+ ) : repositories && editRepo ? ( + +
+ { + const repo = repos.find((r) => r.value === repoId); + + if (repo) { + setSelectedRepo({ id: repo.value, name: repo.label }); + } + }} + value={selectedRepo?.id ?? undefined} + emptyMessage="No repositories found" + /> +
+ +
+ } + description={ + "Select a repository to connect this asset to. This list contains all repositories of all GitHub App Installations belonging to this organization." + } + /> + ) : ( + <> + + GitHub + Add a GitHub App +
+ } + description="DevGuard uses a GitHub App to access your repositories and interact with your code. Due to the excessive permissions granted to the app, it can only be done by the organization owner." + Button={ + + Go to organization settings + + } + /> + + GitHub + Integrate with GitLab + + } + description="DevGuard uses a personal, group or project access token to access your repositories and interact with your code. Due to the excessive permissions granted to the app, it can only be done by the organization owner." + Button={ + + Go to organization settings + + } + /> + + )} + + ); +}; + +export default ConnectToRepoSection; diff --git a/src/hooks/useRepositorySearch.ts b/src/hooks/useRepositorySearch.ts new file mode 100644 index 0000000..2166a30 --- /dev/null +++ b/src/hooks/useRepositorySearch.ts @@ -0,0 +1,43 @@ +import { debounce } from "lodash"; +import { useCallback, useState } from "react"; +import { browserApiClient } from "../services/devGuardApi"; +import { useActiveOrg } from "./useActiveOrg"; + +export const convertRepos = (repos: Array<{ label: string; id: string }>) => + repos.map((r) => ({ value: r.id, label: r.label })); + +export default function useRepositorySearch( + repositories: Array<{ value: string; label: string }> | null, +) { + const activeOrg = useActiveOrg(); + const [repos, setRepositories] = useState(repositories ?? []); + const [searchLoading, setSearchLoading] = useState(false); + + const debouncedSearch = useCallback( + debounce(async (search: string) => { + setSearchLoading(true); + // fetch repositories from the server + const repos = await browserApiClient( + "/organizations/" + + activeOrg.slug + + "/integrations/repositories?search=" + + search, + ); + + const data = await repos.json(); + setRepositories(convertRepos(data) ?? []); + setSearchLoading(false); + }, 500), + [activeOrg.slug], + ); + + const handleSearchRepos = async (value: string) => { + if (value === "") { + setRepositories(repositories ?? []); + return; + } + return debouncedSearch(value); + }; + + return { repos, searchLoading, handleSearchRepos }; +} diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/settings.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/settings.tsx index c86efb8..52af7a6 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/settings.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/settings.tsx @@ -1,10 +1,7 @@ import Page from "@/components/Page"; import AssetForm from "@/components/asset/AssetForm"; import AssetTitle from "@/components/common/AssetTitle"; -import { Combobox } from "@/components/common/Combobox"; -import ListItem from "@/components/common/ListItem"; -import Section from "@/components/common/Section"; -import { Button, buttonVariants } from "@/components/ui/button"; +import { Button } from "@/components/ui/button"; import { Form } from "@/components/ui/form"; import { middleware } from "@/decorators/middleware"; import { withAsset } from "@/decorators/withAsset"; @@ -17,21 +14,19 @@ import { useActiveAsset } from "@/hooks/useActiveAsset"; import { useActiveOrg } from "@/hooks/useActiveOrg"; import { useActiveProject } from "@/hooks/useActiveProject"; import { useAssetMenu } from "@/hooks/useAssetMenu"; -import { cn } from "@/lib/utils"; +import useRepositorySearch, { convertRepos } from "@/hooks/useRepositorySearch"; import { browserApiClient, getApiClientFromContext, } from "@/services/devGuardApi"; import { AssetDTO } from "@/types/api/api"; import { useStore } from "@/zustand/globalStoreProvider"; -import { debounce } from "lodash"; import { GetServerSidePropsContext } from "next"; -import Image from "next/image"; -import Link from "next/link"; import { useRouter } from "next/router"; -import { FunctionComponent, useCallback, useState } from "react"; +import { FunctionComponent } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; +import ConnectToRepoSection from "../../../../../../components/ConnectToRepoSection"; interface Props { repositories: Array<{ value: string; label: string }> | null; // will be null, if repos could not be loaded - probably due to a missing github app installation @@ -45,19 +40,6 @@ const Index: FunctionComponent = ({ repositories }: Props) => { const updateAsset = useStore((s) => s.updateAsset); const router = useRouter(); const form = useForm({ defaultValues: asset }); - const [selectedRepo, setSelectedRepo] = useState<{ - id: string; - name: string; - } | null>( - asset.repositoryId - ? { id: asset.repositoryId!, name: asset.repositoryName! } - : null, - ); - - const [repos, setRepositories] = useState(repositories ?? []); - const [searchLoading, setSearchLoading] = useState(false); - - const [editRepo, setEditRepo] = useState(!Boolean(asset.repositoryId)); const handleUpdate = async (data: Partial) => { const resp = await browserApiClient( @@ -96,31 +78,8 @@ const Index: FunctionComponent = ({ repositories }: Props) => { }); }; - const debouncedSearch = useCallback( - debounce(async (search: string) => { - setSearchLoading(true); - // fetch repositories from the server - const repos = await browserApiClient( - "/organizations/" + - activeOrg.slug + - "/integrations/repositories?search=" + - search, - ); - - const data = await repos.json(); - setRepositories(convertRepos(data) ?? []); - setSearchLoading(false); - }, 500), - [activeOrg.slug], - ); - - const handleSearchRepos = async (value: string) => { - if (value === "") { - setRepositories(repositories ?? []); - return; - } - return debouncedSearch(value); - }; + const { repos, searchLoading, handleSearchRepos } = + useRepositorySearch(repositories); return ( = ({ repositories }: Props) => {

Asset Settings

-
- {Boolean(asset.repositoryId) && repositories && !editRepo ? ( -
- - - - - - - {asset.repositoryName} - - } - description={ - "This asset is connected to a " + - (asset.repositoryId?.startsWith("github:") - ? "GitHub" - : "GitLab") + - " repository " - } - Button={ - <> - - - - } - /> -
- ) : repositories && editRepo ? ( - -
- { - const repo = repos.find((r) => r.value === repoId); - - if (repo) { - setSelectedRepo({ id: repo.value, name: repo.label }); - } - }} - value={selectedRepo?.id ?? undefined} - emptyMessage="No repositories found" - /> -
- - - } - description={ - "Select a repository to connect this asset to. This list contains all repositories of all GitHub App Installations belonging to this organization." - } - /> - ) : ( - <> - - GitHub - Add a GitHub App - - } - description="DevGuard uses a GitHub App to access your repositories and interact with your code. Due to the excessive permissions granted to the app, it can only be done by the organization owner." - Button={ - - Go to organization settings - - } - /> - - GitHub - Integrate with GitLab - - } - description="DevGuard uses a personal, group or project access token to access your repositories and interact with your code. Due to the excessive permissions granted to the app, it can only be done by the organization owner." - Button={ - - Go to organization settings - - } - /> - - )} -
+
@@ -295,9 +115,6 @@ const Index: FunctionComponent = ({ repositories }: Props) => { }; export default Index; -const convertRepos = (repos: Array<{ label: string; id: string }>) => - repos.map((r) => ({ value: r.id, label: r.label })); - export const getServerSideProps = middleware( async (context: GetServerSidePropsContext) => { // fetch the project diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx index 2992747..e4eae9b 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/settings.tsx @@ -4,14 +4,15 @@ import Page from "../../../../components/Page"; import { middleware } from "@/decorators/middleware"; -import { Badge } from "@/components/ui/badge"; import { withOrganization } from "@/decorators/withOrganization"; import { useProjectMenu } from "@/hooks/useProjectMenu"; -import Link from "next/link"; import { withOrgs } from "../../../../decorators/withOrgs"; import { withSession } from "../../../../decorators/withSession"; import { useActiveOrg } from "../../../../hooks/useActiveOrg"; -import { browserApiClient } from "../../../../services/devGuardApi"; +import { + browserApiClient, + getApiClientFromContext, +} from "../../../../services/devGuardApi"; import { ProjectDTO } from "../../../../types/api/api"; import { ProjectForm } from "@/components/project/ProjectForm"; @@ -21,22 +22,23 @@ import { withProject } from "@/decorators/withProject"; import { useActiveProject } from "@/hooks/useActiveProject"; import { useStore } from "@/zustand/globalStoreProvider"; +import { withContentTree } from "@/decorators/withContentTree"; import { useRouter } from "next/router"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; -import { withContentTree } from "@/decorators/withContentTree"; +import CopyInput from "../../../../components/common/CopyInput"; import ProjectTitle from "../../../../components/common/ProjectTitle"; import Section from "../../../../components/common/Section"; -import { Input } from "../../../../components/ui/input"; -import CopyCode from "../../../../components/common/CopyCode"; -import CopyInput from "../../../../components/common/CopyInput"; import { Label } from "../../../../components/ui/label"; +import { convertRepos } from "../../../../hooks/useRepositorySearch"; +import ConnectToRepoSection from "../../../../components/ConnectToRepoSection"; interface Props { project: ProjectDTO; + repositories: Array<{ value: string; label: string }> | null; } -const Index: FunctionComponent = () => { +const Index: FunctionComponent = ({ repositories }) => { const activeOrg = useActiveOrg(); const project = useActiveProject(); const updateProject = useStore((s) => s.updateProject); @@ -89,7 +91,12 @@ const Index: FunctionComponent = () => { - + @@ -105,8 +112,24 @@ const Index: FunctionComponent = () => { export const getServerSideProps = middleware( async (context: GetServerSidePropsContext) => { + // fetch the project + const { organizationSlug } = context.params!; + + const apiClient = getApiClientFromContext(context); + + const resp = await apiClient( + "/organizations/" + organizationSlug + "/integrations/repositories", + ); + + let repos: Array<{ value: string; label: string }> | null = null; + if (resp.ok) { + repos = convertRepos(await resp.json()); + } + return { - props: {}, + props: { + repositories: repos, + }, }; }, { diff --git a/src/types/api/api.ts b/src/types/api/api.ts index 1c656cf..a2d465d 100644 --- a/src/types/api/api.ts +++ b/src/types/api/api.ts @@ -104,6 +104,9 @@ export interface ProjectDTO { parent: ProjectDTO | null; type: "default" | "kubernetesNamespace" | "kubernetesCluster"; + + repositoryId?: string; + repositoryName?: string; } export interface EnvDTO { From d4fd236f8ce6cc92d7c456c10c1d5e8ae1794219 Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Fri, 27 Dec 2024 10:36:38 +0100 Subject: [PATCH 7/9] checking parent project for mitigation if connected to repo --- src/components/ConnectToRepoSection.tsx | 57 +++++++++++++++++ .../[assetSlug]/flaws/[flawId]/index.tsx | 64 +++---------------- .../assets/[assetSlug]/settings.tsx | 6 ++ src/utils/view.ts | 36 ++++++++++- 4 files changed, 107 insertions(+), 56 deletions(-) diff --git a/src/components/ConnectToRepoSection.tsx b/src/components/ConnectToRepoSection.tsx index 213af7d..fe80071 100644 --- a/src/components/ConnectToRepoSection.tsx +++ b/src/components/ConnectToRepoSection.tsx @@ -8,8 +8,11 @@ import Image from "next/image"; import { useActiveOrg } from "../hooks/useActiveOrg"; import Link from "next/link"; import { cn } from "../lib/utils"; +import Callout from "./common/Callout"; interface Props { + parentRepositoryId?: string; + parentRepositoryName?: string; repositoryName?: string; repositoryId?: string; repositories: Array<{ value: string; label: string }> | null; // will be null, if repos could not be loaded - probably due to a missing github app installation @@ -22,6 +25,8 @@ const ConnectToRepoSection: FunctionComponent = ({ repositoryId, repositories, onUpdate, + parentRepositoryId, + parentRepositoryName, }) => { const activeOrg = useActiveOrg(); const { repos, searchLoading, handleSearchRepos } = @@ -38,6 +43,58 @@ const ConnectToRepoSection: FunctionComponent = ({ title="Connect to a repository" description="Connect this asset to a repository to enable automatic scanning and other features." > + {Boolean(parentRepositoryId) && ( +
+ + + + + + + {parentRepositoryName} + + } + description={ + "This asset is connected to a " + + (repositoryId?.startsWith("github:") ? "GitHub" : "GitLab") + + " repository " + } + Button={ + <> + + + + } + /> +
+ )} + {parentRepositoryId && ( + <> +
+ + The parent project is already connected to a repository. You can + override this connection by connecting this asset to a different + repository. + + + )} {Boolean(repositoryId) && repositories && !editRepo ? (
import("@/components/common/MarkdownEditor"), { @@ -347,59 +349,7 @@ const Index: FunctionComponent = (props) => { return ( - - {activeOrg.name}{" "} - - Organization - - - / - - {project?.name} - - Project - - - / - - {asset?.name} - - Asset - - - - / - - {flaw.cve?.cve ?? "Flaw Details"}{" "} - - Flaw - - - - } + Title={} title={flaw.cve?.cve ?? "Flaw Details"} >
@@ -479,7 +429,9 @@ const Index: FunctionComponent = (props) => {
{flaw.ticketId === null && - asset?.repositoryId?.startsWith("gitlab:") && ( + getRepositoryId(asset, project)?.startsWith( + "gitlab:", + ) && ( )} {flaw.ticketId === null && - asset?.repositoryId?.startsWith("github:") && ( + getRepositoryId(asset, project)?.startsWith( + "github:", + ) && (
{ + if (!project) { + return { + parentRepositoryId: undefined, + parentRepositoryName: undefined, + }; + } + + if (project.repositoryId && project.repositoryName) { + return { + parentRepositoryId: project.repositoryId, + parentRepositoryName: project.repositoryName, + }; + } else if (project.parent) { + return getParentRepositoryIdAndName(project.parent); + } + return { + parentRepositoryId: undefined, + parentRepositoryName: undefined, + }; +}; + +export const getRepositoryId = (asset?: AssetDTO, project?: ProjectDTO) => { + if (asset && asset.repositoryId) { + return asset.repositoryId; + } + return getParentRepositoryIdAndName(project).parentRepositoryId; +}; + export const defaultScanner = "github.com/l3montree-dev/devguard/cmd/devguard-scanner/"; // along with this program. If not, see . From 2459b6078b14cbc66e3deb383bd121600d37dd84 Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Fri, 27 Dec 2024 15:47:37 +0100 Subject: [PATCH 8/9] adds support for subprojects in stats --- src/components/common/EmptyParty.tsx | 53 ++++ .../assets/[assetSlug]/risk-handling.tsx | 258 +++++++++--------- .../projects/[projectSlug]/index.tsx | 33 ++- src/utils/view.ts | 2 +- 4 files changed, 210 insertions(+), 136 deletions(-) create mode 100644 src/components/common/EmptyParty.tsx diff --git a/src/components/common/EmptyParty.tsx b/src/components/common/EmptyParty.tsx new file mode 100644 index 0000000..4626caf --- /dev/null +++ b/src/components/common/EmptyParty.tsx @@ -0,0 +1,53 @@ +// Copyright (C) 2024 Tim Bastin, l3montree UG (haftungsbeschränkt) +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +import { FunctionComponent } from "react"; +import SkeletonListItem from "./SkeletonListItem"; + +interface Props { + title: string; + description: string; + Button?: JSX.Element; +} +const EmptyParty: FunctionComponent = ({ + title, + description, + Button, +}) => { + return ( +
+
+
+ +
+
+ +
+
+ +
+
+
+

{title}

+
+

{description}

+
+ {Boolean(Button) &&
{Button}
} +
+
+ ); +}; + +export default EmptyParty; diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx index 13a2a04..edf769e 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/assets/[assetSlug]/risk-handling.tsx @@ -39,6 +39,7 @@ import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { withContentTree } from "@/decorators/withContentTree"; import AssetTitle from "@/components/common/AssetTitle"; import { buttonVariants } from "../../../../../../components/ui/button"; +import EmptyParty from "../../../../../../components/common/EmptyParty"; interface Props { flaws: Paged; @@ -225,136 +226,143 @@ const Index: FunctionComponent = (props) => { [router], ); + const activeScan = Boolean(asset?.lastContainerScan ?? asset?.lastScaScan); + return ( }> - { - /** the query will contain organizationSlug, projectSlug and assetSlug - thus 3 is empty */ - table.getRowCount() === 0 && Object.keys(router.query).length === 3 ? ( - - Start identifying risks - - } - /> - ) : ( -
-
- - - - router.push({ - query: { - ...router.query, - state: "open", - }, - }) - } - value="open" - > - Open - - - router.push({ - query: { - ...router.query, - state: "closed", - }, - }) - } - value="closed" - > - Closed - - - - -
- {isLoading && ( - - )} -
+ {activeScan && + table.getRowCount() === 0 && + Object.keys(router.query).length === 3 ? ( + + ) : /** the query will contain organizationSlug, projectSlug and assetSlug - thus 3 is empty */ + table.getRowCount() === 0 && Object.keys(router.query).length === 3 ? ( + + Start identifying risks + + } + /> + ) : ( +
+
+ + + + router.push({ + query: { + ...router.query, + state: "open", + }, + }) + } + value="open" + > + Open + + + router.push({ + query: { + ...router.query, + state: "closed", + }, + }) + } + value="closed" + > + Closed + + + + +
+ {isLoading && ( + + )}
-
-
- - - {table.getHeaderGroups().map((headerGroup) => ( - - + ))} + + ))} + + + {table.getRowModel().rows.map((row, i, arr) => ( + + ))} + +
- {headerGroup.headers.map((header) => ( - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext(), - )} +
+
+
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + - ))} - - ))} - - - {table.getRowModel().rows.map((row, i, arr) => ( - - ))} - -
+ {headerGroup.headers.map((header) => ( + +
+ {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext(), + )} - -
-
-
-
-
- + +
+
-
- ) - } +
+
+ +
+
+ )}
); }; diff --git a/src/pages/[organizationSlug]/projects/[projectSlug]/index.tsx b/src/pages/[organizationSlug]/projects/[projectSlug]/index.tsx index 203a79c..b2b4252 100644 --- a/src/pages/[organizationSlug]/projects/[projectSlug]/index.tsx +++ b/src/pages/[organizationSlug]/projects/[projectSlug]/index.tsx @@ -54,6 +54,7 @@ interface Props { label: string; slug: string; description: string; + type: "project" | "asset"; }>; flawCountByScanner: FlawCountByScanner; dependencyCountByScanType: DependencyCountByScanType; @@ -100,6 +101,8 @@ const Index: FunctionComponent = ({ ); } + + console.log(riskDistribution); return ( }>
@@ -132,12 +135,14 @@ const Index: FunctionComponent = ({ {riskHistory.slice(0, 5).map((r) => ( r.json() as Promise< - Array<{ riskHistory: RiskHistory[]; asset: AssetDTO }> + Array< + | { riskHistory: RiskHistory[]; asset: AssetDTO } + | { + riskHistory: []; + project: ProjectDTO; + } + > >, ), apiClient( @@ -291,10 +302,12 @@ export const getServerSideProps = middleware( project, riskDistribution, riskHistory: paddedRiskHistory.map((r) => ({ - label: r.asset.name, + label: "asset" in r ? r.asset.name : r.project.name, history: r.riskHistory, - slug: r.asset.slug, - description: r.asset.description, + type: "asset" in r ? "asset" : "project", + slug: "asset" in r ? r.asset.slug : r.project.slug, + description: + "asset" in r ? r.asset.description : r.project.description, })), flawAggregationStateAndChange, avgLowFixingTime, diff --git a/src/utils/view.ts b/src/utils/view.ts index 4b48b0b..a026eeb 100644 --- a/src/utils/view.ts +++ b/src/utils/view.ts @@ -133,5 +133,5 @@ const colors1 = [ export const generateColor = (str: string) => { const hash = Math.abs(hashCode(str)); - return colors1[hash % colors1.length]; + return colors[hash % colors.length]; }; From 0b6d3d075c8002f30f62e2d1b020df40d26fd26d Mon Sep 17 00:00:00 2001 From: Tim Bastin Date: Fri, 27 Dec 2024 16:32:11 +0100 Subject: [PATCH 9/9] updates gitleaks baseline --- leaks-baseline.json | 72 +++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 6 deletions(-) diff --git a/leaks-baseline.json b/leaks-baseline.json index 751235d..d63a89d 100644 --- a/leaks-baseline.json +++ b/leaks-baseline.json @@ -1,11 +1,71 @@ [ + { + "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + "StartLine": 9, + "EndLine": 9, + "StartColumn": 5, + "EndColumn": 79, + "Match": "Secret\": \"d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82\"", + "Secret": "d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82", + "File": "leaks-baseline.json", + "SymlinkFile": "", + "Commit": "a8aa8e4025ffdacc24b6ac5e3c8c7076fe8f1760", + "Entropy": 3.8594732, + "Author": "timbastin", + "Email": "bastin.tim@gmail.com", + "Date": "2024-08-14T17:37:24Z", + "Message": "adds gitleaks baseline", + "Tags": [], + "RuleID": "generic-api-key", + "Fingerprint": "a8aa8e4025ffdacc24b6ac5e3c8c7076fe8f1760:leaks-baseline.json:generic-api-key:9" + }, + { + "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + "StartLine": 29, + "EndLine": 29, + "StartColumn": 5, + "EndColumn": 79, + "Match": "Secret\": \"d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82\"", + "Secret": "d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82", + "File": "leaks-baseline.json", + "SymlinkFile": "", + "Commit": "a8aa8e4025ffdacc24b6ac5e3c8c7076fe8f1760", + "Entropy": 3.8594732, + "Author": "timbastin", + "Email": "bastin.tim@gmail.com", + "Date": "2024-08-14T17:37:24Z", + "Message": "adds gitleaks baseline", + "Tags": [], + "RuleID": "generic-api-key", + "Fingerprint": "a8aa8e4025ffdacc24b6ac5e3c8c7076fe8f1760:leaks-baseline.json:generic-api-key:29" + }, + { + "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", + "StartLine": 49, + "EndLine": 49, + "StartColumn": 5, + "EndColumn": 79, + "Match": "Secret\": \"cb76b2575012202a6fdf4bdcabb35f0cb873e54eddfd1795bced05f1cd3c361a\"", + "Secret": "cb76b2575012202a6fdf4bdcabb35f0cb873e54eddfd1795bced05f1cd3c361a", + "File": "leaks-baseline.json", + "SymlinkFile": "", + "Commit": "a8aa8e4025ffdacc24b6ac5e3c8c7076fe8f1760", + "Entropy": 3.8406746, + "Author": "timbastin", + "Email": "bastin.tim@gmail.com", + "Date": "2024-08-14T17:37:24Z", + "Message": "adds gitleaks baseline", + "Tags": [], + "RuleID": "generic-api-key", + "Fingerprint": "a8aa8e4025ffdacc24b6ac5e3c8c7076fe8f1760:leaks-baseline.json:generic-api-key:49" + }, { "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", "StartLine": 185, "EndLine": 185, - "StartColumn": 145, + "StartColumn": 143, "EndColumn": 216, - "Match": "token=\"d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82\"", + "Match": "--token=\"d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82\"", "Secret": "d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82", "File": "README.md", "SymlinkFile": "", @@ -23,9 +83,9 @@ "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", "StartLine": 188, "EndLine": 188, - "StartColumn": 170, + "StartColumn": 168, "EndColumn": 241, - "Match": "token=\"d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82\"", + "Match": "--token=\"d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82\"", "Secret": "d4ff2f36b983b69dd36e441f58fcc0bc4f372ace1d12ecc040f1d860ea254f82", "File": "README.md", "SymlinkFile": "", @@ -43,9 +103,9 @@ "Description": "Detected a Generic API Key, potentially exposing access to various services and sensitive operations.", "StartLine": 178, "EndLine": 178, - "StartColumn": 188, + "StartColumn": 186, "EndColumn": 259, - "Match": "token=\"cb76b2575012202a6fdf4bdcabb35f0cb873e54eddfd1795bced05f1cd3c361a\"", + "Match": "--token=\"cb76b2575012202a6fdf4bdcabb35f0cb873e54eddfd1795bced05f1cd3c361a\"", "Secret": "cb76b2575012202a6fdf4bdcabb35f0cb873e54eddfd1795bced05f1cd3c361a", "File": "README.md", "SymlinkFile": "",