Skip to content

Commit

Permalink
fix: counts to exclude resolved and ignored
Browse files Browse the repository at this point in the history
  • Loading branch information
aarlaud committed Feb 14, 2024
1 parent 9159a8f commit 2a6c428
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 79 deletions.
4 changes: 4 additions & 0 deletions config.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export interface Config {
* @visibility frontend
*/
apiVersion?: string;
/**
* @visibility frontend
*/
issuesApiVersion?: string;
/**
* @visibility frontend
*/
Expand Down
91 changes: 70 additions & 21 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 = "";

Expand Down Expand Up @@ -71,6 +71,8 @@ export interface SnykApi {

getSnykApiVersion(): string;

getSnykIssuesApiVersion(): string;

getOrgSlug(orgId: string): Promise<string>;

isMocked(): boolean;
Expand All @@ -80,6 +82,8 @@ export interface SnykApi {
isShowResolvedInGraphs(entity: Entity): boolean;

getIssuesCount(issues: Array<Issue>): IssuesCount;

getIgnoredIssuesCount(issues: Array<Issue>): IssuesCount;
}

export class SnykApiClient implements SnykApi {
Expand Down Expand Up @@ -126,47 +130,92 @@ 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])))
);
}

getIssuesCount = (issues: Array<Issue>): 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,
medium: mediumSevCount,
low: lowSevCount,
};
};

getIgnoredIssuesCount = (issues: Array<Issue>): 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,
Expand All @@ -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}`, {
Expand Down Expand Up @@ -307,7 +356,7 @@ 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();
Expand Down
100 changes: 58 additions & 42 deletions src/components/SnykEntityComponent/SnykOverviewComponent.tsx
Original file line number Diff line number Diff line change
@@ -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) {
Expand Down Expand Up @@ -45,19 +45,20 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => {
/>
</Grid>
<Grid item>
<img src="https://i.gifer.com/yH.gif" alt=""/>
<img src="https://i.gifer.com/yH.gif" alt="" />
</Grid>
</Grid>
);
}

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,
Expand All @@ -68,40 +69,55 @@ 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;
aggregatedIssuesCount.low += currentProjectIssuesCount.low;
}
}

return {aggregatedIssuesCount, projectsCount};
return { aggregatedIssuesCount, projectsCount };
});

const issuesCount: IssuesCountType = value?.aggregatedIssuesCount || {
Expand All @@ -114,7 +130,7 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => {
return (
<InfoCard
title="Loading..."
deepLink={{title: "Retrieving Vulnerabilities from Snyk", link: ""}}
deepLink={{ title: "Retrieving Vulnerabilities from Snyk", link: "" }}
>
<SnykCircularCounter
issuesCount={{
Expand All @@ -125,14 +141,14 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => {
}}
loading={loading}
/>
<Progress/>
<Progress />
</InfoCard>
);
} else if (error) {
return (
<InfoCard
title="Snyk - Failed to retrieve"
deepLink={{title: "Error", link: ""}}
deepLink={{ title: "Error", link: "" }}
>
<Alert severity="error">{error?.message}</Alert>
</InfoCard>
Expand All @@ -146,12 +162,12 @@ export const SnykOverviewComponent = ({entity}: { entity: Entity }) => {

return (
<InfoCard title="Snyk Issues" deepLink={linkInfo}>
<SnykCircularCounter issuesCount={issuesCount} loading={loading}/>
<SnykCircularCounter issuesCount={issuesCount} loading={loading} />
</InfoCard>
);
};

export const SnykOverview = () => {
const {entity} = useEntity();
return <SnykOverviewComponent entity={entity}/>;
const { entity } = useEntity();
return <SnykOverviewComponent entity={entity} />;
};
Loading

0 comments on commit 2a6c428

Please sign in to comment.