capscan 0.1.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/index.js ADDED
@@ -0,0 +1,727 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { defineCommand as defineCommand8, runMain } from "citty";
5
+
6
+ // src/commands/scan.ts
7
+ import { defineCommand } from "citty";
8
+ import { scan } from "@capscan/engine";
9
+
10
+ // src/reporters/terminal.ts
11
+ var CATEGORY_ICONS = {
12
+ filesystem: "\u{1F4C1}",
13
+ network: "\u{1F310}",
14
+ process: "\u2699\uFE0F",
15
+ environment: "\u{1F511}",
16
+ crypto: "\u{1F510}",
17
+ dynamic_code: "\u{1F4E6}",
18
+ native: "\u{1F9E9}",
19
+ installation: "\u{1F527}",
20
+ obfuscation: "\u{1F3AD}"
21
+ };
22
+ var CATEGORY_LABELS = {
23
+ filesystem: "Filesystem",
24
+ network: "Network",
25
+ process: "Process",
26
+ environment: "Environment",
27
+ crypto: "Crypto",
28
+ dynamic_code: "Dynamic Code",
29
+ native: "Native",
30
+ installation: "Installation",
31
+ obfuscation: "Obfuscation"
32
+ };
33
+ var CONFIDENCE_COLORS = {
34
+ high: (s) => `\x1B[32m${s}\x1B[0m`,
35
+ medium: (s) => `\x1B[33m${s}\x1B[0m`,
36
+ low: (s) => `\x1B[31m${s}\x1B[0m`
37
+ };
38
+ function groupByCategory(findings) {
39
+ const grouped = /* @__PURE__ */ new Map();
40
+ for (const f of findings) {
41
+ const arr = grouped.get(f.capability.category) || [];
42
+ arr.push(f);
43
+ grouped.set(f.capability.category, arr);
44
+ }
45
+ return grouped;
46
+ }
47
+ function formatEvidenceLine(e, indent) {
48
+ const confColor = CONFIDENCE_COLORS[e.confidence] || CONFIDENCE_COLORS.medium;
49
+ const location = e.line > 0 ? `${e.file}:${e.line}` : e.file;
50
+ return `${indent}${confColor("\u25CF")} ${dim(location)} ${bold(e.symbol)}`;
51
+ }
52
+ function dim(s) {
53
+ return `\x1B[2m${s}\x1B[0m`;
54
+ }
55
+ function bold(s) {
56
+ return `\x1B[1m${s}\x1B[0m`;
57
+ }
58
+ function formatPackage(pkg) {
59
+ const lines = [];
60
+ const grouped = groupByCategory(pkg.capabilities);
61
+ lines.push(` \x1B[33m\u25CF\x1B[0m ${bold(pkg.name)}@${dim(pkg.version)}`);
62
+ for (const [category, findings] of grouped) {
63
+ const icon = CATEGORY_ICONS[category];
64
+ const label = CATEGORY_LABELS[category];
65
+ lines.push(` ${icon} ${bold(label)}`);
66
+ const byPermission = /* @__PURE__ */ new Map();
67
+ for (const f of findings) {
68
+ const perm = f.capability.permission;
69
+ const arr = byPermission.get(perm) || [];
70
+ arr.push(f);
71
+ byPermission.set(perm, arr);
72
+ }
73
+ for (const [perm, permFindings] of byPermission) {
74
+ lines.push(` ${dim(perm)}`);
75
+ for (const f of permFindings.slice(0, 3)) {
76
+ for (const e of f.evidence.slice(0, 2)) {
77
+ lines.push(formatEvidenceLine(e, " "));
78
+ }
79
+ }
80
+ if (permFindings.length > 3) lines.push(dim(` ... and ${permFindings.length - 3} more`));
81
+ }
82
+ }
83
+ return lines;
84
+ }
85
+ function reportTerminal(result) {
86
+ const lines = [];
87
+ lines.push("");
88
+ lines.push(`${bold("\x1B[36mCapScan\x1B[0m")} ${dim("v0.1.0 \u2014 deterministic capability analysis engine")}`);
89
+ lines.push("");
90
+ lines.push(`${bold("Project:")} ${result.meta.scanPath}`);
91
+ lines.push(`${bold("Package Manager:")} ${result.meta.packageManager}`);
92
+ lines.push(`${bold("Dependencies:")} ${result.summary.totalPackages.toString()}`);
93
+ lines.push("");
94
+ lines.push(bold("\x1B[4mCapabilities\x1B[0m"));
95
+ lines.push(" " + "\u2500".repeat(35));
96
+ for (const [cat, perms] of Object.entries(result.summary.capabilities)) {
97
+ const catLabel = CATEGORY_LABELS[cat];
98
+ const icon = CATEGORY_ICONS[cat];
99
+ const entries = Object.entries(perms).filter(([, c]) => c > 0);
100
+ if (entries.length === 0) continue;
101
+ lines.push(` ${icon} ${bold(catLabel)}`);
102
+ for (const [perm, count] of entries) {
103
+ const dots = ".".repeat(Math.max(0, 25 - perm.length));
104
+ const pkgText = count === 1 ? "package" : "packages";
105
+ lines.push(` ${dim(perm)} ${dots} ${count.toString()} ${pkgText}`);
106
+ }
107
+ }
108
+ const packagesWithCaps = result.packages.filter((p) => p.capabilities.length > 0);
109
+ if (packagesWithCaps.length > 0) {
110
+ lines.push("");
111
+ lines.push(bold("\x1B[4mPackages with Capabilities\x1B[0m"));
112
+ lines.push(" " + "\u2500".repeat(35));
113
+ lines.push("");
114
+ for (const pkg of packagesWithCaps) {
115
+ lines.push(...formatPackage(pkg));
116
+ lines.push("");
117
+ }
118
+ }
119
+ lines.push(dim(` Scan completed at ${result.meta.timestamp}`));
120
+ lines.push("");
121
+ return lines.join("\n");
122
+ }
123
+
124
+ // src/reporters/json.ts
125
+ import { writeFile } from "fs/promises";
126
+ import { resolve } from "path";
127
+ async function reportJson(result, outputPath) {
128
+ const json = JSON.stringify(result, null, 2);
129
+ if (outputPath) {
130
+ await writeFile(resolve(outputPath), json, "utf-8");
131
+ return outputPath;
132
+ }
133
+ return json;
134
+ }
135
+
136
+ // src/reporters/markdown.ts
137
+ var CAPABILITY_LABELS = {
138
+ filesystem: "Filesystem",
139
+ network: "Network",
140
+ process: "Process",
141
+ environment: "Environment",
142
+ crypto: "Crypto",
143
+ dynamic_code: "Dynamic Code",
144
+ native: "Native",
145
+ installation: "Installation",
146
+ obfuscation: "Obfuscation"
147
+ };
148
+ function generateCapabilityTable(summary) {
149
+ const lines = [];
150
+ lines.push("| Category | Permission | Count |");
151
+ lines.push("|----------|------------|-------|");
152
+ for (const [cat, perms] of Object.entries(summary.capabilities)) {
153
+ for (const [perm, count] of Object.entries(perms)) {
154
+ if (count > 0) lines.push(`| ${CAPABILITY_LABELS[cat]} | \`${perm}\` | ${count} |`);
155
+ }
156
+ }
157
+ return lines.join("\n");
158
+ }
159
+ function reportMarkdown(result) {
160
+ const lines = [];
161
+ lines.push("# CapScan Report");
162
+ lines.push("");
163
+ lines.push(`**Generated**: ${result.meta.timestamp}`);
164
+ lines.push(`**Engine**: ${result.meta.engine}`);
165
+ lines.push(`**Package Manager**: ${result.meta.packageManager}`);
166
+ lines.push(`**Total Dependencies**: ${result.summary.totalPackages}`);
167
+ lines.push(`**Packages with Capabilities**: ${result.summary.packagesWithCapabilities}`);
168
+ lines.push("");
169
+ lines.push("## Summary");
170
+ lines.push("");
171
+ lines.push(generateCapabilityTable(result.summary));
172
+ lines.push("");
173
+ const packagesWithCaps = result.packages.filter((p) => p.capabilities.length > 0);
174
+ if (packagesWithCaps.length > 0) {
175
+ lines.push("## Packages");
176
+ lines.push("");
177
+ for (const pkg of packagesWithCaps) {
178
+ lines.push(`### ${pkg.name}@${pkg.version}`);
179
+ lines.push("");
180
+ lines.push("| Permission | Symbol | File | Line | Confidence |");
181
+ lines.push("|------------|--------|------|------|------------|");
182
+ for (const f of pkg.capabilities) {
183
+ for (const e of f.evidence) {
184
+ lines.push(`| \`${f.capability.permission}\` | \`${e.symbol}\` | \`${e.file}\` | ${e.line} | ${e.confidence} |`);
185
+ }
186
+ }
187
+ lines.push("");
188
+ }
189
+ }
190
+ return lines.join("\n");
191
+ }
192
+
193
+ // src/commands/scan.ts
194
+ import ora from "ora";
195
+ import { writeFile as writeFile2 } from "fs/promises";
196
+ var scanCommand = defineCommand({
197
+ meta: { name: "scan", description: "Scan a project for dependency capabilities" },
198
+ args: {
199
+ path: { type: "positional", description: "Path to project directory", default: "." },
200
+ format: { type: "string", description: "Output format (json, terminal, markdown)", default: "terminal", alias: "f" },
201
+ output: { type: "string", description: "Output file path", alias: "o" }
202
+ },
203
+ async run({ args }) {
204
+ const spinner = ora("Scanning project...").start();
205
+ try {
206
+ const result = await scan({ path: args.path });
207
+ spinner.succeed(`Found ${result.summary.packagesWithCapabilities} packages with capabilities`);
208
+ if (args.format === "json") {
209
+ const out = await reportJson(result, args.output);
210
+ if (args.output) console.log(`Report saved to ${args.output}`);
211
+ else console.log(out);
212
+ } else if (args.format === "markdown") {
213
+ const md = reportMarkdown(result);
214
+ if (args.output) {
215
+ await writeFile2(args.output, md, "utf-8");
216
+ console.log(`Report saved to ${args.output}`);
217
+ } else {
218
+ console.log(md);
219
+ }
220
+ } else {
221
+ console.log(reportTerminal(result));
222
+ }
223
+ } catch (error) {
224
+ spinner.fail("Scan failed");
225
+ console.error(error);
226
+ process.exit(1);
227
+ }
228
+ }
229
+ });
230
+
231
+ // src/commands/diff.ts
232
+ import { defineCommand as defineCommand2 } from "citty";
233
+ import { scan as scan2, createSnapshot, loadSnapshot, diffSnapshots } from "@capscan/engine";
234
+ import { join } from "path";
235
+ import ora2 from "ora";
236
+ var diffCommand = defineCommand2({
237
+ meta: { name: "diff", description: "Compare current scan with last snapshot" },
238
+ args: {
239
+ path: { type: "positional", description: "Path to project directory", default: "." },
240
+ snapshot: { type: "string", description: "Snapshot file path", default: ".capscan/snapshot.json" }
241
+ },
242
+ async run({ args }) {
243
+ const spinner = ora2("Scanning project...").start();
244
+ try {
245
+ const snapshotPath = join(args.path, args.snapshot);
246
+ const oldSnap = await loadSnapshot(snapshotPath);
247
+ if (!oldSnap) {
248
+ spinner.fail(`No snapshot found at ${args.snapshot}`);
249
+ console.log("\nRun `capscan snapshot` first to create a baseline.");
250
+ return;
251
+ }
252
+ const result = await scan2({ path: args.path });
253
+ const newSnap = await createSnapshot(result, snapshotPath);
254
+ const diff = diffSnapshots(oldSnap, newSnap);
255
+ spinner.succeed("Diff complete");
256
+ if (diff.added.length === 0 && diff.removed.length === 0) {
257
+ console.log("\n No capability changes detected.\n");
258
+ return;
259
+ }
260
+ const lines = [];
261
+ lines.push("");
262
+ if (diff.added.length > 0) {
263
+ lines.push(" \x1B[32m+ Added\x1B[0m");
264
+ for (const item of diff.added) {
265
+ lines.push(` + ${item.permission} in ${item.package}`);
266
+ }
267
+ lines.push("");
268
+ }
269
+ if (diff.removed.length > 0) {
270
+ lines.push(" \x1B[31m- Removed\x1B[0m");
271
+ for (const item of diff.removed) {
272
+ lines.push(` - ${item.permission} in ${item.package}`);
273
+ }
274
+ lines.push("");
275
+ }
276
+ console.log(lines.join("\n"));
277
+ } catch (error) {
278
+ spinner.fail("Diff failed");
279
+ console.error(error);
280
+ process.exit(1);
281
+ }
282
+ }
283
+ });
284
+
285
+ // src/commands/snapshot.ts
286
+ import { defineCommand as defineCommand3 } from "citty";
287
+ import { scan as scan3, createSnapshot as createSnapshot2 } from "@capscan/engine";
288
+ import { join as join2 } from "path";
289
+ import ora3 from "ora";
290
+ var snapshotCommand = defineCommand3({
291
+ meta: { name: "snapshot", description: "Create a capability snapshot for diffing" },
292
+ args: {
293
+ path: { type: "positional", description: "Path to project directory", default: "." },
294
+ output: { type: "string", description: "Snapshot file path", default: ".capscan/snapshot.json" }
295
+ },
296
+ async run({ args }) {
297
+ const spinner = ora3("Creating snapshot...").start();
298
+ try {
299
+ const result = await scan3({ path: args.path });
300
+ const snapshotPath = join2(args.path, args.output);
301
+ const snapshot = await createSnapshot2(result, snapshotPath);
302
+ const pkgCount = Object.keys(snapshot.packages).length;
303
+ spinner.succeed(`Snapshot saved to ${args.output}`);
304
+ console.log(` ${pkgCount} packages with capabilities recorded.`);
305
+ } catch (error) {
306
+ spinner.fail("Snapshot failed");
307
+ console.error(error);
308
+ process.exit(1);
309
+ }
310
+ }
311
+ });
312
+
313
+ // src/commands/why.ts
314
+ import { defineCommand as defineCommand4 } from "citty";
315
+ import { scan as scan4 } from "@capscan/engine";
316
+ function dim2(s) {
317
+ return `\x1B[2m${s}\x1B[0m`;
318
+ }
319
+ function bold2(s) {
320
+ return `\x1B[1m${s}\x1B[0m`;
321
+ }
322
+ var CATEGORY_LABELS2 = {
323
+ filesystem: "Filesystem",
324
+ network: "Network",
325
+ process: "Process",
326
+ environment: "Environment",
327
+ crypto: "Crypto",
328
+ dynamic_code: "Dynamic Code",
329
+ native: "Native",
330
+ installation: "Installation",
331
+ obfuscation: "Obfuscation"
332
+ };
333
+ var CATEGORY_ICONS2 = {
334
+ filesystem: "\u{1F4C1}",
335
+ network: "\u{1F310}",
336
+ process: "\u2699\uFE0F",
337
+ environment: "\u{1F511}",
338
+ crypto: "\u{1F510}",
339
+ dynamic_code: "\u{1F4E6}",
340
+ native: "\u{1F9E9}",
341
+ installation: "\u{1F527}",
342
+ obfuscation: "\u{1F3AD}"
343
+ };
344
+ function isCategory(input) {
345
+ return Object.keys(CATEGORY_LABELS2).includes(input);
346
+ }
347
+ function printFindings(label, findings) {
348
+ const lines = [];
349
+ lines.push("");
350
+ lines.push(` ${bold2(label)}`);
351
+ lines.push(` ${dim2("\u2500".repeat(50))}`);
352
+ lines.push("");
353
+ const byPkg = /* @__PURE__ */ new Map();
354
+ for (const { pkg, finding } of findings) {
355
+ const arr = byPkg.get(pkg.name) || [];
356
+ arr.push({ finding });
357
+ byPkg.set(pkg.name, arr);
358
+ }
359
+ let totalEvidence = 0;
360
+ for (const [pkgName, items] of byPkg) {
361
+ const pkgVersion = items[0] ? findings.find((f) => f.pkg.name === pkgName)?.pkg.version : "";
362
+ lines.push(` ${bold2(pkgName)}@${dim2(pkgVersion || "")}`);
363
+ for (const { finding } of items) {
364
+ for (const e of finding.evidence) {
365
+ totalEvidence++;
366
+ const loc = e.line > 0 ? `${e.file}:${e.line}` : e.file;
367
+ lines.push(` \u2514\u2500\u2500 ${dim2(loc)} ${bold2(e.symbol)}`);
368
+ }
369
+ }
370
+ lines.push("");
371
+ }
372
+ lines.push(` ${dim2(`${totalEvidence} evidence points across ${byPkg.size} packages`)}`);
373
+ lines.push("");
374
+ console.log(lines.join("\n"));
375
+ }
376
+ var whyCommand = defineCommand4({
377
+ meta: { name: "why", description: "Explain why a capability, permission, or package is detected" },
378
+ args: {
379
+ target: { type: "positional", description: "Category, permission, or package name", required: true },
380
+ extra: { type: "positional", description: "Second filter (permission or package)", required: false },
381
+ path: { type: "string", description: "Path to project directory", default: ".", alias: "p" }
382
+ },
383
+ async run({ args }) {
384
+ const result = await scan4({ path: args.path });
385
+ const target = args.target.toLowerCase();
386
+ const extra = args.extra?.toLowerCase();
387
+ const matches = [];
388
+ for (const pkg of result.packages) {
389
+ for (const finding of pkg.capabilities) {
390
+ const matchesTarget = isCategory(target) && finding.capability.category === target || finding.capability.permission === target || pkg.name.toLowerCase() === target || pkg.name.toLowerCase().includes(target);
391
+ const matchesExtra = !extra || finding.capability.permission === extra || pkg.name.toLowerCase() === extra || pkg.name.toLowerCase().includes(extra);
392
+ if (matchesTarget && matchesExtra) {
393
+ matches.push({ pkg, finding });
394
+ }
395
+ }
396
+ }
397
+ if (matches.length === 0) {
398
+ console.log(`
399
+ No findings for "${args.target}${extra ? " " + extra : ""}".
400
+ `);
401
+ return;
402
+ }
403
+ let label;
404
+ if (isCategory(target)) {
405
+ label = `${CATEGORY_ICONS2[target]} ${CATEGORY_LABELS2[target]}`;
406
+ } else if (matches[0]?.finding.capability.permission === target) {
407
+ label = target;
408
+ } else {
409
+ label = `Package: ${target}`;
410
+ }
411
+ printFindings(label, matches);
412
+ }
413
+ });
414
+
415
+ // src/commands/compare.ts
416
+ import { defineCommand as defineCommand5 } from "citty";
417
+ import { scan as scan5 } from "@capscan/engine";
418
+ function bold3(s) {
419
+ return `\x1B[1m${s}\x1B[0m`;
420
+ }
421
+ function getPermissions(pkg) {
422
+ const perms = /* @__PURE__ */ new Map();
423
+ for (const f of pkg.capabilities) {
424
+ const key = f.capability.category;
425
+ const arr = perms.get(key) || [];
426
+ arr.push(f.capability.permission);
427
+ perms.set(key, [...new Set(arr)]);
428
+ }
429
+ return perms;
430
+ }
431
+ var compareCommand = defineCommand5({
432
+ meta: { name: "compare", description: "Compare capabilities of two dependency versions" },
433
+ args: {
434
+ pkgA: { type: "positional", description: "First package (name or name@version)", required: true },
435
+ pkgB: { type: "positional", description: "Second package (name or name@version)", required: true },
436
+ path: { type: "string", description: "Path to project directory", default: ".", alias: "p" }
437
+ },
438
+ async run({ args }) {
439
+ const [nameA, verA] = args.pkgA.includes("@") ? args.pkgA.split("@") : [args.pkgA, ""];
440
+ const [nameB, verB] = args.pkgB.includes("@") ? args.pkgB.split("@") : [args.pkgB, ""];
441
+ console.log(`
442
+ Scanning packages...`);
443
+ const result = await scan5({ path: args.path });
444
+ const pkgA = result.packages.find((p) => p.name === nameA);
445
+ const pkgB = result.packages.find((p) => p.name === nameB);
446
+ if (!pkgA && !pkgB) {
447
+ console.log(`
448
+ Neither "${nameA}" nor "${nameB}" found in dependencies.
449
+ `);
450
+ return;
451
+ }
452
+ if (!pkgA) {
453
+ console.log(`
454
+ "${nameA}" not found in dependencies.
455
+ `);
456
+ return;
457
+ }
458
+ if (!pkgB) {
459
+ console.log(`
460
+ "${nameB}" not found in dependencies.
461
+ `);
462
+ return;
463
+ }
464
+ const permsA = getPermissions(pkgA);
465
+ const permsB = getPermissions(pkgB);
466
+ const flatA = /* @__PURE__ */ new Set();
467
+ const flatB = /* @__PURE__ */ new Set();
468
+ for (const perms of permsA.values()) for (const p of perms) flatA.add(p);
469
+ for (const perms of permsB.values()) for (const p of perms) flatB.add(p);
470
+ const added = [...flatB].filter((p) => !flatA.has(p));
471
+ const removed = [...flatA].filter((p) => !flatB.has(p));
472
+ const unchanged = [...flatA].filter((p) => flatB.has(p));
473
+ const lines = [];
474
+ lines.push("");
475
+ lines.push(` ${bold3(`Capability changes: ${pkgA.name}@${pkgA.version} \u2192 ${pkgB.name}@${pkgB.version}`)}`);
476
+ lines.push("");
477
+ if (added.length > 0) {
478
+ lines.push(" \x1B[32m+ Added\x1B[0m");
479
+ for (const p of added) lines.push(` + ${p}`);
480
+ lines.push("");
481
+ }
482
+ if (removed.length > 0) {
483
+ lines.push(" \x1B[31m- Removed\x1B[0m");
484
+ for (const p of removed) lines.push(` - ${p}`);
485
+ lines.push("");
486
+ }
487
+ if (unchanged.length > 0) {
488
+ lines.push(" \x1B[36m\u2713 Unchanged\x1B[0m");
489
+ for (const p of unchanged) lines.push(` \u2713 ${p}`);
490
+ lines.push("");
491
+ }
492
+ if (added.length === 0 && removed.length === 0) {
493
+ lines.push(" No capability differences found.");
494
+ lines.push("");
495
+ }
496
+ console.log(lines.join("\n"));
497
+ }
498
+ });
499
+
500
+ // src/commands/check.ts
501
+ import { defineCommand as defineCommand6 } from "citty";
502
+ import { scan as scan6 } from "@capscan/engine";
503
+ function parseAllowList(allow) {
504
+ if (!allow) return [];
505
+ return allow.split(",").map((s) => s.trim());
506
+ }
507
+ function checkCapabilities(result, allowedPermissions) {
508
+ const blocked = [];
509
+ const allowedSet = new Set(allowedPermissions);
510
+ for (const pkg of result.packages) {
511
+ for (const finding of pkg.capabilities) {
512
+ if (!allowedSet.has(finding.capability.permission)) {
513
+ blocked.push({
514
+ package: pkg.name,
515
+ permission: finding.capability.permission,
516
+ evidence: finding.evidence.map((e) => ({
517
+ file: e.file,
518
+ line: e.line,
519
+ symbol: e.symbol
520
+ }))
521
+ });
522
+ }
523
+ }
524
+ }
525
+ return {
526
+ allowed: blocked.length === 0,
527
+ blocked,
528
+ summary: {
529
+ totalPackages: result.summary.totalPackages,
530
+ packagesWithCapabilities: result.summary.packagesWithCapabilities,
531
+ blockedCount: blocked.length
532
+ }
533
+ };
534
+ }
535
+ function formatCheckOutput(result, quiet) {
536
+ if (quiet && result.allowed) {
537
+ return "";
538
+ }
539
+ const lines = [];
540
+ if (result.allowed) {
541
+ lines.push("\x1B[32m\u2713\x1B[0m All capabilities allowed");
542
+ lines.push(` ${result.summary.packagesWithCapabilities} packages with capabilities`);
543
+ return lines.join("\n");
544
+ }
545
+ lines.push("");
546
+ lines.push("\x1B[31m\u2717\x1B[0m \x1B[1mBlocked capabilities detected\x1B[0m");
547
+ lines.push("");
548
+ const byPackage = /* @__PURE__ */ new Map();
549
+ for (const item of result.blocked) {
550
+ const arr = byPackage.get(item.package) || [];
551
+ arr.push(item);
552
+ byPackage.set(item.package, arr);
553
+ }
554
+ for (const [pkg, items] of byPackage) {
555
+ lines.push(` \x1B[33m\u25CF\x1B[0m ${pkg}`);
556
+ for (const item of items) {
557
+ const evidence = item.evidence[0];
558
+ const location = evidence ? `${evidence.file}:${evidence.line}` : "";
559
+ lines.push(` \x1B[31m\u2717\x1B[0m ${item.permission} ${location ? `\x1B[2m${location}\x1B[0m` : ""}`);
560
+ }
561
+ lines.push("");
562
+ }
563
+ lines.push(` \x1B[2m${result.summary.blockedCount} blocked capabilities\x1B[0m`);
564
+ lines.push("");
565
+ return lines.join("\n");
566
+ }
567
+ var checkCommand = defineCommand6({
568
+ meta: {
569
+ name: "check",
570
+ description: "Check if project capabilities are within allowed permissions"
571
+ },
572
+ args: {
573
+ path: { type: "positional", description: "Path to project directory", default: "." },
574
+ allow: {
575
+ type: "string",
576
+ description: "Comma-separated list of allowed permissions (e.g., fs:read,env:read)",
577
+ alias: "a"
578
+ },
579
+ quiet: {
580
+ type: "boolean",
581
+ description: "Only output if blocked",
582
+ default: false,
583
+ alias: "q"
584
+ }
585
+ },
586
+ async run({ args }) {
587
+ try {
588
+ const result = await scan6({ path: args.path });
589
+ const allowedPermissions = parseAllowList(args.allow);
590
+ const checkResult = checkCapabilities(result, allowedPermissions);
591
+ const output = formatCheckOutput(checkResult, args.quiet);
592
+ if (output) {
593
+ console.log(output);
594
+ }
595
+ process.exit(checkResult.allowed ? 0 : 1);
596
+ } catch (error) {
597
+ console.error("\x1B[31m\u2717\x1B[0m Check failed:", error);
598
+ process.exit(1);
599
+ }
600
+ }
601
+ });
602
+
603
+ // src/commands/init.ts
604
+ import { defineCommand as defineCommand7 } from "citty";
605
+ import { readFile, writeFile as writeFile3, access } from "fs/promises";
606
+ import { join as join3 } from "path";
607
+ var NPMRC_CONTENT = `
608
+ # CapScan pre-install hook
609
+ # Checks dependency capabilities before installing
610
+ preinstall = npx capscan check --quiet
611
+ `.trim();
612
+ var GITIGNORE_LINE = "node_modules/";
613
+ async function fileExists(path) {
614
+ try {
615
+ await access(path);
616
+ return true;
617
+ } catch {
618
+ return false;
619
+ }
620
+ }
621
+ async function readOrCreateNpmrc(projectPath) {
622
+ const npmrcPath = join3(projectPath, ".npmrc");
623
+ if (await fileExists(npmrcPath)) {
624
+ return await readFile(npmrcPath, "utf-8");
625
+ }
626
+ return "";
627
+ }
628
+ async function readOrCreateGitignore(projectPath) {
629
+ const gitignorePath = join3(projectPath, ".gitignore");
630
+ if (await fileExists(gitignorePath)) {
631
+ return await readFile(gitignorePath, "utf-8");
632
+ }
633
+ return "";
634
+ }
635
+ function hasCapscanHook(content) {
636
+ return content.includes("capscan check");
637
+ }
638
+ function addCapscanHook(content) {
639
+ if (hasCapscanHook(content)) {
640
+ return content;
641
+ }
642
+ const lines = content.split("\n");
643
+ const lastEmptyIndex = lines.length - 1;
644
+ let insertIndex = lastEmptyIndex;
645
+ for (let i = lines.length - 1; i >= 0; i--) {
646
+ if (lines[i].trim() === "") {
647
+ insertIndex = i;
648
+ break;
649
+ }
650
+ }
651
+ if (insertIndex === lastEmptyIndex && lines[lastEmptyIndex]?.trim() !== "") {
652
+ lines.push("");
653
+ insertIndex = lines.length;
654
+ }
655
+ lines.splice(insertIndex, 0, ...NPMRC_CONTENT.split("\n"));
656
+ return lines.join("\n");
657
+ }
658
+ function addNodeModulesToGitignore(content) {
659
+ if (content.includes("node_modules/")) {
660
+ return content;
661
+ }
662
+ return content.trim() + "\n\n" + GITIGNORE_LINE + "\n";
663
+ }
664
+ var initCommand = defineCommand7({
665
+ meta: {
666
+ name: "init",
667
+ description: "Initialize CapScan hooks in your project"
668
+ },
669
+ args: {
670
+ path: { type: "positional", description: "Path to project directory", default: "." },
671
+ force: {
672
+ type: "boolean",
673
+ description: "Overwrite existing configuration",
674
+ default: false,
675
+ alias: "f"
676
+ }
677
+ },
678
+ async run({ args }) {
679
+ const projectPath = args.path;
680
+ try {
681
+ console.log("\n \x1B[36mCapScan\x1B[0m \x1B[2mInit\x1B[0m\n");
682
+ const npmrcContent = await readOrCreateNpmrc(projectPath);
683
+ if (hasCapscanHook(npmrcContent) && !args.force) {
684
+ console.log(" \x1B[33m\u25CF\x1B[0m .npmrc already configured");
685
+ } else {
686
+ const newNpmrc = addCapscanHook(npmrcContent);
687
+ const npmrcPath = join3(projectPath, ".npmrc");
688
+ await writeFile3(npmrcPath, newNpmrc, "utf-8");
689
+ console.log(" \x1B[32m\u2713\x1B[0m .npmrc updated");
690
+ }
691
+ const gitignoreContent = await readOrCreateGitignore(projectPath);
692
+ if (gitignoreContent.includes("node_modules/")) {
693
+ console.log(" \x1B[33m\u25CF\x1B[0m .gitignore already has node_modules/");
694
+ } else {
695
+ const newGitignore = addNodeModulesToGitignore(gitignoreContent);
696
+ const gitignorePath = join3(projectPath, ".gitignore");
697
+ await writeFile3(gitignorePath, newGitignore, "utf-8");
698
+ console.log(" \x1B[32m\u2713\x1B[0m .gitignore updated");
699
+ }
700
+ console.log("\n \x1B[2mCapScan will now check capabilities before each install.\x1B[0m");
701
+ console.log(' \x1B[2mTo disable: remove the "preinstall" line from .npmrc\x1B[0m\n');
702
+ } catch (error) {
703
+ console.error("\x1B[31m\u2717\x1B[0m Init failed:", error);
704
+ process.exit(1);
705
+ }
706
+ }
707
+ });
708
+
709
+ // src/index.ts
710
+ var main = defineCommand8({
711
+ meta: {
712
+ name: "capscan",
713
+ version: "0.1.0",
714
+ description: "Deterministic capability analysis engine for software dependencies"
715
+ },
716
+ subCommands: {
717
+ scan: scanCommand,
718
+ why: whyCommand,
719
+ diff: diffCommand,
720
+ snapshot: snapshotCommand,
721
+ compare: compareCommand,
722
+ check: checkCommand,
723
+ init: initCommand
724
+ }
725
+ });
726
+ runMain(main);
727
+ //# sourceMappingURL=index.js.map