diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/README.md b/README.md index 8326b01..a7309c3 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # yarn-audit-sonar +Convert yarn audit into a Sonar compatible format + +## Usage + +``` +yarn audit --group dependencies --json | npx yarn-audit-sonar > yarn-sonar.json +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..9cafea2 --- /dev/null +++ b/index.js @@ -0,0 +1,110 @@ +const fs = require('fs'); +const split = require('split'); + +if (!fs.existsSync('yarn.lock')) { + console.error('yarn.lock is missing'); + process.exit(1); +} + +function parseYarnLock() { + let pos = 0; + let currentModule = ''; + const yarnLock = {}; + for (const row of fs.readFileSync('yarn.lock', 'utf8').split('\n')) { + pos++; + if (row.length > 0 && row[0] != ' ') { + currentModule = row.replace(/"/g, '').replace(/(..*?)@.*/, '$1'); + } + if (new RegExp(' +version').test(row)) { + const version = row.replace(/\s+version "(.*)"/, '$1'); + if (!yarnLock[currentModule]) yarnLock[currentModule] = {}; + yarnLock[currentModule][version] = { + 'startLine': pos, + 'endLine': pos, + 'startColumn': 1, + 'endColumn': row.length, + }; + } + } + return yarnLock; +} + +function yarnLockRange(yarnLock, moduleName, version) { + if (yarnLock[moduleName] && yarnLock[moduleName][version]) { + return yarnLock[moduleName][version]; + } else { + return { + 'startLine': 1, + }; + } +} + +const yarnLock = parseYarnLock(); + +const severities = { + info: 'INFO', + low: 'MINOR', + moderate: 'MINOR', + high: 'CRITICAL', + critical: 'BLOCKER', +}; + +const resolvedIds = new Set(); +const stats = {}; +let firstLine = true; + +function processRow(row) { + if (!row) return; + const {type, data} = JSON.parse(row); + if (type !== 'auditAdvisory') return; + const {advisory, resolution} = data; + if (resolvedIds.has(resolution.id)) return; + + const [mainVersion, ...otherVersions] = new Set(advisory.findings.map((f) => f.version)); + + if (!firstLine) { + process.stdout.write(','); + } else { + firstLine = false; + } + process.stdout.write(JSON.stringify({ + engineId: 'yarn-audit', + ruleId: resolution.id.toString(), + severity: severities[advisory.severity] || 'INFO', + type: 'VULNERABILITY', + efforMinutes: 0, + primaryLocation: { + 'message': advisory.overview, + 'filePath': 'yarn.lock', + 'textRange': yarnLockRange(yarnLock, advisory.module_name, mainVersion), + }, + secondaryLocations: otherVersions.map((version) => { + return { + 'message': advisory.overview, + 'filePath': 'yarn.lock', + 'textRange': yarnLockRange(yarnLock, advisory.module_name, version), + }; + }), + })); + stats[advisory.severity] = (stats[advisory.severity] || 0) + 1; +} + +process.stdout.write('{"issues":['); +process + .stdin + .pipe(split()) + .on('data', processRow) + .on('end', () => { + process.stdout.write(']}\n'); + const out = []; + let total = 0; + for(const [k,v] of Object.entries(stats)) { + out.push(`${v} ${k}`); + total += v; + } + console.error('yarn audit:'); + console.error(` ${total} vulnerabilities found`); + if (total > 0) { + console.error(` Severity: ${out.join(' | ')}`); + } + }); diff --git a/package.json b/package.json new file mode 100644 index 0000000..a154e91 --- /dev/null +++ b/package.json @@ -0,0 +1,18 @@ +{ + "name": "yarn-audit-sonar", + "version": "1.0.0", + "description": "Convert YARN audit to Sonar compatible format", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1" + }, + "repository": { + "type": "git", + "url": "https://git.celogeek.com/celogeek/yarn-audit-sonar.git" + }, + "author": "Celogeek", + "license": "ISC", + "dependencies": { + "split": "^1.0.1" + } +} diff --git a/yarn.lock b/yarn.lock new file mode 100644 index 0000000..870d719 --- /dev/null +++ b/yarn.lock @@ -0,0 +1,15 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +split@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/split/-/split-1.0.1.tgz#605bd9be303aa59fb35f9229fbea0ddec9ea07d9" + integrity sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg== + dependencies: + through "2" + +through@2: + version "2.3.8" + resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" + integrity sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=