@maverick006/scanner-npm-audit 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +99 -0
- package/dist/index.js.map +1 -0
- package/dist/parser.d.ts +3 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +53 -0
- package/dist/parser.js.map +1 -0
- package/jest.config.js +5 -0
- package/package.json +23 -0
- package/src/index.ts +69 -0
- package/src/parser.ts +55 -0
- package/tests/fixtures/npm-audit-output.json +87 -0
- package/tests/parser.test.ts +41 -0
- package/tsconfig.json +15 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { SecurityScanner } from '@maverick006/security-engine';
|
|
2
|
+
import { ScanInput, ScannerResult } from '@maverick006/types';
|
|
3
|
+
export declare class NpmAuditScanner implements SecurityScanner {
|
|
4
|
+
name: string;
|
|
5
|
+
version: string;
|
|
6
|
+
scan(input: ScanInput): Promise<ScannerResult>;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAC/D,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAQ9D,qBAAa,eAAgB,YAAW,eAAe;IAC9C,IAAI,SAAe;IACnB,OAAO,SAAa;IAErB,IAAI,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC;CAuDrD"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.NpmAuditScanner = void 0;
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const util_1 = require("util");
|
|
39
|
+
const parser_1 = require("./parser");
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const execAsync = (0, util_1.promisify)(child_process_1.exec);
|
|
42
|
+
class NpmAuditScanner {
|
|
43
|
+
name = 'npm-audit';
|
|
44
|
+
version = 'unknown';
|
|
45
|
+
async scan(input) {
|
|
46
|
+
const startTime = new Date();
|
|
47
|
+
let rawOutput = '';
|
|
48
|
+
try {
|
|
49
|
+
const safePath = path.resolve(input.repositoryPath);
|
|
50
|
+
// npm audit --json outputs audit data. We run it in the repository path.
|
|
51
|
+
// Note: this assumes package.json exists. If it doesn't, npm audit will fail, which we can catch.
|
|
52
|
+
const { stdout } = await execAsync(`npm audit --json --prefix "${safePath}"`, {
|
|
53
|
+
timeout: 300000,
|
|
54
|
+
maxBuffer: 1024 * 1024 * 50
|
|
55
|
+
});
|
|
56
|
+
rawOutput = stdout;
|
|
57
|
+
const findings = (0, parser_1.parseNpmAuditOutput)(input.scanId, rawOutput);
|
|
58
|
+
return {
|
|
59
|
+
scanner: this.name,
|
|
60
|
+
success: true,
|
|
61
|
+
findings,
|
|
62
|
+
rawOutput,
|
|
63
|
+
startTime,
|
|
64
|
+
endTime: new Date()
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
// npm audit returns exit code > 0 if vulnerabilities are found
|
|
69
|
+
if (error.stdout && error.stdout.includes('"auditReportVersion":')) {
|
|
70
|
+
try {
|
|
71
|
+
rawOutput = error.stdout;
|
|
72
|
+
const findings = (0, parser_1.parseNpmAuditOutput)(input.scanId, rawOutput);
|
|
73
|
+
return {
|
|
74
|
+
scanner: this.name,
|
|
75
|
+
success: true,
|
|
76
|
+
findings,
|
|
77
|
+
rawOutput,
|
|
78
|
+
startTime,
|
|
79
|
+
endTime: new Date()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
catch (parseError) {
|
|
83
|
+
// ignore parsing error
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
scanner: this.name,
|
|
88
|
+
success: false,
|
|
89
|
+
findings: [],
|
|
90
|
+
error: error.message || 'npm audit execution failed',
|
|
91
|
+
rawOutput: error.stdout || '',
|
|
92
|
+
startTime,
|
|
93
|
+
endTime: new Date()
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
exports.NpmAuditScanner = NpmAuditScanner;
|
|
99
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAEA,iDAAqC;AACrC,+BAAiC;AACjC,qCAA+C;AAC/C,2CAA6B;AAE7B,MAAM,SAAS,GAAG,IAAA,gBAAS,EAAC,oBAAI,CAAC,CAAC;AAElC,MAAa,eAAe;IACnB,IAAI,GAAG,WAAW,CAAC;IACnB,OAAO,GAAG,SAAS,CAAC;IAE3B,KAAK,CAAC,IAAI,CAAC,KAAgB;QACzB,MAAM,SAAS,GAAG,IAAI,IAAI,EAAE,CAAC;QAC7B,IAAI,SAAS,GAAG,EAAE,CAAC;QAEnB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,cAAc,CAAC,CAAC;YAEpD,yEAAyE;YACzE,kGAAkG;YAClG,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,SAAS,CAAC,8BAA8B,QAAQ,GAAG,EAAE;gBAC5E,OAAO,EAAE,MAAM;gBACf,SAAS,EAAE,IAAI,GAAG,IAAI,GAAG,EAAE;aAC5B,CAAC,CAAC;YAEH,SAAS,GAAG,MAAM,CAAC;YACnB,MAAM,QAAQ,GAAG,IAAA,4BAAmB,EAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;YAE9D,OAAO;gBACL,OAAO,EAAE,IAAI,CAAC,IAAI;gBAClB,OAAO,EAAE,IAAI;gBACb,QAAQ;gBACR,SAAS;gBACT,SAAS;gBACT,OAAO,EAAE,IAAI,IAAI,EAAE;aACpB,CAAC;QACJ,CAAC;QAAC,OAAO,KAAU,EAAE,CAAC;YACpB,+DAA+D;YAC/D,IAAI,KAAK,CAAC,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,CAAC;gBACnE,IAAI,CAAC;oBACH,SAAS,GAAG,KAAK,CAAC,MAAM,CAAC;oBACzB,MAAM,QAAQ,GAAG,IAAA,4BAAmB,EAAC,KAAK,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;oBAC9D,OAAO;wBACL,OAAO,EAAE,IAAI,CAAC,IAAI;wBAClB,OAAO,EAAE,IAAI;wBACb,QAAQ;wBACR,SAAS;wBACT,SAAS;wBACT,OAAO,EAAE,IAAI,IAAI,EAAE;qBACpB,CAAC;gBACJ,CAAC;gBAAC,OAAO,UAAU,EAAE,CAAC;oBACpB,uBAAuB;gBACzB,CAAC;YACH,CAAC;YAED,OAAO;gBACL,OAAO,EAAE,IAAI,CAAC,IAAI;gBAClB,OAAO,EAAE,KAAK;gBACd,QAAQ,EAAE,EAAE;gBACZ,KAAK,EAAE,KAAK,CAAC,OAAO,IAAI,4BAA4B;gBACpD,SAAS,EAAE,KAAK,CAAC,MAAM,IAAI,EAAE;gBAC7B,SAAS;gBACT,OAAO,EAAE,IAAI,IAAI,EAAE;aACpB,CAAC;QACJ,CAAC;IACH,CAAC;CACF;AA3DD,0CA2DC"}
|
package/dist/parser.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.d.ts","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAwB,MAAM,oBAAoB,CAAC;AAE7E,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,iBAAiB,EAAE,CAoDvF"}
|
package/dist/parser.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseNpmAuditOutput = parseNpmAuditOutput;
|
|
4
|
+
const types_1 = require("@maverick006/types");
|
|
5
|
+
function parseNpmAuditOutput(scanId, output) {
|
|
6
|
+
try {
|
|
7
|
+
const data = JSON.parse(output);
|
|
8
|
+
const findings = [];
|
|
9
|
+
// npm audit JSON output format: version 2
|
|
10
|
+
if (data.auditReportVersion !== 2 || !data.vulnerabilities) {
|
|
11
|
+
return findings;
|
|
12
|
+
}
|
|
13
|
+
for (const [pkgName, vulnData] of Object.entries(data.vulnerabilities)) {
|
|
14
|
+
const viaArray = Array.isArray(vulnData.via) ? vulnData.via : [];
|
|
15
|
+
// Some 'via' entries are just strings (referencing other packages),
|
|
16
|
+
// we look for objects that contain actual vulnerability details.
|
|
17
|
+
for (const via of viaArray) {
|
|
18
|
+
if (typeof via === 'object' && via.title) {
|
|
19
|
+
let severity = types_1.Severity.INFO;
|
|
20
|
+
if (via.severity === 'critical')
|
|
21
|
+
severity = types_1.Severity.CRITICAL;
|
|
22
|
+
else if (via.severity === 'high')
|
|
23
|
+
severity = types_1.Severity.HIGH;
|
|
24
|
+
else if (via.severity === 'moderate')
|
|
25
|
+
severity = types_1.Severity.MEDIUM;
|
|
26
|
+
else if (via.severity === 'low')
|
|
27
|
+
severity = types_1.Severity.LOW;
|
|
28
|
+
const finding = {
|
|
29
|
+
scanId,
|
|
30
|
+
scanner: 'npm-audit',
|
|
31
|
+
scannerVersion: 'v2', // refers to report version
|
|
32
|
+
ruleId: via.url || `npm-audit-${pkgName}`,
|
|
33
|
+
title: via.title,
|
|
34
|
+
description: `Dependency vulnerability in ${pkgName}. Range: ${via.range}. Fix: ${vulnData.fixAvailable ? (typeof vulnData.fixAvailable === 'object' ? vulnData.fixAvailable.name + '@' + vulnData.fixAvailable.version : 'Available') : 'None'}`,
|
|
35
|
+
severity,
|
|
36
|
+
confidence: types_1.Confidence.HIGH, // SCA scans matching a CVE have high confidence
|
|
37
|
+
category: 'dependency',
|
|
38
|
+
file: 'package.json', // Best guess for location
|
|
39
|
+
line: 1, // Doesn't map well to line numbers
|
|
40
|
+
cwe: Array.isArray(via.cwe) ? via.cwe.join(', ') : via.cwe,
|
|
41
|
+
owasp: 'A06:2021', // Vulnerable and Outdated Components
|
|
42
|
+
};
|
|
43
|
+
findings.push(finding);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return findings;
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
throw new Error('Failed to parse npm audit JSON output');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=parser.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parser.js","sourceRoot":"","sources":["../src/parser.ts"],"names":[],"mappings":";;AAEA,kDAoDC;AAtDD,8CAA6E;AAE7E,SAAgB,mBAAmB,CAAC,MAAc,EAAE,MAAc;IAChE,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QAChC,MAAM,QAAQ,GAAwB,EAAE,CAAC;QAEzC,0CAA0C;QAC1C,IAAI,IAAI,CAAC,kBAAkB,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,EAAE,CAAC;YAC3D,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,KAAK,MAAM,CAAC,OAAO,EAAE,QAAQ,CAAC,IAAI,MAAM,CAAC,OAAO,CAAM,IAAI,CAAC,eAAe,CAAC,EAAE,CAAC;YAE5E,MAAM,QAAQ,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YAEjE,qEAAqE;YACrE,iEAAiE;YACjE,KAAK,MAAM,GAAG,IAAI,QAAQ,EAAE,CAAC;gBAC3B,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,KAAK,EAAE,CAAC;oBAEzC,IAAI,QAAQ,GAAG,gBAAQ,CAAC,IAAI,CAAC;oBAC7B,IAAI,GAAG,CAAC,QAAQ,KAAK,UAAU;wBAAE,QAAQ,GAAG,gBAAQ,CAAC,QAAQ,CAAC;yBACzD,IAAI,GAAG,CAAC,QAAQ,KAAK,MAAM;wBAAE,QAAQ,GAAG,gBAAQ,CAAC,IAAI,CAAC;yBACtD,IAAI,GAAG,CAAC,QAAQ,KAAK,UAAU;wBAAE,QAAQ,GAAG,gBAAQ,CAAC,MAAM,CAAC;yBAC5D,IAAI,GAAG,CAAC,QAAQ,KAAK,KAAK;wBAAE,QAAQ,GAAG,gBAAQ,CAAC,GAAG,CAAC;oBAEzD,MAAM,OAAO,GAAsB;wBACjC,MAAM;wBACN,OAAO,EAAE,WAAW;wBACpB,cAAc,EAAE,IAAI,EAAE,2BAA2B;wBACjD,MAAM,EAAE,GAAG,CAAC,GAAG,IAAI,aAAa,OAAO,EAAE;wBACzC,KAAK,EAAE,GAAG,CAAC,KAAK;wBAChB,WAAW,EAAE,+BAA+B,OAAO,YAAY,GAAG,CAAC,KAAK,UACtE,QAAQ,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,OAAO,QAAQ,CAAC,YAAY,KAAK,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,YAAY,CAAC,IAAI,GAAG,GAAG,GAAG,QAAQ,CAAC,YAAY,CAAC,OAAO,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,MACzJ,EAAE;wBACF,QAAQ;wBACR,UAAU,EAAE,kBAAU,CAAC,IAAI,EAAE,gDAAgD;wBAC7E,QAAQ,EAAE,YAAY;wBACtB,IAAI,EAAE,cAAc,EAAE,0BAA0B;wBAChD,IAAI,EAAE,CAAC,EAAE,mCAAmC;wBAC5C,GAAG,EAAE,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG;wBAC1D,KAAK,EAAE,UAAU,EAAE,qCAAqC;qBACzD,CAAC;oBAEF,QAAQ,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACzB,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO,QAAQ,CAAC;IAClB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,IAAI,KAAK,CAAC,uCAAuC,CAAC,CAAC;IAC3D,CAAC;AACH,CAAC"}
|
package/jest.config.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@maverick006/scanner-npm-audit",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"build": "tsc",
|
|
8
|
+
"clean": "rm -rf dist",
|
|
9
|
+
"test": "jest",
|
|
10
|
+
"typecheck": "tsc --noEmit"
|
|
11
|
+
},
|
|
12
|
+
"dependencies": {
|
|
13
|
+
"@maverick006/types": "*",
|
|
14
|
+
"@maverick006/security-engine": "*"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"typescript": "^5.0.0",
|
|
18
|
+
"jest": "^29.5.0",
|
|
19
|
+
"@types/jest": "^29.5.0",
|
|
20
|
+
"ts-jest": "^29.1.0",
|
|
21
|
+
"@types/node": "^20.0.0"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { SecurityScanner } from '@maverick006/security-engine';
|
|
2
|
+
import { ScanInput, ScannerResult } from '@maverick006/types';
|
|
3
|
+
import { exec } from 'child_process';
|
|
4
|
+
import { promisify } from 'util';
|
|
5
|
+
import { parseNpmAuditOutput } from './parser';
|
|
6
|
+
import * as path from 'path';
|
|
7
|
+
|
|
8
|
+
const execAsync = promisify(exec);
|
|
9
|
+
|
|
10
|
+
export class NpmAuditScanner implements SecurityScanner {
|
|
11
|
+
public name = 'npm-audit';
|
|
12
|
+
public version = 'unknown';
|
|
13
|
+
|
|
14
|
+
async scan(input: ScanInput): Promise<ScannerResult> {
|
|
15
|
+
const startTime = new Date();
|
|
16
|
+
let rawOutput = '';
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const safePath = path.resolve(input.repositoryPath);
|
|
20
|
+
|
|
21
|
+
// npm audit --json outputs audit data. We run it in the repository path.
|
|
22
|
+
// Note: this assumes package.json exists. If it doesn't, npm audit will fail, which we can catch.
|
|
23
|
+
const { stdout } = await execAsync(`npm audit --json --prefix "${safePath}"`, {
|
|
24
|
+
timeout: 300000,
|
|
25
|
+
maxBuffer: 1024 * 1024 * 50
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
rawOutput = stdout;
|
|
29
|
+
const findings = parseNpmAuditOutput(input.scanId, rawOutput);
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
scanner: this.name,
|
|
33
|
+
success: true,
|
|
34
|
+
findings,
|
|
35
|
+
rawOutput,
|
|
36
|
+
startTime,
|
|
37
|
+
endTime: new Date()
|
|
38
|
+
};
|
|
39
|
+
} catch (error: any) {
|
|
40
|
+
// npm audit returns exit code > 0 if vulnerabilities are found
|
|
41
|
+
if (error.stdout && error.stdout.includes('"auditReportVersion":')) {
|
|
42
|
+
try {
|
|
43
|
+
rawOutput = error.stdout;
|
|
44
|
+
const findings = parseNpmAuditOutput(input.scanId, rawOutput);
|
|
45
|
+
return {
|
|
46
|
+
scanner: this.name,
|
|
47
|
+
success: true,
|
|
48
|
+
findings,
|
|
49
|
+
rawOutput,
|
|
50
|
+
startTime,
|
|
51
|
+
endTime: new Date()
|
|
52
|
+
};
|
|
53
|
+
} catch (parseError) {
|
|
54
|
+
// ignore parsing error
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
scanner: this.name,
|
|
60
|
+
success: false,
|
|
61
|
+
findings: [],
|
|
62
|
+
error: error.message || 'npm audit execution failed',
|
|
63
|
+
rawOutput: error.stdout || '',
|
|
64
|
+
startTime,
|
|
65
|
+
endTime: new Date()
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/parser.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { NormalizedFinding, Severity, Confidence } from '@maverick006/types';
|
|
2
|
+
|
|
3
|
+
export function parseNpmAuditOutput(scanId: string, output: string): NormalizedFinding[] {
|
|
4
|
+
try {
|
|
5
|
+
const data = JSON.parse(output);
|
|
6
|
+
const findings: NormalizedFinding[] = [];
|
|
7
|
+
|
|
8
|
+
// npm audit JSON output format: version 2
|
|
9
|
+
if (data.auditReportVersion !== 2 || !data.vulnerabilities) {
|
|
10
|
+
return findings;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
for (const [pkgName, vulnData] of Object.entries<any>(data.vulnerabilities)) {
|
|
14
|
+
|
|
15
|
+
const viaArray = Array.isArray(vulnData.via) ? vulnData.via : [];
|
|
16
|
+
|
|
17
|
+
// Some 'via' entries are just strings (referencing other packages),
|
|
18
|
+
// we look for objects that contain actual vulnerability details.
|
|
19
|
+
for (const via of viaArray) {
|
|
20
|
+
if (typeof via === 'object' && via.title) {
|
|
21
|
+
|
|
22
|
+
let severity = Severity.INFO;
|
|
23
|
+
if (via.severity === 'critical') severity = Severity.CRITICAL;
|
|
24
|
+
else if (via.severity === 'high') severity = Severity.HIGH;
|
|
25
|
+
else if (via.severity === 'moderate') severity = Severity.MEDIUM;
|
|
26
|
+
else if (via.severity === 'low') severity = Severity.LOW;
|
|
27
|
+
|
|
28
|
+
const finding: NormalizedFinding = {
|
|
29
|
+
scanId,
|
|
30
|
+
scanner: 'npm-audit',
|
|
31
|
+
scannerVersion: 'v2', // refers to report version
|
|
32
|
+
ruleId: via.url || `npm-audit-${pkgName}`,
|
|
33
|
+
title: via.title,
|
|
34
|
+
description: `Dependency vulnerability in ${pkgName}. Range: ${via.range}. Fix: ${
|
|
35
|
+
vulnData.fixAvailable ? (typeof vulnData.fixAvailable === 'object' ? vulnData.fixAvailable.name + '@' + vulnData.fixAvailable.version : 'Available') : 'None'
|
|
36
|
+
}`,
|
|
37
|
+
severity,
|
|
38
|
+
confidence: Confidence.HIGH, // SCA scans matching a CVE have high confidence
|
|
39
|
+
category: 'dependency',
|
|
40
|
+
file: 'package.json', // Best guess for location
|
|
41
|
+
line: 1, // Doesn't map well to line numbers
|
|
42
|
+
cwe: Array.isArray(via.cwe) ? via.cwe.join(', ') : via.cwe,
|
|
43
|
+
owasp: 'A06:2021', // Vulnerable and Outdated Components
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
findings.push(finding);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return findings;
|
|
52
|
+
} catch (error) {
|
|
53
|
+
throw new Error('Failed to parse npm audit JSON output');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
{
|
|
2
|
+
"auditReportVersion": 2,
|
|
3
|
+
"vulnerabilities": {
|
|
4
|
+
"express": {
|
|
5
|
+
"name": "express",
|
|
6
|
+
"severity": "high",
|
|
7
|
+
"isDirect": true,
|
|
8
|
+
"via": [
|
|
9
|
+
{
|
|
10
|
+
"source": 1085149,
|
|
11
|
+
"name": "express",
|
|
12
|
+
"dependency": "express",
|
|
13
|
+
"title": "Path Traversal in express",
|
|
14
|
+
"url": "https://github.com/advisories/GHSA-9234-5678",
|
|
15
|
+
"severity": "high",
|
|
16
|
+
"cwe": [
|
|
17
|
+
"CWE-22"
|
|
18
|
+
],
|
|
19
|
+
"cvss": {
|
|
20
|
+
"score": 7.5,
|
|
21
|
+
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N"
|
|
22
|
+
},
|
|
23
|
+
"range": "<4.19.2"
|
|
24
|
+
}
|
|
25
|
+
],
|
|
26
|
+
"effects": [],
|
|
27
|
+
"range": "<4.19.2",
|
|
28
|
+
"nodes": [
|
|
29
|
+
"node_modules/express"
|
|
30
|
+
],
|
|
31
|
+
"fixAvailable": {
|
|
32
|
+
"name": "express",
|
|
33
|
+
"version": "4.19.2",
|
|
34
|
+
"isSemVerMajor": false
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
"lodash": {
|
|
38
|
+
"name": "lodash",
|
|
39
|
+
"severity": "critical",
|
|
40
|
+
"isDirect": false,
|
|
41
|
+
"via": [
|
|
42
|
+
{
|
|
43
|
+
"source": 1012345,
|
|
44
|
+
"name": "lodash",
|
|
45
|
+
"dependency": "lodash",
|
|
46
|
+
"title": "Prototype Pollution in lodash",
|
|
47
|
+
"url": "https://github.com/advisories/GHSA-1234-abcd",
|
|
48
|
+
"severity": "critical",
|
|
49
|
+
"cwe": [
|
|
50
|
+
"CWE-1321"
|
|
51
|
+
],
|
|
52
|
+
"cvss": {
|
|
53
|
+
"score": 9.8,
|
|
54
|
+
"vectorString": "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H"
|
|
55
|
+
},
|
|
56
|
+
"range": "<4.17.21"
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
"effects": [
|
|
60
|
+
"some-other-pkg"
|
|
61
|
+
],
|
|
62
|
+
"range": "<4.17.21",
|
|
63
|
+
"nodes": [
|
|
64
|
+
"node_modules/lodash"
|
|
65
|
+
],
|
|
66
|
+
"fixAvailable": true
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
"metadata": {
|
|
70
|
+
"vulnerabilities": {
|
|
71
|
+
"info": 0,
|
|
72
|
+
"low": 0,
|
|
73
|
+
"moderate": 0,
|
|
74
|
+
"high": 1,
|
|
75
|
+
"critical": 1,
|
|
76
|
+
"total": 2
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"prod": 10,
|
|
80
|
+
"dev": 20,
|
|
81
|
+
"optional": 0,
|
|
82
|
+
"peer": 0,
|
|
83
|
+
"peerOptional": 0,
|
|
84
|
+
"total": 30
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { parseNpmAuditOutput } from '../src/parser';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { Severity, Confidence } from '@maverick006/types';
|
|
5
|
+
|
|
6
|
+
describe('npm audit Parser', () => {
|
|
7
|
+
it('should parse npm audit json output correctly', () => {
|
|
8
|
+
const fixturePath = path.join(__dirname, 'fixtures', 'npm-audit-output.json');
|
|
9
|
+
const output = fs.readFileSync(fixturePath, 'utf8');
|
|
10
|
+
|
|
11
|
+
const findings = parseNpmAuditOutput('test-scan-123', output);
|
|
12
|
+
|
|
13
|
+
expect(findings.length).toBe(2);
|
|
14
|
+
|
|
15
|
+
// Check first finding (express Path Traversal)
|
|
16
|
+
const expressVuln = findings[0];
|
|
17
|
+
expect(expressVuln.scanId).toBe('test-scan-123');
|
|
18
|
+
expect(expressVuln.scanner).toBe('npm-audit');
|
|
19
|
+
expect(expressVuln.title).toBe('Path Traversal in express');
|
|
20
|
+
expect(expressVuln.severity).toBe(Severity.HIGH);
|
|
21
|
+
expect(expressVuln.confidence).toBe(Confidence.HIGH);
|
|
22
|
+
expect(expressVuln.cwe).toContain('CWE-22');
|
|
23
|
+
expect(expressVuln.file).toBe('package.json');
|
|
24
|
+
expect(expressVuln.category).toBe('dependency');
|
|
25
|
+
expect(expressVuln.description).toContain('Fix: express@4.19.2');
|
|
26
|
+
|
|
27
|
+
// Check second finding (lodash Prototype Pollution)
|
|
28
|
+
const lodashVuln = findings[1];
|
|
29
|
+
expect(lodashVuln.title).toBe('Prototype Pollution in lodash');
|
|
30
|
+
expect(lodashVuln.severity).toBe(Severity.CRITICAL);
|
|
31
|
+
expect(lodashVuln.cwe).toContain('CWE-1321');
|
|
32
|
+
expect(lodashVuln.description).toContain('Fix: Available');
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('should handle malformed output gracefully', () => {
|
|
36
|
+
expect(() => parseNpmAuditOutput('123', '{ invalid json }')).toThrow();
|
|
37
|
+
|
|
38
|
+
const emptyFindings = parseNpmAuditOutput('123', '{"auditReportVersion": 2, "vulnerabilities": {}}');
|
|
39
|
+
expect(emptyFindings.length).toBe(0);
|
|
40
|
+
});
|
|
41
|
+
});
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDir": "./src",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"declarationMap": true,
|
|
8
|
+
"sourceMap": true
|
|
9
|
+
},
|
|
10
|
+
"references": [
|
|
11
|
+
{ "path": "../../packages/types" },
|
|
12
|
+
{ "path": "../../packages/security-engine" }
|
|
13
|
+
],
|
|
14
|
+
"include": ["src/**/*"]
|
|
15
|
+
}
|