solana-privacy-scanner 0.3.2 → 0.4.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.
Files changed (2) hide show
  1. package/dist/index.js +426 -0
  2. package/package.json +5 -4
package/dist/index.js CHANGED
@@ -325,6 +325,430 @@ async function scanProgram(programId, options) {
325
325
  }
326
326
  }
327
327
 
328
+ // src/commands/analyze.ts
329
+ import chalk2 from "chalk";
330
+ import { analyze } from "solana-privacy-scanner-core";
331
+ import * as path from "path";
332
+ import * as fs from "fs";
333
+ async function analyzeCommand(paths, options) {
334
+ try {
335
+ if (!options.quiet) {
336
+ console.log(chalk2.blue("\u{1F512} Running Solana Privacy Analyzer...\n"));
337
+ }
338
+ const result = await analyze(paths, {
339
+ includeLow: options.low !== false
340
+ });
341
+ if (options.json) {
342
+ const output = JSON.stringify(result, null, 2);
343
+ if (options.output) {
344
+ fs.writeFileSync(options.output, output);
345
+ console.log(chalk2.green(`\u2705 Results written to ${options.output}`));
346
+ } else {
347
+ console.log(output);
348
+ }
349
+ } else {
350
+ printResults(result, options.quiet || false);
351
+ if (options.output) {
352
+ const jsonOutput = JSON.stringify(result, null, 2);
353
+ fs.writeFileSync(options.output, jsonOutput);
354
+ console.log(chalk2.green(`
355
+ \u2705 Results also written to ${options.output}`));
356
+ }
357
+ }
358
+ if (result.summary.critical > 0 || result.summary.high > 0) {
359
+ process.exit(1);
360
+ }
361
+ } catch (error) {
362
+ console.error(chalk2.red("\u274C Analysis failed:"), error);
363
+ process.exit(1);
364
+ }
365
+ }
366
+ function printResults(result, quiet) {
367
+ console.log(chalk2.bold("\u{1F4CA} Scan Summary\n"));
368
+ console.log(`Files analyzed: ${result.filesAnalyzed}`);
369
+ console.log(`Total issues: ${result.summary.total}
370
+ `);
371
+ if (result.summary.critical > 0) {
372
+ console.log(chalk2.red(` \u{1F534} CRITICAL: ${result.summary.critical}`));
373
+ }
374
+ if (result.summary.high > 0) {
375
+ console.log(chalk2.yellow(` \u{1F7E1} HIGH: ${result.summary.high}`));
376
+ }
377
+ if (result.summary.medium > 0) {
378
+ console.log(chalk2.blue(` \u{1F535} MEDIUM: ${result.summary.medium}`));
379
+ }
380
+ if (result.summary.low > 0) {
381
+ console.log(chalk2.gray(` \u26AA LOW: ${result.summary.low}`));
382
+ }
383
+ if (result.issues.length === 0) {
384
+ console.log(chalk2.green("\n\u2705 No issues found!"));
385
+ return;
386
+ }
387
+ if (quiet) {
388
+ return;
389
+ }
390
+ const byFile = result.issues.reduce((acc, issue) => {
391
+ if (!acc[issue.file]) acc[issue.file] = [];
392
+ acc[issue.file].push(issue);
393
+ return acc;
394
+ }, {});
395
+ console.log(chalk2.bold("\n\u{1F4CB} Detailed Issues\n"));
396
+ for (const [file, issues] of Object.entries(byFile)) {
397
+ console.log(chalk2.bold(`
398
+ \u{1F4C1} ${path.relative(process.cwd(), file)}`));
399
+ console.log("\u2501".repeat(80));
400
+ issues.forEach((issue, index) => {
401
+ const severityColor = issue.severity === "CRITICAL" ? chalk2.red : issue.severity === "HIGH" ? chalk2.yellow : issue.severity === "MEDIUM" ? chalk2.blue : chalk2.gray;
402
+ const severityEmoji = issue.severity === "CRITICAL" ? "\u{1F534}" : issue.severity === "HIGH" ? "\u{1F7E1}" : issue.severity === "MEDIUM" ? "\u{1F535}" : "\u26AA";
403
+ console.log(`${index + 1}. ${severityEmoji} ${severityColor(issue.severity)} ${issue.message}`);
404
+ console.log(` Line ${issue.line}:${issue.column}`);
405
+ if (issue.suggestion) {
406
+ console.log(chalk2.dim(` Suggestion: ${issue.suggestion}`));
407
+ }
408
+ if (issue.codeSnippet) {
409
+ console.log(chalk2.dim("\n Code:"));
410
+ console.log(chalk2.dim(issue.codeSnippet.split("\n").map((l) => ` ${l}`).join("\n")));
411
+ }
412
+ console.log();
413
+ });
414
+ }
415
+ if (result.summary.critical > 0 || result.summary.high > 0) {
416
+ console.log(chalk2.yellow("\n\u{1F4A1} Recommendations\n"));
417
+ console.log("Critical and high severity issues should be fixed before deployment.");
418
+ console.log("Run with --json to get machine-readable output for CI/CD integration.");
419
+ }
420
+ }
421
+
422
+ // src/commands/init.ts
423
+ import { writeFileSync as writeFileSync5, existsSync, mkdirSync, chmodSync } from "fs";
424
+ import { join } from "path";
425
+ import prompts from "prompts";
426
+ import chalk3 from "chalk";
427
+ async function initCommand() {
428
+ console.log(chalk3.bold.blue("\n\u{1F50D} Solana Privacy Scanner - Setup Wizard\n"));
429
+ const configPath = join(process.cwd(), ".privacyrc");
430
+ if (existsSync(configPath)) {
431
+ const { overwrite } = await prompts({
432
+ type: "confirm",
433
+ name: "overwrite",
434
+ message: "Configuration file already exists. Overwrite?",
435
+ initial: false
436
+ });
437
+ if (!overwrite) {
438
+ console.log(chalk3.yellow("\nSetup cancelled."));
439
+ return;
440
+ }
441
+ }
442
+ const responses = await prompts([
443
+ {
444
+ type: "select",
445
+ name: "preset",
446
+ message: "Choose a configuration preset:",
447
+ choices: [
448
+ {
449
+ title: "Development (Permissive)",
450
+ value: "dev",
451
+ description: "Relaxed rules for development"
452
+ },
453
+ {
454
+ title: "Production (Strict)",
455
+ value: "prod",
456
+ description: "Strict rules for production code"
457
+ },
458
+ {
459
+ title: "Custom",
460
+ value: "custom",
461
+ description: "Configure manually"
462
+ }
463
+ ],
464
+ initial: 0
465
+ },
466
+ {
467
+ type: (prev) => prev === "custom" ? "select" : null,
468
+ name: "maxRiskLevel",
469
+ message: "Maximum acceptable risk level:",
470
+ choices: [
471
+ { title: "LOW", value: "LOW" },
472
+ { title: "MEDIUM", value: "MEDIUM" },
473
+ { title: "HIGH", value: "HIGH" }
474
+ ],
475
+ initial: 1
476
+ },
477
+ {
478
+ type: (_, values) => values.preset === "custom" ? "confirm" : null,
479
+ name: "enforceInCI",
480
+ message: "Enforce privacy checks in CI/CD?",
481
+ initial: true
482
+ },
483
+ {
484
+ type: (_, values) => values.preset === "custom" ? "confirm" : null,
485
+ name: "blockOnFailure",
486
+ message: "Block builds/commits on privacy policy violations?",
487
+ initial: false
488
+ },
489
+ {
490
+ type: "text",
491
+ name: "devnetWallet",
492
+ message: "Test wallet address (devnet):",
493
+ validate: (value) => !value || value.length === 44 || "Invalid Solana address"
494
+ },
495
+ {
496
+ type: "multiselect",
497
+ name: "integrations",
498
+ message: "Which integrations would you like to set up?",
499
+ choices: [
500
+ { title: "GitHub Actions", value: "github", selected: true },
501
+ { title: "Pre-commit Hook", value: "precommit", selected: true },
502
+ { title: "Testing Matchers", value: "testing", selected: true }
503
+ ]
504
+ }
505
+ ]);
506
+ if (!responses.preset) {
507
+ console.log(chalk3.yellow("\nSetup cancelled."));
508
+ return;
509
+ }
510
+ let config2;
511
+ switch (responses.preset) {
512
+ case "dev":
513
+ config2 = {
514
+ maxRiskLevel: "HIGH",
515
+ enforceInCI: false,
516
+ blockOnFailure: false,
517
+ thresholds: {
518
+ maxHighSeverity: 5
519
+ }
520
+ };
521
+ break;
522
+ case "prod":
523
+ config2 = {
524
+ maxRiskLevel: "LOW",
525
+ enforceInCI: true,
526
+ blockOnFailure: true,
527
+ thresholds: {
528
+ maxHighSeverity: 0,
529
+ maxMediumSeverity: 2,
530
+ minPrivacyScore: 80
531
+ }
532
+ };
533
+ break;
534
+ case "custom":
535
+ config2 = {
536
+ maxRiskLevel: responses.maxRiskLevel || "MEDIUM",
537
+ enforceInCI: responses.enforceInCI ?? true,
538
+ blockOnFailure: responses.blockOnFailure ?? false,
539
+ thresholds: {
540
+ maxHighSeverity: 0
541
+ }
542
+ };
543
+ break;
544
+ default:
545
+ config2 = {
546
+ maxRiskLevel: "MEDIUM",
547
+ enforceInCI: true,
548
+ blockOnFailure: false,
549
+ thresholds: {
550
+ maxHighSeverity: 0
551
+ }
552
+ };
553
+ }
554
+ if (responses.devnetWallet) {
555
+ config2.testWallets = {
556
+ devnet: responses.devnetWallet
557
+ };
558
+ }
559
+ writeFileSync5(configPath, JSON.stringify(config2, null, 2));
560
+ console.log(chalk3.green(`
561
+ \u2713 Created ${configPath}`));
562
+ const integrations = responses.integrations || [];
563
+ if (integrations.includes("github")) {
564
+ setupGitHubActions();
565
+ }
566
+ if (integrations.includes("precommit")) {
567
+ setupPreCommitHook();
568
+ }
569
+ if (integrations.includes("testing")) {
570
+ setupTestingMatchers();
571
+ }
572
+ console.log(chalk3.bold.green("\n\u2713 Setup complete!\n"));
573
+ console.log(chalk3.bold("Next steps:\n"));
574
+ console.log(" 1. Review your configuration in " + chalk3.cyan(".privacyrc"));
575
+ console.log(" 2. Add a test wallet address to scan");
576
+ console.log(" 3. Run " + chalk3.cyan("npx solana-privacy-scanner scan-wallet <ADDRESS>"));
577
+ console.log("\nFor more info: " + chalk3.cyan("https://sps.guide"));
578
+ }
579
+ function setupGitHubActions() {
580
+ const workflowDir = join(process.cwd(), ".github", "workflows");
581
+ const workflowPath = join(workflowDir, "privacy-check.yml");
582
+ if (!existsSync(workflowDir)) {
583
+ mkdirSync(workflowDir, { recursive: true });
584
+ }
585
+ const workflow = `name: Privacy Check
586
+
587
+ on:
588
+ pull_request:
589
+ push:
590
+ branches: [main, develop]
591
+
592
+ jobs:
593
+ privacy-scan:
594
+ runs-on: ubuntu-latest
595
+
596
+ steps:
597
+ - uses: actions/checkout@v3
598
+
599
+ - name: Setup Node.js
600
+ uses: actions/setup-node@v3
601
+ with:
602
+ node-version: '20'
603
+
604
+ - name: Install Dependencies
605
+ run: npm ci
606
+
607
+ - name: Install Privacy Scanner
608
+ run: npm install -g solana-privacy-scanner
609
+
610
+ - name: Run Privacy Check
611
+ run: |
612
+ # Get test wallet from config
613
+ TEST_WALLET=$(node -e "console.log(require('./.privacyrc').testWallets?.devnet || '')")
614
+
615
+ if [ -n "$TEST_WALLET" ]; then
616
+ solana-privacy-scanner scan-wallet $TEST_WALLET --json --output privacy-report.json
617
+ else
618
+ echo "No test wallet configured, skipping scan"
619
+ exit 0
620
+ fi
621
+
622
+ - name: Check Privacy Policy
623
+ run: |
624
+ # Parse report and check against policy
625
+ RISK_LEVEL=$(node -e "console.log(require('./privacy-report.json').overallRisk)")
626
+ MAX_RISK=$(node -e "console.log(require('./.privacyrc').maxRiskLevel)")
627
+
628
+ echo "Risk Level: $RISK_LEVEL"
629
+ echo "Max Allowed: $MAX_RISK"
630
+
631
+ # Simple comparison (can be enhanced)
632
+ if [ "$RISK_LEVEL" = "HIGH" ] && [ "$MAX_RISK" != "HIGH" ]; then
633
+ echo "Privacy policy violated!"
634
+ exit 1
635
+ fi
636
+
637
+ - name: Upload Report
638
+ if: always()
639
+ uses: actions/upload-artifact@v3
640
+ with:
641
+ name: privacy-report
642
+ path: privacy-report.json
643
+ `;
644
+ writeFileSync5(workflowPath, workflow);
645
+ console.log(chalk3.green(`
646
+ \u2713 Created ${workflowPath}`));
647
+ }
648
+ function setupPreCommitHook() {
649
+ const hookPath = join(process.cwd(), ".husky", "pre-commit");
650
+ const huskyDir = join(process.cwd(), ".husky");
651
+ if (!existsSync(huskyDir)) {
652
+ mkdirSync(huskyDir, { recursive: true });
653
+ }
654
+ const hook = `#!/bin/sh
655
+ . "$(dirname "$0")/_/husky.sh"
656
+
657
+ # Load config
658
+ CONFIG_FILE=".privacyrc"
659
+
660
+ if [ ! -f "$CONFIG_FILE" ]; then
661
+ echo "No .privacyrc found, skipping privacy check"
662
+ exit 0
663
+ fi
664
+
665
+ # Get test wallet
666
+ TEST_WALLET=$(node -e "const config = require('./.privacyrc'); console.log(config.testWallets?.devnet || '');" 2>/dev/null)
667
+
668
+ if [ -z "$TEST_WALLET" ]; then
669
+ echo "No test wallet configured, skipping privacy check"
670
+ exit 0
671
+ fi
672
+
673
+ echo "\u{1F50D} Running privacy check..."
674
+
675
+ # Run privacy scanner
676
+ npx solana-privacy-scanner scan-wallet $TEST_WALLET --json > /tmp/privacy-report.json 2>&1
677
+
678
+ # Check result
679
+ RISK_LEVEL=$(node -e "const report = require('/tmp/privacy-report.json'); console.log(report.overallRisk);" 2>/dev/null)
680
+
681
+ if [ "$RISK_LEVEL" = "HIGH" ]; then
682
+ echo "\u274C HIGH privacy risk detected!"
683
+ echo "Run 'npx solana-privacy-scanner scan-wallet $TEST_WALLET' for details"
684
+ echo ""
685
+ echo "To bypass this check, use: git commit --no-verify"
686
+ exit 1
687
+ fi
688
+
689
+ echo "\u2713 Privacy check passed"
690
+ exit 0
691
+ `;
692
+ writeFileSync5(hookPath, hook);
693
+ try {
694
+ chmodSync(hookPath, "755");
695
+ } catch (error) {
696
+ console.warn(chalk3.yellow("Could not make hook executable. Run: chmod +x .husky/pre-commit"));
697
+ }
698
+ console.log(chalk3.green(`
699
+ \u2713 Created ${hookPath}`));
700
+ console.log(chalk3.yellow(" Note: Install husky with: npm install --save-dev husky && npx husky install"));
701
+ }
702
+ function setupTestingMatchers() {
703
+ const testSetupPath = join(process.cwd(), "tests", "setup.ts");
704
+ const testDir = join(process.cwd(), "tests");
705
+ if (!existsSync(testDir)) {
706
+ mkdirSync(testDir, { recursive: true });
707
+ }
708
+ const setup = `// Privacy testing setup
709
+ import '@solana-privacy-scanner/ci-tools/matchers';
710
+
711
+ // This file is automatically loaded by Vitest
712
+ // Add any additional test setup here
713
+ `;
714
+ writeFileSync5(testSetupPath, setup);
715
+ console.log(chalk3.green(`
716
+ \u2713 Created ${testSetupPath}`));
717
+ const exampleTestPath = join(testDir, "privacy.example.test.ts");
718
+ const exampleTest = `import { describe, it, expect } from 'vitest';
719
+ import { Connection, Transaction, SystemProgram, Keypair } from '@solana/web3.js';
720
+ import { simulateTransactionPrivacy } from '@solana-privacy-scanner/ci-tools/simulator';
721
+
722
+ describe('Privacy Tests Example', () => {
723
+ const connection = new Connection('https://api.devnet.solana.com');
724
+
725
+ it('should maintain privacy for basic transfer', async () => {
726
+ // Create a simple transfer transaction
727
+ const from = Keypair.generate();
728
+ const to = Keypair.generate();
729
+
730
+ const tx = new Transaction().add(
731
+ SystemProgram.transfer({
732
+ fromPubkey: from.publicKey,
733
+ toPubkey: to.publicKey,
734
+ lamports: 1000000,
735
+ })
736
+ );
737
+
738
+ // Simulate and analyze privacy
739
+ const report = await simulateTransactionPrivacy(tx, connection);
740
+
741
+ // Make privacy assertions
742
+ expect(report).toHavePrivacyRisk('LOW');
743
+ expect(report).toHaveNoHighRiskSignals();
744
+ expect(report).toHaveAtMostSignals(2);
745
+ });
746
+ });
747
+ `;
748
+ writeFileSync5(exampleTestPath, exampleTest);
749
+ console.log(chalk3.green(`\u2713 Created ${exampleTestPath}`));
750
+ }
751
+
328
752
  // src/index.ts
329
753
  import { VERSION } from "solana-privacy-scanner-core";
330
754
  dotenv.config({ path: ".env.local" });
@@ -334,5 +758,7 @@ program.name("solana-privacy-scanner").description("Privacy risk scanner for Sol
334
758
  program.command("scan-wallet").alias("wallet").description("Scan a Solana wallet address for privacy risks").argument("<address>", "Wallet address to scan").option("--rpc <url>", "Custom RPC endpoint URL (optional)", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--max-signatures <number>", "Maximum number of signatures to fetch", "100").option("--output <file>", "Write output to file").action(scanWallet);
335
759
  program.command("scan-transaction").alias("tx").description("Scan a single Solana transaction for privacy risks").argument("<signature>", "Transaction signature to scan").option("--rpc <url>", "Custom RPC endpoint URL (optional)", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--output <file>", "Write output to file").action(scanTransaction);
336
760
  program.command("scan-program").alias("program").description("Scan a Solana program for privacy risks").argument("<programId>", "Program ID to scan").option("--rpc <url>", "Custom RPC endpoint URL (optional)", process.env.SOLANA_RPC).option("--json", "Output as JSON", false).option("--max-accounts <number>", "Maximum number of accounts to fetch", "100").option("--max-transactions <number>", "Maximum number of transactions to fetch", "50").option("--output <file>", "Write output to file").action(scanProgram);
761
+ program.command("analyze").description("Analyze source code for privacy vulnerabilities").argument("<paths...>", "Files or directories to analyze").option("--json", "Output as JSON", false).option("--no-low", "Exclude low severity issues").option("--quiet", "Only show summary").option("--output <file>", "Write output to file").action(analyzeCommand);
762
+ program.command("init").description("Interactive setup wizard for privacy configuration").action(initCommand);
337
763
  program.parse();
338
764
  //# sourceMappingURL=index.js.map
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "solana-privacy-scanner",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "type": "module",
5
- "description": "CLI tool for analyzing Solana wallet privacy and on-chain exposure - scan wallets, transactions, and programs",
5
+ "description": "CLI tool for analyzing Solana privacy - scan wallets/transactions/programs, analyze source code, and set up CI/CD privacy checks",
6
6
  "bin": {
7
7
  "solana-privacy-scanner": "./dist/index.js"
8
8
  },
@@ -38,10 +38,11 @@
38
38
  "url": "https://github.com/taylorferran/solana-privacy-scanner/issues"
39
39
  },
40
40
  "dependencies": {
41
- "solana-privacy-scanner-core": "^0.4.0",
41
+ "solana-privacy-scanner-core": "^0.5.0",
42
42
  "chalk": "^5.3.0",
43
43
  "commander": "^12.1.0",
44
- "dotenv": "^16.4.7"
44
+ "dotenv": "^16.4.7",
45
+ "prompts": "^2.4.2"
45
46
  },
46
47
  "devDependencies": {
47
48
  "typescript": "^5.7.2",