Skip to content

Commit

Permalink
Merge pull request #188 from snyk/feat/ZEN-668/add-scm-report-test-su…
Browse files Browse the repository at this point in the history
…pport
  • Loading branch information
novalex committed Jul 14, 2023
2 parents 0c019e1 + c1799f1 commit 8e74d19
Show file tree
Hide file tree
Showing 11 changed files with 462 additions and 141 deletions.
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,25 @@ const results = await codeClient.analyzeFolders({
});
```

### Run analysis and report results to platform

```javascript
const results = await codeClient.analyzeFolders({
connection: { baseURL, sessionToken, source },
analysisOptions: {
severity: 1,
},
fileOptions: {
paths: ['/home/user/repo'],
symlinksEnabled: false,
},
reportOptions: {
enabled: true,
projectName: 'example-project',
},
});
```

### Creates a new bundle based on a previously uploaded one

```javascript
Expand All @@ -129,6 +148,21 @@ const results = await codeClient.extendAnalysis({

```

### Run analysis on an existing SCM project and report results to platform

```javascript
const results = await codeClient.analyzeScmProject({
connection: { baseURL, sessionToken, source },
analysisOptions: {
severity: 1,
},
reportOptions: {
projectId: '<Snyk Project UUID>',
commitId: '<Commit SHA to scan>',
},
});
```

### Errors

If there are any errors the result of every call will contain the following:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
"jest": "^26.4.2",
"jest-extended": "^0.8.0",
"jsonschema": "^1.2.11",
"nock": "^13.3.2",
"npm-run-all": "^4.1.5",
"prettier": "^2.1.1",
"ts-jest": "^26.3.0",
Expand Down
54 changes: 46 additions & 8 deletions src/analysis.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
GetAnalysisResponseDto,
AnalysisFailedResponse,
GetAnalysisOptions,
ConnectionOptions,
} from './http';
import { createBundleFromFolders, remoteBundleFactory } from './bundles';
import { reportBundle } from './report';
import { reportBundle, reportScm } from './report';
import { emitter } from './emitter';
import {
AnalysisResult,
Expand All @@ -23,12 +24,27 @@ import {
AnalysisFiles,
Suggestion,
ReportUploadResult,
ScmAnalysis,
} from './interfaces/analysis-result.interface';
import { FileAnalysisOptions, ReportOptions } from './interfaces/analysis-options.interface';
import { AnalysisContext, FileAnalysisOptions, ScmAnalysisOptions } from './interfaces/analysis-options.interface';
import { FileAnalysis } from './interfaces/files.interface';

const sleep = (duration: number) => new Promise(resolve => setTimeout(resolve, duration));

function getConnectionOptions(connectionOptions: ConnectionOptions): ConnectionOptions {
return {
...connectionOptions,
// Ensure requestId is set.
requestId: connectionOptions.requestId ?? uuidv4(),
};
}

function getAnalysisContext(
analysisContext: AnalysisContext['analysisContext'] | undefined,
): AnalysisContext | Record<string, never> {
return analysisContext ? { analysisContext } : {};
}

async function pollAnalysis(
options: GetAnalysisOptions,
): Promise<Result<AnalysisFailedResponse | AnalysisResult, GetAnalysisErrorCodes>> {
Expand Down Expand Up @@ -92,23 +108,27 @@ function normalizeResultFiles(files: AnalysisFiles, baseDir: string): AnalysisFi
}, {});
}

/**
* Perform a file-based analysis.
* Optionally with reporting of results to the platform.
*/
export async function analyzeFolders(options: FileAnalysisOptions): Promise<FileAnalysis | null> {
if (!options.connection.requestId) {
options.connection.requestId = uuidv4();
}
const connectionOptions = getConnectionOptions(options.connection);
const analysisContext = getAnalysisContext(options.analysisContext);

const fileBundle = await createBundleFromFolders({
...options.connection,
...connectionOptions,
...options.fileOptions,
languages: options.languages,
});
if (fileBundle === null) return null;

const config = {
bundleHash: fileBundle.bundleHash,
...options.connection,
...connectionOptions,
...options.analysisOptions,
shard: calcHash(fileBundle.baseDir),
...(options.analysisContext ? { analysisContext: options.analysisContext } : {}),
...analysisContext,
};

let analysisResults: AnalysisResult;
Expand Down Expand Up @@ -330,3 +350,21 @@ export async function extendAnalysis(options: FileAnalysis & { files: string[] }

return { ...options, fileBundle, analysisResults };
}

/**
* Perform an SCM-based analysis for an existing project,
* with reporting of results to the platform.
*/
export async function analyzeScmProject(options: ScmAnalysisOptions): Promise<ScmAnalysis | null> {
const connectionOptions = getConnectionOptions(options.connection);
const analysisContext = getAnalysisContext(options.analysisContext);

const { analysisResult: analysisResults, uploadResult: reportResults } = await reportScm({
...connectionOptions,
...options.analysisOptions,
...options.reportOptions,
...analysisContext,
});

return { analysisResults, reportResults };
}
78 changes: 69 additions & 9 deletions src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import { ErrorCodes, GenericErrorTypes, DEFAULT_ERROR_MESSAGES, MAX_RETRY_ATTEMP
import { BundleFiles, SupportedFiles } from './interfaces/files.interface';
import { AnalysisResult, ReportResult } from './interfaces/analysis-result.interface';
import { FailedResponse, makeRequest, Payload } from './needle';
import { AnalysisOptions, AnalysisContext, ReportOptions } from './interfaces/analysis-options.interface';
import {
AnalysisOptions,
AnalysisContext,
ReportOptions,
ScmReportOptions,
} from './interfaces/analysis-options.interface';

type ResultSuccess<T> = { type: 'success'; value: T };
type ResultError<E> = {
Expand All @@ -32,6 +37,7 @@ export interface ConnectionOptions {
}

// The trick to typecast union type alias
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function isSubsetErrorCode<T>(code: any, messages: { [c: number]: string }): code is T {
if (code in messages) {
return true;
Expand Down Expand Up @@ -98,6 +104,10 @@ export function startSession(options: StartSessionOptions): StartSessionResponse
};
}

export function getVerifyCallbackUrl(authHost: string): string {
return `${authHost}/api/verify/callback`;
}

export type IpFamily = 6 | undefined;
/**
* Dispatches a FORCED IPv6 request to test client's ISP and network capability.
Expand Down Expand Up @@ -403,19 +413,27 @@ const REPORT_ERROR_MESSAGES: { [P in ReportErrorCodes]: string } = {
export interface UploadReportOptions extends GetAnalysisOptions {
report: ReportOptions;
}

export interface ScmUploadReportOptions extends ConnectionOptions, AnalysisOptions, AnalysisContext, ScmReportOptions {}

export interface GetReportOptions extends ConnectionOptions {
reportId: string;
pollId: string;
}

export type InitUploadResponseDto = {
reportId: string;
};

export type InitScmUploadResponseDto = {
testId: string;
};

export type UploadReportResponseDto = ReportResult | AnalysisFailedResponse | AnalysisResponseProgress;

export async function initReport(
options: UploadReportOptions,
): Promise<Result<InitUploadResponseDto, ReportErrorCodes>> {
/**
* Trigger a file-based test with reporting.
*/
export async function initReport(options: UploadReportOptions): Promise<Result<string, ReportErrorCodes>> {
const config: Payload = {
headers: {
...commonHttpHeaders(options),
Expand All @@ -440,16 +458,19 @@ export async function initReport(
};

const res = await makeRequest<InitUploadResponseDto>(config);
if (res.success) return { type: 'success', value: res.body };
if (res.success) return { type: 'success', value: res.body.reportId };
return generateError<ReportErrorCodes>(res.errorCode, REPORT_ERROR_MESSAGES, 'initReport');
}

/**
* Retrieve a file-based test with reporting.
*/
export async function getReport(options: GetReportOptions): Promise<Result<UploadReportResponseDto, ReportErrorCodes>> {
const config: Payload = {
headers: {
...commonHttpHeaders(options),
},
url: `${options.baseURL}/report/${options.reportId}`,
url: `${options.baseURL}/report/${options.pollId}`,
method: 'get',
};

Expand All @@ -458,6 +479,45 @@ export async function getReport(options: GetReportOptions): Promise<Result<Uploa
return generateError<ReportErrorCodes>(res.errorCode, REPORT_ERROR_MESSAGES, 'getReport', res.error?.message);
}

export function getVerifyCallbackUrl(authHost: string): string {
return `${authHost}/api/verify/callback`;
/**
* Trigger an SCM-based test with reporting.
*/
export async function initScmReport(options: ScmUploadReportOptions): Promise<Result<string, ReportErrorCodes>> {
const config: Payload = {
headers: {
...commonHttpHeaders(options),
},
url: `${options.baseURL}/test`,
method: 'post',
body: {
workflowData: {
projectId: options.projectId,
commitHash: options.commitId,
},
...pick(options, ['severity', 'prioritized', 'analysisContext']),
},
};

const res = await makeRequest<InitScmUploadResponseDto>(config);
if (res.success) return { type: 'success', value: res.body.testId };
return generateError<ReportErrorCodes>(res.errorCode, REPORT_ERROR_MESSAGES, 'initReport');
}

/**
* Fetch an SCM-based test with reporting.
*/
export async function getScmReport(
options: GetReportOptions,
): Promise<Result<UploadReportResponseDto, ReportErrorCodes>> {
const config: Payload = {
headers: {
...commonHttpHeaders(options),
},
url: `${options.baseURL}/test/${options.pollId}`,
method: 'get',
};

const res = await makeRequest<UploadReportResponseDto>(config);
if (res.success) return { type: 'success', value: res.body };
return generateError<ReportErrorCodes>(res.errorCode, REPORT_ERROR_MESSAGES, 'getReport', res.error?.message);
}
5 changes: 4 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { analyzeFolders, extendAnalysis } from './analysis';
import { analyzeFolders, extendAnalysis, analyzeScmProject } from './analysis';
import { getSupportedFiles, createBundleFromFolders, createBundleWithCustomFiles } from './bundles';
import { emitter } from './emitter';
import { startSession, checkSession, getAnalysis, getIpFamily, IpFamily } from './http';
Expand All @@ -17,11 +17,13 @@ import {
Suggestion,
Marker,
ReportResult,
ScmAnalysis,
} from './interfaces/analysis-result.interface';

export {
getGlobPatterns,
analyzeFolders,
analyzeScmProject,
getSupportedFiles,
createBundleFromFolders,
createBundleWithCustomFiles,
Expand All @@ -46,4 +48,5 @@ export {
IpFamily,
AnalysisContext,
ReportResult,
ScmAnalysis,
};
11 changes: 11 additions & 0 deletions src/interfaces/analysis-options.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,21 @@ export interface ReportOptions {
remoteRepoUrl?: string;
}

export interface ScmReportOptions {
projectId?: string;
commitId?: string;
}

export interface FileAnalysisOptions extends AnalysisContext {
connection: ConnectionOptions;
analysisOptions: AnalysisOptions;
fileOptions: AnalyzeFoldersOptions;
reportOptions?: ReportOptions;
languages?: string[];
}

export interface ScmAnalysisOptions extends AnalysisContext {
connection: ConnectionOptions;
analysisOptions: AnalysisOptions;
reportOptions: ScmReportOptions;
}
5 changes: 5 additions & 0 deletions src/interfaces/analysis-result.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,3 +99,8 @@ export interface ReportResult {
}

export type AnalysisResult = AnalysisResultSarif | AnalysisResultLegacy;

export interface ScmAnalysis {
analysisResults: AnalysisResultSarif;
reportResults?: ReportUploadResult;
}
Loading

0 comments on commit 8e74d19

Please sign in to comment.