diff --git a/package.json b/package.json index 7ddeba2752f..6b9d028d0c1 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "snyk-config": "3.1.1", "snyk-cpp-plugin": "2.0.0", "legacy-snyk-docker-plugin": "snyk/snyk-docker-plugin#v3.26.2", + "snyk-docker-plugin": "4.1.1", "snyk-go-plugin": "1.16.2", "snyk-gradle-plugin": "3.9.0", "snyk-module": "3.1.0", diff --git a/src/cli/commands/test/index.ts b/src/cli/commands/test/index.ts index 9c3bc7d481c..e8ceae9c962 100644 --- a/src/cli/commands/test/index.ts +++ b/src/cli/commands/test/index.ts @@ -49,7 +49,7 @@ import { } from './formatters'; import * as utils from './utils'; import { getIacDisplayedOutput, createSarifOutputForIac } from './iac-output'; -import { getEcosystem, testEcosystem } from '../../../lib/ecosystems'; +import { getEcosystemForTest, testEcosystem } from '../../../lib/ecosystems'; import { TestLimitReachedError } from '../../../lib/errors'; import { isMultiProjectScan } from '../../../lib/is-multi-project-scan'; import { createSarifOutputForContainers } from './sarif-output'; @@ -115,7 +115,7 @@ async function test(...args: MethodArgs): Promise { } } - const ecosystem = getEcosystem(options); + const ecosystem = getEcosystemForTest(options); if (ecosystem) { try { const commandResult = await testEcosystem( diff --git a/src/lib/ecosystems.ts b/src/lib/ecosystems.ts deleted file mode 100644 index 6c238328139..00000000000 --- a/src/lib/ecosystems.ts +++ /dev/null @@ -1,160 +0,0 @@ -import * as cppPlugin from 'snyk-cpp-plugin'; -import { DepGraphData } from '@snyk/dep-graph'; -import * as snyk from './index'; -import * as config from './config'; -import { isCI } from './is-ci'; -import { makeRequest } from './request/promise'; -import { Options } from './types'; -import { TestCommandResult } from '../cli/commands/types'; -import * as spinner from '../lib/spinner'; - -export interface PluginResponse { - scanResults: ScanResult[]; -} - -export interface GitTarget { - remoteUrl: string; - branch: string; -} - -export interface ContainerTarget { - image: string; -} - -export interface ScanResult { - identity: Identity; - facts: Facts[]; - name?: string; - policy?: string; - target?: GitTarget | ContainerTarget; -} - -export interface Identity { - type: string; - targetFile?: string; - args?: { [key: string]: string }; -} - -export interface Facts { - type: string; - data: any; -} - -export interface Issue { - pkgName: string; - pkgVersion?: string; - issueId: string; - fixInfo: { - nearestFixedInVersion?: string; - }; -} - -export interface IssuesData { - [issueId: string]: { - id: string; - severity: string; - title: string; - }; -} - -export interface TestResult { - issues: Issue[]; - issuesData: IssuesData; - depGraphData: DepGraphData; -} - -export interface EcosystemPlugin { - scan: (options: Options) => Promise; - display: ( - scanResults: ScanResult[], - testResults: TestResult[], - errors: string[], - options: Options, - ) => Promise; -} - -export type Ecosystem = 'cpp'; - -const EcosystemPlugins: { - readonly [ecosystem in Ecosystem]: EcosystemPlugin; -} = { - cpp: cppPlugin, -}; - -export function getPlugin(ecosystem: Ecosystem): EcosystemPlugin { - return EcosystemPlugins[ecosystem]; -} - -export function getEcosystem(options: Options): Ecosystem | null { - if (options.source) { - return 'cpp'; - } - return null; -} - -export async function testEcosystem( - ecosystem: Ecosystem, - paths: string[], - options: Options, -): Promise { - const plugin = getPlugin(ecosystem); - const scanResultsByPath: { [dir: string]: ScanResult[] } = {}; - for (const path of paths) { - options.path = path; - const pluginResponse = await plugin.scan(options); - scanResultsByPath[path] = pluginResponse.scanResults; - } - const [testResults, errors] = await testDependencies(scanResultsByPath); - const stringifiedData = JSON.stringify(testResults, null, 2); - if (options.json) { - return TestCommandResult.createJsonTestCommandResult(stringifiedData); - } - const emptyResults: ScanResult[] = []; - const scanResults = emptyResults.concat(...Object.values(scanResultsByPath)); - const readableResult = await plugin.display( - scanResults, - testResults, - errors, - options, - ); - - return TestCommandResult.createHumanReadableTestCommandResult( - readableResult, - stringifiedData, - ); -} - -export async function testDependencies(scans: { - [dir: string]: ScanResult[]; -}): Promise<[TestResult[], string[]]> { - const results: TestResult[] = []; - const errors: string[] = []; - for (const [path, scanResults] of Object.entries(scans)) { - await spinner(`Testing dependencies in ${path}`); - for (const scanResult of scanResults) { - const payload = { - method: 'POST', - url: `${config.API}/test-dependencies`, - json: true, - headers: { - 'x-is-ci': isCI(), - authorization: 'token ' + snyk.api, - }, - body: { - ...scanResult, - }, - }; - try { - const response = await makeRequest(payload); - results.push(response); - } catch (error) { - if (error.code >= 400 && error.code < 500) { - throw new Error(error.message); - } - errors.push('Could not test dependencies in ' + path); - } - } - } - spinner.clearAll(); - return [results, errors]; -} diff --git a/src/lib/ecosystems/index.ts b/src/lib/ecosystems/index.ts new file mode 100644 index 00000000000..01f5c0603c2 --- /dev/null +++ b/src/lib/ecosystems/index.ts @@ -0,0 +1,32 @@ +import { Options } from '../types'; +import { Ecosystem } from './types'; + +export { testEcosystem } from './test'; +export { getPlugin } from './plugins'; + +/** + * Ecosystems are listed here if you opt in to the new plugin test flow. + * This is a breaking change to the old plugin formats, so only a select few + * plugins currently work with it. + * + * Currently container scanning is not yet ready to work with this flow, + * hence this is in a separate function from getEcosystem(). + */ +export function getEcosystemForTest(options: Options): Ecosystem | null { + if (options.source) { + return 'cpp'; + } + return null; +} + +export function getEcosystem(options: Options): Ecosystem | null { + if (options.source) { + return 'cpp'; + } + + const isDockerDesktopIntegration = options['isDockerUser']; + if (options.docker && !isDockerDesktopIntegration) { + return 'docker'; + } + return null; +} diff --git a/src/lib/ecosystems/plugins.ts b/src/lib/ecosystems/plugins.ts new file mode 100644 index 00000000000..92845800233 --- /dev/null +++ b/src/lib/ecosystems/plugins.ts @@ -0,0 +1,15 @@ +import * as cppPlugin from 'snyk-cpp-plugin'; +import * as dockerPlugin from 'snyk-docker-plugin'; +import { Ecosystem, EcosystemPlugin } from './types'; + +const EcosystemPlugins: { + readonly [ecosystem in Ecosystem]: EcosystemPlugin; +} = { + cpp: cppPlugin, + // TODO: not any + docker: dockerPlugin as any, +}; + +export function getPlugin(ecosystem: Ecosystem): EcosystemPlugin { + return EcosystemPlugins[ecosystem]; +} diff --git a/src/lib/ecosystems/policy.ts b/src/lib/ecosystems/policy.ts new file mode 100644 index 00000000000..386ec9e0db5 --- /dev/null +++ b/src/lib/ecosystems/policy.ts @@ -0,0 +1,33 @@ +import * as path from 'path'; + +import { SupportedPackageManagers } from '../package-managers'; +import { findAndLoadPolicy } from '../policy'; +import { Options, PolicyOptions } from '../types'; +import { ScanResult } from './types'; + +export async function findAndLoadPolicyForScanResult( + scanResult: ScanResult, + options: Options & PolicyOptions, +): Promise { + const targetFileRelativePath = scanResult.identity.targetFile + ? path.join(path.resolve(`${options.path}`), scanResult.identity.targetFile) + : undefined; + const targetFileDir = targetFileRelativePath + ? path.parse(targetFileRelativePath).dir + : undefined; + const scanType = options.docker + ? 'docker' + : (scanResult.identity.type as SupportedPackageManagers); + // TODO: fix this and send only send when we used resolve-deps for node + // it should be a ExpandedPkgTree type instead + const packageExpanded = undefined; + + const policy = (await findAndLoadPolicy( + options.path, + scanType, + options, + packageExpanded, + targetFileDir, + )) as object | undefined; // TODO: findAndLoadPolicy() does not return a string! + return policy; +} diff --git a/src/lib/ecosystems/test.ts b/src/lib/ecosystems/test.ts new file mode 100644 index 00000000000..16a46d28afa --- /dev/null +++ b/src/lib/ecosystems/test.ts @@ -0,0 +1,89 @@ +import * as snyk from '../index'; +import * as config from '../config'; +import { isCI } from '../is-ci'; +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 { getPlugin } from './plugins'; +import { TestDependenciesResponse } from '../snyk-test/legacy'; +import { assembleQueryString } from '../snyk-test/common'; + +export async function testEcosystem( + ecosystem: Ecosystem, + paths: string[], + options: Options, +): Promise { + const plugin = getPlugin(ecosystem); + const scanResultsByPath: { [dir: string]: ScanResult[] } = {}; + for (const path of paths) { + options.path = path; + const pluginResponse = await plugin.scan(options); + scanResultsByPath[path] = pluginResponse.scanResults; + } + const [testResults, errors] = await testDependencies( + scanResultsByPath, + options, + ); + const stringifiedData = JSON.stringify(testResults, null, 2); + if (options.json) { + return TestCommandResult.createJsonTestCommandResult(stringifiedData); + } + const emptyResults: ScanResult[] = []; + const scanResults = emptyResults.concat(...Object.values(scanResultsByPath)); + const readableResult = await plugin.display( + scanResults, + testResults, + errors, + options, + ); + + return TestCommandResult.createHumanReadableTestCommandResult( + readableResult, + stringifiedData, + ); +} + +async function testDependencies( + scans: { + [dir: string]: ScanResult[]; + }, + options: Options, +): Promise<[TestResult[], string[]]> { + const results: TestResult[] = []; + const errors: string[] = []; + for (const [path, scanResults] of Object.entries(scans)) { + await spinner(`Testing dependencies in ${path}`); + for (const scanResult of scanResults) { + const payload = { + method: 'POST', + url: `${config.API}/test-dependencies`, + json: true, + headers: { + 'x-is-ci': isCI(), + authorization: 'token ' + snyk.api, + }, + body: { + scanResult, + }, + qs: assembleQueryString(options), + }; + try { + const response = await makeRequest(payload); + results.push({ + issues: response.result.issues, + issuesData: response.result.issuesData, + depGraphData: response.result.depGraphData, + }); + } catch (error) { + if (error.code >= 400 && error.code < 500) { + throw new Error(error.message); + } + errors.push('Could not test dependencies in ' + path); + } + } + } + spinner.clearAll(); + return [results, errors]; +} diff --git a/src/lib/ecosystems/types.ts b/src/lib/ecosystems/types.ts new file mode 100644 index 00000000000..9dcd9e0dae9 --- /dev/null +++ b/src/lib/ecosystems/types.ts @@ -0,0 +1,84 @@ +import { DepGraphData } from '@snyk/dep-graph'; +import { Options } from '../types'; + +export type Ecosystem = 'cpp' | 'docker'; + +export interface PluginResponse { + scanResults: ScanResult[]; +} + +export interface GitTarget { + remoteUrl: string; + branch: string; +} + +export interface ContainerTarget { + image: string; +} + +export interface ScanResult { + identity: Identity; + facts: Facts[]; + name?: string; + policy?: string; + target?: GitTarget | ContainerTarget; +} + +export interface Identity { + type: string; + targetFile?: string; + args?: { [key: string]: string }; +} + +export interface Facts { + type: string; + data: any; +} + +interface UpgradePathItem { + name: string; + version: string; + newVersion?: string; + isDropped?: boolean; +} + +interface UpgradePath { + path: UpgradePathItem[]; +} + +interface FixInfo { + upgradePaths: UpgradePath[]; + isPatchable: boolean; + nearestFixedInVersion?: string; +} + +export interface Issue { + pkgName: string; + pkgVersion?: string; + issueId: string; + fixInfo: FixInfo; +} + +export interface IssuesData { + [issueId: string]: { + id: string; + severity: string; + title: string; + }; +} + +export interface TestResult { + issues: Issue[]; + issuesData: IssuesData; + depGraphData: DepGraphData; +} + +export interface EcosystemPlugin { + scan: (options: Options) => Promise; + display: ( + scanResults: ScanResult[], + testResults: TestResult[], + errors: string[], + options: Options, + ) => Promise; +} diff --git a/src/lib/print-deps.ts b/src/lib/print-deps.ts index f86cedbe3a0..7aa63fbbf24 100644 --- a/src/lib/print-deps.ts +++ b/src/lib/print-deps.ts @@ -62,7 +62,11 @@ function printDepsForTree(depDict: DepDict, prefix = '') { branch = '└─ '; } console.log( - prefix + (prefix ? branch : '') + dep.name + ' @ ' + dep.version, + prefix + + (prefix ? branch : '') + + dep.name + + ' @ ' + + (dep.version ? dep.version : ''), ); if (dep.dependencies) { printDepsForTree(dep.dependencies, prefix + (last ? ' ' : '│ ')); diff --git a/src/lib/snyk-test/assemble-payloads.ts b/src/lib/snyk-test/assemble-payloads.ts new file mode 100644 index 00000000000..ee4a28e35eb --- /dev/null +++ b/src/lib/snyk-test/assemble-payloads.ts @@ -0,0 +1,69 @@ +import * as path from 'path'; +import * as snyk from '../'; +import * as config from '../config'; +import { isCI } from '../is-ci'; +import { getPlugin } from '../ecosystems'; +import { Ecosystem } from '../ecosystems/types'; +import { Options, PolicyOptions, TestOptions } from '../types'; +import { Payload } from './types'; +import { assembleQueryString } from './common'; +import spinner = require('../spinner'); +import { findAndLoadPolicyForScanResult } from '../ecosystems/policy'; + +export async function assembleEcosystemPayloads( + ecosystem: Ecosystem, + options: Options & TestOptions & PolicyOptions, +): Promise { + // For --all-projects packageManager is yet undefined here. Use 'all' + let analysisTypeText = 'all dependencies for '; + if (options.docker) { + analysisTypeText = 'container dependencies for '; + } else if (options.iac) { + analysisTypeText = 'Infrastructure as code configurations for '; + } else if (options.packageManager) { + analysisTypeText = options.packageManager + ' dependencies for '; + } + + const spinnerLbl = + 'Analyzing ' + + analysisTypeText + + (path.relative('.', path.join(options.path, options.file || '')) || + path.relative('..', '.') + ' project dir'); + + spinner.clear(spinnerLbl)(); + await spinner(spinnerLbl); + + const plugin = getPlugin(ecosystem); + const pluginResponse = await plugin.scan(options); + + const payloads: Payload[] = []; + + // TODO: This is a temporary workaround until the plugins themselves can read policy files and set names! + for (const scanResult of pluginResponse.scanResults) { + // WARNING! This mutates the payload. Policy logic should be in the plugin. + const policy = await findAndLoadPolicyForScanResult(scanResult, options); + if (policy !== undefined) { + scanResult.policy = policy.toString(); + } + + // WARNING! This mutates the payload. The project name logic should be handled in the plugin. + scanResult.name = + options['project-name'] || config.PROJECT_NAME || scanResult.name; + + payloads.push({ + method: 'POST', + url: `${config.API}/test-dependencies`, + json: true, + headers: { + 'x-is-ci': isCI(), + authorization: 'token ' + snyk.api, + }, + body: { + scanResult, + }, + qs: assembleQueryString(options), + }); + } + + return payloads; +} diff --git a/src/lib/snyk-test/legacy.ts b/src/lib/snyk-test/legacy.ts index 870265d1ff7..31ed187f342 100644 --- a/src/lib/snyk-test/legacy.ts +++ b/src/lib/snyk-test/legacy.ts @@ -201,21 +201,20 @@ interface FixInfo { nearestFixedInVersion?: string; } +export interface AffectedPackages { + [pkgId: string]: { + pkg: Pkg; + issues: { + [issueId: string]: Issue; + }; + }; +} + interface TestDepGraphResult { issuesData: { [issueId: string]: IssueData; }; - affectedPkgs: { - [pkgId: string]: { - pkg: Pkg; - issues: { - [issueId: string]: { - issueId: string; - fixInfo: FixInfo; - }; - }; - }; - }; + affectedPkgs: AffectedPackages; docker: { binariesVulns?: TestDepGraphResult; baseImage?: any; @@ -223,6 +222,27 @@ interface TestDepGraphResult { remediation?: RemediationChanges; } +interface Issue { + pkgName: string; + pkgVersion?: string; + issueId: string; + fixInfo: FixInfo; +} + +interface TestDependenciesResult { + issuesData: { + [issueId: string]: IssueData; + }; + issues: Issue[]; + docker?: { + baseImage: string; + baseImageRemediation: BaseImageRemediation; + binariesVulns: TestDepGraphResult; + }; + remediation?: RemediationChanges; + depGraphData: depGraphLib.DepGraphData; +} + export interface TestDepGraphMeta { isPublic: boolean; isLicensesEnabled: boolean; @@ -242,6 +262,11 @@ export interface TestDepGraphResponse { meta: TestDepGraphMeta; } +export interface TestDependenciesResponse { + result: TestDependenciesResult; + meta: TestDepGraphMeta; +} + export interface Ignores { [path: string]: { paths: string[][]; diff --git a/src/lib/snyk-test/run-test.ts b/src/lib/snyk-test/run-test.ts index 5b06d633315..a430a00fd7a 100644 --- a/src/lib/snyk-test/run-test.ts +++ b/src/lib/snyk-test/run-test.ts @@ -15,6 +15,8 @@ import { TestDepGraphResponse, convertTestDepGraphResultToLegacy, LegacyVulnApiResult, + TestDependenciesResponse, + AffectedPackages, } from './legacy'; import { IacTestResponse } from './iac-test-result'; import { @@ -56,16 +58,159 @@ import { serializeCallGraphWithMetrics } from '../reachable-vulns'; import { validateOptions } from '../options-validator'; import { findAndLoadPolicy } from '../policy'; import { assembleIacLocalPayloads, parseIacTestResult } from './run-iac-test'; -import { Payload, PayloadBody, DepTreeFromResolveDeps } from './types'; +import { + Payload, + PayloadBody, + DepTreeFromResolveDeps, + TestDependenciesRequest, +} from './types'; import { CallGraphError, CallGraph } from '@snyk/cli-interface/legacy/common'; import * as alerts from '../alerts'; import { abridgeErrorMessage } from '../error-format'; import { getDockerToken } from '../api-token'; +import { getEcosystem } from '../ecosystems'; +import { Issue } from '../ecosystems/types'; +import { assembleEcosystemPayloads } from './assemble-payloads'; const debug = debugModule('snyk:run-test'); const ANALYTICS_PAYLOAD_MAX_LENGTH = 1024; +function prepareResponseForParsing( + payload: Payload, + response: TestDependenciesResponse, + options: Options & TestOptions, +): any { + const ecosystem = getEcosystem(options); + return ecosystem + ? prepareEcosystemResponseForParsing(payload, response, options) + : prepareLanguagesResponseForParsing(payload); +} + +function prepareEcosystemResponseForParsing( + payload: Payload, + response: TestDependenciesResponse, + options: Options & TestOptions, +) { + const testDependenciesRequest = payload.body as + | TestDependenciesRequest + | undefined; + const payloadBody = testDependenciesRequest?.scanResult; + const depGraphData: depGraphLib.DepGraphData | undefined = + response?.result?.depGraphData; + const depGraph = + depGraphData !== undefined + ? depGraphLib.createFromJSON(depGraphData) + : undefined; + const dockerfileAnalysisFact = payloadBody?.facts.find( + (fact) => fact.type === 'dockerfileAnalysis', + ); + const dockerfilePackages = dockerfileAnalysisFact?.data?.dockerfilePackages; + const projectName = payloadBody?.name || depGraph?.rootPkg.name; + const packageManager = payloadBody?.identity?.type as SupportedProjectTypes; + const targetFile = payloadBody?.identity?.targetFile || options.file; + return { + depGraph, + dockerfilePackages, + projectName, + targetFile, + pkgManager: packageManager, + displayTargetFile: targetFile, + foundProjectCount: undefined, + payloadPolicy: payloadBody?.policy, + }; +} + +function prepareLanguagesResponseForParsing(payload: Payload) { + const payloadBody = payload.body as PayloadBody | undefined; + const payloadPolicy = payloadBody && payloadBody.policy; + const depGraph = payloadBody && payloadBody.depGraph; + const pkgManager = + depGraph && + depGraph.pkgManager && + (depGraph.pkgManager.name as SupportedProjectTypes); + const targetFile = payloadBody && payloadBody.targetFile; + const projectName = + payloadBody?.projectNameOverride || payloadBody?.originalProjectName; + const foundProjectCount = payloadBody?.foundProjectCount; + const displayTargetFile = payloadBody?.displayTargetFile; + let dockerfilePackages; + if ( + payloadBody && + payloadBody.docker && + payloadBody.docker.dockerfilePackages + ) { + dockerfilePackages = payloadBody.docker.dockerfilePackages; + } + analytics.add('depGraph', !!depGraph); + analytics.add('isDocker', !!(payloadBody && payloadBody.docker)); + return { + depGraph, + payloadPolicy, + pkgManager, + targetFile, + projectName, + foundProjectCount, + displayTargetFile, + dockerfilePackages, + }; +} + +function isTestDependenciesResponse( + response: + | IacTestResponse + | TestDepGraphResponse + | TestDependenciesResponse + | LegacyVulnApiResult, +): response is TestDependenciesResponse { + const assumedTestDependenciesResponse = response as TestDependenciesResponse; + return assumedTestDependenciesResponse?.result?.issues !== undefined; +} + +function convertIssuesToAffectedPkgs( + response: + | IacTestResponse + | TestDepGraphResponse + | TestDependenciesResponse + | LegacyVulnApiResult, +): + | IacTestResponse + | TestDepGraphResponse + | TestDependenciesResponse + | LegacyVulnApiResult { + if (!(response as any).result) { + return response; + } + + if (!isTestDependenciesResponse(response)) { + return response; + } + + response.result['affectedPkgs'] = getAffectedPkgsFromIssues( + response.result.issues, + ); + return response; +} + +function getAffectedPkgsFromIssues(issues: Issue[]): AffectedPackages { + const result: AffectedPackages = {}; + + for (const issue of issues) { + const packageId = `${issue.pkgName}@${issue.pkgVersion || ''}`; + + if (result[packageId] === undefined) { + result[packageId] = { + pkg: { name: issue.pkgName, version: issue.pkgVersion }, + issues: {}, + }; + } + + result[packageId].issues[issue.issueId] = issue; + } + + return result; +} + async function sendAndParseResults( payloads: Payload[], spinnerLbl: string, @@ -91,36 +236,36 @@ async function sendAndParseResults( ); results.push(result); } else { - const payloadBody: PayloadBody = payload.body as PayloadBody; - const payloadPolicy = payloadBody && payloadBody.policy; - const depGraph = payloadBody && payloadBody.depGraph; - const pkgManager = - depGraph && - depGraph.pkgManager && - (depGraph.pkgManager.name as SupportedProjectTypes); - const targetFile = payloadBody && payloadBody.targetFile; - const projectName = - _.get(payload, 'body.projectNameOverride') || - _.get(payload, 'body.originalProjectName'); - const foundProjectCount = _.get(payload, 'body.foundProjectCount'); - const displayTargetFile = _.get(payload, 'body.displayTargetFile'); - let dockerfilePackages; - if ( - payloadBody && - payloadBody.docker && - payloadBody.docker.dockerfilePackages - ) { - dockerfilePackages = payloadBody.docker.dockerfilePackages; + /** TODO: comment why */ + const payloadCopy = Object.assign({}, payload); + const res = await sendTestPayload(payload); + const { + depGraph, + payloadPolicy, + pkgManager, + targetFile, + projectName, + foundProjectCount, + displayTargetFile, + dockerfilePackages, + } = prepareResponseForParsing( + payloadCopy, + res as TestDependenciesResponse, + options, + ); + + const ecosystem = getEcosystem(options); + if (ecosystem && options['print-deps']) { + await spinner.clear(spinnerLbl)(); + await maybePrintDepGraph(options, depGraph); } - analytics.add('depGraph', !!depGraph); - analytics.add('isDocker', !!(payloadBody && payloadBody.docker)); - // Type assertion might be a lie, but we are correcting that below - const res = (await sendTestPayload(payload)) as LegacyVulnApiResult; + + const legacyRes = convertIssuesToAffectedPkgs(res); const result = await parseRes( depGraph, pkgManager, - res, + legacyRes as LegacyVulnApiResult, options, payload, payloadPolicy, @@ -267,8 +412,15 @@ async function parseRes( function sendTestPayload( payload: Payload, -): Promise { - const filesystemPolicy = payload.body && !!payload.body.policy; +): Promise< + | LegacyVulnApiResult + | TestDepGraphResponse + | IacTestResponse + | TestDependenciesResponse +> { + const payloadBody = payload.body as any; + const filesystemPolicy = + payload.body && !!(payloadBody?.policy || payloadBody?.scanResult?.policy); return new Promise((resolve, reject) => { request(payload, (error, res, body) => { if (error) { @@ -322,6 +474,11 @@ function assemblePayloads( isLocal = fs.existsSync(root); } analytics.add('local', isLocal); + + const ecosystem = getEcosystem(options); + if (ecosystem) { + return assembleEcosystemPayloads(ecosystem, options); + } if (isLocal) { return assembleLocalPayloads(root, options); } diff --git a/src/lib/snyk-test/types.ts b/src/lib/snyk-test/types.ts index 23b39c131d6..f4ccb77e663 100644 --- a/src/lib/snyk-test/types.ts +++ b/src/lib/snyk-test/types.ts @@ -1,4 +1,5 @@ import * as depGraphLib from '@snyk/dep-graph'; +import { ScanResult } from '../ecosystems/types'; import { GitTarget, ContainerTarget } from '../project-metadata/types'; import { DepTree } from '../types'; import { IacScan } from './payload-schema'; @@ -18,6 +19,10 @@ export interface PayloadBody { target?: GitTarget | ContainerTarget | null; } +export interface TestDependenciesRequest { + scanResult: ScanResult; +} + export interface DepTreeFromResolveDeps extends DepTree { numDependencies: number; pluck: any; @@ -31,7 +36,7 @@ export interface Payload { 'x-is-ci': boolean; authorization: string; }; - body?: PayloadBody | IacScan; + body?: PayloadBody | IacScan | TestDependenciesRequest; qs?: object | null; modules?: DepTreeFromResolveDeps; } diff --git a/test/ecosystems.spec.ts b/test/ecosystems.spec.ts index 4b15702de76..20fd12c9681 100644 --- a/test/ecosystems.spec.ts +++ b/test/ecosystems.spec.ts @@ -2,6 +2,7 @@ import * as fs from 'fs'; import * as path from 'path'; import * as cppPlugin from 'snyk-cpp-plugin'; import * as ecosystems from '../src/lib/ecosystems'; +import * as ecosystemsTypes from '../src/lib/ecosystems/types'; import * as request from '../src/lib/request/promise'; import { Options } from '../src/lib/types'; import { TestCommandResult } from '../src/cli/commands/types'; @@ -72,7 +73,7 @@ describe('ecosystems', () => { const errorTxt = readFixture('error.txt'); const testResult = readJsonFixture( 'testResults.json', - ) as ecosystems.TestResult; + ) as ecosystemsTypes.TestResult; const stringifyTestResults = JSON.stringify([testResult], null, 2); beforeAll(() => { @@ -90,7 +91,7 @@ describe('ecosystems', () => { it('should return human readable result when no json option given', async () => { const makeRequestSpy = jest .spyOn(request, 'makeRequest') - .mockResolvedValue(testResult); + .mockResolvedValue({ result: testResult }); const expected = TestCommandResult.createHumanReadableTestCommandResult( displayTxt, stringifyTestResults, @@ -100,27 +101,29 @@ describe('ecosystems', () => { }); expect(makeRequestSpy.mock.calls[0][0]).toEqual({ body: { - facts: [ - { - type: 'cpp-fingerprints', - data: [ - { - filePath: 'add.cpp', - hash: '52d1b046047db9ea0c581cafd4c68fe5', - }, - { - filePath: 'add.h', - hash: 'aeca71a6e39f99a24ecf4c088eee9cb8', - }, - { - filePath: 'main.cpp', - hash: 'ad3365b3370ef6b1c3e778f875055f19', - }, - ], + scanResult: { + facts: [ + { + type: 'cpp-fingerprints', + data: [ + { + filePath: 'add.cpp', + hash: '52d1b046047db9ea0c581cafd4c68fe5', + }, + { + filePath: 'add.h', + hash: 'aeca71a6e39f99a24ecf4c088eee9cb8', + }, + { + filePath: 'main.cpp', + hash: 'ad3365b3370ef6b1c3e778f875055f19', + }, + ], + }, + ], + identity: { + type: 'cpp', }, - ], - identity: { - type: 'cpp', }, }, headers: { @@ -130,6 +133,7 @@ describe('ecosystems', () => { json: true, method: 'POST', url: expect.stringContaining('/test-dependencies'), + qs: expect.any(Object), }); expect(actual).toEqual(expected); }); @@ -137,7 +141,7 @@ describe('ecosystems', () => { it('should return json result when json option', async () => { const makeRequestSpy = jest .spyOn(request, 'makeRequest') - .mockResolvedValue(testResult); + .mockResolvedValue({ result: testResult }); const expected = TestCommandResult.createJsonTestCommandResult( stringifyTestResults, ); @@ -147,27 +151,29 @@ describe('ecosystems', () => { }); expect(makeRequestSpy.mock.calls[0][0]).toEqual({ body: { - facts: [ - { - type: 'cpp-fingerprints', - data: [ - { - filePath: 'add.cpp', - hash: '52d1b046047db9ea0c581cafd4c68fe5', - }, - { - filePath: 'add.h', - hash: 'aeca71a6e39f99a24ecf4c088eee9cb8', - }, - { - filePath: 'main.cpp', - hash: 'ad3365b3370ef6b1c3e778f875055f19', - }, - ], + scanResult: { + facts: [ + { + type: 'cpp-fingerprints', + data: [ + { + filePath: 'add.cpp', + hash: '52d1b046047db9ea0c581cafd4c68fe5', + }, + { + filePath: 'add.h', + hash: 'aeca71a6e39f99a24ecf4c088eee9cb8', + }, + { + filePath: 'main.cpp', + hash: 'ad3365b3370ef6b1c3e778f875055f19', + }, + ], + }, + ], + identity: { + type: 'cpp', }, - ], - identity: { - type: 'cpp', }, }, headers: { @@ -177,6 +183,7 @@ describe('ecosystems', () => { json: true, method: 'POST', url: expect.stringContaining('/test-dependencies'), + qs: expect.any(Object), }); expect(actual).toEqual(expected); }); @@ -184,7 +191,7 @@ describe('ecosystems', () => { it('should return fingerprints when debug option is set', async () => { const mock = jest .spyOn(request, 'makeRequest') - .mockResolvedValue(testResult); + .mockResolvedValue({ result: testResult }); const expected = TestCommandResult.createHumanReadableTestCommandResult( debugDisplayTxt, stringifyTestResults,