Last active
October 20, 2021 22:25
-
-
Save carwin/dd4e5cfb3a3f51e2db1453c727bf89ab to your computer and use it in GitHub Desktop.
Construct a Snyk Fix URL for project vulnerability.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// This is a helper function referenced in our fix_url_creator.ts file. | |
// It's been yanked from the Snyk Apps Demo repository and added here | |
// for context. You can likely skip to the next file. | |
import { AxiosInstance } from 'axios'; | |
import { APIVersion } from '../../types'; | |
import { API_BASE } from '../../../app'; | |
import axios from 'axios'; | |
import { refreshTokenInterceptor } from '.'; | |
/** | |
* Utility function to call the Snyk API | |
* @param {String} tokenType ex: bearer, token, etc | |
* @param {String} token authentication token | |
* @param {APIVersion} version API version to call, defaults to V1 | |
* @returns {AxiosInstance} | |
*/ | |
export function callSnykApi(tokenType: string, token: string, version: APIVersion = APIVersion.V1): AxiosInstance { | |
// Snyk instance for API V1 | |
let axiosInstance = axios.create({ | |
baseURL: `${API_BASE}/v1`, | |
headers: { | |
'Content-Type': 'application/json', | |
Authorization: `${token} ${tokenType}`, | |
}, | |
}); | |
// If user selection V3 | |
if (version === APIVersion.V3) { | |
axiosInstance = axios.create({ | |
baseURL: `${API_BASE}/v3`, | |
headers: { | |
'Content-Type': 'application/vnd.api+json', | |
Authorization: `${tokenType} ${token}`, | |
}, | |
}); | |
} | |
axiosInstance.interceptors.request.use(refreshTokenInterceptor, Promise.reject); | |
// Returns the axios instance | |
return axiosInstance; | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { callSnykApi } from 'api'; | |
// = Construct a Fix URL = | |
// ----------------------- | |
// Snyk's API does not yet provide an endpoint for creating automated pull | |
// requests for vulnerabilities it discovers in projects. Let's make one. | |
// | |
// This example goes over a number of different options for walking through | |
// Snyk data, not all of which are strictly necessary or optimized for performance. | |
// The idea is to illustrate some things that could work in an imagined integration. | |
// | |
// The end goal is to construct a "fix" link that a user can click to have Snyk | |
// automatically create a Pull Request in their SCM integration for a vulnerability | |
// which we are displaying in our own imaginary system. | |
// | |
// For reference, the fix links look like this and follow the same pattern: | |
// app.snyk.io/org/<org name>/fix/<project ID?vuln=<type>:<package>:<vuln date?> | |
// app.snyk.io/org/carwin.young/fix/12345?vuln=npm:minimatch:20160620 | |
// Get the Snyk organization name | |
// - If we've built this into a Snyk App, then it's likely that the user | |
// authenticated the app and provided a scope for a specific Organization. | |
// - Since the User authenticated the app to a specific Org, when we query, | |
// we'll be getting the proper one, even though the API call we're using | |
// would normally get ALL the user's orgs. | |
// - Technically, this bit is probably unnecessary since we could get this | |
// group information from other calls like the List All Projects endpoint | |
// we'll see later. | |
// | |
// @see https://snyk.docs.apiary.io/#reference/users/my-user-details/get-my-details | |
// @see https://snyk.docs.apiary.io/#reference/organizations/the-snyk-organization-for-a-request/list-all-the-organizations-a-user-belongs-to | |
const getOrgName = await callSnykApi(access_token, token_type).get(`/orgs`); // This is unnecessary since we could get this from the call to get All Projects. | |
const orgName = getOrgName.data.orgs[0].name; | |
console.log('Org Name: ', orgName); | |
// Get project(s) & match with our data. | |
// - In our imaginary integration we'll pretend we have a UI that displays vulnerability | |
// information via Snyk to the user and that we have either a link to the vulnerability | |
// over on Snyk or our own page for it. | |
// - The vuln page's URL should have / probably has the ID we need to match on during a loop. | |
// - If we've got vulnerability information, we need a way to find out which project(s) | |
// have it. | |
// - This is likely one of the core problems to solve in an integration like this. | |
// + In an ideal scenario, the best option would be to pull the user's projects | |
// from Snyk and let them match their data to data points in our system. | |
// This would make life easier, but for illustrative purposes, let's pretend life is hard. | |
// - After finding a project, we need a mechanism to match it with what's in our system.. | |
// This could be done a number of ways. The query for listing all projects accepts a | |
// request body containing a filter object, so if, for instance, we know the project | |
// name already from our system, we could limit the request's response data. | |
// - Assuming we don't know anything but what the user configured in our system, | |
// we might be able to make some assumptions and use them to filter. | |
// - To add to our example, lets say our system stores info about k8s. | |
// Our user has a kubernetes deployment in our system called `goof-troop` | |
// and an SCM project in Snyk called k8s-goof. | |
// - The ultimate need is to match these two. | |
// - We may be able to compare the Image IDs if our system has that information, or perhaps use something | |
// like k8s tags, etc if we have info on the environment. Either way, we need to match. | |
// | |
// @see: https://snyk.docs.apiary.io/#reference/projects/all-projects/list-all-projects | |
const getProjects = await callSnykApi(access_token, token_type).get(`/org/${data?.orgId}/projects`); | |
let matchedProjectID = '0'; | |
for (let i = 0; i < getProjects.data.projects.length; i++) { | |
// This is where we would loop through all the projects and do some comparison if | |
// there's no better option for matching what's in our system to what's in Snyk. | |
// | |
// Once we match a project, we'll parse the info we care about and store it | |
// in the upper scope. | |
const project = getProjects.data.projects[i]; | |
const projectID: string = project.id; | |
// this could be 'apk', 'maven', 'npm', etc... | |
const projectType: string = project.type; | |
// This would only exist if the project type makes sense for it. | |
const projectImageID: string = typeof project.imageId !== null || false ? project.imageId : null; | |
// There are a few more details available when calling a project directly, if necessary, Snyk's | |
// API could be queried for each project in the loop. | |
// | |
// @see: https://snyk.docs.apiary.io/#reference/projects/individual-project | |
// E.g.: const getProjectDetail = await callSnykApi(access_token, token_type).get(`/org/${data?.orgId}/project/${projectID}`); | |
// ========== | |
// This is the black hole. | |
// Everything depends on what information we have. | |
(() => { console.log('matching magic')})() | |
// ========== | |
// Let's assume the filtering is done and we've found a match. | |
// We'll just pick one for now. | |
if (i === 23) { | |
matchedProjectID = projectID; | |
break; | |
} | |
} | |
console.log('Matched Project ID: ', matchedProjectID); | |
// Get vulnerability info from the project | |
// - Now that we have the Org and the Project, we'll need to get info on the vulnerability, | |
// which we may have some info on in our system, earlier we decided our system had a link | |
// out to Snyk's vuln page, or a page of it's own. If we've got that we can use it | |
// to grab the vuln's name string. | |
// - Snyk vulnerability pages use a URL like: https://snyk.io/vuln/SNYK-GOLANG-K8SIOKUBERNETES-1585630 | |
// - Our vulnerability page mimics that for example purposes. | |
// - We can parse out the end of that string to get the vuln name to use as filter for | |
// querying the Snyk API issue endpoint(s). | |
// - The most appropriate endpoint is likely List All Aggregated Issues | |
// - This endpoint doesn't have a built in request Body filter for "name", so we'd need to loop again. | |
// | |
// @see https://snyk.docs.apiary.io/#reference/projects/aggregated-project-issues/list-all-aggregated-issues | |
// We'll pretend we parsed this from a URL string in our system. Let's find out | |
// whether snyk thinks this project has that issue. | |
const vulnName: string = 'npm:adm-zip:20180415'; | |
const getAggregatedProjectIssues = await callSnykApi(access_token, token_type).post(`/org/${data?.orgId}/project/${matchedProjectID}/aggregated-issues`); | |
let matchedVulnID: string = ''; | |
// Going over the project's issues: | |
for (let i = 0; i < getAggregatedProjectIssues.data.issues.length; i++) { | |
const issue = getAggregatedProjectIssues.data.issues[i]; | |
console.log('Issue: ', issue); | |
// At this point we have a match, and if we wanted to do something in addition | |
// to constructing that Fix URL, like send the user a link to the actual patch | |
// or give them a CVSSv3 score - we could. | |
if (vulnName === issue.id || i === 0) { // Since we're just testing. | |
// Success! | |
// Now push the matched issue ID to the parent scope and exit the loop. | |
matchedVulnID = issue.id; | |
break; | |
} | |
} | |
// Finally, we can construct the fix URL string: | |
const fixLink = `https://app.snyk.io/org/${orgName}/fix/${matchedProjectID}?vuln=${matchedVulnID}`; | |
console.log('Fix Link: ', fixLink); | |
// Huzzah! |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment