Skip to content

Commit

Permalink
feat: resolveAndTestFacts w/ async polling(c/c++)
Browse files Browse the repository at this point in the history
Introduces async request for C/C++ with polling, where its Facts are resolved/computed in our platform
  • Loading branch information
anthogez committed Jul 13, 2021
1 parent 5150231 commit 9ed6542
Show file tree
Hide file tree
Showing 9 changed files with 353 additions and 6 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ snyk_report.html
cert.pem
key.pem
help/commands-md
help/commands-txt
help/commands-docs
help/commands-man
**/tsconfig.tsbuildinfo
__outputs__
Expand Down
2 changes: 1 addition & 1 deletion .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ src/cli/commands/test/iac-local-execution/parsers/hcl-to-json/parser.js

# Generated Docs
help/commands-md
help/commands-txt
help/commands-docs
help/commands-man

# Has empty lines for templating convenience
Expand Down
Empty file added help/commands-txt/snyk.txt
Empty file.
2 changes: 1 addition & 1 deletion help/generator/generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ const COMMANDS: Record<string, { optionsFile?: string }> = {

const GENERATED_MARKDOWN_FOLDER = './help/commands-md';
const GENERATED_MAN_FOLDER = './help/commands-man';
const GENERATED_TXT_FOLDER = './help/commands-txt';
const GENERATED_TXT_FOLDER = './help/commands-docs';

function execShellCommand(cmd): Promise<string> {
return new Promise((resolve) => {
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ export = async function help(item: string | boolean) {
try {
const filename = path.resolve(
__dirname,
'../../../help/commands-txt',
'../../../help/commands-docs',
item === DEFAULT_HELP ? DEFAULT_HELP + '.txt' : `snyk-${item}.txt`,
);
return readHelpFile(filename);
} catch (error) {
const filename = path.resolve(
__dirname,
'../../../help/commands-txt',
'../../../help/commands-docs',
DEFAULT_HELP + '.txt',
);
return readHelpFile(filename);
Expand Down
120 changes: 120 additions & 0 deletions src/lib/ecosystems/polling.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as config from '../config';
import { isCI } from '../is-ci';
import { makeRequest } from '../request/promise';
import { Options } from '../types';

import { ResolveAndTestFactsResponse } from '../snyk-test/legacy';
import { assembleQueryString } from '../snyk-test/common';
import { getAuthHeader } from '../api-token';
import { ScanResult } from './types';

export async function requestPollingToken(
options: Options,
isAsync: boolean,
scanResult: ScanResult,
): Promise<ResolveAndTestFactsResponse> {
const payload = {
method: 'POST',
url: `${config.API}/test-dependencies`,
json: true,
headers: {
'x-is-ci': isCI(),
authorization: getAuthHeader(),
},
body: {
isAsync,
scanResult,
},
qs: assembleQueryString(options),
};
return await makeRequest<ResolveAndTestFactsResponse>(payload);
}

export async function pollingWithTokenUntilDone(
token: string,
type: string,
options: Options,
pollInterval: number,
attemptsCount: number,
maxAttempts = Infinity,
): Promise<ResolveAndTestFactsResponse> {
const payload = {
method: 'GET',
url: `${config.API}/test-dependencies/${token}`,
json: true,
headers: {
'x-is-ci': isCI(),
authorization: getAuthHeader(),
},
qs: { ...assembleQueryString(options), type },
};

const response = await makeRequest<ResolveAndTestFactsResponse>(payload);

if (response.result && response.meta) {
return response;
}

if (pollingRequestHasFailed(response)) {
throw new Error('polling request has failed.');
}

//TODO: (@snyk/tundra): move to backend
checkPollingAttempts(maxAttempts)(attemptsCount);

return await retryWithPollInterval(
token,
type,
options,
pollInterval,
attemptsCount,
maxAttempts,
);
}

async function retryWithPollInterval(
token: string,
type: string,
options: Options,
pollInterval: number,
attemptsCount: number,
maxAttempts: number,
): Promise<ResolveAndTestFactsResponse> {
return new Promise((resolve, reject) =>
setTimeout(async () => {
const res = await pollingWithTokenUntilDone(
token,
type,
options,
pollInterval,
attemptsCount,
maxAttempts,
);

if (pollingRequestHasFailed(res)) {
return reject(res);
}

if (res.result && res.meta) {
return resolve(res);
}
}, pollInterval),
);
}

function checkPollingAttempts(maxAttempts: number) {
return (attemptsCount: number) => {
if (attemptsCount > maxAttempts) {
throw new Error('Exceeded Polling maxAttempts');
}
};
}
function pollingRequestHasFailed(
response: ResolveAndTestFactsResponse,
): boolean {
const { token, result, meta, status, error, code, message } = response;
const hasError = !!error && !!code && !!message;
const pollingContextIsMissing = !token && !result && !meta && !status;

return !!pollingContextIsMissing || hasError;
}
95 changes: 94 additions & 1 deletion src/lib/ecosystems/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,12 @@ import { makeRequest } from '../request/promise';
import { Options } from '../types';
import { TestCommandResult } from '../../cli/commands/types';
import * as spinner from '../../lib/spinner';
import { Ecosystem, ScanResult, TestResult } from './types';
import { Ecosystem, EcosystemPlugin, ScanResult, TestResult } from './types';
import { getPlugin } from './plugins';
import { TestDependenciesResponse } from '../snyk-test/legacy';
import { assembleQueryString } from '../snyk-test/common';
import { getAuthHeader } from '../api-token';
import { requestPollingToken, pollingWithTokenUntilDone } from './polling';

export async function testEcosystem(
ecosystem: Ecosystem,
Expand All @@ -30,10 +31,43 @@ export async function testEcosystem(
scanResultsByPath[path] = pluginResponse.scanResults;
}
spinner.clearAll();

if (ecosystem === 'cpp') {
const [testResults, errors] = await resolveAndTestFacts(
ecosystem,
scanResultsByPath,
options,
);
return await getTestResultsOutput(
errors,
options,
testResults,
plugin,
scanResultsByPath,
);
}

const [testResults, errors] = await testDependencies(
scanResultsByPath,
options,
);

return await getTestResultsOutput(
errors,
options,
testResults,
plugin,
scanResultsByPath,
);
}

async function getTestResultsOutput(
errors: string[],
options: Options,
testResults: TestResult[],
plugin: EcosystemPlugin,
scanResultsByPath: { [dir: string]: ScanResult[] },
) {
const stringifiedData = JSON.stringify(testResults, null, 2);
if (options.json) {
return TestCommandResult.createJsonTestCommandResult(stringifiedData);
Expand All @@ -53,6 +87,65 @@ export async function testEcosystem(
);
}

export async function resolveAndTestFacts(
ecosystem: Ecosystem,
scans: {
[dir: string]: ScanResult[];
},
options: Options,
): Promise<[TestResult[], string[]]> {
const results: any[] = [];
const errors: string[] = [];
for (const [path, scanResults] of Object.entries(scans)) {
await spinner(`Resolving and Testing C/C++ fileSignatures in ${path}`);
for (const scanResult of scanResults) {
try {
const res = await requestPollingToken(options, true, scanResult);
if (!res.token && !res.status) {
throw 'Something went wrong, token and status n/a';
}
//TODO (@snyk/tundra): poll interval could be based on amount of fileSignatures to be resolved?
const pollInterval = 30000;
const attemptsCount = 0;
const maxAttempts = 250;

const response = await pollingWithTokenUntilDone(
res.token,
ecosystem,
options,
pollInterval,
attemptsCount,
maxAttempts,
);

if (response.meta && response.result) {
results.push({
issues: response.result.issues,
issuesData: response.result.issuesData,
depGraphData: response.result.depGraphData,
});
}
} catch (error) {
const hasStatusCodeError = error.code >= 400 && error.code < 500;
const pollingMaxAttemptsExceeded =
error.message === 'Exceeded Polling maxAttempts';
if (hasStatusCodeError || pollingMaxAttemptsExceeded) {
throw new Error(error.message);
}

if (error.code >= 500) {
const errorMsg = `Could not test dependencies in ${path} \n${error.message}`;
errors.push(errorMsg);
continue;
}
errors.push(`Could not test dependencies in ${path}`);
}
}
}
spinner.clearAll();
return [results, errors];
}

async function testDependencies(
scans: {
[dir: string]: ScanResult[];
Expand Down
11 changes: 11 additions & 0 deletions src/lib/snyk-test/legacy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,17 @@ export interface TestDependenciesResponse {
meta: TestDepGraphMeta;
}

export type ResolveAndTestFactsStatus = 'CANCELLED' | 'ERROR' | 'PENDING' | 'RUNNING' | 'OK';
export interface ResolveAndTestFactsResponse {
token: string;
result: TestDependenciesResult;
meta: TestDepGraphMeta;
status?: ResolveAndTestFactsStatus;
code?: number;
error?: string;
message?: string;
userMessage?: string;
}
export interface Ignores {
[path: string]: {
paths: string[][];
Expand Down

0 comments on commit 9ed6542

Please sign in to comment.