supply-chain-attack 0.1.7 → 0.1.8
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/README.md +1 -1
- package/lib/cli.js +101 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,7 +16,7 @@ A cache/store hit means the package was fetched or stored on this machine. A glo
|
|
|
16
16
|
npx supply-chain-attack
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
The CLI prints a compact verdict and exits non-zero when it finds a risky package or suspicious IOC.
|
|
19
|
+
The CLI prints a compact verdict, checks the latest 4 embedded supply-chain attacks against packages found on the machine, shows either the affected libraries present locally or “Nice” for each attack, and exits non-zero when it finds a risky package or suspicious IOC.
|
|
20
20
|
|
|
21
21
|
Interactive terminals also get a small one-line menu:
|
|
22
22
|
|
package/lib/cli.js
CHANGED
|
@@ -120,16 +120,24 @@ function formatResult(result, options = {}) {
|
|
|
120
120
|
lines.push(color.bold(COPY.noFindingTitle));
|
|
121
121
|
lines.push(COPY.noFindingBody);
|
|
122
122
|
lines.push('');
|
|
123
|
+
lines.push(...formatRecentAttackSummary(result, options));
|
|
124
|
+
lines.push('');
|
|
123
125
|
lines.push(`${color.dim('scan')} ${result.locations.length} store(s), ${result.packages.length} package/version pair(s), snapshot ${result.snapshotDate}`);
|
|
124
126
|
return `${lines.join('\n')}\n`;
|
|
125
127
|
}
|
|
126
128
|
|
|
127
|
-
lines.push(
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
lines.push(...formatRecentAttackSummary(result, options));
|
|
130
|
+
|
|
131
|
+
const additionalFindings = findingsOutsideRecentAttacks(result.findings || []);
|
|
132
|
+
if (additionalFindings.length) {
|
|
133
|
+
lines.push('');
|
|
134
|
+
lines.push(color.red(color.bold('Additional matched packages')));
|
|
135
|
+
for (const finding of additionalFindings.slice(0, 8)) {
|
|
136
|
+
const locations = finding.locations.length ? formatCompactList(finding.locations, 3) : 'unknown location';
|
|
137
|
+
lines.push(`- ${color.dim(finding.ecosystem)} ${color.yellow(`${finding.name}@${finding.version}`)} ${color.dim('(')}${color.cyan(locations)}${color.dim(')')}`);
|
|
138
|
+
}
|
|
139
|
+
if (additionalFindings.length > 8) lines.push(`- ...and ${pluralize(additionalFindings.length - 8, 'more package hit')}. Run with --json for raw evidence.`);
|
|
131
140
|
}
|
|
132
|
-
if (result.findings.length > 8) lines.push(`- ...and ${pluralize(result.findings.length - 8, 'more package hit')}. Run with --json for raw evidence.`);
|
|
133
141
|
|
|
134
142
|
if (result.iocs.length) {
|
|
135
143
|
lines.push('');
|
|
@@ -208,44 +216,86 @@ function formatVerdictHeader(result, options = {}) {
|
|
|
208
216
|
|
|
209
217
|
function formatEducation(result, options = {}) {
|
|
210
218
|
const color = createColor(options.color);
|
|
211
|
-
const lines = [color.bold(
|
|
219
|
+
const lines = [color.bold('Learn: attacks explained')];
|
|
220
|
+
const recent = recentAdvisories(4);
|
|
221
|
+
|
|
222
|
+
recent.forEach((advisory, index) => {
|
|
223
|
+
const hits = (result.findings || []).filter((finding) => finding.advisory && finding.advisory.id === advisory.id);
|
|
224
|
+
const artifactCount = (advisory.packages || []).reduce((count, item) => count + (item.versions || []).length, 0);
|
|
225
|
+
|
|
226
|
+
if (index > 0) lines.push('');
|
|
227
|
+
lines.push(color.yellow(`${index + 1}. ${advisory.title}`));
|
|
228
|
+
lines.push(`Published: ${advisory.published}`);
|
|
229
|
+
lines.push(advisory.summary);
|
|
230
|
+
lines.push(`Affected package/version artifacts tracked: ${artifactCount}`);
|
|
231
|
+
|
|
232
|
+
if (!hits.length) {
|
|
233
|
+
const clean = !hasAnyFindings(result);
|
|
234
|
+
const message = clean ? 'Nice. You do not have affected packages from this attack.' : 'No affected packages from this attack.';
|
|
235
|
+
lines.push(clean ? color.green(message) : color.dim(message));
|
|
236
|
+
}
|
|
237
|
+
});
|
|
212
238
|
|
|
213
|
-
if (
|
|
214
|
-
lines.push(
|
|
215
|
-
lines.push(
|
|
216
|
-
lines.push(
|
|
217
|
-
lines.push('The good news: this scan is offline and privacy-safe. Your package list was not uploaded anywhere.');
|
|
218
|
-
return `${lines.join('\n')}\n`;
|
|
239
|
+
if (result.iocs && result.iocs.length) {
|
|
240
|
+
lines.push('');
|
|
241
|
+
lines.push(`${color.red('IOC')} ${color.bold('Suspicious local-file indicators')}`);
|
|
242
|
+
lines.push(COPY.iocWarning);
|
|
219
243
|
}
|
|
220
244
|
|
|
221
|
-
lines.
|
|
245
|
+
return `${lines.join('\n')}\n`;
|
|
246
|
+
}
|
|
222
247
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
const summaries = formatCompactList(group.advisories.map((advisory) => advisory.summary), 2);
|
|
248
|
+
function formatRecentAttackSummary(result, options = {}) {
|
|
249
|
+
const color = createColor(options.color);
|
|
250
|
+
const recent = recentAdvisories(4);
|
|
251
|
+
const [latest, ...previous] = recent;
|
|
252
|
+
const lines = [];
|
|
229
253
|
|
|
230
|
-
|
|
231
|
-
lines.push(color.bold(
|
|
232
|
-
|
|
233
|
-
lines.push(
|
|
234
|
-
for (const step of attackExplanation(group)) lines.push(`- ${step}`);
|
|
235
|
-
lines.push(`Evidence on this machine: ${packages} showed up in ${locations}.`);
|
|
236
|
-
lines.push(`Risk read: ${riskSentence(group.findings)}`);
|
|
237
|
-
lines.push(`How to interpret the location: ${locationMeaning(group.findings)}`);
|
|
238
|
-
lines.push('Why this matters: developer machines and CI usually contain npm/GitHub/cloud/deploy/AI-provider credentials. Malware in a devtool can turn one bad install into stolen tokens, poisoned packages, or compromised deployments.');
|
|
239
|
-
if (receipts) lines.push(`Receipts: ${receipts}`);
|
|
254
|
+
if (latest) {
|
|
255
|
+
lines.push(color.bold('LATEST ATTACK'));
|
|
256
|
+
lines.push(`${color.yellow(latest.title)} ${color.dim(`(${latest.published})`)}`);
|
|
257
|
+
lines.push(...formatAttackHitLines(latest, result, color));
|
|
240
258
|
}
|
|
241
259
|
|
|
242
|
-
if (
|
|
260
|
+
if (previous.length) {
|
|
243
261
|
lines.push('');
|
|
244
|
-
lines.push(
|
|
245
|
-
|
|
262
|
+
lines.push(color.bold('Previous attacks'));
|
|
263
|
+
previous.forEach((advisory, index) => {
|
|
264
|
+
if (index > 0) lines.push('');
|
|
265
|
+
lines.push(`${index + 1}. ${color.yellow(advisory.title)} ${color.dim(`(${advisory.published})`)}`);
|
|
266
|
+
lines.push(...formatAttackHitLines(advisory, result, color));
|
|
267
|
+
});
|
|
246
268
|
}
|
|
247
269
|
|
|
248
|
-
return
|
|
270
|
+
return lines;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function formatAttackHitLines(advisory, result, color) {
|
|
274
|
+
const hits = (result.findings || []).filter((finding) => finding.advisory && finding.advisory.id === advisory.id);
|
|
275
|
+
if (!hits.length) {
|
|
276
|
+
const clean = !hasAnyFindings(result);
|
|
277
|
+
const message = clean ? 'Nice. You do not have affected packages from this attack.' : 'No affected packages from this attack.';
|
|
278
|
+
return [clean ? color.green(message) : color.dim(message)];
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const lines = [color.red(`Affected: ${pluralize(hits.length, 'package')}`), color.bold('Libraries you had:')];
|
|
282
|
+
for (const finding of hits.slice(0, 6)) {
|
|
283
|
+
const locations = formatCompactList(finding.locations || [], 3) || 'unknown location';
|
|
284
|
+
lines.push(`- ${color.dim(finding.ecosystem)} ${color.yellow(`${finding.name}@${finding.version}`)} ${color.dim('(')}${color.cyan(locations)}${color.dim(')')}`);
|
|
285
|
+
}
|
|
286
|
+
if (hits.length > 6) lines.push(`- ...and ${pluralize(hits.length - 6, 'more package hit')}`);
|
|
287
|
+
return lines;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function findingsOutsideRecentAttacks(findings) {
|
|
291
|
+
const recentIds = new Set(recentAdvisories(4).map((advisory) => advisory.id));
|
|
292
|
+
return findings.filter((finding) => !finding.advisory || !recentIds.has(finding.advisory.id));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function recentAdvisories(limit) {
|
|
296
|
+
return [...advisories]
|
|
297
|
+
.sort((a, b) => String(b.published || '').localeCompare(String(a.published || '')))
|
|
298
|
+
.slice(0, limit);
|
|
249
299
|
}
|
|
250
300
|
|
|
251
301
|
function formatNextActions(result, options = {}) {
|
|
@@ -371,15 +421,26 @@ function severityColor(severity, color) {
|
|
|
371
421
|
}
|
|
372
422
|
|
|
373
423
|
function createColor(enabled = DEFAULT_COLOR) {
|
|
424
|
+
// Muted Material-inspired palette: minimal, lower saturation, still readable
|
|
425
|
+
// on dark terminals, while respecting NO_COLOR.
|
|
426
|
+
const palette = {
|
|
427
|
+
red: '210;96;112',
|
|
428
|
+
yellow: '198;128;92',
|
|
429
|
+
green: '150;176;128',
|
|
430
|
+
cyan: '112;164;178',
|
|
431
|
+
magenta: '158;132;176',
|
|
432
|
+
dim: '102;116;128',
|
|
433
|
+
};
|
|
374
434
|
const wrap = (code, value) => enabled ? `\u001b[${code}m${value}\u001b[0m` : String(value);
|
|
435
|
+
const rgb = (name, value) => wrap(`38;2;${palette[name]}`, value);
|
|
375
436
|
return {
|
|
376
437
|
bold: (value) => wrap('1', value),
|
|
377
|
-
dim: (value) =>
|
|
378
|
-
green: (value) =>
|
|
379
|
-
red: (value) =>
|
|
380
|
-
yellow: (value) =>
|
|
381
|
-
cyan: (value) =>
|
|
382
|
-
magenta: (value) =>
|
|
438
|
+
dim: (value) => rgb('dim', value),
|
|
439
|
+
green: (value) => rgb('green', value),
|
|
440
|
+
red: (value) => rgb('red', value),
|
|
441
|
+
yellow: (value) => rgb('yellow', value),
|
|
442
|
+
cyan: (value) => rgb('cyan', value),
|
|
443
|
+
magenta: (value) => rgb('magenta', value),
|
|
383
444
|
};
|
|
384
445
|
}
|
|
385
446
|
|
|
@@ -597,5 +658,6 @@ module.exports = {
|
|
|
597
658
|
parseArgs,
|
|
598
659
|
formatResult,
|
|
599
660
|
formatEducation,
|
|
661
|
+
formatRecentAttackSummary,
|
|
600
662
|
formatNextActions,
|
|
601
663
|
};
|