dependencyiq 2.0.0
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/LICENSE +21 -0
- package/package.json +50 -0
- package/src/activityFetcher.js +66 -0
- package/src/agent.js +506 -0
- package/src/blastRadius.js +134 -0
- package/src/configLoader.js +61 -0
- package/src/crossProjectFanOut.js +180 -0
- package/src/dashboardGenerator.js +642 -0
- package/src/executiveSummary.js +76 -0
- package/src/fleetAggregator.js +155 -0
- package/src/fleetDashboardGenerator.js +199 -0
- package/src/fleetSnapshot.js +103 -0
- package/src/freshnessChecker.js +306 -0
- package/src/freshnessPolicy.js +73 -0
- package/src/gitlabAuth.js +38 -0
- package/src/httpRetry.js +48 -0
- package/src/impactReport.js +92 -0
- package/src/mrReviewer.js +245 -0
- package/src/orbitClient.js +214 -0
- package/src/prGenerator.js +228 -0
- package/src/remoteFixer.js +129 -0
- package/src/riskCalculator.js +143 -0
- package/src/scanners/cvss.js +78 -0
- package/src/scanners/dependencyTreeBuilder.js +227 -0
- package/src/scanners/ecosystemFixers.js +371 -0
- package/src/scanners/manifestParser.js +99 -0
- package/src/scanners/osvScanner.js +228 -0
- package/src/scanners/supplyChainTrustSignals.js +472 -0
- package/src/strategyGenerator.js +384 -0
- package/src/upgradeImpactSimulator.js +241 -0
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Multi-language vulnerability scanner backed by OSV-Scanner.
|
|
3
|
+
* OSV-Scanner reads native manifests/lockfiles for each ecosystem
|
|
4
|
+
* (package.json/package-lock.json, requirements.txt/poetry.lock,
|
|
5
|
+
* go.mod/go.sum, pom.xml/gradle.lockfile, Gemfile.lock, Cargo.lock, ...)
|
|
6
|
+
* and queries the OSV.dev vulnerability database, so one scan call
|
|
7
|
+
* covers npm, PyPI, Go, Maven, RubyGems, crates.io, etc.
|
|
8
|
+
*
|
|
9
|
+
* Install: https://github.com/google/osv-scanner (binary `osv-scanner`)
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const { execFileSync } = require('child_process');
|
|
13
|
+
const fs = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const { cvss3BaseScore } = require('./cvss');
|
|
16
|
+
|
|
17
|
+
const OSV_BINARY = process.env.OSV_SCANNER_PATH || 'osv-scanner';
|
|
18
|
+
|
|
19
|
+
// Last-resort fallback only: when an advisory carries neither a numeric
|
|
20
|
+
// score nor a parseable CVSS vector, a coarse severity-bucket midpoint
|
|
21
|
+
// keeps risk scoring functional. Preferred order (most → least precise):
|
|
22
|
+
// numeric score → computed from CVSS vector → this bucket.
|
|
23
|
+
const SEVERITY_TO_CVSS = {
|
|
24
|
+
CRITICAL: 9.5,
|
|
25
|
+
HIGH: 7.5,
|
|
26
|
+
MODERATE: 5.0,
|
|
27
|
+
MEDIUM: 5.0,
|
|
28
|
+
LOW: 2.5,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function isOsvScannerInstalled() {
|
|
32
|
+
try {
|
|
33
|
+
execFileSync(OSV_BINARY, ['--version'], { stdio: 'ignore' });
|
|
34
|
+
return true;
|
|
35
|
+
} catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run osv-scanner recursively over a repository and normalize results.
|
|
42
|
+
* @param {string} repoPath - Path to repository
|
|
43
|
+
* @returns {Promise<Array>} Normalized vulnerability objects
|
|
44
|
+
*/
|
|
45
|
+
async function detectVulnerabilities(repoPath) {
|
|
46
|
+
if (!isOsvScannerInstalled()) {
|
|
47
|
+
console.warn(
|
|
48
|
+
` ⚠️ ${OSV_BINARY} not found on PATH. Install it from ` +
|
|
49
|
+
'https://github.com/google/osv-scanner/releases, or set OSV_SCANNER_PATH. ' +
|
|
50
|
+
'Returning no results instead of guessing.'
|
|
51
|
+
);
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
console.log('🔍 Scanning for vulnerabilities (OSV-Scanner, multi-language)...');
|
|
56
|
+
|
|
57
|
+
let raw;
|
|
58
|
+
try {
|
|
59
|
+
// v1.x OSV-Scanner CLI syntax (the pinned OSV_SCANNER_VERSION in
|
|
60
|
+
// .gitlab-ci.yml): no "scan source" subcommand — that's v2.x syntax,
|
|
61
|
+
// and passing it here made v1.9.2 treat "source" itself as a literal
|
|
62
|
+
// path to scan ("Failed to walk source: lstat source: no such file
|
|
63
|
+
// or directory") rather than recognizing it as a mode keyword.
|
|
64
|
+
raw = execFileSync(
|
|
65
|
+
OSV_BINARY,
|
|
66
|
+
['--format', 'json', '--recursive', repoPath],
|
|
67
|
+
{ encoding: 'utf-8', maxBuffer: 1024 * 1024 * 50 }
|
|
68
|
+
);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// osv-scanner exits non-zero when vulnerabilities are found; the JSON is
|
|
71
|
+
// still on stdout in that case.
|
|
72
|
+
raw = error.stdout || '';
|
|
73
|
+
if (!raw) {
|
|
74
|
+
// A real scan failure (e.g. osv-scanner itself errored with no JSON
|
|
75
|
+
// at all) is NOT the same thing as "scanned cleanly, zero findings"
|
|
76
|
+
// — throw rather than returning [], so callers can't accidentally
|
|
77
|
+
// report a failed scan as "no vulnerabilities found".
|
|
78
|
+
throw new Error(`osv-scanner failed to run: ${error.message}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
let report;
|
|
83
|
+
try {
|
|
84
|
+
report = JSON.parse(raw);
|
|
85
|
+
} catch (firstError) {
|
|
86
|
+
// Some osv-scanner versions print a status/warning line to stdout
|
|
87
|
+
// before the JSON despite --format json (e.g. a "Scanning dir ..."
|
|
88
|
+
// line, or a git-ignore-parsing warning in a shallow CI checkout).
|
|
89
|
+
// Tolerate that by parsing from the first '{' instead of assuming
|
|
90
|
+
// the whole stream is pure JSON — only throw if that also fails,
|
|
91
|
+
// since a genuine scan failure still must not be reported as clean.
|
|
92
|
+
const firstBrace = raw.indexOf('{');
|
|
93
|
+
if (firstBrace === -1) {
|
|
94
|
+
throw new Error(`osv-scanner produced unparseable output (scan likely failed, not clean): ${firstError.message}\nRaw output: ${raw.slice(0, 500)}`);
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
report = JSON.parse(raw.slice(firstBrace));
|
|
98
|
+
} catch (secondError) {
|
|
99
|
+
throw new Error(`osv-scanner produced unparseable output even after stripping leading text (scan likely failed, not clean): ${secondError.message}\nRaw output: ${raw.slice(0, 500)}`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const vulnerabilities = normalizeOsvReport(report, repoPath);
|
|
104
|
+
console.log(` ✓ Found ${vulnerabilities.length} vulnerabilities across ${countEcosystems(vulnerabilities)} ecosystem(s)`);
|
|
105
|
+
return vulnerabilities;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function countEcosystems(vulnerabilities) {
|
|
109
|
+
return new Set(vulnerabilities.map(v => v.ecosystem)).size;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Flatten osv-scanner's `results[].packages[].vulnerabilities[]` tree into
|
|
114
|
+
* one row per (package, vulnerability) pair, matching the shape the rest of
|
|
115
|
+
* the agent (riskCalculator, prGenerator) already expects.
|
|
116
|
+
*/
|
|
117
|
+
function normalizeOsvReport(report, repoPath) {
|
|
118
|
+
const vulnerabilities = [];
|
|
119
|
+
const results = report.results || [];
|
|
120
|
+
|
|
121
|
+
for (const result of results) {
|
|
122
|
+
const manifestPath = result.source?.path || '';
|
|
123
|
+
const ecosystemHint = detectEcosystemFromManifest(manifestPath);
|
|
124
|
+
|
|
125
|
+
for (const pkg of result.packages || []) {
|
|
126
|
+
const pkgInfo = pkg.package || {};
|
|
127
|
+
const fixedVersion = findFixedVersion(pkg);
|
|
128
|
+
|
|
129
|
+
for (const vuln of pkg.vulnerabilities || []) {
|
|
130
|
+
const cvss = extractCvssScore(vuln);
|
|
131
|
+
vulnerabilities.push({
|
|
132
|
+
id: vuln.id || 'unknown',
|
|
133
|
+
package: pkgInfo.name || 'unknown',
|
|
134
|
+
ecosystem: pkgInfo.ecosystem || ecosystemHint,
|
|
135
|
+
currentVersion: pkgInfo.version || 'unknown',
|
|
136
|
+
vulnerability: vuln.summary || vuln.details?.slice(0, 200) || 'Unknown vulnerability',
|
|
137
|
+
severity: severityLabel(cvss),
|
|
138
|
+
cvss,
|
|
139
|
+
fixedVersion: fixedVersion || 'unknown',
|
|
140
|
+
manifestFile: path.relative(repoPath, manifestPath) || manifestPath,
|
|
141
|
+
references: (vuln.references || []).map(r => r.url).filter(Boolean),
|
|
142
|
+
cveLink: (vuln.aliases || []).find(a => a.startsWith('CVE-')) || vuln.id,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return vulnerabilities;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function detectEcosystemFromManifest(manifestPath = '') {
|
|
152
|
+
const base = path.basename(manifestPath).toLowerCase();
|
|
153
|
+
if (base.includes('package-lock') || base.includes('yarn.lock') || base === 'package.json') return 'npm';
|
|
154
|
+
if (base.includes('requirements') || base.includes('poetry.lock') || base === 'pyproject.toml') return 'PyPI';
|
|
155
|
+
if (base === 'go.sum' || base === 'go.mod') return 'Go';
|
|
156
|
+
if (base.includes('pom.xml') || base.includes('gradle')) return 'Maven';
|
|
157
|
+
if (base === 'gemfile.lock') return 'RubyGems';
|
|
158
|
+
if (base === 'cargo.lock') return 'crates.io';
|
|
159
|
+
if (base.includes('composer')) return 'Packagist';
|
|
160
|
+
return 'unknown';
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function findFixedVersion(pkg) {
|
|
164
|
+
for (const vuln of pkg.vulnerabilities || []) {
|
|
165
|
+
for (const affected of vuln.affected || []) {
|
|
166
|
+
for (const range of affected.ranges || []) {
|
|
167
|
+
const fixedEvent = (range.events || []).find(e => e.fixed);
|
|
168
|
+
if (fixedEvent) return fixedEvent.fixed;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return null;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function extractCvssScore(vuln) {
|
|
176
|
+
const severities = vuln.severity || [];
|
|
177
|
+
// 1. A plain numeric score, if OSV gave one directly.
|
|
178
|
+
for (const sev of severities) {
|
|
179
|
+
const numeric = parseFloat(sev.score);
|
|
180
|
+
if (!Number.isNaN(numeric)) return numeric;
|
|
181
|
+
}
|
|
182
|
+
// 2. Compute the real base score from a CVSS v3 vector string — exact,
|
|
183
|
+
// not a guess. (sev.score holds the vector when it isn't numeric.)
|
|
184
|
+
for (const sev of severities) {
|
|
185
|
+
const computed = typeof sev.score === 'string' ? cvss3BaseScore(sev.score) : null;
|
|
186
|
+
if (computed !== null) return computed;
|
|
187
|
+
}
|
|
188
|
+
// 3. Last resort: coarse severity-bucket midpoint.
|
|
189
|
+
const dbSeverity = (vuln.database_specific?.severity || '').toUpperCase();
|
|
190
|
+
return SEVERITY_TO_CVSS[dbSeverity] ?? 5.0;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function severityLabel(cvss) {
|
|
194
|
+
if (cvss >= 9) return 'critical';
|
|
195
|
+
if (cvss >= 7) return 'high';
|
|
196
|
+
if (cvss >= 4) return 'medium';
|
|
197
|
+
return 'low';
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Detect which ecosystem manifest files are present in a repo, so the
|
|
202
|
+
* agent can report "this is a Go + npm monorepo" instead of assuming npm.
|
|
203
|
+
*/
|
|
204
|
+
function detectEcosystems(repoPath) {
|
|
205
|
+
const checks = [
|
|
206
|
+
{ file: 'package.json', ecosystem: 'npm' },
|
|
207
|
+
{ file: 'requirements.txt', ecosystem: 'PyPI' },
|
|
208
|
+
{ file: 'pyproject.toml', ecosystem: 'PyPI' },
|
|
209
|
+
{ file: 'go.mod', ecosystem: 'Go' },
|
|
210
|
+
{ file: 'pom.xml', ecosystem: 'Maven' },
|
|
211
|
+
{ file: 'build.gradle', ecosystem: 'Maven' },
|
|
212
|
+
{ file: 'Gemfile', ecosystem: 'RubyGems' },
|
|
213
|
+
{ file: 'Cargo.toml', ecosystem: 'crates.io' },
|
|
214
|
+
{ file: 'composer.json', ecosystem: 'Packagist' },
|
|
215
|
+
];
|
|
216
|
+
|
|
217
|
+
return checks
|
|
218
|
+
.filter(c => fs.existsSync(path.join(repoPath, c.file)))
|
|
219
|
+
.map(c => c.ecosystem)
|
|
220
|
+
.filter((v, i, arr) => arr.indexOf(v) === i);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = {
|
|
224
|
+
detectVulnerabilities,
|
|
225
|
+
detectEcosystems,
|
|
226
|
+
isOsvScannerInstalled,
|
|
227
|
+
normalizeOsvReport,
|
|
228
|
+
};
|