diff --git a/config.d.ts b/config.d.ts index 77e24b4..c34f568 100644 --- a/config.d.ts +++ b/config.d.ts @@ -28,6 +28,10 @@ export interface Config { * @visibility frontend */ apiVersion?: string; + /** + * @visibility frontend + */ + issuesApiVersion?: string; /** * @visibility frontend */ diff --git a/src/api/index.ts b/src/api/index.ts index dcf7933..0677cdd 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -20,9 +20,9 @@ import { createApiRef, DiscoveryApi, } from "@backstage/core-plugin-api"; -import {TargetData} from "../types/targetsTypes"; -import {OrgData} from "../types/orgsTypes"; -import {ProjectsData} from "../types/projectsTypes"; +import { TargetData } from "../types/targetsTypes"; +import { OrgData } from "../types/orgsTypes"; +import { ProjectsData } from "../types/projectsTypes"; import { SNYK_ANNOTATION_TARGETID, SNYK_ANNOTATION_TARGETNAME, @@ -32,13 +32,13 @@ import { SNYK_ANNOTATION_ORG, SNYK_ANNOTATION_ORGS, } from "../config"; -import {mockedProjects} from "../utils/mockedProjects"; -import {mockedIssues} from "../utils/mockedIssues"; -import {Entity} from "@backstage/catalog-model"; -import {mockedDepGraphs} from "../utils/mockedDepGraphs"; -import {mockedProjectDetails} from "../utils/mockedProjectDetails"; -import {IssuesCount} from "../types/types"; -import {Issue} from "../types/unifiedIssuesTypes"; +import { mockedProjects } from "../utils/mockedProjects"; +import { mockedIssues } from "../utils/mockedIssues"; +import { Entity } from "@backstage/catalog-model"; +import { mockedDepGraphs } from "../utils/mockedDepGraphs"; +import { mockedProjectDetails } from "../utils/mockedProjectDetails"; +import { IssuesCount } from "../types/types"; +import { Issue } from "../types/unifiedIssuesTypes"; const DEFAULT_PROXY_PATH_BASE = ""; @@ -71,6 +71,8 @@ export interface SnykApi { getSnykApiVersion(): string; + getSnykIssuesApiVersion(): string; + getOrgSlug(orgId: string): Promise; isMocked(): boolean; @@ -80,6 +82,8 @@ export interface SnykApi { isShowResolvedInGraphs(entity: Entity): boolean; getIssuesCount(issues: Array): IssuesCount; + + getIgnoredIssuesCount(issues: Array): IssuesCount; } export class SnykApiClient implements SnykApi { @@ -126,19 +130,21 @@ export class SnykApiClient implements SnykApi { "2023-06-19~experimental" ); } + getSnykIssuesApiVersion(): string { + return ( + this.configApi.getOptionalString("snyk.issueApiVersion") ?? "2024-01-23" + ); + } isAvailableInEntity(entity: Entity): boolean { return ( this.isMocked() || - ( - Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_ORG]) || - Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_ORGS]) - ) && ( - Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETNAME]) || - Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETID]) || - Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETS]) || - Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_PROJECTIDS]) - ) + ((Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_ORG]) || + Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_ORGS])) && + (Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETNAME]) || + Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETID]) || + Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETS]) || + Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_PROJECTIDS]))) ); } @@ -146,27 +152,70 @@ export class SnykApiClient implements SnykApi { const criticalSevCount = issues.filter( (issue) => issue.attributes.effective_severity_level === "critical" && + !issue.attributes.ignored && (issue.attributes.status !== "resolved" || this.isShowResolvedInGraphs()) ).length; const highSevCount = issues.filter( (issue) => issue.attributes.effective_severity_level === "high" && + !issue.attributes.ignored && (issue.attributes.status !== "resolved" || this.isShowResolvedInGraphs()) ).length; const mediumSevCount = issues.filter( (issue) => issue.attributes.effective_severity_level === "medium" && + !issue.attributes.ignored && (issue.attributes.status !== "resolved" || this.isShowResolvedInGraphs()) ).length; const lowSevCount = issues.filter( (issue) => issue.attributes.effective_severity_level === "low" && + !issue.attributes.ignored && (issue.attributes.status !== "resolved" || this.isShowResolvedInGraphs()) ).length; + + return { + critical: criticalSevCount, + high: highSevCount, + medium: mediumSevCount, + low: lowSevCount, + }; + }; + + getIgnoredIssuesCount = (issues: Array): IssuesCount => { + const criticalSevCount = issues.filter( + (issue) => + issue.attributes.effective_severity_level === "critical" && + issue.attributes.ignored && + (issue.attributes.status !== "resolved" || + this.isShowResolvedInGraphs()) + ).length; + const highSevCount = issues.filter( + (issue) => + issue.attributes.effective_severity_level === "high" && + issue.attributes.ignored && + (issue.attributes.status !== "resolved" || + this.isShowResolvedInGraphs()) + ).length; + const mediumSevCount = issues.filter( + (issue) => + issue.attributes.effective_severity_level === "medium" && + issue.attributes.ignored && + (issue.attributes.status !== "resolved" || + this.isShowResolvedInGraphs()) + ).length; + const lowSevCount = issues.filter( + (issue) => + issue.attributes.effective_severity_level === "low" && + issue.attributes.ignored && + (issue.attributes.status !== "resolved" || + this.isShowResolvedInGraphs()) + ).length; + return { critical: criticalSevCount, high: highSevCount, @@ -183,7 +232,7 @@ export class SnykApiClient implements SnykApi { const backendBaseUrl = await this.getApiUrl(); const v3Headers = this.headers; - const version = this.getSnykApiVersion(); + const version = this.getSnykIssuesApiVersion(); v3Headers["Content-Type"] = "application/vnd.api+json"; const apiUrl = `${backendBaseUrl}/rest/orgs/${orgId}/issues?version=${version}&scan_item.id=${projectId}&scan_item.type=project&limit=100`; const response = await fetch(`${apiUrl}`, { @@ -307,53 +356,65 @@ export class SnykApiClient implements SnykApi { `target_id=${await this.getTargetId(orgId, repoName[i])}` ); } catch (e) { - if (!ignoreMissing) throw e + if (!ignoreMissing) throw e; } } - const backendBaseUrl = await this.getApiUrl(); - const v3Headers = this.headers; - v3Headers["Content-Type"] = "application/vnd.api+json"; - const version = this.getSnykApiVersion(); - const projectsForTargetUrl = `${backendBaseUrl}/rest/orgs/${orgId}/projects?${TargetIdsArray.join( - "&" - )}&limit=100&version=${version}`; - const response = await fetch(`${projectsForTargetUrl}`, { - method: "GET", - headers: v3Headers, - }); + if (TargetIdsArray.length > 0) { + const backendBaseUrl = await this.getApiUrl(); + const v3Headers = this.headers; + v3Headers["Content-Type"] = "application/vnd.api+json"; + const version = this.getSnykApiVersion(); + const projectsForTargetUrl = `${backendBaseUrl}/rest/orgs/${orgId}/projects?${TargetIdsArray.join( + "&" + )}&limit=100&version=${version}`; + const response = await fetch(`${projectsForTargetUrl}`, { + method: "GET", + headers: v3Headers, + }); - if (response.status >= 400 && response.status < 600) { + if (response.status >= 400 && response.status < 600) { + throw new Error( + `Error ${response.status} - Failed fetching Projects list snyk data` + ); + } + const jsonResponse = await response.json(); + return jsonResponse.data as ProjectsData[]; + } else { throw new Error( - `Error ${response.status} - Failed fetching Projects list snyk data` + `No target IDs found in org ${orgId} for the targets [${repoName.join( + "," + )}].` ); } - const jsonResponse = await response.json(); - return jsonResponse.data as ProjectsData[]; } async getProjectsListByProjectIds( orgId: string, projectIdsArray: string[] ): Promise { - const backendBaseUrl = await this.getApiUrl(); - const v3Headers = this.headers; - v3Headers["Content-Type"] = "application/vnd.api+json"; - const version = this.getSnykApiVersion(); - const projectsForTargetUrl = `${backendBaseUrl}/rest/orgs/${orgId}/projects?ids=${projectIdsArray.join( - "%2C" - )}&limit=100&version=${version}`; - const response = await fetch(projectsForTargetUrl, { - method: "GET", - headers: v3Headers, - }); + if (projectIdsArray.length > 0) { + const backendBaseUrl = await this.getApiUrl(); + const v3Headers = this.headers; + v3Headers["Content-Type"] = "application/vnd.api+json"; + const version = this.getSnykApiVersion(); + const projectsForProjectIds = `${backendBaseUrl}/rest/orgs/${orgId}/projects?ids=${projectIdsArray.join( + "%2C" + )}&limit=100&version=${version}`; + const response = await fetch(projectsForProjectIds, { + method: "GET", + headers: v3Headers, + }); - if (response.status >= 400 && response.status < 600) { - throw new Error( - `Error ${response.status} - Failed fetching Projects list snyk data` - ); + if (response.status >= 400 && response.status < 600) { + throw new Error( + `Error ${response.status} - Failed fetching Projects list snyk data` + ); + } + const jsonResponse = await response.json(); + return jsonResponse.data as ProjectsData[]; + } else { + throw new Error(`Error loading projects by Project IDs.`); } - const jsonResponse = await response.json(); - return jsonResponse.data as ProjectsData[]; } private async getTargetId( diff --git a/src/components/SnykEntityComponent/SnykOverviewComponent.tsx b/src/components/SnykEntityComponent/SnykOverviewComponent.tsx index 74740b0..0615570 100644 --- a/src/components/SnykEntityComponent/SnykOverviewComponent.tsx +++ b/src/components/SnykEntityComponent/SnykOverviewComponent.tsx @@ -1,19 +1,19 @@ -import {Entity} from "@backstage/catalog-model"; +import { Entity } from "@backstage/catalog-model"; import React from "react"; -import {InfoCard, WarningPanel, Progress} from "@backstage/core-components"; -import {useApi} from "@backstage/core-plugin-api"; -import {snykApiRef} from "../../api"; -import {useAsync} from "react-use"; -import {Alert} from "@material-ui/lab"; - -import {Grid} from "@material-ui/core"; -import {SnykCircularCounter} from "./components/SnykCircularCountersComponent"; -import {IssuesCount as IssuesCountType} from "../../types/types"; -import {useEntity} from "@backstage/plugin-catalog-react"; -import {UnifiedIssues} from "../../types/unifiedIssuesTypes"; -import {SNYK_ANNOTATION_ORG, SNYK_ANNOTATION_ORGS} from "../../config"; - -export const SnykOverviewComponent = ({entity}: { entity: Entity }) => { +import { InfoCard, WarningPanel, Progress } from "@backstage/core-components"; +import { useApi } from "@backstage/core-plugin-api"; +import { snykApiRef } from "../../api"; +import { useAsync } from "react-use"; +import { Alert } from "@material-ui/lab"; + +import { Grid } from "@material-ui/core"; +import { SnykCircularCounter } from "./components/SnykCircularCountersComponent"; +import { IssuesCount as IssuesCountType } from "../../types/types"; +import { useEntity } from "@backstage/plugin-catalog-react"; +import { UnifiedIssues } from "../../types/unifiedIssuesTypes"; +import { SNYK_ANNOTATION_ORG, SNYK_ANNOTATION_ORGS } from "../../config"; + +export const SnykOverviewComponent = ({ entity }: { entity: Entity }) => { const snykApi = useApi(snykApiRef); if (!entity || !entity?.metadata.name) { @@ -45,19 +45,20 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => { /> - + ); } - const orgIds = entity?.metadata.annotations?.[SNYK_ANNOTATION_ORGS]?.split(',') - || entity?.metadata.annotations?.[SNYK_ANNOTATION_ORG]?.split(',') - || []; + const orgIds = + entity?.metadata.annotations?.[SNYK_ANNOTATION_ORGS]?.split(",") || + entity?.metadata.annotations?.[SNYK_ANNOTATION_ORG]?.split(",") || + []; const hasMultipleOrgs = orgIds.length > 1; // eslint-disable-next-line react-hooks/rules-of-hooks - const {value, loading, error} = useAsync(async () => { + const { value, loading, error } = useAsync(async () => { const aggregatedIssuesCount: IssuesCountType = { critical: 0, high: 0, @@ -68,32 +69,47 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => { orgIds.map(async (orgId) => { const projectList = entity?.metadata.annotations ? await snykApi.getCompleteProjectsListFromAnnotations( - orgId, - entity.metadata.annotations, - hasMultipleOrgs - ) : []; - return {projectList, orgId}; + orgId, + entity.metadata.annotations, + hasMultipleOrgs + ) + : []; + return { projectList, orgId }; }) ); let projectsCount = 0; - const allProjects = projectOrgList.flatMap(({projectList}) => projectList); - const projectOrgMap = projectOrgList.reduce((acc, {orgId, projectList}) => { - projectList.forEach(project => { - acc[project.id] = orgId; - }); - return acc; - }, {} as { [key: string]: string }); + const allProjects = projectOrgList.flatMap( + ({ projectList }) => projectList + ); + const projectOrgMap = projectOrgList.reduce( + (acc, { orgId, projectList }) => { + projectList.forEach((project) => { + acc[project.id] = orgId; + }); + return acc; + }, + {} as { [key: string]: string } + ); const projectIds = allProjects.map((project) => project.id); for (let i = 0; i < projectIds.length; i++) { const projectId = projectIds[i]; - if (allProjects?.some((selectedProject) => selectedProject.id === projectId)) { + if ( + allProjects?.some((selectedProject) => selectedProject.id === projectId) + ) { projectsCount++; - const vulnsIssues: UnifiedIssues = await snykApi.listAllAggregatedIssues(projectOrgMap[projectId], projectId); - const currentProjectIssuesCount = snykApi.getIssuesCount(vulnsIssues.data); + const vulnsIssues: UnifiedIssues = + await snykApi.listAllAggregatedIssues( + projectOrgMap[projectId], + projectId + ); + + const currentProjectIssuesCount = snykApi.getIssuesCount( + vulnsIssues.data + ); aggregatedIssuesCount.critical += currentProjectIssuesCount.critical; aggregatedIssuesCount.high += currentProjectIssuesCount.high; aggregatedIssuesCount.medium += currentProjectIssuesCount.medium; @@ -101,7 +117,7 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => { } } - return {aggregatedIssuesCount, projectsCount}; + return { aggregatedIssuesCount, projectsCount }; }); const issuesCount: IssuesCountType = value?.aggregatedIssuesCount || { @@ -114,7 +130,7 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => { return ( { }} loading={loading} /> - + ); } else if (error) { return ( {error?.message} @@ -146,12 +162,12 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => { return ( - + ); }; export const SnykOverview = () => { - const {entity} = useEntity(); - return ; + const { entity } = useEntity(); + return ; }; diff --git a/src/components/SnykEntityComponent/SnykTab.tsx b/src/components/SnykEntityComponent/SnykTab.tsx index a9ff08d..1e44611 100644 --- a/src/components/SnykEntityComponent/SnykTab.tsx +++ b/src/components/SnykEntityComponent/SnykTab.tsx @@ -7,13 +7,13 @@ import { TabbedCard, CardTab, } from "@backstage/core-components"; -import {Grid} from "@material-ui/core"; -import {SnykApi} from "../../api"; -import {useAsync} from "react-use"; -import {Alert} from "@material-ui/lab"; -import {IssuesTable} from "./components/SnykIssuesComponent"; -import {DepGraphInfo} from "./components/SnykDepGraphComponent"; -import {SnykCircularCounter} from "./components/SnykCircularCountersComponent"; +import { Grid } from "@material-ui/core"; +import { SnykApi } from "../../api"; +import { useAsync } from "react-use"; +import { Alert } from "@material-ui/lab"; +import { IssuesTable } from "./components/SnykIssuesComponent"; +import { DepGraphInfo } from "./components/SnykDepGraphComponent"; +import { SnykCircularCounter } from "./components/SnykCircularCountersComponent"; import { ProjectGetResponseType, DepgraphGetResponseType, @@ -35,7 +35,7 @@ export const generateSnykTabForProject = ( (type) => type !== "license" ); return ({}) => { - const {value, loading, error} = useAsync(async () => { + const { value, loading, error } = useAsync(async () => { const allIssues: UnifiedIssues = await snykApi.listAllAggregatedIssues( orgId, projectId @@ -47,7 +47,9 @@ export const generateSnykTabForProject = ( (issue) => issue.attributes.type === "license" ); const ignoredIssues: Array = allIssues.data.filter( - (issue) => issue.attributes.status === IssueAttributesStatusEnum.Ignored + (issue) => + issue.attributes.ignored === true && + issue.attributes.status != IssueAttributesStatusEnum.Resolved ); const depGraph: DepgraphGetResponseType = genericIssues.some( @@ -69,7 +71,7 @@ export const generateSnykTabForProject = ( if (loading) { return ( - + ); } else if (error) { @@ -82,7 +84,9 @@ export const generateSnykTabForProject = ( const issuesCount = snykApi.getIssuesCount(value.genericIssues); const licenseIssuesCount = snykApi.getIssuesCount(value.licenseIssues); - const ignoredIssuesCount = snykApi.getIssuesCount(value.ignoredIssues); + const ignoredIssuesCount = snykApi.getIgnoredIssuesCount( + value.ignoredIssues + ); const metadata = { origin: value.projectDetails.origin, @@ -90,7 +94,7 @@ export const generateSnykTabForProject = ( created: value.projectDetails.created, "last tested": value.projectDetails.lastTestedDate, "Project ID": `${value.projectDetails.id}`, - "Organization": `${orgSlug} (${orgId})` + Organization: `${orgSlug} (${orgId})`, }; const linkInfo = { title: "More details", @@ -109,7 +113,7 @@ export const generateSnykTabForProject = ( > - + @@ -167,7 +171,7 @@ export const generateSnykTabForProject = ( - + @@ -197,7 +201,7 @@ export const generateSnykTabForProject = ( > - + diff --git a/src/types/unifiedIssuesTypes.ts b/src/types/unifiedIssuesTypes.ts index cb8ef3b..af3f26a 100644 --- a/src/types/unifiedIssuesTypes.ts +++ b/src/types/unifiedIssuesTypes.ts @@ -408,7 +408,6 @@ export enum IssueAttributesEffectiveSeverityLevelEnum { export enum IssueAttributesStatusEnum { Open = "open", Resolved = "resolved", - Ignored = "ignored", } export interface IssueLinks {