Skip to content

Commit

Permalink
feat: add snyk code support, as plugin, for test flow
Browse files Browse the repository at this point in the history
* we are using code-clint lib to analyze a project and expecting
to get a sarif typed response.
* creating new formating schema for snyk code scanning
* adding `snyk code` functionality as an internal plugin
* adding `snyk code` behind FF
* adding support for the currect exit code (1) when there
are vulnerabilities.
* we currently have circular import issue. to temporary solve
it in our case, we will dynamicly import a module.
  • Loading branch information
ArturSnyk committed Mar 4, 2021
1 parent 5f59838 commit cba65a3
Show file tree
Hide file tree
Showing 26 changed files with 6,516 additions and 4 deletions.
3 changes: 3 additions & 0 deletions .github/CODEOWNERS
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ help/ @snyk/hammer
src/cli/commands/test/iac-output.ts @snyk/cloudconfig
src/cli/commands/test/iac-local-execution/ @snyk/cloudconfig
src/lib/cloud-config-projects.ts @snyk/cloudconfig
src/lib/plugins/sast/ @snyk/sast-team
src/lib/iac/ @snyk/cloudconfig
src/lib/snyk-test/iac-test-result.ts @snyk/cloudconfig
src/lib/snyk-test/payload-schema.ts @snyk/cloudconfig
Expand All @@ -14,6 +15,8 @@ test/acceptance/cli-test/iac/ @snyk/cloudconfig
test/fixtures/iac/ @snyk/cloudconfig
test/smoke/spec/iac/ @snyk/cloudconfig
test/smoke/.iac-data/ @snyk/cloudconfig
test/fixtures/sast/ @snyk/sast-team
test/snyk-code-test.spec.ts @snyk/sast-team
src/lib/errors/invalid-iac-file.ts @snyk/cloudconfig
src/lib/errors/unsupported-options-iac-error.ts @snyk/cloudconfig
help/commands-docs/iac-examples.md @snyk/cloudconfig
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,5 @@ report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
test/acceptance/workspaces/**/.gradle
test/**/.gradle
.iac-data
!test/smoke/.iac-data
.dccache
!test/smoke/.iac-data
3 changes: 2 additions & 1 deletion config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
"API": "https://snyk.io/api/v1",
"devDeps": false,
"PRUNE_DEPS_THRESHOLD": 40000,
"MAX_PATH_COUNT": 1500000
"MAX_PATH_COUNT": 1500000,
"CODE_CLIENT_PROXY_URL": "http://deeproxy.snyk.io"

This comment has been minimized.

Copy link
@adrukh

adrukh Mar 4, 2021

Contributor

@ArturSnyk maybe https? 🙀

}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@
"dependencies": {
"@open-policy-agent/opa-wasm": "^1.2.0",
"@snyk/cli-interface": "2.11.0",
"@snyk/code-client": "3.1.1",
"@snyk/dep-graph": "1.23.1",
"@snyk/gemfile": "1.2.0",
"@snyk/graphlib": "^2.1.9-patch.3",
Expand Down
6 changes: 5 additions & 1 deletion src/cli/commands/test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,11 @@ async function test(...args: MethodArgs): Promise<TestCommandResult> {
);
return commandResult;
} catch (error) {
throw new Error(error);
if (error instanceof Error) {
throw error;
} else {
throw new Error(error);
}
}
}

Expand Down
8 changes: 8 additions & 0 deletions src/cli/modes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,14 @@ const modes: Record<string, ModeData> = {
config: (args): [] => {
args['iac'] = true;

return args;
},
},
code: {
allowedCommands: ['test'],
config: (args): [] => {
args['code'] = true;

return args;
},
},
Expand Down
1 change: 1 addition & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ interface Config {
timeout: number;
PROJECT_NAME: string;
TOKEN: string;
CODE_CLIENT_PROXY_URL: string;
}

// TODO: fix the types!
Expand Down
3 changes: 3 additions & 0 deletions src/lib/ecosystems/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ export function getEcosystemForTest(options: Options): Ecosystem | null {
if (options.source) {
return 'cpp';
}
if (options.code) {
return 'code';
}
return null;
}

Expand Down
2 changes: 2 additions & 0 deletions src/lib/ecosystems/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as cppPlugin from 'snyk-cpp-plugin';
import * as dockerPlugin from 'snyk-docker-plugin';
import { codePlugin } from '../plugins/sast';
import { Ecosystem, EcosystemPlugin } from './types';

const EcosystemPlugins: {
Expand All @@ -8,6 +9,7 @@ const EcosystemPlugins: {
cpp: cppPlugin as EcosystemPlugin,
// TODO: not any
docker: dockerPlugin as any,
code: codePlugin,
};

export function getPlugin(ecosystem: Ecosystem): EcosystemPlugin {
Expand Down
6 changes: 6 additions & 0 deletions src/lib/ecosystems/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,12 @@ export async function testEcosystem(
options: Options,
): Promise<TestCommandResult> {
const plugin = getPlugin(ecosystem);
// TODO: this is an intermediate step before consolidating ecosystem plugins
// to accept flows that act differently in the testDependencies step
if (plugin.test) {
const { readableResult: res } = await plugin.test(paths, options);
return TestCommandResult.createHumanReadableTestCommandResult(res, '');
}
const scanResultsByPath: { [dir: string]: ScanResult[] } = {};
for (const path of paths) {
await spinner(`Scanning dependencies in ${path}`);
Expand Down
6 changes: 5 additions & 1 deletion src/lib/ecosystems/types.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { DepGraphData } from '@snyk/dep-graph';
import { Options } from '../types';

export type Ecosystem = 'cpp' | 'docker';
export type Ecosystem = 'cpp' | 'docker' | 'code';

export interface PluginResponse {
scanResults: ScanResult[];
Expand Down Expand Up @@ -81,6 +81,10 @@ export interface EcosystemPlugin {
errors: string[],
options: Options,
) => Promise<string>;
test?: (
paths: string[],
options: Options,
) => Promise<{ readableResult: string }>;
}

export interface EcosystemMonitorError {
Expand Down
1 change: 1 addition & 0 deletions src/lib/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export { UnsupportedPackageManagerError } from './unsupported-package-manager-er
export { FailedToRunTestError } from './failed-to-run-test-error';
export { TooManyVulnPaths } from './too-many-vuln-paths';
export { AuthFailedError } from './authentication-failed-error';
export { FeatureNotSupportedForOrgError } from './unsupported-feature-for-org-error';
export { OptionMissingErrorError } from './option-missing-error';
export { ExcludeFlagBadInputError } from './exclude-flag-bad-input';
export { UnsupportedOptionCombinationError } from './unsupported-option-combination-error';
Expand Down
13 changes: 13 additions & 0 deletions src/lib/errors/unsupported-feature-for-org-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CustomError } from './custom-error';

export class FeatureNotSupportedForOrgError extends CustomError {
public readonly org: string;

constructor(org: string, additionalUserHelp = '') {
super(`Unsupported action for org ${org}.`);
this.code = 422;
this.org = org;

this.userMessage = `Feature is not supported for org ${org}. ${additionalUserHelp}`;
}
}
108 changes: 108 additions & 0 deletions src/lib/plugins/sast/analysis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { analyzeFolders, AnalysisSeverity } from '@snyk/code-client';
import { Log, ReportingDescriptor, Result } from 'sarif';
import { SEVERITY } from '../../snyk-test/legacy';
import { api } from '../../api-token';
import * as config from '../../config';
import spinner = require('../../spinner');
import { Options } from '../../types';
import { analysisProgressUpdate } from './utils';
import { FeatureNotSupportedBySnykCodeError } from './errors/unsupported-feature-snyk-code-error';

export async function getCodeAnalysisAndParseResults(
root: string,
options: Options,
): Promise<Log> {
const currentLabel = '';
await spinner.clearAll();
analysisProgressUpdate(currentLabel);
const codeAnalysis = await getCodeAnalysis(root, options);
spinner.clearAll();
return parseSecurityResults(codeAnalysis);
}

async function getCodeAnalysis(root: string, options: Options): Promise<Log> {
const baseURL = config.CODE_CLIENT_PROXY_URL;
const sessionToken = api() || '';

const severity = options.severityThreshold
? severityToAnalysisSeverity(options.severityThreshold)
: AnalysisSeverity.info;
const paths: string[] = [root];
const sarif = true;

const result = await analyzeFolders({
baseURL,
sessionToken,
severity,
paths,
sarif,
});

return result.sarifResults!;
}

function severityToAnalysisSeverity(severity: SEVERITY): AnalysisSeverity {
if (severity === SEVERITY.CRITICAL) {
throw new FeatureNotSupportedBySnykCodeError(SEVERITY.CRITICAL);
}
const severityLevel = {
low: 1,
medium: 2,
high: 3,
};
return severityLevel[severity];
}

function parseSecurityResults(codeAnalysis: Log): Log {
let securityRulesMap;

const rules = codeAnalysis.runs[0].tool.driver.rules;
const results = codeAnalysis.runs[0].results;

if (rules) {
securityRulesMap = getSecurityRulesMap(rules);
codeAnalysis.runs[0].tool.driver.rules = Object.values(securityRulesMap);
}
if (results && securityRulesMap) {
codeAnalysis.runs[0].results = getSecurityResultsOnly(
results,
Object.keys(securityRulesMap),
);
}

return codeAnalysis;
}

function getSecurityRulesMap(
rules: ReportingDescriptor[],
): { [ruleId: string]: ReportingDescriptor[] } {
const securityRulesMap = rules.reduce((acc, rule) => {
const { id: ruleId, properties } = rule;
const isSecurityRule = properties?.tags?.some(
(tag) => tag.toLowerCase() === 'security',
);
if (isSecurityRule) {
acc[ruleId] = rule;
}
return acc;
}, {});

return securityRulesMap;
}

function getSecurityResultsOnly(
results: Result[],
securityRules: string[],
): Result[] {
const securityResults = results.reduce((acc: Result[], result: Result) => {
const isSecurityResult = securityRules.some(
(securityRule) => securityRule === result?.ruleId,
);
if (isSecurityResult) {
acc.push(result);
}
return acc;
}, []);

return securityResults;
}
13 changes: 13 additions & 0 deletions src/lib/plugins/sast/errors/unsupported-feature-snyk-code-error.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { CustomError } from '../../../errors/custom-error';

export class FeatureNotSupportedBySnykCodeError extends CustomError {
public readonly feature: string;

constructor(feature: string, additionalUserHelp = '') {
super(`Unsupported action for ${feature}.`);
this.code = 422;
this.feature = feature;

this.userMessage = `'${feature}' is not supported for snyk code. ${additionalUserHelp}`;
}
}
Loading

0 comments on commit cba65a3

Please sign in to comment.