Skip to content

Commit

Permalink
Merge pull request #158 from snyk-tech-services/develop
Browse files Browse the repository at this point in the history
release changes
  • Loading branch information
aarlaud authored Sep 3, 2023
2 parents 65d53c9 + f07488d commit 6809a94
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 89 deletions.
23 changes: 14 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,35 +102,40 @@ snyk:
export SNYK_TOKEN="123-123-123-123"
```

6. Add the following annotations to your entities.
6. Add this following annotation to your entities.
- `snyk.io/org-name` is the Snyk organization name where your project is. Use the slug (like in url, or in the org settings page), not the display name

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.
- `snyk.io/targets` specify one or more targets, 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.
- `snyk.io/project-ids` are the project ID (see slug in url or ID in project settings)
If multiple projects (like multiple package.json or pom files, add them with increasing number), add them comma separated
- `snyk.io/exclude-project-ids` to exclude specific projects you might not want.
....


Example:
```yaml
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: Java-goof
description: Java Goof
....
name: goof
description: Goof
annotations:
snyk.io/org-name: snyk-demo-org
snyk.io/project-ids: 12345678-1234-1234-1234-123456789012,12345678-1234-1234-1234-123456789013,12345678-1234-1234-1234-123456789014
...
snyk.io/org-id: 361fd3c0-41d4-4ea4-ba77-09bb17890967
snyk.io/targets: Snyk Demo/java-goof,508d2263-ea8a-4e42-bc9d-844de21f4172
snyk.io/target-id: aarlaud-snyk/github-stats
snyk.io/project-ids: 7439e322-f9c1-4c42-8367-002b33b9d946,db066cb9-b373-46da-b918-b49b541e0d63
snyk.io/exclude-project-ids: 4737fc9c-3894-40ba-9dc5-aa8ae658c9f6,38e02916-0cf7-4927-ba98-06afae9fef36
spec:
type: service
lifecycle: production
owner: guest
....
```
Some more examples can be found in [here](https://github.com/snyk-tech-services/backstage-plugin-snyk/tree/develop/test/fixtures)

## Migration steps from version 1.x to 2.x
- Update the proxy target to not contain /v1
- snyk.io/project-ids annotations are no longer in use, instead replaced by targets designated by github.com/project-slug or snyk.io/target-id.

## Troubleshooting

Expand Down
120 changes: 97 additions & 23 deletions src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,14 @@ import {
} from "@backstage/core-plugin-api";
import { TargetData } from "../types/targetsTypes";
import { OrgData } from "../types/orgsTypes";

import { ProjectsData } from "../types/projectsTypes";
import {
SNYK_ANNOTATION_TARGETID,
SNYK_ANNOTATION_TARGETNAME,
SNYK_ANNOTATION_TARGETS,
SNYK_ANNOTATION_PROJECTIDS,
SNYK_ANNOTATION_EXCLUDE_PROJECTIDS
} from "../config";
const DEFAULT_PROXY_PATH_BASE = "";

type Options = {
Expand All @@ -41,7 +48,7 @@ export const snykApiRef: ApiRef<SnykApi> = createApiRef<SnykApi>({
export interface SnykApi {
ListAllAggregatedIssues(orgName: string, projectId: string): Promise<any>;
ProjectDetails(orgName: string, projectId: string): Promise<any>;
ProjectsList(orgName: string, repoName:string): Promise<any>;
GetCompleteProjectsListFromAnnotations(orgId: string, annotations: Record<string,string>):Promise<ProjectsData[]>;
GetDependencyGraph(orgName: string, projectId: string): Promise<any>;
GetSnykAppHost(): string;
GetOrgSlug(orgId: string): Promise<string>;
Expand Down Expand Up @@ -129,8 +136,87 @@ export class SnykApiClient implements SnykApi {
return orgData.attributes.slug
}

async ProjectsList(orgId: string, repoName: string) {
if(repoName == ''){
async GetCompleteProjectsListFromAnnotations(orgId: string, annotations: Record<string,string>):Promise<ProjectsData[]> {
let completeProjectsList: ProjectsData[] = []
const targetsArray = annotations?.[SNYK_ANNOTATION_TARGETS] ? annotations?.[SNYK_ANNOTATION_TARGETS].split(',') : []

if(annotations?.[SNYK_ANNOTATION_TARGETNAME] ){
targetsArray.push(annotations?.[SNYK_ANNOTATION_TARGETNAME])
} else if(annotations?.[SNYK_ANNOTATION_TARGETID]){
targetsArray.push(annotations?.[SNYK_ANNOTATION_TARGETID])
}
if(targetsArray.length>0){
const fullProjectByTargetList = await this.ProjectsListByTargets(
orgId,
Array.isArray(targetsArray)? targetsArray: [...targetsArray]
);
completeProjectsList.push(...fullProjectByTargetList)
}

if(annotations?.[SNYK_ANNOTATION_PROJECTIDS]){
const fullProjectByIdList = await this.ProjectsListByProjectIds(
orgId,
annotations?.[SNYK_ANNOTATION_PROJECTIDS].split(',')
);
completeProjectsList.push(...fullProjectByIdList)
}

if(annotations?.[SNYK_ANNOTATION_EXCLUDE_PROJECTIDS]){
let idsToExclude = annotations?.[SNYK_ANNOTATION_EXCLUDE_PROJECTIDS].split(',')
idsToExclude = idsToExclude.filter(id => /^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/.test(id))
completeProjectsList = completeProjectsList.filter((project) => {
return !idsToExclude.includes(project.id)
})
}
return completeProjectsList
}

async ProjectsListByTargets(orgId: string, repoName: string[]):Promise<ProjectsData[]> {
const TargetIdsArray:string[] = []
for(let i=0;i<repoName.length;i++){
TargetIdsArray.push(`target_id=${await this.GetTargetId(orgId,repoName[i])}`)
}
const backendBaseUrl = await this.getApiUrl();
let v3Headers = this.headers;
v3Headers["Content-Type"] = "application/vnd.api+json";

const projectsForTargetUrl = `${backendBaseUrl}/rest/orgs/${orgId}/projects?${TargetIdsArray.join('&')}&limit=100&version=2023-06-19~beta`;
const response = await fetch(`${projectsForTargetUrl}`, {
method: "GET",
headers: v3Headers,
});

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[];
}

async ProjectsListByProjectIds(orgId: string, projectIdsArray: string[]):Promise<ProjectsData[]> {
const backendBaseUrl = await this.getApiUrl();
let v3Headers = this.headers;
v3Headers["Content-Type"] = "application/vnd.api+json";

const projectsForTargetUrl = `${backendBaseUrl}/rest/orgs/${orgId}/projects?ids=${projectIdsArray.join('%2C')}&limit=100&version=2023-06-19~beta`;
const response = await fetch(projectsForTargetUrl, {
method: "GET",
headers: v3Headers,
});

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[];
}

private async GetTargetId(orgId:string,targetIdentifier:string): Promise<string> {
if(targetIdentifier == ''){
throw new Error(
`Error - Unable to find repo name. Please add github.com/project-slug or snyk.io/target-id annotation`
);
Expand All @@ -139,10 +225,10 @@ export class SnykApiClient implements SnykApi {
let v3Headers = this.headers;
v3Headers["Content-Type"] = "application/vnd.api+json";
let targetId
if(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/.test(repoName)){
targetId = repoName
if(/^[0-9a-fA-F]{8}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{4}\b-[0-9a-fA-F]{12}$/.test(targetIdentifier)){
targetId = targetIdentifier
} else {
const targetsAPIUrl = `${backendBaseUrl}/rest/orgs/${orgId}/targets?displayName=${encodeURIComponent(repoName)}&version=2023-06-19~beta`;
const targetsAPIUrl = `${backendBaseUrl}/rest/orgs/${orgId}/targets?displayName=${encodeURIComponent(targetIdentifier)}&version=2023-06-19~beta`;
const targetResponse = await fetch(`${targetsAPIUrl}`, {
method: "GET",
headers: v3Headers,
Expand All @@ -155,28 +241,16 @@ export class SnykApiClient implements SnykApi {
const targetsList = await targetResponse.json()
const targetsListData = targetsList.data as TargetData[]
targetId = targetsListData.find(target => {
return target.attributes.displayName == repoName
return target.attributes.displayName == targetIdentifier
})?.id
if(!targetId){
throw new Error(
`Error - Failed finding Target snyk data for repo ${repoName}`
`Error - Failed finding Target snyk data for repo ${targetIdentifier}`
);
}
}
return targetId

const projectsForTargetUrl = `${backendBaseUrl}/rest/orgs/${orgId}/projects?target_id=${targetId}&limit=100&version=2023-06-19~beta`;
const response = await fetch(`${projectsForTargetUrl}`, {
method: "GET",
headers: v3Headers,
});

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;
}

async GetDependencyGraph(orgName: string, projectId: string) {
Expand All @@ -192,6 +266,6 @@ export class SnykApiClient implements SnykApi {
);
}
const jsonResponse = await response.json();
return jsonResponse;
return jsonResponse as ProjectsData[];
}
}
99 changes: 67 additions & 32 deletions src/components/SnykEntityComponent/SnykEntityComponent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,13 @@ import {
Progress,
Content,
TabbedLayout,
Link

Link,
} from "@backstage/core-components";
import { useApi } from "@backstage/core-plugin-api";
import { MissingAnnotationEmptyState, InfoCard } from "@backstage/core-components";
import {
MissingAnnotationEmptyState,
InfoCard,
} from "@backstage/core-components";
import { snykApiRef } from "../../api";
import { useAsync } from "react-use";
import { Alert } from "@material-ui/lab";
Expand All @@ -25,11 +27,18 @@ import {
} from "./svgs";
import { useEntity } from "@backstage/plugin-catalog-react";
import { ProjectsData } from "../../types/projectsTypes";
import { SNYK_ANNOTATION_ORG, SNYK_ANNOTATION_TARGETID, SNYK_ANNOTATION_TARGETNAME } from "../../config";
import {
SNYK_ANNOTATION_ORG,
SNYK_ANNOTATION_TARGETID,
SNYK_ANNOTATION_TARGETNAME,
SNYK_ANNOTATION_PROJECTIDS,
SNYK_ANNOTATION_TARGETS,
} from "../../config";

type SnykTab = {
name: string;
icon: any;
projectId: string,
tabContent: any;
type: string;
};
Expand Down Expand Up @@ -67,28 +76,45 @@ const getIconForProjectType = (projectOrigin: string) => {
}
};


export const SnykEntityComponent = () => {
const { entity } = useEntity();
if (!entity || !entity?.metadata.name) {
return <>No Snyk org/project-ids listed</>;
}
const containerStyle = { width: '60%', padding: '20px' };
const containerStyle = { width: "60%", padding: "20px" };
if (
!entity ||
!entity?.metadata.annotations ||
!entity?.metadata.annotations[SNYK_ANNOTATION_ORG] ||
!(entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETNAME] || entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETID])
!(
entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETNAME] ||
entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETID] ||
entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETS] ||
entity.metadata.annotations?.[SNYK_ANNOTATION_PROJECTIDS]
)
) {
return (
<Content>
<div style={containerStyle}>
<MissingAnnotationEmptyState annotation={[SNYK_ANNOTATION_ORG,SNYK_ANNOTATION_TARGETNAME]} />
<MissingAnnotationEmptyState
annotation={[SNYK_ANNOTATION_ORG, SNYK_ANNOTATION_TARGETNAME]}
/>
</div>
or alternatively using the target ID you can retrieve using the <Link to={'https://apidocs.snyk.io/?version=2023-06-19%7Ebeta#get-/orgs/-org_id-/targets'}>Targets endpoint</Link> endpoint.
or alternatively using the target name or ID (you can retrieve using the{" "}
<Link
to={
"https://apidocs.snyk.io/?version=2023-06-19%7Ebeta#get-/orgs/-org_id-/targets"
}
>
Targets endpoint)
</Link>{" "}
endpoint.
<div style={containerStyle}>
<MissingAnnotationEmptyState annotation={[SNYK_ANNOTATION_ORG,SNYK_ANNOTATION_TARGETID]} />
<MissingAnnotationEmptyState
annotation={[SNYK_ANNOTATION_ORG, SNYK_ANNOTATION_TARGETID]}
/>
</div>
Other combinations are possible, please checkout the README.
</Content>
);
}
Expand All @@ -99,12 +125,9 @@ export const SnykEntityComponent = () => {
const orgId = entity?.metadata.annotations?.[SNYK_ANNOTATION_ORG] || "null";

const { value, loading, error } = useAsync(async () => {
const fullProjectList = await snykApi.ProjectsList(
orgId,
entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETNAME] || entity.metadata.annotations?.[SNYK_ANNOTATION_TARGETID] || ''
);
const orgSlug = await snykApi.GetOrgSlug(orgId)
return { fullProjectList, orgSlug };
const completeProjectsList: ProjectsData[] = entity?.metadata.annotations ? await snykApi.GetCompleteProjectsListFromAnnotations(orgId,entity?.metadata.annotations): []
const orgSlug = await snykApi.GetOrgSlug(orgId);
return { completeProjectsList, orgSlug };
});
if (loading) {
return (
Expand All @@ -117,30 +140,42 @@ export const SnykEntityComponent = () => {
return <Alert severity="error">{error.message}</Alert>;
}

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

const infoCardTitle = `${tabs.length} Project${tabs.length>1? 's' : '' }`
const infoCardTitle = `${tabs.length} Project${tabs.length > 1 ? "s" : ""}`;

return (
<><InfoCard title={infoCardTitle} cardClassName="infocardstyle">

<TabbedLayout>
{tabs.map(tab => (
<TabbedLayout.Route key={tab.name+tab.type} path={tab.name} title={`(${tab.type}) ${tab.name.substring(0,15)}${tab.name.length > 15 ? '...':''}`}>
<Content><tab.tabContent /></Content>
</TabbedLayout.Route>
))}</TabbedLayout>
</InfoCard>
</>

<>
<InfoCard title={infoCardTitle} cardClassName="infocardstyle">
<TabbedLayout>
{tabs.map((tab) => (
<TabbedLayout.Route
key={tab.projectId}
path={tab.name}
title={`(${tab.type}-${tab.projectId.substring(0,3)}) ${tab.name}`}
>
<Content>
<tab.tabContent />
</Content>
</TabbedLayout.Route>
))}
</TabbedLayout>
</InfoCard>
</>
);
};
Loading

0 comments on commit 6809a94

Please sign in to comment.