Skip to content

Commit

Permalink
feat: add support for multiple orgs
Browse files Browse the repository at this point in the history
  • Loading branch information
Silthus committed Feb 1, 2024
1 parent ce54763 commit 806718d
Show file tree
Hide file tree
Showing 7 changed files with 342 additions and 308 deletions.
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,10 +124,11 @@ snyk:
export SNYK_TOKEN="123-123-123-123"
```

6. Add this following annotation to your entities.
6. Add one of the following annotation to your entities.

- `snyk.io/org-id` is the ID of the Snyk organization where your project is. You can find the ID in the Organization Settings in the Snyk dashboard.

- `snyk.io/org-ids` specify one or more Snyk organization ids, comma separated. This will try to find any of the targets or projects in any of the organizations. `snyk.io/org-id` is ignored when this annotation is set.

7. Then add one or more than one of the following annotations to your entities.

- `snyk.io/target-id` specify a single target by name or ID. Target ID will avoid an API call and be therefore faster. Use this [API endpoint](https://apidocs.snyk.io/?version=2023-06-19%7Ebeta#get-/orgs/-org_id-/targets) to get the Target IDs.
Expand Down
33 changes: 23 additions & 10 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
SNYK_ANNOTATION_PROJECTIDS,
SNYK_ANNOTATION_EXCLUDE_PROJECTIDS,
SNYK_ANNOTATION_ORG,
SNYK_ANNOTATION_ORGS,
} from "../config";
import { mockedProjects } from "../utils/mockedProjects";
import { mockedIssues } from "../utils/mockedIssues";
Expand Down Expand Up @@ -58,7 +59,8 @@ export interface SnykApi {
getProjectDetails(orgName: string, projectId: string): Promise<any>;
getCompleteProjectsListFromAnnotations(
orgId: string,
annotations: Record<string, string>
annotations: Record<string, string>,
ignoreMissingTargets: boolean
): Promise<ProjectsData[]>;
getDependencyGraph(orgName: string, projectId: string): Promise<any>;
getSnykAppHost(): string;
Expand Down Expand Up @@ -117,11 +119,15 @@ export class SnykApiClient implements SnykApi {
isAvailableInEntity(entity: Entity): boolean {
return (
this.isMocked() ||
(Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_ORG]) &&
(Boolean(entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETNAME]) ||
(
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_PROJECTIDS])
)
);
}

Expand Down Expand Up @@ -228,7 +234,8 @@ export class SnykApiClient implements SnykApi {

async getCompleteProjectsListFromAnnotations(
orgId: string,
annotations: Record<string, string>
annotations: Record<string, string>,
ignoreMissingTargets = false
): Promise<ProjectsData[]> {
let completeProjectsList: ProjectsData[] = [];

Expand All @@ -248,7 +255,8 @@ export class SnykApiClient implements SnykApi {
if (targetsArray.length > 0) {
const fullProjectByTargetList = await this.getProjectsListByTargets(
orgId,
Array.isArray(targetsArray) ? targetsArray : [...targetsArray]
Array.isArray(targetsArray) ? targetsArray : [...targetsArray],
ignoreMissingTargets
);
completeProjectsList.push(...fullProjectByTargetList);
}
Expand Down Expand Up @@ -278,13 +286,18 @@ export class SnykApiClient implements SnykApi {

async getProjectsListByTargets(
orgId: string,
repoName: string[]
repoName: string[],
ignoreMissing = false
): Promise<ProjectsData[]> {
const TargetIdsArray: string[] = [];
for (let i = 0; i < repoName.length; i++) {
TargetIdsArray.push(
`target_id=${await this.getTargetId(orgId, repoName[i])}`
);
try {
TargetIdsArray.push(
`target_id=${await this.getTargetId(orgId, repoName[i])}`
);
} catch (e) {
if (!ignoreMissing) throw e
}
}
const backendBaseUrl = await this.getApiUrl();
const v3Headers = this.headers;
Expand Down
64 changes: 33 additions & 31 deletions src/components/SnykEntityComponent/SnykEntityComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ import {
TabbedLayout,
Link,
} from "@backstage/core-components";
import { MissingAnnotationEmptyState } from "@backstage/plugin-catalog-react";
import { useApi } from "@backstage/core-plugin-api";
import {
MissingAnnotationEmptyState,
InfoCard,
} from "@backstage/core-components";
import { snykApiRef } from "../../api";
Expand All @@ -29,12 +29,14 @@ import { useEntity } from "@backstage/plugin-catalog-react";
import { ProjectsData } from "../../types/projectsTypes";
import {
SNYK_ANNOTATION_ORG,
SNYK_ANNOTATION_ORGS,
SNYK_ANNOTATION_TARGETID,
SNYK_ANNOTATION_TARGETNAME,
} from "../../config";

type SnykTab = {
name: string;
slug: string;
icon: any;
projectId: string;
tabContent: any;
Expand Down Expand Up @@ -110,17 +112,18 @@ export const SnykEntityComponent = () => {

const tabs: Array<SnykTab> = [];

const orgId = entity?.metadata.annotations?.[SNYK_ANNOTATION_ORG] || "null";
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 completeProjectsList: ProjectsData[] = entity?.metadata.annotations
? await snykApi.getCompleteProjectsListFromAnnotations(
orgId,
entity?.metadata.annotations
)
: [];
const orgSlug = await snykApi.getOrgSlug(orgId);
return { completeProjectsList, orgSlug };
return Promise.all(orgIds.map(async (orgId) => {
const projectList: ProjectsData[] = entity?.metadata.annotations ? await snykApi.getCompleteProjectsListFromAnnotations(orgId, entity?.metadata.annotations, hasMultipleOrgs): []
const orgSlug = await snykApi.getOrgSlug(orgId);
return { projectList, orgSlug, orgId };
}));
});
if (loading) {
return (
Expand All @@ -129,27 +132,28 @@ export const SnykEntityComponent = () => {
</Content>
);
} else if (error) {
// eslint-disable-next-line no-console
console.log(error);
return <Alert severity="error">{error.message}</Alert>;
}

const projectList = value?.completeProjectsList as ProjectsData[];
const orgSlug = value?.orgSlug || "";
projectList.forEach((project) => {
tabs.push({
name: `${utils.extractTargetShortname(
project.attributes.name || "unknown"
)}`,
icon: getIconForProjectType(project.attributes.origin || ""),
projectId: project.id,
tabContent: generateSnykTabForProject(
snykApi,
orgId,
orgSlug,
project.id
),
type: project.attributes.type,
});
value?.forEach(({orgId, orgSlug, projectList}) => {
projectList.forEach((project) => {
const name = `${utils.extractTargetShortname(project.attributes.name || "unknown")}`;
tabs.push({
name: name,
slug: hasMultipleOrgs ? `${orgSlug}/${name}` : name,
icon: getIconForProjectType(project.attributes.origin || ""),
projectId: project.id,
tabContent: generateSnykTabForProject(
snykApi,
orgId,
orgSlug,
project.id
),
type: project.attributes.type,
});
})
});

const infoCardTitle = `${tabs.length} Project${tabs.length > 1 ? "s" : ""}`;
Expand All @@ -161,10 +165,8 @@ export const SnykEntityComponent = () => {
{tabs.map((tab) => (
<TabbedLayout.Route
key={tab.projectId}
path={tab.name}
title={`(${tab.type}-${tab.projectId.substring(0, 3)}) ${
tab.name
}`}
path={tab.slug}
title={`(${tab.type}-${tab.projectId.substring(0,3)}) ${tab.name}`}
>
<Content>
<tab.tabContent />
Expand Down
53 changes: 33 additions & 20 deletions src/components/SnykEntityComponent/SnykOverviewComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ 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 } from "../../config";
import { SNYK_ANNOTATION_ORG, SNYK_ANNOTATION_ORGS } from "../../config";

export const SnykOverviewComponent = ({ entity }: { entity: Entity }) => {
const snykApi = useApi(snykApiRef);
Expand All @@ -25,7 +25,7 @@ export const SnykOverviewComponent = ({ entity }: { entity: Entity }) => {
<Grid
container
spacing={2}
justify="center"
justifyContent="center"
direction="column"
alignItems="center"
>
Expand All @@ -50,44 +50,57 @@ export const SnykOverviewComponent = ({ entity }: { entity: Entity }) => {
</Grid>
);
}
const orgId = entity?.metadata.annotations?.[SNYK_ANNOTATION_ORG] || "null";

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 aggregatedIssuesCount: IssuesCountType = {
critical: 0,
high: 0,
medium: 0,
low: 0,
};
const projectList = entity?.metadata.annotations
? await snykApi.getCompleteProjectsListFromAnnotations(
orgId,
entity.metadata.annotations
)
: [];
const projectOrgList = await Promise.all(
orgIds.map(async (orgId) => {
const projectList = entity?.metadata.annotations
? await snykApi.getCompleteProjectsListFromAnnotations(
orgId,
entity.metadata.annotations,
hasMultipleOrgs
) : [];
return { projectList, orgId };
})
);

let projectsCount = 0;

const projectIds = projectList.map((project) => project.id);
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++) {
if (
projectList?.some(
(selectedProject) => selectedProject.id === projectIds[i]
)
) {
const projectId = projectIds[i];
if (allProjects?.some((selectedProject) => selectedProject.id === projectId)) {
projectsCount++;

const vulnsIssues: UnifiedIssues =
await snykApi.listAllAggregatedIssues(orgId, projectIds[i]);
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 };
});

Expand Down
Loading

0 comments on commit 806718d

Please sign in to comment.