Skip to content

Instantly share code, notes, and snippets.

@carwin
Last active October 20, 2021 22:25
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save carwin/dd4e5cfb3a3f51e2db1453c727bf89ab to your computer and use it in GitHub Desktop.
Save carwin/dd4e5cfb3a3f51e2db1453c727bf89ab to your computer and use it in GitHub Desktop.
Construct a Snyk Fix URL for project vulnerability.
// 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;
}
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