preship 1.0.4 → 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/dist/cli.js CHANGED
@@ -29,6 +29,8 @@ var import_commander = require("commander");
29
29
  var path = __toESM(require("path"));
30
30
  var import_core = require("@preship/core");
31
31
  var import_license = require("@preship/license");
32
+ var import_security = require("@preship/security");
33
+ var import_secrets = require("@preship/secrets");
32
34
  async function scan(options) {
33
35
  const startTime = Date.now();
34
36
  const projectPath = path.resolve(options?.projectPath ?? process.cwd());
@@ -84,6 +86,121 @@ async function scan(options) {
84
86
  scanDurationMs
85
87
  };
86
88
  }
89
+ async function unifiedScan(options) {
90
+ const startTime = Date.now();
91
+ const projectPath = path.resolve(options?.projectPath ?? process.cwd());
92
+ let config = (0, import_core.loadConfig)(projectPath);
93
+ if (options?.config) {
94
+ config = {
95
+ ...config,
96
+ ...options.config,
97
+ modules: { ...config.modules, ...options.config.modules },
98
+ security: { ...config.security, ...options.config.security },
99
+ secrets: { ...config.secrets, ...options.config.secrets }
100
+ };
101
+ }
102
+ const modules = {
103
+ license: config.modules?.license ?? true,
104
+ security: config.modules?.security ?? true,
105
+ secrets: config.modules?.secrets ?? true
106
+ };
107
+ if (!modules.license && !modules.security && !modules.secrets) {
108
+ console.warn("\u26A0\uFE0F No scan modules enabled. Use --no-license, --no-security, --no-secrets to selectively disable.");
109
+ }
110
+ const mode = config.mode ?? "auto";
111
+ const policy = config.policy ?? "commercial-safe";
112
+ const projects = (0, import_core.detectProjects)(projectPath);
113
+ const project = projects[0];
114
+ const packageJsonPath = path.join(project.path, "package.json");
115
+ let parsedDeps;
116
+ switch (project.type) {
117
+ case "npm":
118
+ parsedDeps = (0, import_core.parseNpmLockfile)(project.lockFile, packageJsonPath);
119
+ break;
120
+ case "yarn":
121
+ parsedDeps = (0, import_core.parseYarnLockfile)(project.lockFile, packageJsonPath);
122
+ break;
123
+ case "pnpm":
124
+ parsedDeps = (0, import_core.parsePnpmLockfile)(project.lockFile, packageJsonPath);
125
+ break;
126
+ default:
127
+ throw new Error(`Unsupported project type: ${project.type}`);
128
+ }
129
+ if (!config.scanDevDependencies) {
130
+ parsedDeps = parsedDeps.filter((dep) => !dep.isDevDependency);
131
+ }
132
+ let licenseResult;
133
+ let securityResult;
134
+ let secretsResult;
135
+ const promises = [];
136
+ if (modules.license) {
137
+ promises.push(
138
+ (async () => {
139
+ const licenseStart = Date.now();
140
+ const dependencies = await (0, import_license.resolveLicenses)(parsedDeps, projectPath, {
141
+ mode,
142
+ networkTimeout: config.networkTimeout ?? 5e3,
143
+ networkConcurrency: config.networkConcurrency ?? 10,
144
+ cache: config.cache ?? true,
145
+ cacheTTL: config.cacheTTL ?? 604800,
146
+ scanTimeout: config.scanTimeout ?? 6e4
147
+ });
148
+ const results = (0, import_license.evaluatePolicy)(dependencies, config);
149
+ const allowed = results.filter((r) => r.verdict === "allowed");
150
+ const warned = results.filter((r) => r.verdict === "warned");
151
+ const rejected = results.filter((r) => r.verdict === "rejected");
152
+ const unknown = results.filter((r) => r.verdict === "unknown");
153
+ licenseResult = {
154
+ projectPath,
155
+ projectType: project.type,
156
+ framework: project.framework,
157
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
158
+ totalPackages: dependencies.length,
159
+ allowed,
160
+ warned,
161
+ rejected,
162
+ unknown,
163
+ passed: rejected.length === 0,
164
+ scanDurationMs: Date.now() - licenseStart
165
+ };
166
+ })()
167
+ );
168
+ }
169
+ if (modules.security) {
170
+ promises.push(
171
+ (async () => {
172
+ const securityConfig = (0, import_security.mergeSecurityConfig)(config.security);
173
+ securityResult = await (0, import_security.scanSecurity)(parsedDeps, projectPath, securityConfig, mode);
174
+ })()
175
+ );
176
+ }
177
+ if (modules.secrets) {
178
+ promises.push(
179
+ (async () => {
180
+ secretsResult = await (0, import_secrets.scanSecrets)(projectPath, config.secrets);
181
+ })()
182
+ );
183
+ }
184
+ await Promise.all(promises);
185
+ const allPassed = (licenseResult?.passed ?? true) && (securityResult?.passed ?? true) && (secretsResult?.passed ?? true);
186
+ const totalScanDurationMs = Date.now() - startTime;
187
+ return {
188
+ version: "2.0.0",
189
+ projectPath,
190
+ projectType: project.type,
191
+ framework: project.framework,
192
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
193
+ passed: allPassed,
194
+ mode,
195
+ policy,
196
+ modules: {
197
+ license: licenseResult,
198
+ security: securityResult,
199
+ secrets: secretsResult
200
+ },
201
+ totalScanDurationMs
202
+ };
203
+ }
87
204
 
88
205
  // src/output/terminal.ts
89
206
  var import_chalk = __toESM(require("chalk"));
@@ -95,9 +212,9 @@ function formatTerminalScan(result, warnOnly = false) {
95
212
  }
96
213
  lines.push("");
97
214
  lines.push(import_chalk.default.bold("\u{1F50D} PreShip: Scanning project..."));
98
- lines.push(` Project: ${import_chalk.default.cyan(getProjectName(result))}${result.framework ? ` (${result.framework})` : ""}`);
99
- lines.push(` Lock file: ${import_chalk.default.cyan(getLockFileName(result))} (${result.projectType})`);
100
- lines.push(` Policy: ${import_chalk.default.cyan(getPolicyName(result))}`);
215
+ lines.push(` Project: ${import_chalk.default.cyan(getProjectName(result.projectPath))}${result.framework ? ` (${result.framework})` : ""}`);
216
+ lines.push(` Lock file: ${import_chalk.default.cyan(getLockFileName(result.projectType))} (${result.projectType})`);
217
+ lines.push(` Policy: ${import_chalk.default.cyan(getPolicyName())}`);
101
218
  lines.push("");
102
219
  lines.push(`\u{1F4E6} Scanned ${import_chalk.default.bold(String(result.totalPackages))} packages in ${(result.scanDurationMs / 1e3).toFixed(1)}s`);
103
220
  lines.push("");
@@ -146,7 +263,7 @@ function formatTerminalList(result) {
146
263
  const allResults = [...result.allowed, ...result.warned, ...result.rejected, ...result.unknown];
147
264
  allResults.sort((a, b) => a.dependency.name.localeCompare(b.dependency.name));
148
265
  lines.push("");
149
- lines.push(`\u{1F4E6} ${result.totalPackages} packages in ${getProjectName(result)}`);
266
+ lines.push(`\u{1F4E6} ${result.totalPackages} packages in ${getProjectName(result.projectPath)}`);
150
267
  lines.push("");
151
268
  const table = new import_cli_table3.default({
152
269
  head: ["Package", "Version", "License", "Source", "Status"],
@@ -181,6 +298,135 @@ function formatTerminalList(result) {
181
298
  lines.push("");
182
299
  return lines.join("\n");
183
300
  }
301
+ function formatTerminalUnified(result, warnOnly = false) {
302
+ const lines = [];
303
+ if (warnOnly) {
304
+ return formatWarnOnlyUnified(result);
305
+ }
306
+ lines.push("");
307
+ lines.push(import_chalk.default.bold("\u{1F50D} PreShip: Pre-ship verification"));
308
+ lines.push(` Project: ${import_chalk.default.cyan(getProjectName(result.projectPath))}${result.framework ? ` (${result.framework})` : ""}`);
309
+ lines.push(` Lock file: ${import_chalk.default.cyan(getLockFileName(result.projectType))} (${result.projectType})`);
310
+ lines.push(` Policy: ${import_chalk.default.cyan(result.policy)}`);
311
+ lines.push(` Mode: ${import_chalk.default.cyan(result.mode)}`);
312
+ const enabledModules = [];
313
+ if (result.modules.license) enabledModules.push("license");
314
+ if (result.modules.security) enabledModules.push("security");
315
+ if (result.modules.secrets) enabledModules.push("secrets");
316
+ if (enabledModules.length === 0) {
317
+ lines.push(` Modules: ${import_chalk.default.yellow("none (all disabled)")}`);
318
+ lines.push("");
319
+ lines.push(import_chalk.default.yellow("\u26A0\uFE0F No scan modules are enabled. Nothing to check."));
320
+ lines.push(import_chalk.default.dim(" Enable modules in preship-config.yml or remove --no-* flags."));
321
+ lines.push("");
322
+ lines.push("\u2501".repeat(60));
323
+ lines.push(import_chalk.default.yellow.bold("\u26A0\uFE0F RESULT: No modules enabled \u2014 scan skipped"));
324
+ lines.push("");
325
+ return lines.join("\n");
326
+ }
327
+ lines.push(` Modules: ${import_chalk.default.cyan(enabledModules.join(", "))}`);
328
+ lines.push("");
329
+ if (result.modules.license) {
330
+ const license = result.modules.license;
331
+ lines.push(import_chalk.default.bold.underline("\u{1F4DC} License Compliance"));
332
+ lines.push(` Scanned ${import_chalk.default.bold(String(license.totalPackages))} packages in ${(license.scanDurationMs / 1e3).toFixed(1)}s`);
333
+ lines.push("");
334
+ if (license.rejected.length === 0 && license.warned.length === 0 && license.unknown.length === 0) {
335
+ lines.push(import_chalk.default.green(` \u2705 All ${license.totalPackages} packages passed license check`));
336
+ } else {
337
+ if (license.allowed.length > 0) {
338
+ lines.push(import_chalk.default.green(` \u2705 ${license.allowed.length} packages \u2014 Allowed`));
339
+ }
340
+ if (license.warned.length > 0) {
341
+ lines.push(import_chalk.default.yellow(` \u26A0\uFE0F ${license.warned.length} package${license.warned.length > 1 ? "s" : ""} \u2014 Warning (weak copyleft)`));
342
+ formatPolicyResultsIndented(license.warned, lines, import_chalk.default.yellow);
343
+ }
344
+ if (license.rejected.length > 0) {
345
+ lines.push(import_chalk.default.red(` \u274C ${license.rejected.length} package${license.rejected.length > 1 ? "s" : ""} \u2014 Rejected`));
346
+ formatPolicyResultsIndented(license.rejected, lines, import_chalk.default.red);
347
+ }
348
+ if (license.unknown.length > 0) {
349
+ lines.push(import_chalk.default.yellow(` \u26A0\uFE0F ${license.unknown.length} package${license.unknown.length > 1 ? "s" : ""} \u2014 Unknown License`));
350
+ formatPolicyResultsIndented(license.unknown, lines, import_chalk.default.yellow);
351
+ }
352
+ }
353
+ lines.push("");
354
+ }
355
+ if (result.modules.security) {
356
+ const security = result.modules.security;
357
+ lines.push(import_chalk.default.bold.underline("\u{1F6E1}\uFE0F Security Vulnerabilities"));
358
+ lines.push(` Scanned ${import_chalk.default.bold(String(security.totalPackages))} packages in ${(security.scanDurationMs / 1e3).toFixed(1)}s`);
359
+ lines.push("");
360
+ if (security.findings.length === 0) {
361
+ lines.push(import_chalk.default.green(" \u2705 No security issues found"));
362
+ } else {
363
+ const statParts = [];
364
+ if (security.stats.critical > 0) statParts.push(import_chalk.default.red(`${security.stats.critical} critical`));
365
+ if (security.stats.high > 0) statParts.push(import_chalk.default.red(`${security.stats.high} high`));
366
+ if (security.stats.medium > 0) statParts.push(import_chalk.default.yellow(`${security.stats.medium} medium`));
367
+ if (security.stats.low > 0) statParts.push(import_chalk.default.dim(`${security.stats.low} low`));
368
+ if (security.stats.deprecated > 0) statParts.push(import_chalk.default.yellow(`${security.stats.deprecated} deprecated`));
369
+ if (security.stats.outdated > 0) statParts.push(import_chalk.default.yellow(`${security.stats.outdated} outdated`));
370
+ if (security.stats.unmaintained > 0) statParts.push(import_chalk.default.yellow(`${security.stats.unmaintained} unmaintained`));
371
+ lines.push(` ${statParts.join(" \u2502 ")}`);
372
+ lines.push("");
373
+ const vulnFindings = security.findings.filter((f) => f.type === "vulnerability");
374
+ const healthFindings = security.findings.filter((f) => f.type !== "vulnerability");
375
+ if (vulnFindings.length > 0) {
376
+ formatSecurityFindings(vulnFindings, lines, "Vulnerabilities");
377
+ }
378
+ if (healthFindings.length > 0) {
379
+ formatSecurityFindings(healthFindings, lines, "Health Issues");
380
+ }
381
+ }
382
+ lines.push("");
383
+ }
384
+ if (result.modules.secrets) {
385
+ const secrets = result.modules.secrets;
386
+ lines.push(import_chalk.default.bold.underline("\u{1F511} Secret Detection"));
387
+ lines.push(` Scanned ${import_chalk.default.bold(String(secrets.filesScanned))} files in ${(secrets.scanDurationMs / 1e3).toFixed(1)}s`);
388
+ lines.push("");
389
+ if (secrets.findings.length === 0) {
390
+ lines.push(import_chalk.default.green(" \u2705 No leaked secrets found"));
391
+ } else {
392
+ const statParts = [];
393
+ if (secrets.stats.critical > 0) statParts.push(import_chalk.default.red(`${secrets.stats.critical} critical`));
394
+ if (secrets.stats.high > 0) statParts.push(import_chalk.default.red(`${secrets.stats.high} high`));
395
+ if (secrets.stats.medium > 0) statParts.push(import_chalk.default.yellow(`${secrets.stats.medium} medium`));
396
+ if (secrets.stats.low > 0) statParts.push(import_chalk.default.dim(`${secrets.stats.low} low`));
397
+ lines.push(` ${statParts.join(" \u2502 ")}`);
398
+ lines.push("");
399
+ formatSecretFindings(secrets.findings, lines);
400
+ }
401
+ lines.push("");
402
+ }
403
+ lines.push("\u2501".repeat(60));
404
+ if (result.passed) {
405
+ lines.push(import_chalk.default.green.bold(`\u2705 RESULT: All checks passed (${(result.totalScanDurationMs / 1e3).toFixed(1)}s)`));
406
+ } else {
407
+ const failedModules = [];
408
+ if (result.modules.license && !result.modules.license.passed) failedModules.push("license");
409
+ if (result.modules.security && !result.modules.security.passed) failedModules.push("security");
410
+ if (result.modules.secrets && !result.modules.secrets.passed) failedModules.push("secrets");
411
+ lines.push(import_chalk.default.red.bold(`\u274C RESULT: Failed \u2014 ${failedModules.join(", ")} check${failedModules.length > 1 ? "s" : ""} did not pass`));
412
+ lines.push("");
413
+ lines.push(import_chalk.default.dim("\u{1F4A1} To fix:"));
414
+ if (result.modules.license && !result.modules.license.passed) {
415
+ lines.push(import_chalk.default.dim(" \u2022 Remove or replace rejected packages"));
416
+ lines.push(import_chalk.default.dim(' \u2022 Or add exceptions: preship allow <package> --reason "justification"'));
417
+ }
418
+ if (result.modules.security && !result.modules.security.passed) {
419
+ lines.push(import_chalk.default.dim(" \u2022 Update packages with known vulnerabilities"));
420
+ lines.push(import_chalk.default.dim(" \u2022 Replace deprecated or unmaintained packages"));
421
+ }
422
+ if (result.modules.secrets && !result.modules.secrets.passed) {
423
+ lines.push(import_chalk.default.dim(" \u2022 Remove leaked secrets from source code"));
424
+ lines.push(import_chalk.default.dim(" \u2022 Use environment variables or secret managers instead"));
425
+ }
426
+ }
427
+ lines.push("");
428
+ return lines.join("\n");
429
+ }
184
430
  function formatWarnOnly(result) {
185
431
  const lines = [];
186
432
  lines.push("\u{1F4E6} PreShip: Quick license check...");
@@ -197,6 +443,39 @@ function formatWarnOnly(result) {
197
443
  }
198
444
  return lines.join("\n");
199
445
  }
446
+ function formatWarnOnlyUnified(result) {
447
+ const lines = [];
448
+ lines.push("\u{1F4E6} PreShip: Quick check...");
449
+ lines.push("");
450
+ let issueCount = 0;
451
+ if (result.modules.license) {
452
+ for (const r of [...result.modules.license.rejected, ...result.modules.license.warned]) {
453
+ lines.push(`\u26A0\uFE0F [license] ${r.dependency.name}@${r.dependency.version} \u2014 ${r.dependency.license} (${r.reason})`);
454
+ issueCount++;
455
+ }
456
+ }
457
+ if (result.modules.security) {
458
+ for (const f of result.modules.security.findings) {
459
+ const icon = f.severity === "critical" || f.severity === "high" ? "\u274C" : "\u26A0\uFE0F";
460
+ lines.push(`${icon} [security] ${f.package}@${f.version} \u2014 ${f.message}`);
461
+ issueCount++;
462
+ }
463
+ }
464
+ if (result.modules.secrets) {
465
+ for (const f of result.modules.secrets.findings) {
466
+ const icon = f.severity === "critical" || f.severity === "high" ? "\u274C" : "\u26A0\uFE0F";
467
+ lines.push(`${icon} [secrets] ${f.file}:${f.line} \u2014 ${f.description} (${f.match})`);
468
+ issueCount++;
469
+ }
470
+ }
471
+ if (issueCount === 0) {
472
+ lines.push(`\u2705 All checks passed. Run 'preship scan' for full report.`);
473
+ } else {
474
+ lines.push("");
475
+ lines.push(`${issueCount} issue${issueCount > 1 ? "s" : ""} found. Run 'preship scan' for full report.`);
476
+ }
477
+ return lines.join("\n");
478
+ }
200
479
  function formatPolicyResults(results, lines, colorFn) {
201
480
  for (let i = 0; i < results.length; i++) {
202
481
  const r = results[i];
@@ -207,6 +486,62 @@ function formatPolicyResults(results, lines, colorFn) {
207
486
  lines.push(import_chalk.default.dim(`${reasonPrefix}Reason: ${getShortReason(r)}`));
208
487
  }
209
488
  }
489
+ function formatPolicyResultsIndented(results, lines, colorFn) {
490
+ for (let i = 0; i < results.length; i++) {
491
+ const r = results[i];
492
+ const isLast = i === results.length - 1;
493
+ const prefix = isLast ? " \u2514\u2500\u2500 " : " \u251C\u2500\u2500 ";
494
+ const reasonPrefix = isLast ? " " : " \u2502 ";
495
+ lines.push(colorFn(`${prefix}${r.dependency.name}@${r.dependency.version} \u2014 ${r.dependency.license}`));
496
+ lines.push(import_chalk.default.dim(`${reasonPrefix}Reason: ${getShortReason(r)}`));
497
+ }
498
+ }
499
+ function formatSecurityFindings(findings, lines, heading) {
500
+ lines.push(import_chalk.default.dim(` ${heading}:`));
501
+ for (let i = 0; i < findings.length; i++) {
502
+ const f = findings[i];
503
+ const isLast = i === findings.length - 1;
504
+ const prefix = isLast ? " \u2514\u2500\u2500 " : " \u251C\u2500\u2500 ";
505
+ const severityColor = getSeverityColor(f.severity);
506
+ const severityBadge = severityColor(`[${f.severity.toUpperCase()}]`);
507
+ lines.push(`${prefix}${severityBadge} ${f.package}@${f.version}`);
508
+ lines.push(import_chalk.default.dim(`${isLast ? " " : " \u2502 "}${f.message}`));
509
+ }
510
+ }
511
+ function formatSecretFindings(findings, lines) {
512
+ const byFile = /* @__PURE__ */ new Map();
513
+ for (const f of findings) {
514
+ const existing = byFile.get(f.file) || [];
515
+ existing.push(f);
516
+ byFile.set(f.file, existing);
517
+ }
518
+ for (const [file, fileFindings] of byFile) {
519
+ lines.push(import_chalk.default.dim(` ${file}:`));
520
+ for (let i = 0; i < fileFindings.length; i++) {
521
+ const f = fileFindings[i];
522
+ const isLast = i === fileFindings.length - 1;
523
+ const prefix = isLast ? " \u2514\u2500\u2500 " : " \u251C\u2500\u2500 ";
524
+ const severityColor = getSeverityColor(f.severity);
525
+ const severityBadge = severityColor(`[${f.severity.toUpperCase()}]`);
526
+ lines.push(`${prefix}${severityBadge} Line ${f.line}: ${f.description}`);
527
+ lines.push(import_chalk.default.dim(`${isLast ? " " : " \u2502 "}Match: ${f.match}`));
528
+ }
529
+ }
530
+ }
531
+ function getSeverityColor(severity) {
532
+ switch (severity) {
533
+ case "critical":
534
+ return import_chalk.default.red.bold;
535
+ case "high":
536
+ return import_chalk.default.red;
537
+ case "medium":
538
+ return import_chalk.default.yellow;
539
+ case "low":
540
+ return import_chalk.default.dim;
541
+ default:
542
+ return import_chalk.default.white;
543
+ }
544
+ }
210
545
  function getShortReason(r) {
211
546
  if (r.dependency.license === "UNKNOWN") return "unable to detect license";
212
547
  if (r.verdict === "rejected") return "strong copyleft \u2014 requires source disclosure";
@@ -249,12 +584,12 @@ function getSourceLabel(source) {
249
584
  return "\u2014";
250
585
  }
251
586
  }
252
- function getProjectName(result) {
253
- const parts = result.projectPath.split("/");
587
+ function getProjectName(projectPath) {
588
+ const parts = projectPath.split("/");
254
589
  return parts[parts.length - 1] || "project";
255
590
  }
256
- function getLockFileName(result) {
257
- switch (result.projectType) {
591
+ function getLockFileName(projectType) {
592
+ switch (projectType) {
258
593
  case "npm":
259
594
  return "package-lock.json";
260
595
  case "yarn":
@@ -265,7 +600,7 @@ function getLockFileName(result) {
265
600
  return "unknown";
266
601
  }
267
602
  }
268
- function getPolicyName(_result) {
603
+ function getPolicyName() {
269
604
  return "commercial-safe";
270
605
  }
271
606
 
@@ -287,6 +622,9 @@ function formatJsonList(result) {
287
622
  }));
288
623
  return JSON.stringify({ totalPackages: result.totalPackages, dependencies: deps }, null, 2);
289
624
  }
625
+ function formatJsonUnified(result) {
626
+ return JSON.stringify(result, null, 2);
627
+ }
290
628
 
291
629
  // src/output/csv.ts
292
630
  function formatCsvScan(result) {
@@ -310,6 +648,72 @@ function formatCsvScan(result) {
310
648
  function formatCsvList(result) {
311
649
  return formatCsvScan(result);
312
650
  }
651
+ function formatCsvUnified(result) {
652
+ const sections = [];
653
+ if (!result.modules.license && !result.modules.security && !result.modules.secrets) {
654
+ return "# No modules enabled \u2014 scan skipped";
655
+ }
656
+ if (result.modules.license) {
657
+ const lines = [];
658
+ lines.push("# License Compliance");
659
+ lines.push("Module,Package,Version,License,Source,Verdict,Reason,IsDirect,IsDevDependency");
660
+ const allResults = [
661
+ ...result.modules.license.allowed,
662
+ ...result.modules.license.warned,
663
+ ...result.modules.license.rejected,
664
+ ...result.modules.license.unknown
665
+ ];
666
+ for (const r of allResults) {
667
+ lines.push([
668
+ "license",
669
+ escapeCsv(r.dependency.name),
670
+ escapeCsv(r.dependency.version),
671
+ escapeCsv(r.dependency.license),
672
+ escapeCsv(r.dependency.licenseSource),
673
+ escapeCsv(r.verdict),
674
+ escapeCsv(r.reason),
675
+ String(r.dependency.isDirect),
676
+ String(r.dependency.isDevDependency)
677
+ ].join(","));
678
+ }
679
+ sections.push(lines.join("\n"));
680
+ }
681
+ if (result.modules.security) {
682
+ const lines = [];
683
+ lines.push("# Security Findings");
684
+ lines.push("Module,Package,Version,Type,Severity,Message");
685
+ for (const f of result.modules.security.findings) {
686
+ lines.push([
687
+ "security",
688
+ escapeCsv(f.package),
689
+ escapeCsv(f.version),
690
+ escapeCsv(f.type),
691
+ escapeCsv(f.severity),
692
+ escapeCsv(f.message)
693
+ ].join(","));
694
+ }
695
+ sections.push(lines.join("\n"));
696
+ }
697
+ if (result.modules.secrets) {
698
+ const lines = [];
699
+ lines.push("# Secret Detection Findings");
700
+ lines.push("Module,File,Line,Column,RuleId,Severity,Description,Match");
701
+ for (const f of result.modules.secrets.findings) {
702
+ lines.push([
703
+ "secrets",
704
+ escapeCsv(f.file),
705
+ String(f.line),
706
+ String(f.column),
707
+ escapeCsv(f.ruleId),
708
+ escapeCsv(f.severity),
709
+ escapeCsv(f.description),
710
+ escapeCsv(f.match)
711
+ ].join(","));
712
+ }
713
+ sections.push(lines.join("\n"));
714
+ }
715
+ return sections.join("\n\n");
716
+ }
313
717
  function escapeCsv(value) {
314
718
  if (value.includes(",") || value.includes('"') || value.includes("\n")) {
315
719
  return `"${value.replace(/"/g, '""')}"`;
@@ -319,9 +723,47 @@ function escapeCsv(value) {
319
723
 
320
724
  // src/commands/scan.ts
321
725
  function registerScanCommand(program2) {
322
- program2.command("scan").description("Scan the current project for license violations").option("--format <type>", "Output format: table, json, csv", "table").option("--strict", "Exit code 1 on warnings too (not just rejections)").option("--dev", "Include devDependencies in scan").option("--project <path>", "Path to project root", process.cwd()).option("--config <path>", "Path to config file").option("--warn-only", "Never exit with error (for postinstall hook usage)").option("--silent", "No output, only exit code").option("--mode <type>", "Resolution mode: auto (online+local fallback), online (registry only), local (offline only)", "auto").option("--no-cache", "Disable license cache (.preship-cache.json)").option("--cache-ttl <seconds>", "Cache TTL in seconds (default: 604800 = 7 days)", parseInt).option("--scan-timeout <ms>", "Total scan timeout in milliseconds (default: 60000, 0 = disabled)", parseInt).action(async (options) => {
726
+ program2.command("scan").description("Scan the current project for license, security, and secret issues").option("--format <type>", "Output format: table, json, csv", "table").option("--strict", "Exit code 1 on warnings too (not just rejections)").option("--dev", "Include devDependencies in scan").option("--project <path>", "Path to project root", process.cwd()).option("--config <path>", "Path to config file").option("--warn-only", "Never exit with error (for postinstall hook usage)").option("--silent", "No output, only exit code").option("--mode <type>", "Resolution mode: auto (online+local fallback), online (registry only), local (offline only)", "auto").option("--no-cache", "Disable license cache (.preship-cache.json)").option("--cache-ttl <seconds>", "Cache TTL in seconds (default: 604800 = 7 days)", parseInt).option("--scan-timeout <ms>", "Total scan timeout in milliseconds (default: 60000, 0 = disabled)", parseInt).option("--license-only", "Run license scan only (legacy mode)").option("--no-license", "Disable license scanning").option("--no-security", "Disable security vulnerability scanning").option("--no-secrets", "Disable secret detection scanning").option("--security-severity <level>", "Security policy level: default, strict, lenient").option("--security-fail-on <severity>", "Minimum severity to fail: critical, high, medium, low").action(async (options) => {
323
727
  try {
324
- const result = await scan({
728
+ if (options.licenseOnly) {
729
+ const result2 = await scan({
730
+ projectPath: options.project,
731
+ config: {
732
+ scanDevDependencies: options.dev || void 0,
733
+ output: options.format !== "table" ? options.format : void 0,
734
+ mode: options.mode !== "auto" ? options.mode : void 0,
735
+ cache: options.cache !== void 0 ? options.cache : void 0,
736
+ cacheTTL: options.cacheTtl !== void 0 ? options.cacheTtl : void 0,
737
+ scanTimeout: options.scanTimeout !== void 0 ? options.scanTimeout : void 0
738
+ }
739
+ });
740
+ if (!options.silent) {
741
+ switch (options.format) {
742
+ case "json":
743
+ console.log(formatJsonScan(result2));
744
+ break;
745
+ case "csv":
746
+ console.log(formatCsvScan(result2));
747
+ break;
748
+ default:
749
+ console.log(formatTerminalScan(result2, options.warnOnly));
750
+ break;
751
+ }
752
+ }
753
+ if (options.warnOnly) {
754
+ process.exit(0);
755
+ return;
756
+ }
757
+ if (result2.rejected.length > 0) {
758
+ process.exit(1);
759
+ } else if (options.strict && result2.warned.length > 0) {
760
+ process.exit(1);
761
+ } else {
762
+ process.exit(0);
763
+ }
764
+ return;
765
+ }
766
+ const result = await unifiedScan({
325
767
  projectPath: options.project,
326
768
  config: {
327
769
  scanDevDependencies: options.dev || void 0,
@@ -329,19 +771,30 @@ function registerScanCommand(program2) {
329
771
  mode: options.mode !== "auto" ? options.mode : void 0,
330
772
  cache: options.cache !== void 0 ? options.cache : void 0,
331
773
  cacheTTL: options.cacheTtl !== void 0 ? options.cacheTtl : void 0,
332
- scanTimeout: options.scanTimeout !== void 0 ? options.scanTimeout : void 0
774
+ scanTimeout: options.scanTimeout !== void 0 ? options.scanTimeout : void 0,
775
+ // Module toggles from CLI flags
776
+ modules: {
777
+ license: options.license !== void 0 ? options.license : void 0,
778
+ security: options.security !== void 0 ? options.security : void 0,
779
+ secrets: options.secrets !== void 0 ? options.secrets : void 0
780
+ },
781
+ // Security config from CLI flags
782
+ security: {
783
+ severity: options.securitySeverity || void 0,
784
+ failOn: options.securityFailOn || void 0
785
+ }
333
786
  }
334
787
  });
335
788
  if (!options.silent) {
336
789
  switch (options.format) {
337
790
  case "json":
338
- console.log(formatJsonScan(result));
791
+ console.log(formatJsonUnified(result));
339
792
  break;
340
793
  case "csv":
341
- console.log(formatCsvScan(result));
794
+ console.log(formatCsvUnified(result));
342
795
  break;
343
796
  default:
344
- console.log(formatTerminalScan(result, options.warnOnly));
797
+ console.log(formatTerminalUnified(result, options.warnOnly));
345
798
  break;
346
799
  }
347
800
  }
@@ -349,10 +802,17 @@ function registerScanCommand(program2) {
349
802
  process.exit(0);
350
803
  return;
351
804
  }
352
- if (result.rejected.length > 0) {
353
- process.exit(1);
354
- } else if (options.strict && result.warned.length > 0) {
805
+ if (!result.passed) {
355
806
  process.exit(1);
807
+ } else if (options.strict) {
808
+ const hasLicenseWarnings = (result.modules.license?.warned?.length ?? 0) > 0;
809
+ const hasSecurityFindings = (result.modules.security?.findings?.length ?? 0) > 0;
810
+ const hasSecretFindings = (result.modules.secrets?.findings?.length ?? 0) > 0;
811
+ if (hasLicenseWarnings || hasSecurityFindings || hasSecretFindings) {
812
+ process.exit(1);
813
+ } else {
814
+ process.exit(0);
815
+ }
356
816
  } else {
357
817
  process.exit(0);
358
818
  }
@@ -370,7 +830,7 @@ function registerScanCommand(program2) {
370
830
  var fs = __toESM(require("fs"));
371
831
  var path2 = __toESM(require("path"));
372
832
  function registerInitCommand(program2) {
373
- program2.command("init").description("Set up PreShip in the current project").option("--policy <name>", "Policy template: commercial-safe, strict, permissive-only", "commercial-safe").option("--skip-hooks", "Don't add npm lifecycle hooks to package.json").action((options) => {
833
+ program2.command("init").description("Set up PreShip in the current project").option("--policy <name>", "Policy template: commercial-safe, saas-safe, distribution-safe, strict, permissive-only", "commercial-safe").option("--skip-hooks", "Don't add npm lifecycle hooks to package.json").action((options) => {
374
834
  try {
375
835
  const projectPath = process.cwd();
376
836
  const configContent = generateConfigContent(options.policy);
@@ -407,7 +867,7 @@ function registerInitCommand(program2) {
407
867
  });
408
868
  }
409
869
  function generateConfigContent(policy) {
410
- return `# PreShip License Compliance Configuration
870
+ return `# PreShip Configuration
411
871
  # Docs: https://github.com/dipen-code/preship
412
872
  #
413
873
  # Policy: ${policy}
@@ -415,6 +875,14 @@ ${getPolicyDescription(policy)}
415
875
 
416
876
  policy: ${policy}
417
877
 
878
+ # ===== Module Toggles =====
879
+ # Enable or disable individual scan modules (all enabled by default)
880
+ # modules:
881
+ # license: true
882
+ # security: true
883
+ # secrets: true
884
+
885
+ # ===== License Compliance =====
418
886
  # Scan devDependencies too? (default: false)
419
887
  # scanDevDependencies: false
420
888
 
@@ -435,12 +903,32 @@ policy: ${policy}
435
903
  # reason: "Used internally only, not distributed"
436
904
  # approvedBy: your-name
437
905
  # date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
906
+
907
+ # ===== Security Scanning =====
908
+ # security:
909
+ # severity: default # Policy level: default, strict, lenient
910
+ # failOn: high # Minimum severity to fail: critical, high, medium, low
911
+ # checkOutdated: true # Flag outdated packages
912
+ # checkDeprecated: true # Flag deprecated packages
913
+ # checkUnmaintained: true # Flag unmaintained packages
914
+ # outdatedMajorThreshold: 3 # Major versions behind to flag
915
+ # unmaintainedYears: 2 # Years without publish to flag
916
+
917
+ # ===== Secret Detection =====
918
+ # secrets:
919
+ # scanPaths: [] # Paths to scan (default: entire project)
920
+ # allowPaths: [] # Paths to skip (e.g., test fixtures)
921
+ # allowRules: [] # Disable specific rules (e.g., generic-password)
438
922
  `;
439
923
  }
440
924
  function getPolicyDescription(policy) {
441
925
  switch (policy) {
442
926
  case "commercial-safe":
443
927
  return "# Rejects strong copyleft licenses (GPL, AGPL, EUPL)\n# Warns on weak copyleft (LGPL, MPL)\n# Allows permissive licenses (MIT, Apache-2.0, BSD, ISC)";
928
+ case "saas-safe":
929
+ return "# Safe for SaaS applications\n# Rejects strong copyleft and network-trigger licenses (AGPL, SSPL)\n# Warns on weak copyleft (LGPL, MPL)";
930
+ case "distribution-safe":
931
+ return "# Safe for distributed software (binaries, desktop apps)\n# Rejects ALL copyleft licenses that pose risk when distributing\n# Only allows permissive + MPL-2.0 (file-level copyleft)";
444
932
  case "strict":
445
933
  return "# Rejects all copyleft licenses including weak copyleft\n# Only allows permissive licenses";
446
934
  case "permissive-only":
@@ -452,31 +940,64 @@ function getPolicyDescription(policy) {
452
940
 
453
941
  // src/commands/list.ts
454
942
  function registerListCommand(program2) {
455
- program2.command("list").description("List all dependencies with their licenses").option("--format <type>", "Output format: table, json, csv", "table").option("--dev", "Include devDependencies").option("--filter <license>", "Filter by license (e.g., --filter GPL-3.0)").option("--project <path>", "Path to project root", process.cwd()).action(async (options) => {
943
+ program2.command("list").description("List all dependencies with their licenses, vulnerabilities, and secrets").option("--format <type>", "Output format: table, json, csv", "table").option("--dev", "Include devDependencies").option("--filter <license>", "Filter by license (e.g., --filter GPL-3.0)").option("--project <path>", "Path to project root", process.cwd()).option("--license-only", "Show license information only (legacy mode)").option("--no-license", "Disable license module").option("--no-security", "Disable security module").option("--no-secrets", "Disable secrets module").action(async (options) => {
456
944
  try {
457
- const result = await scan({
945
+ if (options.licenseOnly) {
946
+ const result2 = await scan({
947
+ projectPath: options.project,
948
+ config: {
949
+ scanDevDependencies: options.dev || void 0
950
+ }
951
+ });
952
+ if (options.filter) {
953
+ const filter = options.filter.toUpperCase();
954
+ result2.allowed = result2.allowed.filter((r) => r.dependency.license.toUpperCase().includes(filter));
955
+ result2.warned = result2.warned.filter((r) => r.dependency.license.toUpperCase().includes(filter));
956
+ result2.rejected = result2.rejected.filter((r) => r.dependency.license.toUpperCase().includes(filter));
957
+ result2.unknown = result2.unknown.filter((r) => r.dependency.license.toUpperCase().includes(filter));
958
+ result2.totalPackages = result2.allowed.length + result2.warned.length + result2.rejected.length + result2.unknown.length;
959
+ }
960
+ switch (options.format) {
961
+ case "json":
962
+ console.log(formatJsonList(result2));
963
+ break;
964
+ case "csv":
965
+ console.log(formatCsvList(result2));
966
+ break;
967
+ default:
968
+ console.log(formatTerminalList(result2));
969
+ break;
970
+ }
971
+ return;
972
+ }
973
+ const result = await unifiedScan({
458
974
  projectPath: options.project,
459
975
  config: {
460
- scanDevDependencies: options.dev || void 0
976
+ scanDevDependencies: options.dev || void 0,
977
+ modules: {
978
+ license: options.license !== void 0 ? options.license : void 0,
979
+ security: options.security !== void 0 ? options.security : void 0,
980
+ secrets: options.secrets !== void 0 ? options.secrets : void 0
981
+ }
461
982
  }
462
983
  });
463
- if (options.filter) {
984
+ if (options.filter && result.modules.license) {
464
985
  const filter = options.filter.toUpperCase();
465
- result.allowed = result.allowed.filter((r) => r.dependency.license.toUpperCase().includes(filter));
466
- result.warned = result.warned.filter((r) => r.dependency.license.toUpperCase().includes(filter));
467
- result.rejected = result.rejected.filter((r) => r.dependency.license.toUpperCase().includes(filter));
468
- result.unknown = result.unknown.filter((r) => r.dependency.license.toUpperCase().includes(filter));
469
- result.totalPackages = result.allowed.length + result.warned.length + result.rejected.length + result.unknown.length;
986
+ result.modules.license.allowed = result.modules.license.allowed.filter((r) => r.dependency.license.toUpperCase().includes(filter));
987
+ result.modules.license.warned = result.modules.license.warned.filter((r) => r.dependency.license.toUpperCase().includes(filter));
988
+ result.modules.license.rejected = result.modules.license.rejected.filter((r) => r.dependency.license.toUpperCase().includes(filter));
989
+ result.modules.license.unknown = result.modules.license.unknown.filter((r) => r.dependency.license.toUpperCase().includes(filter));
990
+ result.modules.license.totalPackages = result.modules.license.allowed.length + result.modules.license.warned.length + result.modules.license.rejected.length + result.modules.license.unknown.length;
470
991
  }
471
992
  switch (options.format) {
472
993
  case "json":
473
- console.log(formatJsonList(result));
994
+ console.log(formatJsonUnified(result));
474
995
  break;
475
996
  case "csv":
476
- console.log(formatCsvList(result));
997
+ console.log(formatCsvUnified(result));
477
998
  break;
478
999
  default:
479
- console.log(formatTerminalList(result));
1000
+ console.log(formatTerminalUnified(result));
480
1001
  break;
481
1002
  }
482
1003
  } catch (error) {
@@ -491,40 +1012,73 @@ function registerListCommand(program2) {
491
1012
  var fs2 = __toESM(require("fs"));
492
1013
  var path3 = __toESM(require("path"));
493
1014
  function registerReportCommand(program2) {
494
- program2.command("report").description("Generate a license attribution/NOTICE file").option("--out <path>", "Output file path", "NOTICE.txt").option("--format <type>", "Format: text, json, csv", "text").option("--project <path>", "Path to project root", process.cwd()).action(async (options) => {
1015
+ program2.command("report").description("Generate a license attribution/NOTICE file or full scan report").option("--out <path>", "Output file path", "NOTICE.txt").option("--format <type>", "Format: text, json, csv", "text").option("--project <path>", "Path to project root", process.cwd()).option("--license-only", "Generate license-only report (legacy mode)").option("--no-license", "Exclude license information from report").option("--no-security", "Exclude security information from report").option("--no-secrets", "Exclude secrets information from report").action(async (options) => {
495
1016
  try {
496
- const result = await scan({
1017
+ if (options.licenseOnly) {
1018
+ const result2 = await scan({
1019
+ projectPath: options.project,
1020
+ config: { scanDevDependencies: true }
1021
+ });
1022
+ const allResults = [...result2.allowed, ...result2.warned, ...result2.rejected, ...result2.unknown];
1023
+ allResults.sort((a, b) => a.dependency.name.localeCompare(b.dependency.name));
1024
+ let content2;
1025
+ switch (options.format) {
1026
+ case "json":
1027
+ content2 = JSON.stringify(
1028
+ allResults.map((r) => ({
1029
+ name: r.dependency.name,
1030
+ version: r.dependency.version,
1031
+ license: r.dependency.license,
1032
+ source: r.dependency.licenseSource
1033
+ })),
1034
+ null,
1035
+ 2
1036
+ );
1037
+ break;
1038
+ case "csv":
1039
+ content2 = "Package,Version,License\n" + allResults.map(
1040
+ (r) => `${r.dependency.name},${r.dependency.version},${r.dependency.license}`
1041
+ ).join("\n");
1042
+ break;
1043
+ default:
1044
+ content2 = generateNoticeText(allResults, result2.projectPath);
1045
+ break;
1046
+ }
1047
+ const outPath2 = path3.resolve(options.out);
1048
+ fs2.writeFileSync(outPath2, content2, "utf-8");
1049
+ console.log(`\u2705 Generated ${options.out} with ${allResults.length} package attributions`);
1050
+ return;
1051
+ }
1052
+ const result = await unifiedScan({
497
1053
  projectPath: options.project,
498
- config: { scanDevDependencies: true }
1054
+ config: {
1055
+ scanDevDependencies: true,
1056
+ modules: {
1057
+ license: options.license !== void 0 ? options.license : void 0,
1058
+ security: options.security !== void 0 ? options.security : void 0,
1059
+ secrets: options.secrets !== void 0 ? options.secrets : void 0
1060
+ }
1061
+ }
499
1062
  });
500
- const allResults = [...result.allowed, ...result.warned, ...result.rejected, ...result.unknown];
501
- allResults.sort((a, b) => a.dependency.name.localeCompare(b.dependency.name));
502
1063
  let content;
503
1064
  switch (options.format) {
504
1065
  case "json":
505
- content = JSON.stringify(
506
- allResults.map((r) => ({
507
- name: r.dependency.name,
508
- version: r.dependency.version,
509
- license: r.dependency.license,
510
- source: r.dependency.licenseSource
511
- })),
512
- null,
513
- 2
514
- );
1066
+ content = JSON.stringify(result, null, 2);
515
1067
  break;
516
1068
  case "csv":
517
- content = "Package,Version,License\n" + allResults.map(
518
- (r) => `${r.dependency.name},${r.dependency.version},${r.dependency.license}`
519
- ).join("\n");
1069
+ content = generateUnifiedCsvReport(result);
520
1070
  break;
521
1071
  default:
522
- content = generateNoticeText(allResults, result.projectPath);
1072
+ content = generateUnifiedTextReport(result);
523
1073
  break;
524
1074
  }
525
- const outPath = path3.resolve(options.out);
1075
+ const outPath = path3.resolve(options.out === "NOTICE.txt" ? "preship-report.txt" : options.out);
526
1076
  fs2.writeFileSync(outPath, content, "utf-8");
527
- console.log(`\u2705 Generated ${options.out} with ${allResults.length} package attributions`);
1077
+ const modulesRan = [];
1078
+ if (result.modules.license) modulesRan.push("license");
1079
+ if (result.modules.security) modulesRan.push("security");
1080
+ if (result.modules.secrets) modulesRan.push("secrets");
1081
+ console.log(`\u2705 Generated ${path3.basename(outPath)} (modules: ${modulesRan.join(", ")})`);
528
1082
  } catch (error) {
529
1083
  const message = error instanceof Error ? error.message : String(error);
530
1084
  console.error(`\u274C ${message}`);
@@ -564,6 +1118,113 @@ function generateNoticeText(results, projectPath) {
564
1118
  lines.push("=".repeat(80));
565
1119
  return lines.join("\n");
566
1120
  }
1121
+ function generateUnifiedTextReport(result) {
1122
+ const lines = [];
1123
+ lines.push("PRESHIP SCAN REPORT");
1124
+ lines.push(`Generated: ${result.timestamp}`);
1125
+ lines.push(`Project: ${result.projectPath}`);
1126
+ lines.push(`Mode: ${result.mode}`);
1127
+ lines.push(`Policy: ${result.policy}`);
1128
+ lines.push(`Overall: ${result.passed ? "PASSED" : "FAILED"}`);
1129
+ lines.push("");
1130
+ if (result.modules.license) {
1131
+ const license = result.modules.license;
1132
+ lines.push("=".repeat(80));
1133
+ lines.push("LICENSE COMPLIANCE");
1134
+ lines.push("=".repeat(80));
1135
+ lines.push(`Total packages: ${license.totalPackages}`);
1136
+ lines.push(`Passed: ${license.passed}`);
1137
+ lines.push(`Allowed: ${license.allowed.length}`);
1138
+ lines.push(`Warned: ${license.warned.length}`);
1139
+ lines.push(`Rejected: ${license.rejected.length}`);
1140
+ lines.push(`Unknown: ${license.unknown.length}`);
1141
+ lines.push("");
1142
+ const allResults = [...license.allowed, ...license.warned, ...license.rejected, ...license.unknown];
1143
+ allResults.sort((a, b) => a.dependency.name.localeCompare(b.dependency.name));
1144
+ for (const r of allResults) {
1145
+ lines.push(` ${r.dependency.name}@${r.dependency.version} \u2014 ${r.dependency.license} [${r.verdict}]`);
1146
+ }
1147
+ lines.push("");
1148
+ }
1149
+ if (result.modules.security) {
1150
+ const security = result.modules.security;
1151
+ lines.push("=".repeat(80));
1152
+ lines.push("SECURITY VULNERABILITIES");
1153
+ lines.push("=".repeat(80));
1154
+ lines.push(`Total packages scanned: ${security.totalPackages}`);
1155
+ lines.push(`Passed: ${security.passed}`);
1156
+ lines.push(`Findings: ${security.findings.length}`);
1157
+ lines.push("");
1158
+ if (security.findings.length > 0) {
1159
+ for (const f of security.findings) {
1160
+ lines.push(` [${f.severity.toUpperCase()}] ${f.package}@${f.version} \u2014 ${f.type}: ${f.message}`);
1161
+ }
1162
+ lines.push("");
1163
+ }
1164
+ }
1165
+ if (result.modules.secrets) {
1166
+ const secrets = result.modules.secrets;
1167
+ lines.push("=".repeat(80));
1168
+ lines.push("SECRET DETECTION");
1169
+ lines.push("=".repeat(80));
1170
+ lines.push(`Files scanned: ${secrets.filesScanned}`);
1171
+ lines.push(`Passed: ${secrets.passed}`);
1172
+ lines.push(`Findings: ${secrets.findings.length}`);
1173
+ lines.push("");
1174
+ if (secrets.findings.length > 0) {
1175
+ for (const f of secrets.findings) {
1176
+ lines.push(` [${f.severity.toUpperCase()}] ${f.file}:${f.line} \u2014 ${f.description} (${f.match})`);
1177
+ }
1178
+ lines.push("");
1179
+ }
1180
+ }
1181
+ lines.push("=".repeat(80));
1182
+ lines.push(`Scan duration: ${(result.totalScanDurationMs / 1e3).toFixed(1)}s`);
1183
+ return lines.join("\n");
1184
+ }
1185
+ function generateUnifiedCsvReport(result) {
1186
+ const sections = [];
1187
+ if (result.modules.license) {
1188
+ const lines = [];
1189
+ lines.push("# License Compliance");
1190
+ lines.push("Module,Package,Version,License,Verdict,Reason");
1191
+ const allResults = [
1192
+ ...result.modules.license.allowed,
1193
+ ...result.modules.license.warned,
1194
+ ...result.modules.license.rejected,
1195
+ ...result.modules.license.unknown
1196
+ ];
1197
+ for (const r of allResults) {
1198
+ lines.push(`license,${escapeCsv2(r.dependency.name)},${escapeCsv2(r.dependency.version)},${escapeCsv2(r.dependency.license)},${r.verdict},${escapeCsv2(r.reason)}`);
1199
+ }
1200
+ sections.push(lines.join("\n"));
1201
+ }
1202
+ if (result.modules.security) {
1203
+ const lines = [];
1204
+ lines.push("# Security Findings");
1205
+ lines.push("Module,Package,Version,Type,Severity,Message");
1206
+ for (const f of result.modules.security.findings) {
1207
+ lines.push(`security,${escapeCsv2(f.package)},${escapeCsv2(f.version)},${f.type},${f.severity},${escapeCsv2(f.message)}`);
1208
+ }
1209
+ sections.push(lines.join("\n"));
1210
+ }
1211
+ if (result.modules.secrets) {
1212
+ const lines = [];
1213
+ lines.push("# Secret Detection Findings");
1214
+ lines.push("Module,File,Line,RuleId,Severity,Description");
1215
+ for (const f of result.modules.secrets.findings) {
1216
+ lines.push(`secrets,${escapeCsv2(f.file)},${f.line},${f.ruleId},${f.severity},${escapeCsv2(f.description)}`);
1217
+ }
1218
+ sections.push(lines.join("\n"));
1219
+ }
1220
+ return sections.join("\n\n");
1221
+ }
1222
+ function escapeCsv2(value) {
1223
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1224
+ return `"${value.replace(/"/g, '""')}"`;
1225
+ }
1226
+ return value;
1227
+ }
567
1228
 
568
1229
  // src/commands/allow.ts
569
1230
  var fs3 = __toESM(require("fs"));
@@ -3203,7 +3864,7 @@ function registerAllowCommand(program2) {
3203
3864
 
3204
3865
  // src/cli.ts
3205
3866
  var program = new import_commander.Command();
3206
- program.name("preship").description("The ESLint of license compliance. Detect GPL, AGPL, EUPL and other problematic licenses before you ship.").version("1.0.0");
3867
+ program.name("preship").description("Pre-ship verification: license compliance, security scanning, and secret detection \u2014 all before you ship.").version("2.0.0");
3207
3868
  registerScanCommand(program);
3208
3869
  registerInitCommand(program);
3209
3870
  registerListCommand(program);
package/dist/index.d.ts CHANGED
@@ -1,10 +1,19 @@
1
- import { PreshipConfig, ScanResult } from '@preship/core';
2
- export { Dependency, DetectedProject, ExceptionEntry, LicenseSource, ParsedDependency, PolicyResult, PolicyTemplate, PolicyVerdict, PreshipConfig, ProjectType, ResolverMode, ScanResult } from '@preship/core';
1
+ import { PreshipConfig, ScanResult, UnifiedScanResult } from '@preship/core';
2
+ export { Dependency, DetectedProject, ExceptionEntry, LicenseSource, ModuleConfig, PackageHealth, ParsedDependency, PolicyResult, PolicyTemplate, PolicyVerdict, PreshipConfig, ProjectType, ResolverMode, ScanResult, SecretFinding, SecretRule, SecretSeverity, SecretsConfig, SecretsResult, SecurityConfig, SecurityFinding, SecurityPolicyLevel, SecurityResult, SecuritySeverity, SecurityVulnerability, UnifiedScanResult } from '@preship/core';
3
3
 
4
4
  interface ScanOptions {
5
5
  projectPath?: string;
6
6
  config?: Partial<PreshipConfig>;
7
7
  }
8
+ /**
9
+ * Legacy scan function — license-only scan for backwards compatibility.
10
+ * Returns ScanResult (license module output only).
11
+ */
8
12
  declare function scan(options?: ScanOptions): Promise<ScanResult>;
13
+ /**
14
+ * Unified scan — runs all enabled modules (license, security, secrets).
15
+ * Returns UnifiedScanResult with combined results from all modules.
16
+ */
17
+ declare function unifiedScan(options?: ScanOptions): Promise<UnifiedScanResult>;
9
18
 
10
- export { type ScanOptions, scan };
19
+ export { type ScanOptions, scan, unifiedScan };
package/dist/index.js CHANGED
@@ -30,7 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
- scan: () => scan
33
+ scan: () => scan,
34
+ unifiedScan: () => unifiedScan
34
35
  });
35
36
  module.exports = __toCommonJS(index_exports);
36
37
 
@@ -38,6 +39,8 @@ module.exports = __toCommonJS(index_exports);
38
39
  var path = __toESM(require("path"));
39
40
  var import_core = require("@preship/core");
40
41
  var import_license = require("@preship/license");
42
+ var import_security = require("@preship/security");
43
+ var import_secrets = require("@preship/secrets");
41
44
  async function scan(options) {
42
45
  const startTime = Date.now();
43
46
  const projectPath = path.resolve(options?.projectPath ?? process.cwd());
@@ -93,7 +96,123 @@ async function scan(options) {
93
96
  scanDurationMs
94
97
  };
95
98
  }
99
+ async function unifiedScan(options) {
100
+ const startTime = Date.now();
101
+ const projectPath = path.resolve(options?.projectPath ?? process.cwd());
102
+ let config = (0, import_core.loadConfig)(projectPath);
103
+ if (options?.config) {
104
+ config = {
105
+ ...config,
106
+ ...options.config,
107
+ modules: { ...config.modules, ...options.config.modules },
108
+ security: { ...config.security, ...options.config.security },
109
+ secrets: { ...config.secrets, ...options.config.secrets }
110
+ };
111
+ }
112
+ const modules = {
113
+ license: config.modules?.license ?? true,
114
+ security: config.modules?.security ?? true,
115
+ secrets: config.modules?.secrets ?? true
116
+ };
117
+ if (!modules.license && !modules.security && !modules.secrets) {
118
+ console.warn("\u26A0\uFE0F No scan modules enabled. Use --no-license, --no-security, --no-secrets to selectively disable.");
119
+ }
120
+ const mode = config.mode ?? "auto";
121
+ const policy = config.policy ?? "commercial-safe";
122
+ const projects = (0, import_core.detectProjects)(projectPath);
123
+ const project = projects[0];
124
+ const packageJsonPath = path.join(project.path, "package.json");
125
+ let parsedDeps;
126
+ switch (project.type) {
127
+ case "npm":
128
+ parsedDeps = (0, import_core.parseNpmLockfile)(project.lockFile, packageJsonPath);
129
+ break;
130
+ case "yarn":
131
+ parsedDeps = (0, import_core.parseYarnLockfile)(project.lockFile, packageJsonPath);
132
+ break;
133
+ case "pnpm":
134
+ parsedDeps = (0, import_core.parsePnpmLockfile)(project.lockFile, packageJsonPath);
135
+ break;
136
+ default:
137
+ throw new Error(`Unsupported project type: ${project.type}`);
138
+ }
139
+ if (!config.scanDevDependencies) {
140
+ parsedDeps = parsedDeps.filter((dep) => !dep.isDevDependency);
141
+ }
142
+ let licenseResult;
143
+ let securityResult;
144
+ let secretsResult;
145
+ const promises = [];
146
+ if (modules.license) {
147
+ promises.push(
148
+ (async () => {
149
+ const licenseStart = Date.now();
150
+ const dependencies = await (0, import_license.resolveLicenses)(parsedDeps, projectPath, {
151
+ mode,
152
+ networkTimeout: config.networkTimeout ?? 5e3,
153
+ networkConcurrency: config.networkConcurrency ?? 10,
154
+ cache: config.cache ?? true,
155
+ cacheTTL: config.cacheTTL ?? 604800,
156
+ scanTimeout: config.scanTimeout ?? 6e4
157
+ });
158
+ const results = (0, import_license.evaluatePolicy)(dependencies, config);
159
+ const allowed = results.filter((r) => r.verdict === "allowed");
160
+ const warned = results.filter((r) => r.verdict === "warned");
161
+ const rejected = results.filter((r) => r.verdict === "rejected");
162
+ const unknown = results.filter((r) => r.verdict === "unknown");
163
+ licenseResult = {
164
+ projectPath,
165
+ projectType: project.type,
166
+ framework: project.framework,
167
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
168
+ totalPackages: dependencies.length,
169
+ allowed,
170
+ warned,
171
+ rejected,
172
+ unknown,
173
+ passed: rejected.length === 0,
174
+ scanDurationMs: Date.now() - licenseStart
175
+ };
176
+ })()
177
+ );
178
+ }
179
+ if (modules.security) {
180
+ promises.push(
181
+ (async () => {
182
+ const securityConfig = (0, import_security.mergeSecurityConfig)(config.security);
183
+ securityResult = await (0, import_security.scanSecurity)(parsedDeps, projectPath, securityConfig, mode);
184
+ })()
185
+ );
186
+ }
187
+ if (modules.secrets) {
188
+ promises.push(
189
+ (async () => {
190
+ secretsResult = await (0, import_secrets.scanSecrets)(projectPath, config.secrets);
191
+ })()
192
+ );
193
+ }
194
+ await Promise.all(promises);
195
+ const allPassed = (licenseResult?.passed ?? true) && (securityResult?.passed ?? true) && (secretsResult?.passed ?? true);
196
+ const totalScanDurationMs = Date.now() - startTime;
197
+ return {
198
+ version: "2.0.0",
199
+ projectPath,
200
+ projectType: project.type,
201
+ framework: project.framework,
202
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
203
+ passed: allPassed,
204
+ mode,
205
+ policy,
206
+ modules: {
207
+ license: licenseResult,
208
+ security: securityResult,
209
+ secrets: secretsResult
210
+ },
211
+ totalScanDurationMs
212
+ };
213
+ }
96
214
  // Annotate the CommonJS export names for ESM import in node:
97
215
  0 && (module.exports = {
98
- scan
216
+ scan,
217
+ unifiedScan
99
218
  });
package/package.json CHANGED
@@ -1,11 +1,15 @@
1
1
  {
2
2
  "name": "preship",
3
- "version": "1.0.4",
4
- "description": "License compliance for modern dev teams. Detect GPL, AGPL, EUPL and other problematic licenses before you ship. Zero config. Fully offline. Free forever.",
3
+ "version": "2.0.0",
4
+ "description": "Pre-ship verification for modern dev teams. License compliance, security vulnerability scanning, and secret detection all in one CLI. Zero config. Fully offline. Free forever.",
5
5
  "keywords": [
6
6
  "license",
7
7
  "compliance",
8
8
  "scanner",
9
+ "security",
10
+ "vulnerability",
11
+ "secrets",
12
+ "credentials",
9
13
  "gpl",
10
14
  "agpl",
11
15
  "eupl",
@@ -19,7 +23,9 @@
19
23
  "fossa-alternative",
20
24
  "snyk-alternative",
21
25
  "offline",
22
- "air-gap"
26
+ "air-gap",
27
+ "osv",
28
+ "cve"
23
29
  ],
24
30
  "author": "Cyfox Inc.",
25
31
  "license": "Apache-2.0",
@@ -49,8 +55,10 @@
49
55
  "lint": "tsc --noEmit"
50
56
  },
51
57
  "dependencies": {
52
- "@preship/core": "1.0.2",
53
- "@preship/license": "1.0.0",
58
+ "@preship/core": "2.0.0",
59
+ "@preship/license": "2.0.0",
60
+ "@preship/security": "1.0.0",
61
+ "@preship/secrets": "1.0.0",
54
62
  "chalk": "^4.1.2",
55
63
  "cli-table3": "^0.6.5",
56
64
  "commander": "^12.1.0"