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.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/lib/cli.js +101 -39
  3. 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(color.red(color.bold(COPY.findingsTitle)));
128
- for (const finding of result.findings.slice(0, 8)) {
129
- const locations = finding.locations.length ? formatCompactList(finding.locations, 3) : 'unknown location';
130
- lines.push(`- ${color.dim(finding.ecosystem)} ${color.yellow(color.bold(`${finding.name}@${finding.version}`))} ${color.dim('(')}${color.cyan(locations)}${color.dim(')')}`);
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(COPY.educationTitle)];
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 (!hasAnyFindings(result)) {
214
- lines.push(COPY.noMatchExplanation);
215
- lines.push(COPY.noGuarantee);
216
- lines.push('How to read this: a clean result lowers concern for the specific campaigns in this snapshot, but it does not audit every dependency, shell history entry, CI run, browser extension, or secret on the machine.');
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.push('This is not a normal vulnerability finding. Supply-chain malware is dangerous because install scripts, package CLIs, and postinstall hooks can run before your app imports anything. The important question is: was the package merely cached, or did attacker-controlled code likely run in a place with secrets?');
245
+ return `${lines.join('\n')}\n`;
246
+ }
222
247
 
223
- const groups = groupFindingsByCampaign(result.findings);
224
- for (const group of groups) {
225
- const packages = formatCompactList(group.findings.map((finding) => `${finding.name}@${finding.version}`), 7);
226
- const locations = formatCompactList(group.findings.flatMap((finding) => finding.locations || []), 5) || 'unknown location';
227
- const receipts = formatCompactList(group.advisories.map((advisory) => advisory.source), 3);
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
- lines.push('');
231
- lines.push(color.bold(`${group.name} attack`));
232
- if (summaries) lines.push(`What was reported: ${summaries}`);
233
- lines.push('Attack chain:');
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 (result.iocs.length) {
260
+ if (previous.length) {
243
261
  lines.push('');
244
- lines.push(`${color.red('IOC')} ${color.bold('Suspicious local-file indicators')}`);
245
- lines.push(COPY.iocWarning);
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 `${lines.join('\n')}\n`;
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) => wrap('2', value),
378
- green: (value) => wrap('32', value),
379
- red: (value) => wrap('31', value),
380
- yellow: (value) => wrap('33', value),
381
- cyan: (value) => wrap('36', value),
382
- magenta: (value) => wrap('35', 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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "supply-chain-attack",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Scan local package-manager state for known supply-chain attack indicators.",
5
5
  "license": "MIT",
6
6
  "repository": {