preship 1.0.5 → 2.0.2

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,109 @@ 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
+ const MAX_FINDINGS = 15;
501
+ const sorted = [...findings].sort((a, b) => severityRank(a.severity) - severityRank(b.severity));
502
+ lines.push(import_chalk.default.dim(` ${heading}:`));
503
+ const showCount = Math.min(sorted.length, MAX_FINDINGS);
504
+ for (let i = 0; i < showCount; i++) {
505
+ const f = sorted[i];
506
+ const isLast = i === showCount - 1 && sorted.length <= MAX_FINDINGS;
507
+ const prefix = isLast ? " \u2514\u2500\u2500 " : " \u251C\u2500\u2500 ";
508
+ const severityColor = getSeverityColor(f.severity);
509
+ const severityBadge = severityColor(`[${f.severity.toUpperCase()}]`);
510
+ lines.push(`${prefix}${severityBadge} ${f.package}@${f.version}`);
511
+ lines.push(import_chalk.default.dim(`${isLast ? " " : " \u2502 "}${f.message}`));
512
+ }
513
+ if (sorted.length > MAX_FINDINGS) {
514
+ const remaining = sorted.length - MAX_FINDINGS;
515
+ lines.push(import_chalk.default.dim(` \u2514\u2500\u2500 ... and ${remaining} more ${heading.toLowerCase()}`));
516
+ lines.push(import_chalk.default.dim(" Run with --format json for full details"));
517
+ }
518
+ }
519
+ function formatSecretFindings(findings, lines) {
520
+ const MAX_FINDINGS_PER_FILE = 5;
521
+ const MAX_FILES = 10;
522
+ const byFile = /* @__PURE__ */ new Map();
523
+ for (const f of findings) {
524
+ const existing = byFile.get(f.file) || [];
525
+ existing.push(f);
526
+ byFile.set(f.file, existing);
527
+ }
528
+ const sortedFiles = [...byFile.entries()].sort((a, b) => {
529
+ const aSeverity = Math.min(...a[1].map((f) => severityRank(f.severity)));
530
+ const bSeverity = Math.min(...b[1].map((f) => severityRank(f.severity)));
531
+ return aSeverity - bSeverity;
532
+ });
533
+ let filesShown = 0;
534
+ let totalHidden = 0;
535
+ for (const [file, fileFindings] of sortedFiles) {
536
+ if (filesShown >= MAX_FILES) {
537
+ totalHidden += fileFindings.length;
538
+ continue;
539
+ }
540
+ filesShown++;
541
+ lines.push(import_chalk.default.dim(` ${file}:`));
542
+ const showCount = Math.min(fileFindings.length, MAX_FINDINGS_PER_FILE);
543
+ for (let i = 0; i < showCount; i++) {
544
+ const f = fileFindings[i];
545
+ const isLast = i === showCount - 1 && fileFindings.length <= MAX_FINDINGS_PER_FILE;
546
+ const prefix = isLast ? " \u2514\u2500\u2500 " : " \u251C\u2500\u2500 ";
547
+ const severityColor = getSeverityColor(f.severity);
548
+ const severityBadge = severityColor(`[${f.severity.toUpperCase()}]`);
549
+ lines.push(`${prefix}${severityBadge} Line ${f.line}: ${f.description}`);
550
+ lines.push(import_chalk.default.dim(`${isLast ? " " : " \u2502 "}Match: ${f.match}`));
551
+ }
552
+ if (fileFindings.length > MAX_FINDINGS_PER_FILE) {
553
+ const remaining = fileFindings.length - MAX_FINDINGS_PER_FILE;
554
+ lines.push(import_chalk.default.dim(` \u2514\u2500\u2500 ... and ${remaining} more finding${remaining > 1 ? "s" : ""} in this file`));
555
+ }
556
+ }
557
+ if (totalHidden > 0) {
558
+ const hiddenFiles = sortedFiles.length - MAX_FILES;
559
+ lines.push("");
560
+ lines.push(import_chalk.default.dim(` ... and ${totalHidden} more finding${totalHidden > 1 ? "s" : ""} in ${hiddenFiles} more file${hiddenFiles > 1 ? "s" : ""}`));
561
+ lines.push(import_chalk.default.dim(" Run with --format json for full details"));
562
+ }
563
+ }
564
+ function severityRank(severity) {
565
+ switch (severity) {
566
+ case "critical":
567
+ return 0;
568
+ case "high":
569
+ return 1;
570
+ case "medium":
571
+ return 2;
572
+ case "low":
573
+ return 3;
574
+ default:
575
+ return 4;
576
+ }
577
+ }
578
+ function getSeverityColor(severity) {
579
+ switch (severity) {
580
+ case "critical":
581
+ return import_chalk.default.red.bold;
582
+ case "high":
583
+ return import_chalk.default.red;
584
+ case "medium":
585
+ return import_chalk.default.yellow;
586
+ case "low":
587
+ return import_chalk.default.dim;
588
+ default:
589
+ return import_chalk.default.white;
590
+ }
591
+ }
210
592
  function getShortReason(r) {
211
593
  if (r.dependency.license === "UNKNOWN") return "unable to detect license";
212
594
  if (r.verdict === "rejected") return "strong copyleft \u2014 requires source disclosure";
@@ -249,12 +631,12 @@ function getSourceLabel(source) {
249
631
  return "\u2014";
250
632
  }
251
633
  }
252
- function getProjectName(result) {
253
- const parts = result.projectPath.split("/");
634
+ function getProjectName(projectPath) {
635
+ const parts = projectPath.split("/");
254
636
  return parts[parts.length - 1] || "project";
255
637
  }
256
- function getLockFileName(result) {
257
- switch (result.projectType) {
638
+ function getLockFileName(projectType) {
639
+ switch (projectType) {
258
640
  case "npm":
259
641
  return "package-lock.json";
260
642
  case "yarn":
@@ -265,7 +647,7 @@ function getLockFileName(result) {
265
647
  return "unknown";
266
648
  }
267
649
  }
268
- function getPolicyName(_result) {
650
+ function getPolicyName() {
269
651
  return "commercial-safe";
270
652
  }
271
653
 
@@ -287,6 +669,9 @@ function formatJsonList(result) {
287
669
  }));
288
670
  return JSON.stringify({ totalPackages: result.totalPackages, dependencies: deps }, null, 2);
289
671
  }
672
+ function formatJsonUnified(result) {
673
+ return JSON.stringify(result, null, 2);
674
+ }
290
675
 
291
676
  // src/output/csv.ts
292
677
  function formatCsvScan(result) {
@@ -310,6 +695,72 @@ function formatCsvScan(result) {
310
695
  function formatCsvList(result) {
311
696
  return formatCsvScan(result);
312
697
  }
698
+ function formatCsvUnified(result) {
699
+ const sections = [];
700
+ if (!result.modules.license && !result.modules.security && !result.modules.secrets) {
701
+ return "# No modules enabled \u2014 scan skipped";
702
+ }
703
+ if (result.modules.license) {
704
+ const lines = [];
705
+ lines.push("# License Compliance");
706
+ lines.push("Module,Package,Version,License,Source,Verdict,Reason,IsDirect,IsDevDependency");
707
+ const allResults = [
708
+ ...result.modules.license.allowed,
709
+ ...result.modules.license.warned,
710
+ ...result.modules.license.rejected,
711
+ ...result.modules.license.unknown
712
+ ];
713
+ for (const r of allResults) {
714
+ lines.push([
715
+ "license",
716
+ escapeCsv(r.dependency.name),
717
+ escapeCsv(r.dependency.version),
718
+ escapeCsv(r.dependency.license),
719
+ escapeCsv(r.dependency.licenseSource),
720
+ escapeCsv(r.verdict),
721
+ escapeCsv(r.reason),
722
+ String(r.dependency.isDirect),
723
+ String(r.dependency.isDevDependency)
724
+ ].join(","));
725
+ }
726
+ sections.push(lines.join("\n"));
727
+ }
728
+ if (result.modules.security) {
729
+ const lines = [];
730
+ lines.push("# Security Findings");
731
+ lines.push("Module,Package,Version,Type,Severity,Message");
732
+ for (const f of result.modules.security.findings) {
733
+ lines.push([
734
+ "security",
735
+ escapeCsv(f.package),
736
+ escapeCsv(f.version),
737
+ escapeCsv(f.type),
738
+ escapeCsv(f.severity),
739
+ escapeCsv(f.message)
740
+ ].join(","));
741
+ }
742
+ sections.push(lines.join("\n"));
743
+ }
744
+ if (result.modules.secrets) {
745
+ const lines = [];
746
+ lines.push("# Secret Detection Findings");
747
+ lines.push("Module,File,Line,Column,RuleId,Severity,Description,Match");
748
+ for (const f of result.modules.secrets.findings) {
749
+ lines.push([
750
+ "secrets",
751
+ escapeCsv(f.file),
752
+ String(f.line),
753
+ String(f.column),
754
+ escapeCsv(f.ruleId),
755
+ escapeCsv(f.severity),
756
+ escapeCsv(f.description),
757
+ escapeCsv(f.match)
758
+ ].join(","));
759
+ }
760
+ sections.push(lines.join("\n"));
761
+ }
762
+ return sections.join("\n\n");
763
+ }
313
764
  function escapeCsv(value) {
314
765
  if (value.includes(",") || value.includes('"') || value.includes("\n")) {
315
766
  return `"${value.replace(/"/g, '""')}"`;
@@ -319,9 +770,47 @@ function escapeCsv(value) {
319
770
 
320
771
  // src/commands/scan.ts
321
772
  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) => {
773
+ 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").option("--no-outdated", "Disable outdated package checks").option("--no-deprecated", "Disable deprecated package checks").option("--no-unmaintained", "Disable unmaintained package checks").action(async (options) => {
323
774
  try {
324
- const result = await scan({
775
+ if (options.licenseOnly) {
776
+ const result2 = await scan({
777
+ projectPath: options.project,
778
+ config: {
779
+ scanDevDependencies: options.dev || void 0,
780
+ output: options.format !== "table" ? options.format : void 0,
781
+ mode: options.mode !== "auto" ? options.mode : void 0,
782
+ cache: options.cache !== void 0 ? options.cache : void 0,
783
+ cacheTTL: options.cacheTtl !== void 0 ? options.cacheTtl : void 0,
784
+ scanTimeout: options.scanTimeout !== void 0 ? options.scanTimeout : void 0
785
+ }
786
+ });
787
+ if (!options.silent) {
788
+ switch (options.format) {
789
+ case "json":
790
+ console.log(formatJsonScan(result2));
791
+ break;
792
+ case "csv":
793
+ console.log(formatCsvScan(result2));
794
+ break;
795
+ default:
796
+ console.log(formatTerminalScan(result2, options.warnOnly));
797
+ break;
798
+ }
799
+ }
800
+ if (options.warnOnly) {
801
+ process.exit(0);
802
+ return;
803
+ }
804
+ if (result2.rejected.length > 0) {
805
+ process.exit(1);
806
+ } else if (options.strict && result2.warned.length > 0) {
807
+ process.exit(1);
808
+ } else {
809
+ process.exit(0);
810
+ }
811
+ return;
812
+ }
813
+ const result = await unifiedScan({
325
814
  projectPath: options.project,
326
815
  config: {
327
816
  scanDevDependencies: options.dev || void 0,
@@ -329,19 +818,33 @@ function registerScanCommand(program2) {
329
818
  mode: options.mode !== "auto" ? options.mode : void 0,
330
819
  cache: options.cache !== void 0 ? options.cache : void 0,
331
820
  cacheTTL: options.cacheTtl !== void 0 ? options.cacheTtl : void 0,
332
- scanTimeout: options.scanTimeout !== void 0 ? options.scanTimeout : void 0
821
+ scanTimeout: options.scanTimeout !== void 0 ? options.scanTimeout : void 0,
822
+ // Module toggles from CLI flags
823
+ modules: {
824
+ license: options.license !== void 0 ? options.license : void 0,
825
+ security: options.security !== void 0 ? options.security : void 0,
826
+ secrets: options.secrets !== void 0 ? options.secrets : void 0
827
+ },
828
+ // Security config from CLI flags
829
+ security: {
830
+ severity: options.securitySeverity || void 0,
831
+ failOn: options.securityFailOn || void 0,
832
+ checkOutdated: options.outdated !== void 0 ? options.outdated : void 0,
833
+ checkDeprecated: options.deprecated !== void 0 ? options.deprecated : void 0,
834
+ checkUnmaintained: options.unmaintained !== void 0 ? options.unmaintained : void 0
835
+ }
333
836
  }
334
837
  });
335
838
  if (!options.silent) {
336
839
  switch (options.format) {
337
840
  case "json":
338
- console.log(formatJsonScan(result));
841
+ console.log(formatJsonUnified(result));
339
842
  break;
340
843
  case "csv":
341
- console.log(formatCsvScan(result));
844
+ console.log(formatCsvUnified(result));
342
845
  break;
343
846
  default:
344
- console.log(formatTerminalScan(result, options.warnOnly));
847
+ console.log(formatTerminalUnified(result, options.warnOnly));
345
848
  break;
346
849
  }
347
850
  }
@@ -349,10 +852,17 @@ function registerScanCommand(program2) {
349
852
  process.exit(0);
350
853
  return;
351
854
  }
352
- if (result.rejected.length > 0) {
353
- process.exit(1);
354
- } else if (options.strict && result.warned.length > 0) {
855
+ if (!result.passed) {
355
856
  process.exit(1);
857
+ } else if (options.strict) {
858
+ const hasLicenseWarnings = (result.modules.license?.warned?.length ?? 0) > 0;
859
+ const hasSecurityFindings = (result.modules.security?.findings?.length ?? 0) > 0;
860
+ const hasSecretFindings = (result.modules.secrets?.findings?.length ?? 0) > 0;
861
+ if (hasLicenseWarnings || hasSecurityFindings || hasSecretFindings) {
862
+ process.exit(1);
863
+ } else {
864
+ process.exit(0);
865
+ }
356
866
  } else {
357
867
  process.exit(0);
358
868
  }
@@ -370,7 +880,7 @@ function registerScanCommand(program2) {
370
880
  var fs = __toESM(require("fs"));
371
881
  var path2 = __toESM(require("path"));
372
882
  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) => {
883
+ 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
884
  try {
375
885
  const projectPath = process.cwd();
376
886
  const configContent = generateConfigContent(options.policy);
@@ -407,7 +917,7 @@ function registerInitCommand(program2) {
407
917
  });
408
918
  }
409
919
  function generateConfigContent(policy) {
410
- return `# PreShip License Compliance Configuration
920
+ return `# PreShip Configuration
411
921
  # Docs: https://github.com/dipen-code/preship
412
922
  #
413
923
  # Policy: ${policy}
@@ -415,6 +925,14 @@ ${getPolicyDescription(policy)}
415
925
 
416
926
  policy: ${policy}
417
927
 
928
+ # ===== Module Toggles =====
929
+ # Enable or disable individual scan modules (all enabled by default)
930
+ # modules:
931
+ # license: true
932
+ # security: true
933
+ # secrets: true
934
+
935
+ # ===== License Compliance =====
418
936
  # Scan devDependencies too? (default: false)
419
937
  # scanDevDependencies: false
420
938
 
@@ -435,12 +953,32 @@ policy: ${policy}
435
953
  # reason: "Used internally only, not distributed"
436
954
  # approvedBy: your-name
437
955
  # date: ${(/* @__PURE__ */ new Date()).toISOString().split("T")[0]}
956
+
957
+ # ===== Security Scanning =====
958
+ # security:
959
+ # severity: default # Policy level: default, strict, lenient
960
+ # failOn: high # Minimum severity to fail: critical, high, medium, low
961
+ # checkOutdated: true # Flag outdated packages
962
+ # checkDeprecated: true # Flag deprecated packages
963
+ # checkUnmaintained: true # Flag unmaintained packages
964
+ # outdatedMajorThreshold: 5 # Major versions behind to flag (direct deps only)
965
+ # unmaintainedYears: 4 # Years without publish to flag
966
+
967
+ # ===== Secret Detection =====
968
+ # secrets:
969
+ # scanPaths: [] # Paths to scan (default: entire project)
970
+ # allowPaths: [] # Paths to skip (e.g., test fixtures)
971
+ # allowRules: [] # Disable specific rules (e.g., generic-password)
438
972
  `;
439
973
  }
440
974
  function getPolicyDescription(policy) {
441
975
  switch (policy) {
442
976
  case "commercial-safe":
443
977
  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)";
978
+ case "saas-safe":
979
+ return "# Safe for SaaS applications\n# Rejects strong copyleft and network-trigger licenses (AGPL, SSPL)\n# Warns on weak copyleft (LGPL, MPL)";
980
+ case "distribution-safe":
981
+ 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
982
  case "strict":
445
983
  return "# Rejects all copyleft licenses including weak copyleft\n# Only allows permissive licenses";
446
984
  case "permissive-only":
@@ -452,31 +990,64 @@ function getPolicyDescription(policy) {
452
990
 
453
991
  // src/commands/list.ts
454
992
  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) => {
993
+ 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
994
  try {
457
- const result = await scan({
995
+ if (options.licenseOnly) {
996
+ const result2 = await scan({
997
+ projectPath: options.project,
998
+ config: {
999
+ scanDevDependencies: options.dev || void 0
1000
+ }
1001
+ });
1002
+ if (options.filter) {
1003
+ const filter = options.filter.toUpperCase();
1004
+ result2.allowed = result2.allowed.filter((r) => r.dependency.license.toUpperCase().includes(filter));
1005
+ result2.warned = result2.warned.filter((r) => r.dependency.license.toUpperCase().includes(filter));
1006
+ result2.rejected = result2.rejected.filter((r) => r.dependency.license.toUpperCase().includes(filter));
1007
+ result2.unknown = result2.unknown.filter((r) => r.dependency.license.toUpperCase().includes(filter));
1008
+ result2.totalPackages = result2.allowed.length + result2.warned.length + result2.rejected.length + result2.unknown.length;
1009
+ }
1010
+ switch (options.format) {
1011
+ case "json":
1012
+ console.log(formatJsonList(result2));
1013
+ break;
1014
+ case "csv":
1015
+ console.log(formatCsvList(result2));
1016
+ break;
1017
+ default:
1018
+ console.log(formatTerminalList(result2));
1019
+ break;
1020
+ }
1021
+ return;
1022
+ }
1023
+ const result = await unifiedScan({
458
1024
  projectPath: options.project,
459
1025
  config: {
460
- scanDevDependencies: options.dev || void 0
1026
+ scanDevDependencies: options.dev || void 0,
1027
+ modules: {
1028
+ license: options.license !== void 0 ? options.license : void 0,
1029
+ security: options.security !== void 0 ? options.security : void 0,
1030
+ secrets: options.secrets !== void 0 ? options.secrets : void 0
1031
+ }
461
1032
  }
462
1033
  });
463
- if (options.filter) {
1034
+ if (options.filter && result.modules.license) {
464
1035
  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;
1036
+ result.modules.license.allowed = result.modules.license.allowed.filter((r) => r.dependency.license.toUpperCase().includes(filter));
1037
+ result.modules.license.warned = result.modules.license.warned.filter((r) => r.dependency.license.toUpperCase().includes(filter));
1038
+ result.modules.license.rejected = result.modules.license.rejected.filter((r) => r.dependency.license.toUpperCase().includes(filter));
1039
+ result.modules.license.unknown = result.modules.license.unknown.filter((r) => r.dependency.license.toUpperCase().includes(filter));
1040
+ 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
1041
  }
471
1042
  switch (options.format) {
472
1043
  case "json":
473
- console.log(formatJsonList(result));
1044
+ console.log(formatJsonUnified(result));
474
1045
  break;
475
1046
  case "csv":
476
- console.log(formatCsvList(result));
1047
+ console.log(formatCsvUnified(result));
477
1048
  break;
478
1049
  default:
479
- console.log(formatTerminalList(result));
1050
+ console.log(formatTerminalUnified(result));
480
1051
  break;
481
1052
  }
482
1053
  } catch (error) {
@@ -491,40 +1062,73 @@ function registerListCommand(program2) {
491
1062
  var fs2 = __toESM(require("fs"));
492
1063
  var path3 = __toESM(require("path"));
493
1064
  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) => {
1065
+ 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
1066
  try {
496
- const result = await scan({
1067
+ if (options.licenseOnly) {
1068
+ const result2 = await scan({
1069
+ projectPath: options.project,
1070
+ config: { scanDevDependencies: true }
1071
+ });
1072
+ const allResults = [...result2.allowed, ...result2.warned, ...result2.rejected, ...result2.unknown];
1073
+ allResults.sort((a, b) => a.dependency.name.localeCompare(b.dependency.name));
1074
+ let content2;
1075
+ switch (options.format) {
1076
+ case "json":
1077
+ content2 = JSON.stringify(
1078
+ allResults.map((r) => ({
1079
+ name: r.dependency.name,
1080
+ version: r.dependency.version,
1081
+ license: r.dependency.license,
1082
+ source: r.dependency.licenseSource
1083
+ })),
1084
+ null,
1085
+ 2
1086
+ );
1087
+ break;
1088
+ case "csv":
1089
+ content2 = "Package,Version,License\n" + allResults.map(
1090
+ (r) => `${r.dependency.name},${r.dependency.version},${r.dependency.license}`
1091
+ ).join("\n");
1092
+ break;
1093
+ default:
1094
+ content2 = generateNoticeText(allResults, result2.projectPath);
1095
+ break;
1096
+ }
1097
+ const outPath2 = path3.resolve(options.out);
1098
+ fs2.writeFileSync(outPath2, content2, "utf-8");
1099
+ console.log(`\u2705 Generated ${options.out} with ${allResults.length} package attributions`);
1100
+ return;
1101
+ }
1102
+ const result = await unifiedScan({
497
1103
  projectPath: options.project,
498
- config: { scanDevDependencies: true }
1104
+ config: {
1105
+ scanDevDependencies: true,
1106
+ modules: {
1107
+ license: options.license !== void 0 ? options.license : void 0,
1108
+ security: options.security !== void 0 ? options.security : void 0,
1109
+ secrets: options.secrets !== void 0 ? options.secrets : void 0
1110
+ }
1111
+ }
499
1112
  });
500
- const allResults = [...result.allowed, ...result.warned, ...result.rejected, ...result.unknown];
501
- allResults.sort((a, b) => a.dependency.name.localeCompare(b.dependency.name));
502
1113
  let content;
503
1114
  switch (options.format) {
504
1115
  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
- );
1116
+ content = JSON.stringify(result, null, 2);
515
1117
  break;
516
1118
  case "csv":
517
- content = "Package,Version,License\n" + allResults.map(
518
- (r) => `${r.dependency.name},${r.dependency.version},${r.dependency.license}`
519
- ).join("\n");
1119
+ content = generateUnifiedCsvReport(result);
520
1120
  break;
521
1121
  default:
522
- content = generateNoticeText(allResults, result.projectPath);
1122
+ content = generateUnifiedTextReport(result);
523
1123
  break;
524
1124
  }
525
- const outPath = path3.resolve(options.out);
1125
+ const outPath = path3.resolve(options.out === "NOTICE.txt" ? "preship-report.txt" : options.out);
526
1126
  fs2.writeFileSync(outPath, content, "utf-8");
527
- console.log(`\u2705 Generated ${options.out} with ${allResults.length} package attributions`);
1127
+ const modulesRan = [];
1128
+ if (result.modules.license) modulesRan.push("license");
1129
+ if (result.modules.security) modulesRan.push("security");
1130
+ if (result.modules.secrets) modulesRan.push("secrets");
1131
+ console.log(`\u2705 Generated ${path3.basename(outPath)} (modules: ${modulesRan.join(", ")})`);
528
1132
  } catch (error) {
529
1133
  const message = error instanceof Error ? error.message : String(error);
530
1134
  console.error(`\u274C ${message}`);
@@ -564,6 +1168,113 @@ function generateNoticeText(results, projectPath) {
564
1168
  lines.push("=".repeat(80));
565
1169
  return lines.join("\n");
566
1170
  }
1171
+ function generateUnifiedTextReport(result) {
1172
+ const lines = [];
1173
+ lines.push("PRESHIP SCAN REPORT");
1174
+ lines.push(`Generated: ${result.timestamp}`);
1175
+ lines.push(`Project: ${result.projectPath}`);
1176
+ lines.push(`Mode: ${result.mode}`);
1177
+ lines.push(`Policy: ${result.policy}`);
1178
+ lines.push(`Overall: ${result.passed ? "PASSED" : "FAILED"}`);
1179
+ lines.push("");
1180
+ if (result.modules.license) {
1181
+ const license = result.modules.license;
1182
+ lines.push("=".repeat(80));
1183
+ lines.push("LICENSE COMPLIANCE");
1184
+ lines.push("=".repeat(80));
1185
+ lines.push(`Total packages: ${license.totalPackages}`);
1186
+ lines.push(`Passed: ${license.passed}`);
1187
+ lines.push(`Allowed: ${license.allowed.length}`);
1188
+ lines.push(`Warned: ${license.warned.length}`);
1189
+ lines.push(`Rejected: ${license.rejected.length}`);
1190
+ lines.push(`Unknown: ${license.unknown.length}`);
1191
+ lines.push("");
1192
+ const allResults = [...license.allowed, ...license.warned, ...license.rejected, ...license.unknown];
1193
+ allResults.sort((a, b) => a.dependency.name.localeCompare(b.dependency.name));
1194
+ for (const r of allResults) {
1195
+ lines.push(` ${r.dependency.name}@${r.dependency.version} \u2014 ${r.dependency.license} [${r.verdict}]`);
1196
+ }
1197
+ lines.push("");
1198
+ }
1199
+ if (result.modules.security) {
1200
+ const security = result.modules.security;
1201
+ lines.push("=".repeat(80));
1202
+ lines.push("SECURITY VULNERABILITIES");
1203
+ lines.push("=".repeat(80));
1204
+ lines.push(`Total packages scanned: ${security.totalPackages}`);
1205
+ lines.push(`Passed: ${security.passed}`);
1206
+ lines.push(`Findings: ${security.findings.length}`);
1207
+ lines.push("");
1208
+ if (security.findings.length > 0) {
1209
+ for (const f of security.findings) {
1210
+ lines.push(` [${f.severity.toUpperCase()}] ${f.package}@${f.version} \u2014 ${f.type}: ${f.message}`);
1211
+ }
1212
+ lines.push("");
1213
+ }
1214
+ }
1215
+ if (result.modules.secrets) {
1216
+ const secrets = result.modules.secrets;
1217
+ lines.push("=".repeat(80));
1218
+ lines.push("SECRET DETECTION");
1219
+ lines.push("=".repeat(80));
1220
+ lines.push(`Files scanned: ${secrets.filesScanned}`);
1221
+ lines.push(`Passed: ${secrets.passed}`);
1222
+ lines.push(`Findings: ${secrets.findings.length}`);
1223
+ lines.push("");
1224
+ if (secrets.findings.length > 0) {
1225
+ for (const f of secrets.findings) {
1226
+ lines.push(` [${f.severity.toUpperCase()}] ${f.file}:${f.line} \u2014 ${f.description} (${f.match})`);
1227
+ }
1228
+ lines.push("");
1229
+ }
1230
+ }
1231
+ lines.push("=".repeat(80));
1232
+ lines.push(`Scan duration: ${(result.totalScanDurationMs / 1e3).toFixed(1)}s`);
1233
+ return lines.join("\n");
1234
+ }
1235
+ function generateUnifiedCsvReport(result) {
1236
+ const sections = [];
1237
+ if (result.modules.license) {
1238
+ const lines = [];
1239
+ lines.push("# License Compliance");
1240
+ lines.push("Module,Package,Version,License,Verdict,Reason");
1241
+ const allResults = [
1242
+ ...result.modules.license.allowed,
1243
+ ...result.modules.license.warned,
1244
+ ...result.modules.license.rejected,
1245
+ ...result.modules.license.unknown
1246
+ ];
1247
+ for (const r of allResults) {
1248
+ lines.push(`license,${escapeCsv2(r.dependency.name)},${escapeCsv2(r.dependency.version)},${escapeCsv2(r.dependency.license)},${r.verdict},${escapeCsv2(r.reason)}`);
1249
+ }
1250
+ sections.push(lines.join("\n"));
1251
+ }
1252
+ if (result.modules.security) {
1253
+ const lines = [];
1254
+ lines.push("# Security Findings");
1255
+ lines.push("Module,Package,Version,Type,Severity,Message");
1256
+ for (const f of result.modules.security.findings) {
1257
+ lines.push(`security,${escapeCsv2(f.package)},${escapeCsv2(f.version)},${f.type},${f.severity},${escapeCsv2(f.message)}`);
1258
+ }
1259
+ sections.push(lines.join("\n"));
1260
+ }
1261
+ if (result.modules.secrets) {
1262
+ const lines = [];
1263
+ lines.push("# Secret Detection Findings");
1264
+ lines.push("Module,File,Line,RuleId,Severity,Description");
1265
+ for (const f of result.modules.secrets.findings) {
1266
+ lines.push(`secrets,${escapeCsv2(f.file)},${f.line},${f.ruleId},${f.severity},${escapeCsv2(f.description)}`);
1267
+ }
1268
+ sections.push(lines.join("\n"));
1269
+ }
1270
+ return sections.join("\n\n");
1271
+ }
1272
+ function escapeCsv2(value) {
1273
+ if (value.includes(",") || value.includes('"') || value.includes("\n")) {
1274
+ return `"${value.replace(/"/g, '""')}"`;
1275
+ }
1276
+ return value;
1277
+ }
567
1278
 
568
1279
  // src/commands/allow.ts
569
1280
  var fs3 = __toESM(require("fs"));
@@ -3203,7 +3914,7 @@ function registerAllowCommand(program2) {
3203
3914
 
3204
3915
  // src/cli.ts
3205
3916
  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");
3917
+ program.name("preship").description("Pre-ship verification: license compliance, security scanning, and secret detection \u2014 all before you ship.").version("2.0.2");
3207
3918
  registerScanCommand(program);
3208
3919
  registerInitCommand(program);
3209
3920
  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.5",
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.2",
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.1",
58
+ "@preship/core": "2.0.1",
59
+ "@preship/license": "2.0.0",
60
+ "@preship/security": "1.0.1",
61
+ "@preship/secrets": "1.0.2",
54
62
  "chalk": "^4.1.2",
55
63
  "cli-table3": "^0.6.5",
56
64
  "commander": "^12.1.0"