Skip to content
This repository has been archived by the owner on Jan 16, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1 from quantum-sec/feature/VAPT-72
Browse files Browse the repository at this point in the history
VAPT-72: Add support for trivy
  • Loading branch information
zenetmi authored Aug 16, 2021
2 parents 8457ec8 + 78e4fc1 commit 55d67a4
Show file tree
Hide file tree
Showing 7 changed files with 233 additions and 7 deletions.
15 changes: 9 additions & 6 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
#!/usr/bin/env node

import { CheckovCollector, Logger } from './lib';
import { CheckovCollector, Logger, TrivyCollector } from './lib';
import fs from 'fs';
import { argv } from 'yargs';

const logger = new Logger();

const collectors = {
checkov: CheckovCollector,
trivy: TrivyCollector,
};

/* eslint-disable complexity */
Expand All @@ -23,10 +24,10 @@ function parseOptions(): any {
}

if (!collectors[tool]) {
logger.error(`The specified tool "${ tool }" is not supported.`, true);
logger.error(`The specified tool "${tool}" is not supported.`, true);
logger.info('Specify one of the following supported tools for instrumentation:');
for (const key in collectors) {
logger.info(` • ${ key }`);
logger.info(` • ${key}`);
}
process.exit(1);
}
Expand All @@ -38,7 +39,7 @@ function parseOptions(): any {
}
else {
logger.error('The supplied --path argument does not exist.', true);
logger.info(`${ args.path } could not be found.`);
logger.info(`${args.path} could not be found.`);
process.exit(1);
}
}
Expand Down Expand Up @@ -77,15 +78,17 @@ async function main(): Promise<void> {
const collector = new collectors[options.tool](logger);

console.log();
logger.info(`Running tool "${ options.tool }"...`, true);
logger.info(`Running tool "${options.tool}"...`, true);


const passing = await collector.exec({
cwd: options.path,
quiet: options.quiet,
});


if (!passing && !options.softFail) {
logger.error(`One or more ${ options.tool } checks failed or errored.`);
logger.error(`One or more ${options.tool} checks failed or errored.`);
process.exit(2);
}
}
Expand Down
10 changes: 9 additions & 1 deletion src/lib/analysis-collector-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { v4 as uuid } from 'uuid';
import cp from 'child_process';
import chalk from 'chalk';
import * as axios from 'axios';
import { argv } from 'yargs';

export abstract class AnalysisCollectorBase {

Expand All @@ -18,6 +19,8 @@ export abstract class AnalysisCollectorBase {

public _request = axios.default;

public _argv = argv;

public constructor(public toolId: string, public logger: Logger) {
this.traceId = uuid();
this.timestamp = new Date();
Expand Down Expand Up @@ -46,7 +49,12 @@ export abstract class AnalysisCollectorBase {
await this.postResults(results, finalOptions);
}

return !results.filter((r) => r.checkResult === CheckResult.FAIL || r.checkResult === CheckResult.ERRORED);
const failingStatuses = [
CheckResult.FAIL,
CheckResult.ERRORED,
];

return !results.some((r) => failingStatuses.includes(r.checkResult));
}

public detectApiToken(): void {
Expand Down
1 change: 1 addition & 0 deletions src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@ export { IResult } from './result.interface';
export { ResultsParserDelegate } from './results-parser-delegate';

export * from './checkov';
export * from './trivy';
export * from './utils';
19 changes: 19 additions & 0 deletions src/lib/result.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,4 +67,23 @@ export interface IResult {
* The raw response from the queried API.
*/
responseBody?: string;

// VULNERABILITY BASED RESULT FIELDS
// The following optional fields only apply to tools that return vulnerabilities.

/**
* The raw response from the queried API.
*/
vulnerabilityId?: string;

/**
* The name of the package in which the vulnerability was found.
*/
packageName?: string;

/**
* The version of the package in which the vulnerability was found.
*/
packageVersion?: string;

}
1 change: 1 addition & 0 deletions src/lib/trivy/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { TrivyCollector } from './trivy-collector';
120 changes: 120 additions & 0 deletions src/lib/trivy/trivy-collector.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { Logger } from '../utils';
import { createLoggerFixture } from '../test/logger.fixture';
import { TrivyCollector } from './trivy-collector';
import { CheckResult } from '../check-result';

describe('TrivyCollector', () => {

let collector: TrivyCollector;
let logger: Logger;

beforeEach(() => {
logger = createLoggerFixture();
collector = new TrivyCollector(logger);

spyOn(collector, 'spawn').and.returnValue(new Promise((resolve) => {
resolve('TEST_OUTPUT');
}));
});

describe('constructor()', () => {
it('should set the tool ID', () => {
expect(collector.toolId).toEqual('trivy');
});
});

describe('getToolVersion()', () => {
it('should call trivy with the --version flag', async () => {
const result = await collector.getToolVersion({});
expect(result).toEqual('TEST_OUTPUT');
expect(collector.spawn).toHaveBeenCalledTimes(1);
expect(collector.spawn).toHaveBeenCalledWith('trivy', ['--version'], {});
});
});

describe('getResults()', () => {
it('should call trivy with preset options', async () => {
collector._argv = {
'image-name': 'TEST_IMAGE',
} as any;
spyOn(collector, 'parseResults').and.returnValue('TEST_RESULTS' as any);
const result = await collector.getResults({});
expect(result).toEqual('TEST_RESULTS' as any);

const expectedArgs = [
'--quiet',
'image',
'--security-checks',
'vuln,config',
'--exit-code',
'0',
'-f',
'json',
'--light',
'TEST_IMAGE',
];
expect(collector.spawn).toHaveBeenCalledTimes(1);
expect(collector.spawn).toHaveBeenCalledWith('trivy', expectedArgs, {});

expect(collector.parseResults).toHaveBeenCalledTimes(1);
expect(collector.parseResults).toHaveBeenCalledWith('TEST_OUTPUT');
});
it('should error when image name is not specified', async () => {
await expectAsync(collector.getResults({}))
.toBeRejectedWith(new Error('You must specify an --image-name argument.'));
});

});

describe('parseResults()', () => {

let raw: string;

beforeEach(()=> {
/* eslint-disable @typescript-eslint/naming-convention */
raw = JSON.stringify([{
Vulnerabilities: [{
VulnerabilityID: 'TEST_VULNERABILITY_ID',
PkgName: 'TEST_PACKAGE_NAME',
InstalledVersion: 'TEST_INSTALLED_VERSION',
Layer: {
Digest: 'TEST_DIGEST',
},
}],
}]);
/* eslint-enable @typescript-eslint/naming-convention */
});

it('should report passing when the result contains no vulnerabilities', () => {
raw = JSON.stringify([{}]);
const result = collector.parseResults(raw)[0];
expect(result.checkResult).toEqual(CheckResult.PASS);
});

it('should report failing when no vulnerabilities were found', () => {
const result = collector.parseResults(raw)[0];
expect(result.checkResult).toEqual(CheckResult.FAIL);
});

it('should set the checkname to the vulnerabilityID', () => {
const result = collector.parseResults(raw)[0];
expect(result.checkName).toEqual('TEST_VULNERABILITY_ID');
});

it('should set the packageName to PkgName', () => {
const result = collector.parseResults(raw)[0];
expect(result.packageName).toEqual('TEST_PACKAGE_NAME');
});

it('should set the packageVersion to InstalledVersion', () => {
const result = collector.parseResults(raw)[0];
expect(result.packageVersion).toEqual('TEST_INSTALLED_VERSION');
});

it('should set the resourceId to Layer.Digest', () => {
const result = collector.parseResults(raw)[0];
expect(result.resourceId).toEqual('TEST_DIGEST');
});
});

});
74 changes: 74 additions & 0 deletions src/lib/trivy/trivy-collector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { AnalysisCollectorBase } from '../analysis-collector-base';
import { CheckResult } from '../check-result';
import { IResult } from '../result.interface';
import { Logger } from '../utils';

export class TrivyCollector extends AnalysisCollectorBase {

public constructor(logger: Logger) {
super('trivy', logger);
}

public override async getToolVersion(options: any): Promise<string> {
const args = ['--version'];
return await this.spawn('trivy', args, options);
}

public override async getResults(options: any): Promise<IResult[]> {
const imageName = this._argv['image-name'];
if (!imageName) {
throw new Error('You must specify an --image-name argument.');
}

const args = ['--quiet', 'image', '--security-checks', 'vuln,config', '--exit-code', '0', '-f', 'json', '--light', imageName];
const output = await this.spawn('trivy', args, options);


this.logger.debug(JSON.stringify(output, null, 2));


return this.parseResults(output);
}

public parseResults(output: string): IResult[] {
const parsed = JSON.parse(output);

const results = [];

for (const set of parsed) {
if (!set.Vulnerabilities) {
const result: IResult = {
checkId: 'vulnerabilities',
checkName: 'No vulnerabilities found.',
checkType: 'container-layer',
checkResult: CheckResult.PASS,
resourceId: set.Target,
vulnerabilityId: set.VulnerabilityID,
packageName: set.PkgName,
packageVersion: set.InstalledVersion,
};

results.push(result);
continue;
}

results.push(...set.Vulnerabilities.map((v) => {
const result: IResult = {
checkId: 'vulnerabilities',
checkName: v.VulnerabilityID,
checkType: 'container-layer',
checkResult: CheckResult.FAIL,
resourceId: v.Layer.Digest,
vulnerabilityId: v.VulnerabilityID,
packageName: v.PkgName,
packageVersion: v.InstalledVersion,
};

return result;
}));

}
return results;
}

}

0 comments on commit 55d67a4

Please sign in to comment.