diff --git a/src/lib/ecosystems/resolve-test-facts.ts b/src/lib/ecosystems/resolve-test-facts.ts index 8d9f001a1e..91cd787b7c 100644 --- a/src/lib/ecosystems/resolve-test-facts.ts +++ b/src/lib/ecosystems/resolve-test-facts.ts @@ -1,14 +1,36 @@ import { Options, PolicyOptions } from '../types'; import { spinner } from '../../lib/spinner'; -import { Ecosystem, ScanResult, TestResult } from './types'; +import { + Ecosystem, + ScanResult, + TestResult, + FileSignaturesDetails, +} from './types'; +import { + CreateDepGraphResponse, + GetIssuesResponse, + FileHashes, + Attributes, +} from './unmanaged/types'; import { requestTestPollingToken, pollingTestWithTokenUntilDone, + createDepGraph, + getDepGraph, + getIssues, } from '../polling/polling-test'; import { extractAndApplyPluginAnalytics } from './plugin-analytics'; import { findAndLoadPolicy } from '../policy'; import { filterIgnoredIssues } from './policy'; import { IssueData, Issue } from '../snyk-test/legacy'; +import { hasFeatureFlag } from '../feature-flags'; +import { delayNextStep } from '../polling/common'; +import { + convertDepGraph, + convertMapCasing, + convertToCamelCase, + getSelf, +} from './unmanaged/utils'; export async function resolveAndTestFacts( ecosystem: Ecosystem, @@ -16,6 +38,194 @@ export async function resolveAndTestFacts( [dir: string]: ScanResult[]; }, options: Options & PolicyOptions, +): Promise<[TestResult[], string[]]> { + const unmanagedDepsOverride = process.env.USE_UNMANAGED_DEPS; + + const featureFlagEnabled = await hasFeatureFlag( + 'snykNewUnmanagedTest', + options, + ); + + return featureFlagEnabled || unmanagedDepsOverride + ? resolveAndTestFactsUnmanagedDeps(scans, options) + : resolveAndTestFactsRegistry(ecosystem, scans, options); +} + +async function submitHashes( + hashes: FileHashes, + orgId: string, +): Promise { + const response: CreateDepGraphResponse = await createDepGraph(hashes, orgId); + + return response.data.id; +} + +async function pollDepGraph(id: string, orgId: string): Promise { + let attempts = 0; + const maxAttempts = 50; + while (attempts < maxAttempts) { + try { + const response = await getDepGraph(id, orgId); + return response.data.attributes; + } catch (e) { + await delayNextStep(attempts, maxAttempts, 1000); + attempts++; + } + } + + return Promise.reject('Failed to get DepGraph'); +} + +async function fetchIssues( + start_time, + dep_graph_data, + component_details, + orgId: string, +) { + const response: GetIssuesResponse = await getIssues( + { + dep_graph: dep_graph_data, + start_time, + component_details, + }, + orgId, + ); + + const issues = response.data.result.issues.map((issue) => { + const converted = convertToCamelCase(issue); + converted.fixInfo = convertToCamelCase(converted.fixInfo); + return converted; + }); + + const issuesData = convertMapCasing<{ + [issueId: string]: IssueData; + }>(response.data.result.issues_data); + + const depGraphData = convertDepGraph(response.data.result.dep_graph); + + const dependencyCount = response.data.result.dep_graph.graph.nodes.find( + (graphNode) => { + return graphNode.node_id === 'root-node'; + }, + )?.deps?.length; + + const depsFilePaths = response.data.result.deps_file_paths; + + const fileSignaturesDetails = convertMapCasing( + response.data.result.file_signatures_details, + ); + + return { + issues, + issuesData, + depGraphData, + dependencyCount, + depsFilePaths, + fileSignaturesDetails, + }; +} + +export async function resolveAndTestFactsUnmanagedDeps( + scans: { + [dir: string]: ScanResult[]; + }, + options: Options & PolicyOptions, +): Promise<[TestResult[], string[]]> { + const results: any[] = []; + const errors: string[] = []; + const packageManager = 'Unmanaged (C/C++)'; + + let orgId = options.org || ''; + + if (orgId === '') { + const self = await getSelf(); + orgId = self.default_org_context; + } + + for (const [path, scanResults] of Object.entries(scans)) { + await spinner(`Resolving and Testing fileSignatures in ${path}`); + for (const scanResult of scanResults) { + try { + const id = await submitHashes( + { hashes: scanResult?.facts[0]?.data }, + orgId, + ); + + const { + start_time, + dep_graph_data, + component_details, + } = await pollDepGraph(id, orgId); + + const { + issues, + issuesData, + depGraphData, + dependencyCount, + depsFilePaths, + fileSignaturesDetails, + } = await fetchIssues( + start_time, + dep_graph_data, + component_details, + orgId, + ); + + const issuesMap: Map = new Map(); + issues.forEach((i) => { + issuesMap[i.issueId] = i; + }); + + const vulnerabilities: IssueData[] = []; + for (const issuesDataKey in issuesData) { + const issueData = issuesData[issuesDataKey]; + const pkgCoordinate = `${issuesMap[issuesDataKey]?.pkgName}@${issuesMap[issuesDataKey]?.pkgVersion}`; + issueData.from = [pkgCoordinate]; + issueData.name = pkgCoordinate; + issueData.packageManager = packageManager; + vulnerabilities.push(issueData); + } + + const policy = await findAndLoadPolicy(path, 'cpp', options); + + const [issuesFiltered, issuesDataFiltered] = filterIgnoredIssues( + issues, + issuesData, + policy, + ); + + results.push({ + issues: issuesFiltered, + issuesData: issuesDataFiltered, + depGraphData, + depsFilePaths, + fileSignaturesDetails, + vulnerabilities, + path, + dependencyCount, + packageManager, + }); + } catch (error) { + const hasStatusCodeError = error.code >= 400 && error.code <= 500; + if (hasStatusCodeError) { + errors.push(error.message); + continue; + } + const failedPath = path ? `in ${path}` : '.'; + errors.push(`Could not test dependencies ${failedPath}`); + } + } + } + spinner.clearAll(); + return [results, errors]; +} + +export async function resolveAndTestFactsRegistry( + ecosystem: Ecosystem, + scans: { + [dir: string]: ScanResult[]; + }, + options: Options & PolicyOptions, ): Promise<[TestResult[], string[]]> { const results: any[] = []; const errors: string[] = []; diff --git a/src/lib/ecosystems/types.ts b/src/lib/ecosystems/types.ts index 59e8197265..52225795ce 100644 --- a/src/lib/ecosystems/types.ts +++ b/src/lib/ecosystems/types.ts @@ -63,11 +63,11 @@ interface UpgradePathItem { isDropped?: boolean; } -interface UpgradePath { +export interface UpgradePath { path: UpgradePathItem[]; } -interface FixInfo { +export interface FixInfo { upgradePaths: UpgradePath[]; isPatchable: boolean; nearestFixedInVersion?: string; diff --git a/src/lib/ecosystems/unmanaged/types.ts b/src/lib/ecosystems/unmanaged/types.ts new file mode 100644 index 0000000000..410137bedf --- /dev/null +++ b/src/lib/ecosystems/unmanaged/types.ts @@ -0,0 +1,264 @@ +import { SEVERITY } from '../../snyk-test/common'; +import { PkgInfo } from '@snyk/dep-graph'; +import { UpgradePath, DepsFilePaths } from '../types'; +import { SupportedProjectTypes } from '../../types'; + +export interface HashFormat { + format: number; + data: string; +} + +export interface FileHash { + size: number; + path: string; + hashes_ffm: HashFormat[]; +} + +export interface FileHashes { + hashes: FileHash[]; +} + +export interface LocationResponse { + id: string; + location: string; + type: string; +} + +export interface JsonApi { + version: string; +} + +export interface Links { + self: string; +} + +export interface CreateDepGraphResponse { + data: LocationResponse; + jsonapi: JsonApi; + links: Links; +} + +export interface DepOpenApi { + node_id: string; +} + +interface NodeOpenApi { + node_id: string; + pkg_id: string; + deps: DepOpenApi[]; +} +export interface Details { + artifact: string; + version: string; + author: string; + path: string; + id: string; + url: string; + score: string; + filePaths: string[]; +} + +export interface DetailsOpenApi { + artifact: string; + version: string; + author: string; + path: string; + id: string; + url: string; + score: number; + file_paths: string[]; +} + +export interface ComponentDetails { + [key: string]: Details; +} + +export interface ComponentDetailsOpenApi { + [key: string]: DetailsOpenApi; +} + +export interface GraphOpenApi { + root_node_id: string; + nodes: NodeOpenApi[]; +} + +export interface Pkg { + id: string; + info: PkgInfo; +} + +export interface PkgManager { + name: string; +} + +export interface DepGraphDataOpenAPI { + schema_version: string; + pkg_manager: PkgManager; + pkgs: Pkg[]; + graph: GraphOpenApi; +} + +export interface Attributes { + start_time: number; + dep_graph_data: DepGraphDataOpenAPI; + component_details: ComponentDetailsOpenApi; +} + +export interface IssuesRequestDetails { + artifact: string; + version: string; + author: string; + path: string; + id: string; + url: string; + score: number; + file_paths: string[]; +} + +export interface IssuesRequestComponentDetails { + [key: string]: IssuesRequestDetails; +} + +export interface IssuesRequestDep { + nodeId: string; +} + +export interface IssuesRequestDepOpenApi { + node_id: string; +} + +export interface IssuesRequestNode { + nodeId: string; + pkgId: string; + deps: IssuesRequestDep[]; +} + +export interface IssuesRequestNodeOpenApi { + node_id: string; + pkg_id: string; + deps: IssuesRequestDepOpenApi[]; +} + +export interface IssuesRequestGraph { + rootNodeId: string; + nodes: IssuesRequestNodeOpenApi[]; + component_details: ComponentDetails; +} + +export interface IssuesRequestGraphOpenApi { + root_node_id: string; + nodes: IssuesRequestNodeOpenApi[]; + component_details: ComponentDetailsOpenApi; +} + +export interface IssuesRequestDepGraphDataOpenAPI { + schema_version: string; + pkg_manager: PkgManager; + pkgs: Pkg[]; + graph: IssuesRequestGraphOpenApi; +} + +export interface IssuesRequestAttributes { + start_time: number; + dep_graph: IssuesRequestDepGraphDataOpenAPI; + component_details: IssuesRequestComponentDetails; +} + +export interface Data { + id: string; + type: string; + attributes: Attributes; +} + +export interface FileSignaturesDetailsOpenApi { + [pkgKey: string]: { + confidence: number; + file_paths: string[]; + }; +} + +export interface FixInfoOpenApi { + upgrade_paths: UpgradePath[]; + is_patchable: boolean; + nearest_fixed_in_version?: string; +} + +export interface IssueOpenApi { + pkg_name: string; + pkg_version?: string; + issue_id: string; + fix_info: FixInfoOpenApi; +} + +export interface IssuesDataOpenApi { + [issueId: string]: IssueDataOpenApi; +} + +export interface GetDepGraphResponse { + data: Data; + jsonapi: JsonApi; + links: Links; +} + +export interface IssuesResponseDataResult { + start_time: string; + issues: IssueOpenApi[]; + issues_data: IssuesDataOpenApi; + dep_graph: DepGraphDataOpenAPI; + deps_file_paths: DepsFilePaths; + file_signatures_details: FileSignaturesDetailsOpenApi; + type: string; +} + +export interface IssuesResponseData { + id: string; + result: IssuesResponseDataResult; +} + +export interface GetIssuesResponse { + jsonapi: JsonApi; + links: Links; + data: IssuesResponseData; +} + +export interface GetDepGraphResponse { + data: Data; + jsonapi: JsonApi; + links: Links; +} + +interface PatchOpenApi { + version: string; + id: string; + urls: string[]; + modification_time: string; +} + +export interface IssueDataOpenApi { + id: string; + package_name: string; + version: string; + module_name?: string; + below: string; // Vulnerable below version + semver: { + vulnerable: string | string[]; + vulnerable_hashes?: string[]; + vulnerable_by_distro?: { + [distro_name_and_version: string]: string[]; + }; + }; + patches: PatchOpenApi[]; + is_new: boolean; + description: string; + title: string; + severity: SEVERITY; + fixed_in: string[]; + legal_instructions?: string; + package_manager?: SupportedProjectTypes; + from?: string[]; + name?: string; + publication_time?: string; + creation_time?: string; + cvsSv3?: string; + credit?: string[]; +} diff --git a/src/lib/ecosystems/unmanaged/utils.ts b/src/lib/ecosystems/unmanaged/utils.ts new file mode 100644 index 0000000000..e0812e9706 --- /dev/null +++ b/src/lib/ecosystems/unmanaged/utils.ts @@ -0,0 +1,80 @@ +import * as camelCase from 'lodash.camelcase'; +import { DepGraphData } from '@snyk/dep-graph'; +import { GraphNode } from '@snyk/dep-graph/dist/core/types'; +import { getAuthHeader } from '../../api-token'; +import { isCI } from '../../is-ci'; +import { makeRequest } from '../../request'; +import config from '../../config'; + +function mapKey(object, iteratee) { + object = Object(object); + const result = {}; + + Object.keys(object).forEach((key) => { + const value = object[key]; + result[iteratee(value, key, object)] = value; + }); + return result; +} + +export function convertToCamelCase(obj: any): T { + return mapKey(obj as Object, (_, key) => camelCase(key)) as any; +} + +export function convertMapCasing(obj: any): T { + const newObj = {} as T; + + for (const [key, data] of Object.entries(obj)) { + newObj[key] = convertToCamelCase(data); + } + return newObj; +} + +export function convertObjectArrayCasing(arr: any[]): T[] { + return arr.map((item) => convertToCamelCase(item)); +} + +export function convertDepGraph(depGraphOpenApi: T) { + const depGraph: DepGraphData = convertToCamelCase(depGraphOpenApi); + depGraph.graph = convertToCamelCase(depGraph.graph); + const nodes: GraphNode[] = depGraph.graph.nodes.map((graphNode) => { + const node: GraphNode = convertToCamelCase(graphNode); + node.deps = node.deps.map((dep) => convertToCamelCase(dep)); + return node; + }); + + depGraph.graph.nodes = nodes; + return depGraph; +} + +export function getSelf() { + return makeSelfRequest().then((res: any) => { + const response = JSON.parse(res.body); + return response?.data?.attributes; + }); +} + +export function makeSelfRequest() { + const payload = { + method: 'GET', + url: `${config.API_REST_URL}/self?version=2022-08-12~experimental`, + json: true, + headers: { + 'x-is-ci': isCI(), + authorization: getAuthHeader(), + }, + }; + + return new Promise((resolve, reject) => { + makeRequest(payload, (error, res, body) => { + if (error) { + return reject(error); + } + + resolve({ + res, + body, + }); + }); + }); +} diff --git a/src/lib/polling/polling-test.ts b/src/lib/polling/polling-test.ts index cd7efe4db6..e98b161128 100644 --- a/src/lib/polling/polling-test.ts +++ b/src/lib/polling/polling-test.ts @@ -6,10 +6,92 @@ import { Options } from '../types'; import { assembleQueryString } from '../snyk-test/common'; import { getAuthHeader } from '../api-token'; import { ScanResult } from '../ecosystems/types'; +import { + CreateDepGraphResponse, + FileHashes, + GetDepGraphResponse, + GetIssuesResponse, + IssuesRequestAttributes, +} from '../ecosystems/unmanaged/types'; import { ResolveAndTestFactsResponse } from './types'; import { delayNextStep, handleProcessingStatus } from './common'; import { TestDependenciesResult } from '../snyk-test/legacy'; +import { sleep } from '../common'; + +export async function getIssues( + issuesRequestAttributes: IssuesRequestAttributes, + orgId: string, +): Promise { + const payload = { + method: 'POST', + url: `${config.API_REST_URL}/orgs/${orgId}/unmanaged_ecosystem/issues?version=2022-06-29~experimental`, + json: true, + headers: { + 'Content-Type': 'application/vnd.api+json', + 'x-is-ci': isCI(), + authorization: getAuthHeader(), + }, + body: issuesRequestAttributes, + }; + + const result = await makeRequest(payload); + return JSON.parse(result.toString()); +} + +export async function getDepGraph( + id: string, + orgId: string, +): Promise { + const payload = { + method: 'GET', + url: `${config.API_REST_URL}/orgs/${orgId}/unmanaged_ecosystem/depgraphs/${id}?version=2022-05-23~experimental`, + json: true, + headers: { + 'Content-Type': 'application/vnd.api+json', + 'x-is-ci': isCI(), + authorization: getAuthHeader(), + }, + }; + + const maxWaitingTimeMs = 30000; + const pollIntervalMs = 5000; + let waitingTimeMs = pollIntervalMs; + let result = {} as GetDepGraphResponse; + while (waitingTimeMs <= maxWaitingTimeMs) { + try { + await sleep(waitingTimeMs); + result = await makeRequest(payload); + break; + } catch (e) { + if (waitingTimeMs < maxWaitingTimeMs) { + waitingTimeMs += pollIntervalMs; + } else { + throw e; + } + } + } + return JSON.parse(result.toString()); +} + +export async function createDepGraph( + hashes: FileHashes, + orgId: string, +): Promise { + const payload = { + method: 'POST', + url: `${config.API_REST_URL}/orgs/${orgId}/unmanaged_ecosystem/depgraphs?version=2022-05-23~experimental`, + json: true, + headers: { + 'Content-Type': 'application/vnd.api+json', + 'x-is-ci': isCI(), + authorization: getAuthHeader(), + }, + body: hashes, + }; + const result = await makeRequest(payload); + return JSON.parse(result.toString()); +} export async function requestTestPollingToken( options: Options, diff --git a/src/lib/snyk-test/legacy.ts b/src/lib/snyk-test/legacy.ts index f53c9f0be1..43450643e3 100644 --- a/src/lib/snyk-test/legacy.ts +++ b/src/lib/snyk-test/legacy.ts @@ -14,7 +14,7 @@ interface Pkg { version?: string; } -interface Patch { +export interface Patch { version: string; id: string; urls: string[]; diff --git a/test/jest/unit/lib/ecosystems/fixtures/create-dep-graph-response.ts b/test/jest/unit/lib/ecosystems/fixtures/create-dep-graph-response.ts new file mode 100644 index 0000000000..3731095763 --- /dev/null +++ b/test/jest/unit/lib/ecosystems/fixtures/create-dep-graph-response.ts @@ -0,0 +1,8 @@ +import {LocationResponse} from "../../../../../../src/lib/ecosystems/unmanaged/types"; + +export const createDepgraphResponse = { + id: '53b2fa4d-3e2a-491e-a73e-c4dafcb0ec6f', + location: + '/rest/unmanaged_ecosystem/depgraphs/53b2fa4d-3e2a-491e-a73e-c4dafcb0ec6f?version=2022-05-23~experimental', + type: 'Location', +} as LocationResponse; \ No newline at end of file diff --git a/test/jest/unit/lib/ecosystems/fixtures/dep-graph-open-api.ts b/test/jest/unit/lib/ecosystems/fixtures/dep-graph-open-api.ts new file mode 100644 index 0000000000..33f1b7fd39 --- /dev/null +++ b/test/jest/unit/lib/ecosystems/fixtures/dep-graph-open-api.ts @@ -0,0 +1,69 @@ +import { + ComponentDetailsOpenApi, + DepGraphDataOpenAPI, + GraphOpenApi, + PkgManager +} from "../../../../../../src/lib/ecosystems/unmanaged/types"; + +const componentDetails: ComponentDetailsOpenApi = { + 'http://cdn.kernel.org/pub/linux/utils/net/iproute2/iproute2-4.2.0.tar.gz@4.2.0': { + artifact: 'iproute2', + version: '4.2.0', + author: 'iproute2_project', + path: 'deps/iproute2-4.2.0', + id: '15292136508eb4f383337eb200000000', + url: + 'http://cdn.kernel.org/pub/linux/utils/net/iproute2/iproute2-4.2.0.tar.gz', + score: 1, + file_paths: [ + 'deps/iproute2-4.2.0/COPYING', + 'deps/iproute2-4.2.0/Makefile', + 'deps/iproute2-4.2.0/README', + 'deps/iproute2-4.2.0/README.decnet', + ], + }, +}; + +const graph: GraphOpenApi = { + root_node_id: 'root-node@0.0.0', + nodes: [ + { + node_id: 'root-node', + pkg_id: 'root-node@0.0.0', + deps: [ + { + node_id: 'https://github.com|nih-at/libzip@1.8.0', + }, + ], + }, + { + node_id: 'https://github.com|nih-at/libzip@1.8.0', + pkg_id: 'https://github.com|nih-at/libzip@1.8.0', + deps: [], + }, + ], +}; + +const pkg_manager: PkgManager = { name: 'cpp' }; + +export const depGraphDataOpenAPI: DepGraphDataOpenAPI = { + schema_version: '1.2.0', + pkg_manager: pkg_manager, + pkgs: [ + { + id: 'root-node@0.0.0', + info: { + name: 'root-node', + version: '0.0.0', + }, + }, + { + id: 'https://github.com|nih-at/libzip@1.8.0', + info: { + name: 'https://github.com|nih-at/libzip', + version: '1.8.0', + }, + }, + ], + graph: graph, +}; diff --git a/test/jest/unit/lib/ecosystems/fixtures/expected-test-result-new-impl.ts b/test/jest/unit/lib/ecosystems/fixtures/expected-test-result-new-impl.ts new file mode 100644 index 0000000000..c417551bae --- /dev/null +++ b/test/jest/unit/lib/ecosystems/fixtures/expected-test-result-new-impl.ts @@ -0,0 +1,153 @@ +import { TestResult } from '../../../../../../src/lib/ecosystems/types'; +import {SEVERITY} from "@snyk/fix/dist/types"; + +const expectedDescription = `## Overview + +Affected versions of this package are vulnerable to Symlink Attack cpio, as used in build 2007.05.10, 2010.07.28, and possibly other versions, allows remote attackers to overwrite arbitrary files via a symlink within an RPM package archive. +## Remediation +There is no fixed version for \`cpio\`. +## References +- [Support.novell.com](http://support.novell.com/security/cve/CVE-2010-4226.html) +`; + +export const expectedTestResult = [ + { + depGraphData: { + graph: { + nodes: [ + { + deps: [ + { + nodeId: 'https://github.com|nih-at/libzip@1.8.0', + }, + ], + nodeId: 'root-node', + pkgId: 'root-node@0.0.0', + }, + { + deps: [], + nodeId: 'https://github.com|nih-at/libzip@1.8.0', + pkgId: 'https://github.com|nih-at/libzip@1.8.0', + }, + ], + rootNodeId: 'root-node@0.0.0', + }, + pkgManager: { + name: 'cpp', + }, + pkgs: [ + { + id: 'root-node@0.0.0', + info: { + name: 'root-node', + version: '0.0.0', + }, + }, + { + id: 'https://github.com|nih-at/libzip@1.8.0', + info: { + name: 'https://github.com|nih-at/libzip', + version: '1.8.0', + }, + }, + ], + schemaVersion: '1.2.0', + }, + dependencyCount: 1, + depsFilePaths: { + "http://github.com/nmoinvaz/minizip/archive/1.1.tar.gz@1.1": [ + "deps/zlib-1.2.11.1/contrib/minizip/Makefile.am", + "deps/zlib-1.2.11.1/contrib/minizip/MiniZip64_Changes.txt", + "deps/zlib-1.2.11.1/contrib/minizip/MiniZip64_info.txt", + "deps/zlib-1.2.11.1/contrib/minizip/configure.ac", + "deps/zlib-1.2.11.1/contrib/minizip/crypt.h", + "deps/zlib-1.2.11.1/contrib/minizip/ioapi.c", + "deps/zlib-1.2.11.1/contrib/minizip/ioapi.h", + "deps/zlib-1.2.11.1/contrib/minizip/iowin32.c", + "deps/zlib-1.2.11.1/contrib/minizip/iowin32.h", + ], + "https://thekelleys.org.uk|dnsmasq@2.80": [ + "deps/dnsmasq-2.80/Android.mk", + "deps/dnsmasq-2.80/CHANGELOG", + "deps/dnsmasq-2.80/CHANGELOG.archive", + "deps/dnsmasq-2.80/COPYING", + "deps/dnsmasq-2.80/COPYING-v3", + "deps/dnsmasq-2.80/FAQ", + "deps/dnsmasq-2.80/Makefile", + ], + }, + "fileSignaturesDetails": { + "https://thekelleys.org.uk|dnsmasq@2.80": { + "confidence": 1, + "filePaths": [ + "deps/dnsmasq-2.80/Android.mk", + "deps/dnsmasq-2.80/CHANGELOG", + "deps/dnsmasq-2.80/CHANGELOG.archive", + "deps/dnsmasq-2.80/COPYING", + "deps/dnsmasq-2.80/COPYING-v3", + "deps/dnsmasq-2.80/FAQ", + "deps/dnsmasq-2.80/Makefile", + "deps/dnsmasq-2.80/VERSION", + ], + }, + }, + issues: [ + { + fixInfo: { + isPatchable: false, + nearestFixedInVersion: '', + upgradePaths: [], + }, + issueId: 'SNYK-UNMANAGED-CPIO-2319543', + pkgName: 'https://ftp.gnu.org|cpio', + pkgVersion: '2.12', + }, + ], + issuesData: { + 'SNYK-UNMANAGED-CPIO-2319543': { + id: 'SNYK-UNMANAGED-CPIO-2319543', + packageName: 'cpio', + version: '', + below: '', + semver: { + "vulnerable": [ + "[0,]" + ] + }, + patches: [], + isNew: false, + description: expectedDescription, + title: 'Symlink Attack', + severity: "medium", + fixedIn: [], + packageManager: "Unmanaged (C/C++)", + from: ['https://ftp.gnu.org|cpio@2.12'], + name: 'https://ftp.gnu.org|cpio@2.12', + }, + }, + packageManager: 'Unmanaged (C/C++)', + path: 'path', + vulnerabilities: [ + { + below: '', + description: expectedDescription, + fixedIn: [], + from: ['https://ftp.gnu.org|cpio@2.12'], + id: 'SNYK-UNMANAGED-CPIO-2319543', + isNew: false, + name: 'https://ftp.gnu.org|cpio@2.12', + packageManager: 'Unmanaged (C/C++)', + packageName: 'cpio', + patches: [], + severity: 'medium', + title: 'Symlink Attack', + version: '', + semver: { + "vulnerable": [ + "[0,]" + ] + }, + }, + ], + }, +]; \ No newline at end of file diff --git a/test/jest/unit/lib/ecosystems/fixtures/get-dep-graph-response.ts b/test/jest/unit/lib/ecosystems/fixtures/get-dep-graph-response.ts new file mode 100644 index 0000000000..38a94bd345 --- /dev/null +++ b/test/jest/unit/lib/ecosystems/fixtures/get-dep-graph-response.ts @@ -0,0 +1,16 @@ +import {Attributes, ComponentDetailsOpenApi, Data} from "../../../../../../src/lib/ecosystems/unmanaged/types"; +import {depGraphDataOpenAPI} from "./dep-graph-open-api"; + +const componentDetailsOpenApi: ComponentDetailsOpenApi = {}; + +const attributes: Attributes = { + start_time: 1660137910316, + dep_graph_data: depGraphDataOpenAPI, + component_details: componentDetailsOpenApi, +}; + +export const getDepGraphResponse: Data = { + id: '1234', + type: 'depgraphs', + attributes: attributes, +} diff --git a/test/jest/unit/lib/ecosystems/fixtures/issues-response.ts b/test/jest/unit/lib/ecosystems/fixtures/issues-response.ts new file mode 100644 index 0000000000..26158f4d0d --- /dev/null +++ b/test/jest/unit/lib/ecosystems/fixtures/issues-response.ts @@ -0,0 +1,95 @@ +import { + FileSignaturesDetailsOpenApi, FixInfoOpenApi, + IssueDataOpenApi, IssueOpenApi, + IssuesDataOpenApi, IssuesResponseData, IssuesResponseDataResult +} from "../../../../../../src/lib/ecosystems/unmanaged/types"; +import {DepsFilePaths} from "snyk-cpp-plugin/dist/types"; +import {SEVERITY} from "../../../../../../src/lib/snyk-test/common"; +import {depGraphDataOpenAPI} from "./dep-graph-open-api"; + +const fileSignaturesDetailsOpenApi: FileSignaturesDetailsOpenApi = { + "https://thekelleys.org.uk|dnsmasq@2.80": { + "confidence": 1, + "file_paths": [ + "deps/dnsmasq-2.80/Android.mk", + "deps/dnsmasq-2.80/CHANGELOG", + "deps/dnsmasq-2.80/CHANGELOG.archive", + "deps/dnsmasq-2.80/COPYING", + "deps/dnsmasq-2.80/COPYING-v3", + "deps/dnsmasq-2.80/FAQ", + "deps/dnsmasq-2.80/Makefile", + "deps/dnsmasq-2.80/VERSION", + ] + } +}; + +const depsFilePaths: DepsFilePaths = { + "https://thekelleys.org.uk|dnsmasq@2.80": [ + "deps/dnsmasq-2.80/Android.mk", + "deps/dnsmasq-2.80/CHANGELOG", + "deps/dnsmasq-2.80/CHANGELOG.archive", + "deps/dnsmasq-2.80/COPYING", + "deps/dnsmasq-2.80/COPYING-v3", + "deps/dnsmasq-2.80/FAQ", + "deps/dnsmasq-2.80/Makefile", + ], + "http://github.com/nmoinvaz/minizip/archive/1.1.tar.gz@1.1": [ + "deps/zlib-1.2.11.1/contrib/minizip/Makefile.am", + "deps/zlib-1.2.11.1/contrib/minizip/MiniZip64_Changes.txt", + "deps/zlib-1.2.11.1/contrib/minizip/MiniZip64_info.txt", + "deps/zlib-1.2.11.1/contrib/minizip/configure.ac", + "deps/zlib-1.2.11.1/contrib/minizip/crypt.h", + "deps/zlib-1.2.11.1/contrib/minizip/ioapi.c", + "deps/zlib-1.2.11.1/contrib/minizip/ioapi.h", + "deps/zlib-1.2.11.1/contrib/minizip/iowin32.c", + "deps/zlib-1.2.11.1/contrib/minizip/iowin32.h", + ] +}; + +const issueDataOpenApi: IssueDataOpenApi = { + id: 'SNYK-UNMANAGED-CPIO-2319543', + package_name: 'cpio', + version: '', + below: '', + semver: { + vulnerable: ['[0,]'], + }, + patches: [], + is_new: false, + description: + '## Overview\n\nAffected versions of this package are vulnerable to Symlink Attack cpio, as used in build 2007.05.10, 2010.07.28, and possibly other versions, allows remote attackers to overwrite arbitrary files via a symlink within an RPM package archive.\n## Remediation\nThere is no fixed version for `cpio`.\n## References\n- [Support.novell.com](http://support.novell.com/security/cve/CVE-2010-4226.html)\n', + title: 'Symlink Attack', + severity: SEVERITY.MEDIUM, + fixed_in: [], + package_manager: 'Unmanaged (C/C++)', + from: [], + name: '', +}; + +const issuesDataOpenApi: IssuesDataOpenApi = { + 'SNYK-UNMANAGED-CPIO-2319543': issueDataOpenApi, +}; + +const fixInfoOpenApi: FixInfoOpenApi = { + upgrade_paths: [], + nearest_fixed_in_version: '', + is_patchable: false, +}; +const issueOpenApi: IssueOpenApi = { + pkg_name: 'https://ftp.gnu.org|cpio', + issue_id: 'SNYK-UNMANAGED-CPIO-2319543', + pkg_version: '2.12', + fix_info: fixInfoOpenApi, +}; +const issuesOpenApi: IssueOpenApi[] = [issueOpenApi]; +const result: IssuesResponseDataResult = { + start_time: '1659598771039', + issues: issuesOpenApi, + issues_data: issuesDataOpenApi, + dep_graph: depGraphDataOpenAPI, + deps_file_paths: depsFilePaths, + file_signatures_details: fileSignaturesDetailsOpenApi, + type: '', +}; + +export const issuesResponseData: IssuesResponseData = { id: '', result: result }; diff --git a/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts b/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts index b93c6afe2d..7314e01553 100644 --- a/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts +++ b/test/jest/unit/lib/ecosystems/resolve-test-facts.spec.ts @@ -1,10 +1,34 @@ import { Options } from '../../../../../src/lib/types'; import * as pollingTest from '../../../../../src/lib/polling/polling-test'; +import * as featureFlags from '../../../../../src/lib/feature-flags/index'; +import * as common from '../../../../../src/lib/polling/common'; import * as promise from '../../../../../src/lib/request/promise'; import { depGraphData, scanResults } from './fixtures/'; import { resolveAndTestFacts } from '../../../../../src/lib/ecosystems/resolve-test-facts'; import * as pluginAnalytics from '../../../../../src/lib/ecosystems/plugin-analytics'; import * as analytics from '../../../../../src/lib/analytics'; +import { + JsonApi, + Links, + LocationResponse, + Data, + Attributes, + DepGraphDataOpenAPI, + ComponentDetailsOpenApi, + PkgManager, + GraphOpenApi, + IssuesResponseData, + IssueOpenApi, + FixInfoOpenApi, + IssuesDataOpenApi, + FileSignaturesDetailsOpenApi, + IssuesResponseDataResult, +} from '../../../../../src/lib/ecosystems/unmanaged/types'; +import { DepsFilePaths } from 'snyk-cpp-plugin/dist/types'; +import { issuesResponseData } from './fixtures/issues-response'; +import { expectedTestResult } from './fixtures/expected-test-result-new-impl'; +import { createDepgraphResponse } from './fixtures/create-dep-graph-response'; +import { getDepGraphResponse } from './fixtures/get-dep-graph-response'; describe('resolve and test facts', () => { afterEach(() => jest.restoreAllMocks()); @@ -18,6 +42,10 @@ describe('resolve and test facts', () => { }; it('failing to resolve and test file-signatures fact for c/c++ projects', async () => { + const hasFeatureFlag: boolean | undefined = false; + const hasFeatureFlagSpy = jest.spyOn(featureFlags, 'hasFeatureFlag'); + hasFeatureFlagSpy.mockResolvedValueOnce(hasFeatureFlag); + const requestTestPollingTokenSpy = jest.spyOn( pollingTest, 'requestTestPollingToken', @@ -27,6 +55,12 @@ describe('resolve and test facts', () => { 'pollingTestWithTokenUntilDone', ); + const createDepGraphSpy = jest.spyOn(pollingTest, 'createDepGraph'); + + const getDepGraphSpy = jest.spyOn(pollingTest, 'getDepGraph'); + + const getIssuesSpy = jest.spyOn(pollingTest, 'getIssues'); + requestTestPollingTokenSpy.mockResolvedValueOnce({ token, status: 'ERROR', @@ -39,6 +73,73 @@ describe('resolve and test facts', () => { 'Internal error (reference: eb9ab16c-1d33-4586-bf99-ef30c144d1f1)', }); + const links: Links = { self: '' }; + const jsonApi: JsonApi = { version: '' }; + const data: LocationResponse = { id: '1234', location: '', type: '' }; + + createDepGraphSpy.mockResolvedValueOnce({ + data: data, + jsonapi: jsonApi, + links: links, + }); + + const graph: GraphOpenApi = { + root_node_id: '', + nodes: [], + }; + const pkg_manager: PkgManager = { name: '' }; + const componentDetailsOpenApi: ComponentDetailsOpenApi = {}; + const depGraphDataOpenAPI: DepGraphDataOpenAPI = { + schema_version: '', + pkg_manager: pkg_manager, + pkgs: [], + graph: graph, + }; + const attributes: Attributes = { + start_time: 0, + dep_graph_data: depGraphDataOpenAPI, + component_details: componentDetailsOpenApi, + }; + const getResponseData: Data = { id: '', type: '', attributes: attributes }; + + getDepGraphSpy.mockResolvedValueOnce({ + data: getResponseData, + jsonapi: jsonApi, + links: links, + }); + + const fileSignaturesDetailsOpenApi: FileSignaturesDetailsOpenApi = {}; + const depsFilePaths: DepsFilePaths = {}; + const issuesDataOpenApi: IssuesDataOpenApi = {}; + const fixInfoOpenApi: FixInfoOpenApi = { + upgrade_paths: [], + nearest_fixed_in_version: '', + is_patchable: false, + }; + const issueOpenApi: IssueOpenApi = { + pkg_name: '', + issue_id: '', + pkg_version: '', + fix_info: fixInfoOpenApi, + }; + const issuesOpenApi: IssueOpenApi[] = [issueOpenApi]; + const result: IssuesResponseDataResult = { + start_time: '', + issues: issuesOpenApi, + issues_data: issuesDataOpenApi, + dep_graph: depGraphDataOpenAPI, + deps_file_paths: depsFilePaths, + file_signatures_details: fileSignaturesDetailsOpenApi, + type: '', + }; + const issuesResponseData: IssuesResponseData = { id: '', result: result }; + + getIssuesSpy.mockResolvedValueOnce({ + data: issuesResponseData, + jsonapi: jsonApi, + links: links, + }); + const [testResults, errors] = await resolveAndTestFacts( 'cpp', scanResults, @@ -58,6 +159,10 @@ describe('resolve and test facts', () => { `( 'should handle different file-signatures processing statuses for the testing flow', async ({ actual, expected }) => { + const hasFeatureFlag: boolean | undefined = false; + const hasFeatureFlagSpy = jest.spyOn(featureFlags, 'hasFeatureFlag'); + hasFeatureFlagSpy.mockResolvedValueOnce(hasFeatureFlag); + const requestTestPollingTokenSpy = jest.spyOn( pollingTest, 'requestTestPollingToken', @@ -88,6 +193,10 @@ describe('resolve and test facts', () => { ); it('successfully resolving and testing file-signatures fact for c/c++ projects', async () => { + const hasFeatureFlag: boolean | undefined = false; + const hasFeatureFlagSpy = jest.spyOn(featureFlags, 'hasFeatureFlag'); + hasFeatureFlagSpy.mockResolvedValueOnce(hasFeatureFlag); + const resolveAndTestFactsSpy = jest.spyOn( pollingTest, 'requestTestPollingToken', @@ -150,4 +259,124 @@ describe('resolve and test facts', () => { ]); expect(errors).toEqual([]); }); + + it('successfully resolving and testing file-signatures fact for c/c++ projects with new unmanaged service', async () => { + const hasFeatureFlag: boolean | undefined = true; + jest + .spyOn(featureFlags, 'hasFeatureFlag') + .mockResolvedValueOnce(hasFeatureFlag); + + jest.spyOn(common, 'delayNextStep').mockImplementation(); + + jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ + data: createDepgraphResponse, + jsonapi: { version: 'v1.0' } as JsonApi, + links: { self: '' } as Links, + }); + + jest.spyOn(pollingTest, 'getDepGraph').mockResolvedValue({ + data: getDepGraphResponse, + jsonapi: { version: 'v1.0' } as JsonApi, + links: { self: '' } as Links, + }); + + jest.spyOn(pollingTest, 'getIssues').mockResolvedValueOnce({ + data: issuesResponseData, + jsonapi: { version: 'v1.0' } as JsonApi, + links: { self: '' } as Links, + }); + + const [testResults, errors] = await resolveAndTestFacts( + 'cpp', + scanResults, + {} as Options, + ); + + expect(testResults).toEqual(expectedTestResult); + expect(errors).toEqual([]); + }); + + it('failed resolving and testing file-signatures since createDepGraph throws exception with new unmanaged service', async () => { + const hasFeatureFlag: boolean | undefined = true; + jest + .spyOn(featureFlags, 'hasFeatureFlag') + .mockResolvedValueOnce(hasFeatureFlag); + + jest.spyOn(common, 'delayNextStep').mockImplementation(); + + jest.spyOn(pollingTest, 'createDepGraph').mockImplementation(() => { + throw new Error('500'); + }); + + const [testResults, errors] = await resolveAndTestFacts( + 'cpp', + scanResults, + {} as Options, + ); + + expect(testResults).toEqual([]); + expect(errors).toEqual(['Could not test dependencies in path']); + }); + + it('failed resolving and testing file-signatures since getDepGraph throws exception with new unmanaged service', async () => { + const hasFeatureFlag: boolean | undefined = true; + jest + .spyOn(featureFlags, 'hasFeatureFlag') + .mockResolvedValueOnce(hasFeatureFlag); + + jest.spyOn(common, 'delayNextStep').mockImplementation(); + + jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ + data: createDepgraphResponse, + jsonapi: { version: 'v1.0' } as JsonApi, + links: { self: '' } as Links, + }); + + jest.spyOn(pollingTest, 'getDepGraph').mockImplementation(() => { + throw new Error('500'); + }); + + const [testResults, errors] = await resolveAndTestFacts( + 'cpp', + scanResults, + {} as Options, + ); + + expect(testResults).toEqual([]); + expect(errors).toEqual(['Could not test dependencies in path']); + }); + + it('failed resolving and testing file-signatures since getIssues throws exception with new unmanaged service', async () => { + const hasFeatureFlag: boolean | undefined = true; + jest + .spyOn(featureFlags, 'hasFeatureFlag') + .mockResolvedValueOnce(hasFeatureFlag); + + jest.spyOn(common, 'delayNextStep').mockImplementation(); + + jest.spyOn(pollingTest, 'createDepGraph').mockResolvedValueOnce({ + data: createDepgraphResponse, + jsonapi: { version: 'v1.0' } as JsonApi, + links: { self: '' } as Links, + }); + + jest.spyOn(pollingTest, 'getDepGraph').mockResolvedValue({ + data: getDepGraphResponse, + jsonapi: { version: 'v1.0' } as JsonApi, + links: { self: '' } as Links, + }); + + jest.spyOn(pollingTest, 'getIssues').mockImplementation(() => { + throw new Error('500'); + }); + + const [testResults, errors] = await resolveAndTestFacts( + 'cpp', + scanResults, + {} as Options, + ); + + expect(testResults).toEqual([]); + expect(errors).toEqual(['Could not test dependencies in path']); + }); });