Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Add support for including specific columns in audit report #100

Merged
merged 3 commits into from
Sep 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,13 +86,14 @@ npm run audit

## Options

| Flag | Short | Description |
| ----------------- | ----- | ----------------------------------------------------------------------------------------------------- |
| `--exclude` | `-x` | Exceptions or the vulnerabilities ID(s) to exclude; the ID can be the numeric ID, CVE, CWE or GHSA ID |
| `--module-ignore` | `-m` | Names of modules to exclude |
| `--level` | `-l` | The minimum audit level to validate; Same as the original `--audit-level` flag |
| `--production` | `-p` | Skip the `devDependencies` |
| `--registry` | `-r` | The npm registry url to use |
| Flag | Short | Description |
| --------------------| ----- | ----------------------------------------------------------------------------------------------------- |
| `--exclude` | `-x` | Exceptions or the vulnerabilities ID(s) to exclude; the ID can be the numeric ID, CVE, CWE or GHSA ID |
| `--module-ignore` | `-m` | Names of modules to exclude |
| `--level` | `-l` | The minimum audit level to validate; Same as the original `--audit-level` flag |
| `--production` | `-p` | Skip the `devDependencies` |
| `--registry` | `-r` | The npm registry url to use |
| `--include-columns` | `-i` | Columns to include in report |

<br />

Expand Down
14 changes: 11 additions & 3 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,16 @@ const program = new Command();
* @param {String} auditCommand The NPM audit command to use (with flags)
* @param {String} auditLevel The level of vulnerabilities we care about
* @param {Array} exceptionIds List of vulnerability IDs to exclude
* @param {Array} modulesToIgnore List of vulnerable modules to ignore in audit results
* @param {Array} modulesToIgnore List of vulnerable modules to ignore in audit results
* @param {Array} columnsToInclude List of columns to include in audit results
*/
export function callback(auditCommand: string, auditLevel: AuditLevel, exceptionIds: string[], modulesToIgnore: string[]): void {
export function callback(
auditCommand: string,
auditLevel: AuditLevel,
exceptionIds: string[],
modulesToIgnore: string[],
columnsToInclude: string[],
): void {
// Increase the default max buffer size (1 MB)
const audit = exec(`${auditCommand} --json`, { maxBuffer: MAX_BUFFER_SIZE });

Expand All @@ -33,7 +40,7 @@ export function callback(auditCommand: string, auditLevel: AuditLevel, exception

// Once the stdout has completed, process the output
if (audit.stderr) {
audit.stderr.on('close', () => handleFinish(jsonBuffer, auditLevel, exceptionIds, modulesToIgnore));
audit.stderr.on('close', () => handleFinish(jsonBuffer, auditLevel, exceptionIds, modulesToIgnore, columnsToInclude));
// stderr
audit.stderr.on('data', console.error);
}
Expand All @@ -49,6 +56,7 @@ program
.option('-l, --level <auditLevel>', 'The minimum audit level to validate.')
.option('-p, --production', 'Skip checking the devDependencies.')
.option('-r, --registry <url>', 'The npm registry url to use.')
.option('-i, --include-columns <columnName1>,<columnName2>,..,<columnNameN>', 'Columns to include in report.')
.action((options: CommandOptions) => handleInput(options, callback));

program.parse(process.argv);
1 change: 0 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 10 additions & 2 deletions src/handlers/handleFinish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,21 @@ import { processAuditJson, handleUnusedExceptions } from '../utils/vulnerability
* @param {Number} auditLevel The level of vulnerabilities we care about
* @param {Array} exceptionIds List of vulnerability IDs to exclude
* @param {Array} exceptionModules List of vulnerable modules to ignore in audit results
* @param {Array} columnsToInclude List of columns to include in audit results
*/
export default function handleFinish(jsonBuffer: string, auditLevel: AuditLevel, exceptionIds: string[], exceptionModules: string[]): void {
export default function handleFinish(
jsonBuffer: string,
auditLevel: AuditLevel,
exceptionIds: string[],
exceptionModules: string[],
columnsToInclude: string[],
): void {
const { unhandledIds, report, failed, unusedExceptionIds, unusedExceptionModules } = processAuditJson(
jsonBuffer,
auditLevel,
exceptionIds,
exceptionModules,
columnsToInclude,
);

// If unable to process the audit JSON
Expand All @@ -27,7 +35,7 @@ export default function handleFinish(jsonBuffer: string, auditLevel: AuditLevel,

// Print the security report
if (report.length) {
printSecurityReport(report);
printSecurityReport(report, columnsToInclude);
}

// Handle unused exceptions
Expand Down
11 changes: 9 additions & 2 deletions src/handlers/handleInput.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ function getProductionOnlyOption() {
* @param {Object} options User's command options or flags
* @param {Function} fn The function to handle the inputs
*/
export default function handleInput(options: CommandOptions, fn: (T1: string, T2: AuditLevel, T3: string[], T4: string[]) => void): void {
export default function handleInput(
options: CommandOptions,
fn: (T1: string, T2: AuditLevel, T3: string[], T4: string[], T5: string[]) => void,
): void {
// Generate NPM Audit command
const auditCommand: string = [
'npm audit',
Expand All @@ -45,6 +48,10 @@ export default function handleInput(options: CommandOptions, fn: (T1: string, T2
.filter((each) => each !== '');
const exceptionIds: string[] = getExceptionsIds(nsprc, cmdExceptions);
const cmdModuleIgnore: string[] = get(options, 'moduleIgnore', '').split(',');
const cmdIncludeColumns: string[] = get(options, 'includeColumns', '')
.split(',')
.map((each: string) => each.trim())
.filter((each: string) => !!each);

fn(auditCommand, auditLevel, exceptionIds, cmdModuleIgnore);
fn(auditCommand, auditLevel, exceptionIds, cmdModuleIgnore, cmdIncludeColumns);
}
1 change: 1 addition & 0 deletions src/types/general.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface CommandOptions {
readonly production?: boolean;
readonly level?: AuditLevel;
readonly registry?: string;
readonly includeColumns?: string;
}

export interface NpmAuditJson {
Expand Down
2 changes: 1 addition & 1 deletion src/types/table.d.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export type SecurityReportHeader = 'ID' | 'Module' | 'Title' | 'Paths' | 'Sev.' | 'URL' | 'Ex.';
export type SecurityReportHeader = 'ID' | 'Module' | 'Title' | 'Paths' | 'Severity' | 'URL' | 'Ex.';
export type ExceptionReportHeader = 'ID' | 'Status' | 'Expiry' | 'Notes';
12 changes: 7 additions & 5 deletions src/utils/print.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import get from 'lodash.get';
import { table, TableUserConfig } from 'table';
import { SecurityReportHeader, ExceptionReportHeader } from 'src/types';

const SECURITY_REPORT_HEADER: SecurityReportHeader[] = ['ID', 'Module', 'Title', 'Paths', 'Sev.', 'URL', 'Ex.'];
const SECURITY_REPORT_HEADER: SecurityReportHeader[] = ['ID', 'Module', 'Title', 'Paths', 'Severity', 'URL', 'Ex.'];
const EXCEPTION_REPORT_HEADER: ExceptionReportHeader[] = ['ID', 'Status', 'Expiry', 'Notes'];

// TODO: Add unit tests
Expand Down Expand Up @@ -35,10 +35,11 @@ export function getColumnWidth(tableData: string[][], columnIndex: number, maxWi

/**
* Print the security report in a table format
* @param {Array} data Array of arrays
* @return {undefined} Returns void
* @param {Array} data Array of arrays
* @return {undefined} Returns void
* @param {Array} columnsToInclude List of columns to include in audit results
*/
export function printSecurityReport(data: string[][]): void {
export function printSecurityReport(data: string[][], columnsToInclude: string[]): void {
const configs: TableUserConfig = {
singleLine: true,
header: {
Expand All @@ -58,8 +59,9 @@ export function printSecurityReport(data: string[][]): void {
},
},
};
const headers = columnsToInclude.length ? SECURITY_REPORT_HEADER.filter((h) => columnsToInclude.includes(h)) : SECURITY_REPORT_HEADER;

console.info(table([SECURITY_REPORT_HEADER, ...data], configs));
console.info(table([headers, ...data], configs));
}

/**
Expand Down
56 changes: 34 additions & 22 deletions src/utils/vulnerability.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,13 +114,15 @@ export function validateV7Vulnerability(
* @param {String} auditLevel User's target audit level
* @param {Array} exceptionIds Exception IDs (ID to be ignored)
* @param {Array} exceptionModules Exception modules (modules to be ignored)
* @param {Array} columnsToInclude List of columns to include in audit results
* @return {Object} Processed vulnerabilities details
*/
export function processAuditJson(
jsonBuffer = '',
auditLevel: AuditLevel = 'info',
exceptionIds: string[] = [],
exceptionModules: string[] = [],
columnsToInclude: string[] = [],
): ProcessedResult {
if (!isJsonString(jsonBuffer)) {
return {
Expand Down Expand Up @@ -156,22 +158,28 @@ export function processAuditJson(
acc.unusedExceptionModules = acc.unusedExceptionModules.filter((module) => module !== cur.module_name);
}

// Record this vulnerability into the report, and highlight it using yellow color if it's new
acc.report.push([
color(cur.id.toString(), isExcepted ? '' : 'yellow'),
color(cur.module_name, isExcepted ? '' : 'yellow'),
color(cur.title, isExcepted ? '' : 'yellow'),
color(
trimArray(
const rowData = [
{ key: 'ID', value: cur.id.toString() },
{ key: 'Module', value: cur.module_name },
{ key: 'Title', value: cur.title },
{
key: 'Paths',
value: trimArray(
cur.findings.reduce((a, c) => [...a, ...c.paths] as [], []),
MAX_PATHS_SIZE,
).join('\n'),
isExcepted ? '' : 'yellow',
),
color(cur.severity, isExcepted ? '' : 'yellow', getSeverityBgColor(cur.severity)),
color(cur.url, isExcepted ? '' : 'yellow'),
isExcepted ? 'y' : color('n', 'yellow'),
]);
},
{ key: 'Severity', value: cur.severity },
{ key: 'URL', value: cur.url },
{ key: 'Ex.', value: isExcepted ? 'y' : 'n' },
]
.filter(({ key }) => (columnsToInclude.length ? columnsToInclude.includes(key) : true))
.map(({ key, value }) =>
color(value, isExcepted ? '' : 'yellow', key === 'Severity' ? getSeverityBgColor(cur.severity) : undefined),
);

// Record this vulnerability into the report, and highlight it using yellow color if it's new
acc.report.push(rowData);

acc.vulnerabilityIds.push(cur.id.toString());
if (!acc.vulnerabilityModules.includes(cur.module_name)) {
Expand Down Expand Up @@ -224,16 +232,20 @@ export function processAuditJson(
acc.unusedExceptionModules = acc.unusedExceptionModules.filter((module) => module !== moduleName);
}

const rowData = [
{ key: 'ID', value: String(id) },
{ key: 'Module', value: vul.name },
{ key: 'Title', value: vul.title },
{ key: 'Paths', value: trimArray(get(cur, 'nodes', []).map(shortenNodePath), MAX_PATHS_SIZE).join('\n') },
{ key: 'Severity', value: vul.severity, bgColor: getSeverityBgColor(vul.severity) },
{ key: 'URL', value: vul.url },
{ key: 'Ex.', value: isExcepted ? 'y' : 'n' },
]
.filter(({ key }) => (columnsToInclude.length ? columnsToInclude.includes(key) : true))
.map(({ key, value, bgColor }) => color(value, isExcepted ? '' : 'yellow', key === 'Severity' ? bgColor : undefined));

// Record this vulnerability into the report, and highlight it using yellow color if it's new
acc.report.push([
color(String(id), isExcepted ? '' : 'yellow'),
color(vul.name, isExcepted ? '' : 'yellow'),
color(vul.title, isExcepted ? '' : 'yellow'),
color(trimArray(get(cur, 'nodes', []).map(shortenNodePath), MAX_PATHS_SIZE).join('\n'), isExcepted ? '' : 'yellow'),
color(vul.severity, isExcepted ? '' : 'yellow', getSeverityBgColor(vul.severity)),
color(vul.url, isExcepted ? '' : 'yellow'),
isExcepted ? 'y' : color('n', 'yellow'),
]);
acc.report.push(rowData);

acc.vulnerabilityIds.push(String(id));
if (!acc.vulnerabilityModules.includes(moduleName)) {
Expand Down
33 changes: 33 additions & 0 deletions test/handlers/flags.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ describe('Flags', () => {

// with space
options.moduleIgnore = 'lodash, moment';

handleInput(options, callbackStub);
expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds, modulesToIgnore)).to.equal(true);

Expand All @@ -217,4 +218,36 @@ describe('Flags', () => {
expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds, modulesToIgnore)).to.equal(true);
});
});

describe('--include-columns', () => {
it('should be able to pass column names using the command flag smoothly', () => {
const callbackStub = sinon.stub();
const options = { includeColumns: 'ID,Module' };
const auditCommand = 'npm audit';
const auditLevel = 'info';
const exceptionIds: string[] = [];
const modulesToIgnore: string[] = [''];
const columnsToInclude = ['ID', 'Module'];

expect(callbackStub.called).to.equal(false);
handleInput(options, callbackStub);
expect(callbackStub.called).to.equal(true);
expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds, modulesToIgnore, columnsToInclude)).to.equal(true);

// with space
options.includeColumns = 'ID, Module';
handleInput(options, callbackStub);
expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds, modulesToIgnore, columnsToInclude)).to.equal(true);

// invalid exceptions
options.includeColumns = 'ID,undefined,Module';
handleInput(options, callbackStub);
expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds, modulesToIgnore, columnsToInclude)).to.equal(true);

// invalid null
options.includeColumns = 'ID,null,Module';
handleInput(options, callbackStub);
expect(callbackStub.calledWith(auditCommand, auditLevel, exceptionIds, modulesToIgnore, columnsToInclude)).to.equal(true);
});
});
});
17 changes: 11 additions & 6 deletions test/handlers/handleFinish.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,12 @@ describe('Events handling', () => {
const auditLevel = 'info';
const exceptionIds: string[] = [];
const exceptionModules: string[] = [];
const columnsToInclude: string[] = [];

expect(processStub.called).to.equal(false);
expect(consoleStub.called).to.equal(false);

handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules);
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules, columnsToInclude);

expect(processStub.called).to.equal(true);
expect(processStub.calledWith(1)).to.equal(true);
Expand All @@ -37,9 +38,10 @@ describe('Events handling', () => {
const auditLevel = 'info';
const exceptionIds: string[] = [];
const exceptionModules: string[] = [];
const columnsToInclude: string[] = [];

expect(consoleStub.called).to.equal(false);
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules);
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules, columnsToInclude);

expect(processStub.called).to.equal(true);
expect(processStub.calledWith(0)).to.equal(true);
Expand All @@ -58,9 +60,10 @@ describe('Events handling', () => {
const auditLevel = 'info';
const exceptionIds = ['975', '985', '1179', '1213', '1500', '1523', '1555', '1556', '1589'];
const exceptionModules = ['swagger-ui', 'mem'];
const columnsToInclude: string[] = [];

expect(consoleStub.called).to.equal(false);
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules);
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules, columnsToInclude);

expect(processStub.called).to.equal(true);
expect(processStub.calledWith(0)).to.equal(true);
Expand All @@ -80,12 +83,13 @@ describe('Events handling', () => {
const auditLevel = 'info';
const exceptionIds = ['975', '976', '985', '1084', '1179', '1213', '1500', '1523', '1555'];
const exceptionModules: string[] = [];
const columnsToInclude: string[] = [];

expect(processStub.called).to.equal(false);
expect(consoleErrorStub.called).to.equal(false);
expect(consoleInfoStub.called).to.equal(false);

handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules);
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules, columnsToInclude);

expect(processStub.called).to.equal(true);
expect(consoleErrorStub.called).to.equal(true);
Expand All @@ -108,13 +112,14 @@ describe('Events handling', () => {
const auditLevel = 'info';
let exceptionModules = ['fakeModule1', 'fakeModule2'];
let exceptionIds = ['975', '976', '985', '1084', '1179', '1213', '1500', '1523', '1555', '2001'];
const columnsToInclude: string[] = [];

expect(processStub.called).to.equal(false);
expect(consoleErrorStub.called).to.equal(false);
expect(consoleWarnStub.called).to.equal(false);
expect(consoleInfoStub.called).to.equal(false);

handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules);
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules, columnsToInclude);

expect(processStub.called).to.equal(true);
expect(processStub.calledWith(1)).to.equal(true);
Expand All @@ -136,7 +141,7 @@ describe('Events handling', () => {
// Message for multiple unused exceptions
exceptionIds = ['975', '976', '985', '1084', '1179', '1213', '1500', '1523', '1555', '2001', '2002'];
exceptionModules = ['fakeModule1'];
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules);
handleFinish(jsonBuffer, auditLevel, exceptionIds, exceptionModules, columnsToInclude);
message = [
'2 of the excluded vulnerabilities did not match any of the found vulnerabilities: 2001, 2002.',
'They can be removed from the .nsprc file or --exclude -x flags.',
Expand Down
4 changes: 2 additions & 2 deletions test/utils/print.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,11 @@ import V7_SECURITY_REPORT_TABLE_DATA from '../__mocks__/v7-security-report-table

describe('Print utils', () => {
it('v6 security report table visual', () => {
printSecurityReport(V6_SECURITY_REPORT_TABLE_DATA);
printSecurityReport(V6_SECURITY_REPORT_TABLE_DATA, []);
});

it('v7 security report table visual', () => {
printSecurityReport(V7_SECURITY_REPORT_TABLE_DATA);
printSecurityReport(V7_SECURITY_REPORT_TABLE_DATA, []);
});

it('exception table visual', () => {
Expand Down
Loading