solana-privacy-scanner-core 0.6.0 → 0.6.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/README.md +61 -20
- package/dist/index.cjs +116 -13
- package/dist/index.cjs.map +3 -3
- package/dist/index.js +117 -16
- package/dist/index.js.map +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# solana-privacy-scanner-core
|
|
2
2
|
|
|
3
|
-
Core scanning engine for Solana privacy analysis. Analyze on-chain privacy exposure using heuristic-based
|
|
3
|
+
Core scanning engine for Solana privacy analysis. Analyze on-chain privacy exposure using 13 heuristic-based detections plus static code analysis.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
7
|
+
- **13 Privacy Heuristics** - Fee payer reuse, signer overlap, memo PII, ATA linkage, priority fee fingerprinting, staking patterns, identity metadata exposure, and more
|
|
8
|
+
- **Static Code Analyzer** - AST-based detection of privacy anti-patterns in TypeScript/JavaScript source code
|
|
9
|
+
- **Known Entity Detection** - Flags interactions with exchanges, bridges, and KYC services
|
|
10
|
+
- **Structured Reports** - JSON reports with risk scores, evidence, and actionable mitigations
|
|
11
|
+
- **Works Out of the Box** - Built-in RPC endpoint, no configuration required
|
|
12
12
|
|
|
13
13
|
## Installation
|
|
14
14
|
|
|
@@ -18,31 +18,72 @@ npm install solana-privacy-scanner-core
|
|
|
18
18
|
|
|
19
19
|
## Quick Start
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
import { scan, RPCClient } from 'solana-privacy-scanner-core';
|
|
21
|
+
### Scan a wallet
|
|
23
22
|
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
```typescript
|
|
24
|
+
import {
|
|
25
|
+
RPCClient,
|
|
26
|
+
collectWalletData,
|
|
27
|
+
normalizeWalletData,
|
|
28
|
+
createDefaultLabelProvider,
|
|
29
|
+
generateReport,
|
|
30
|
+
} from 'solana-privacy-scanner-core';
|
|
26
31
|
|
|
27
|
-
//
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
maxSignatures: 100,
|
|
33
|
-
});
|
|
32
|
+
const rpc = new RPCClient(); // uses built-in RPC
|
|
33
|
+
const raw = await collectWalletData(rpc, 'YourWalletAddress', { maxSignatures: 100 });
|
|
34
|
+
const labels = createDefaultLabelProvider();
|
|
35
|
+
const context = normalizeWalletData(raw, labels);
|
|
36
|
+
const report = generateReport(context);
|
|
34
37
|
|
|
35
|
-
console.log('Risk
|
|
38
|
+
console.log('Risk:', report.overallRisk);
|
|
36
39
|
console.log('Signals:', report.signals.length);
|
|
37
40
|
```
|
|
38
41
|
|
|
42
|
+
### Analyze source code
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { analyze } from 'solana-privacy-scanner-core';
|
|
46
|
+
|
|
47
|
+
const result = await analyze(['src/**/*.ts']);
|
|
48
|
+
console.log(`Found ${result.summary.total} privacy issues`);
|
|
49
|
+
result.issues.forEach(i => console.log(` ${i.severity} ${i.type}: ${i.message}`));
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### Use individual heuristics
|
|
53
|
+
|
|
54
|
+
```typescript
|
|
55
|
+
import { detectFeePayerReuse, detectMemoExposure } from 'solana-privacy-scanner-core';
|
|
56
|
+
import type { ScanContext } from 'solana-privacy-scanner-core';
|
|
57
|
+
|
|
58
|
+
const signals = detectFeePayerReuse(context);
|
|
59
|
+
const memoSignals = detectMemoExposure(context);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## The 13 Heuristics
|
|
63
|
+
|
|
64
|
+
| # | Heuristic | What it checks |
|
|
65
|
+
|---|-----------|---------------|
|
|
66
|
+
| 1 | Fee Payer Reuse | One wallet paying fees for multiple accounts |
|
|
67
|
+
| 2 | Signer Overlap | Same signers appearing across transactions |
|
|
68
|
+
| 3 | Memo Exposure | Personal information in memo fields |
|
|
69
|
+
| 4 | Known Entity Interaction | Transfers to/from exchanges, bridges, KYC services |
|
|
70
|
+
| 5 | Identity Metadata Exposure | .sol domain and NFT metadata linkage |
|
|
71
|
+
| 6 | ATA Linkage | One wallet funding token accounts for multiple owners |
|
|
72
|
+
| 7 | Address Reuse | Using one address across many different protocols |
|
|
73
|
+
| 8 | Counterparty Reuse | Repeated transfers to the same address |
|
|
74
|
+
| 9 | Instruction Fingerprinting | Repeated program call patterns |
|
|
75
|
+
| 10 | Token Account Lifecycle | Frequent create/close cycles |
|
|
76
|
+
| 11 | Priority Fee Fingerprinting | Consistent priority fee amounts |
|
|
77
|
+
| 12 | Staking Delegation | Concentrated validator delegation |
|
|
78
|
+
| 13 | Timing Patterns | Burst activity and regular intervals |
|
|
79
|
+
|
|
39
80
|
## Documentation
|
|
40
81
|
|
|
41
|
-
Full documentation
|
|
82
|
+
Full documentation: https://taylorferran.github.io/solana-privacy-scanner
|
|
42
83
|
|
|
43
84
|
- [Getting Started](https://taylorferran.github.io/solana-privacy-scanner/guide/getting-started)
|
|
44
85
|
- [Library Usage](https://taylorferran.github.io/solana-privacy-scanner/library/usage)
|
|
45
|
-
- [
|
|
86
|
+
- [Heuristics Reference](https://taylorferran.github.io/solana-privacy-scanner/reports/heuristics)
|
|
46
87
|
|
|
47
88
|
## License
|
|
48
89
|
|
package/dist/index.cjs
CHANGED
|
@@ -47,6 +47,8 @@ __export(index_exports, {
|
|
|
47
47
|
detectATALinkage: () => detectATALinkage,
|
|
48
48
|
detectAddressReuse: () => detectAddressReuse,
|
|
49
49
|
detectCounterpartyReuse: () => detectCounterpartyReuse,
|
|
50
|
+
detectFeePayerReuse: () => detectFeePayerReuse,
|
|
51
|
+
detectFeePayerReuseInCode: () => detectFeePayerReuseInCode,
|
|
50
52
|
detectIdentityMetadataExposure: () => detectIdentityMetadataExposure,
|
|
51
53
|
detectInstructionFingerprinting: () => detectInstructionFingerprinting,
|
|
52
54
|
detectKnownEntityInteraction: () => detectKnownEntityInteraction,
|
|
@@ -80,7 +82,7 @@ var import_web3 = require("@solana/web3.js");
|
|
|
80
82
|
// src/constants.ts
|
|
81
83
|
var _RPC_ENCODED = "aHR0cHM6Ly9zZXJlbmUtcm91Z2gtcG9vbC5zb2xhbmEtbWFpbm5ldC5xdWlrbm9kZS5wcm8vYTliM2RkNGRkMzc0MzYwYzQzNzY4YzQyMTI2NmE2ZGNlZDU4MTI3Ny8=";
|
|
82
84
|
var DEFAULT_RPC_URL = /* @__PURE__ */ Buffer.from(_RPC_ENCODED, "base64").toString("utf-8");
|
|
83
|
-
var VERSION = "0.6.
|
|
85
|
+
var VERSION = "0.6.2";
|
|
84
86
|
|
|
85
87
|
// src/rpc/client.ts
|
|
86
88
|
var RateLimiter = class {
|
|
@@ -2695,6 +2697,7 @@ function createDefaultLabelProvider() {
|
|
|
2695
2697
|
var fs = __toESM(require("fs/promises"), 1);
|
|
2696
2698
|
var path = __toESM(require("path"), 1);
|
|
2697
2699
|
var import_glob = require("glob");
|
|
2700
|
+
var CODE_EXTENSIONS = ["ts", "tsx", "js", "jsx"];
|
|
2698
2701
|
async function readFile2(filePath) {
|
|
2699
2702
|
try {
|
|
2700
2703
|
return await fs.readFile(filePath, "utf-8");
|
|
@@ -2702,15 +2705,29 @@ async function readFile2(filePath) {
|
|
|
2702
2705
|
throw new Error(`Failed to read file ${filePath}: ${error}`);
|
|
2703
2706
|
}
|
|
2704
2707
|
}
|
|
2708
|
+
async function expandPattern(pattern) {
|
|
2709
|
+
try {
|
|
2710
|
+
const cleaned = pattern.replace(/\/+$/, "");
|
|
2711
|
+
const stat2 = await fs.stat(cleaned);
|
|
2712
|
+
if (stat2.isDirectory()) {
|
|
2713
|
+
return CODE_EXTENSIONS.map((ext) => path.join(cleaned, "**", `*.${ext}`));
|
|
2714
|
+
}
|
|
2715
|
+
} catch {
|
|
2716
|
+
}
|
|
2717
|
+
return [pattern];
|
|
2718
|
+
}
|
|
2705
2719
|
async function findFiles(patterns, options) {
|
|
2706
2720
|
const files = [];
|
|
2707
2721
|
for (const pattern of patterns) {
|
|
2708
|
-
const
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2722
|
+
const expanded = await expandPattern(pattern);
|
|
2723
|
+
for (const p of expanded) {
|
|
2724
|
+
const matches = await (0, import_glob.glob)(p, {
|
|
2725
|
+
ignore: options?.exclude || [],
|
|
2726
|
+
nodir: true,
|
|
2727
|
+
absolute: true
|
|
2728
|
+
});
|
|
2729
|
+
files.push(...matches);
|
|
2730
|
+
}
|
|
2714
2731
|
}
|
|
2715
2732
|
return [...new Set(files)];
|
|
2716
2733
|
}
|
|
@@ -2781,7 +2798,7 @@ function isNodeInsideNode(searchNode, parentNode) {
|
|
|
2781
2798
|
}
|
|
2782
2799
|
|
|
2783
2800
|
// src/analyzer/detectors/fee-payer-reuse.ts
|
|
2784
|
-
function
|
|
2801
|
+
function detectFeePayerReuseInCode(content, filePath) {
|
|
2785
2802
|
const issues = [];
|
|
2786
2803
|
try {
|
|
2787
2804
|
const ast = (0, import_typescript_estree.parse)(content, {
|
|
@@ -2800,7 +2817,7 @@ function detectFeePayerReuse2(content, filePath) {
|
|
|
2800
2817
|
traverse(ast, (node) => {
|
|
2801
2818
|
if (node.type === "VariableDeclarator" && node.id.type === "Identifier" && node.init && isKeypairGenerate(node.init)) {
|
|
2802
2819
|
const varName = node.id.name;
|
|
2803
|
-
if (
|
|
2820
|
+
if (isFeePayerName(varName)) {
|
|
2804
2821
|
const isInsideLoop = loops.some((loop) => {
|
|
2805
2822
|
return isNodeInsideNode(node, loop.body);
|
|
2806
2823
|
});
|
|
@@ -2817,6 +2834,22 @@ function detectFeePayerReuse2(content, filePath) {
|
|
|
2817
2834
|
}
|
|
2818
2835
|
}
|
|
2819
2836
|
}
|
|
2837
|
+
if ((node.type === "FunctionDeclaration" || node.type === "FunctionExpression" || node.type === "ArrowFunctionExpression") && node.params) {
|
|
2838
|
+
for (const param of node.params) {
|
|
2839
|
+
const paramName = param.type === "Identifier" ? param.name : param.type === "AssignmentPattern" && param.left.type === "Identifier" ? param.left.name : null;
|
|
2840
|
+
if (paramName && isFeePayerName(paramName)) {
|
|
2841
|
+
feePayerVars.set(paramName, {
|
|
2842
|
+
name: paramName,
|
|
2843
|
+
declaration: {
|
|
2844
|
+
line: param.loc.start.line,
|
|
2845
|
+
column: param.loc.start.column,
|
|
2846
|
+
file: filePath
|
|
2847
|
+
},
|
|
2848
|
+
usages: []
|
|
2849
|
+
});
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2820
2853
|
});
|
|
2821
2854
|
for (const [varName, varInfo] of feePayerVars) {
|
|
2822
2855
|
for (const loop of loops) {
|
|
@@ -2831,6 +2864,34 @@ function detectFeePayerReuse2(content, filePath) {
|
|
|
2831
2864
|
});
|
|
2832
2865
|
}
|
|
2833
2866
|
}
|
|
2867
|
+
if (node.type === "CallExpression" && !isTransactionCall(node)) {
|
|
2868
|
+
for (const arg of node.arguments) {
|
|
2869
|
+
if (arg.type === "Identifier" && arg.name === varName) {
|
|
2870
|
+
const alreadyTracked = varInfo.usages.some(
|
|
2871
|
+
(u) => u.line === node.loc.start.line && u.column === node.loc.start.column
|
|
2872
|
+
);
|
|
2873
|
+
if (!alreadyTracked) {
|
|
2874
|
+
varInfo.usages.push({
|
|
2875
|
+
line: node.loc.start.line,
|
|
2876
|
+
column: node.loc.start.column,
|
|
2877
|
+
file: filePath
|
|
2878
|
+
});
|
|
2879
|
+
}
|
|
2880
|
+
break;
|
|
2881
|
+
}
|
|
2882
|
+
}
|
|
2883
|
+
}
|
|
2884
|
+
if (node.type === "AssignmentExpression" && node.left.type === "MemberExpression" && node.left.property.type === "Identifier" && node.left.property.name === "feePayer") {
|
|
2885
|
+
const right = node.right;
|
|
2886
|
+
const assignedName = right.type === "MemberExpression" && right.object.type === "Identifier" ? right.object.name : right.type === "Identifier" ? right.name : null;
|
|
2887
|
+
if (assignedName === varName) {
|
|
2888
|
+
varInfo.usages.push({
|
|
2889
|
+
line: node.loc.start.line,
|
|
2890
|
+
column: node.loc.start.column,
|
|
2891
|
+
file: filePath
|
|
2892
|
+
});
|
|
2893
|
+
}
|
|
2894
|
+
}
|
|
2834
2895
|
});
|
|
2835
2896
|
}
|
|
2836
2897
|
if (varInfo.usages.length > 0) {
|
|
@@ -2902,6 +2963,10 @@ function traverse(node, visitor) {
|
|
|
2902
2963
|
}
|
|
2903
2964
|
}
|
|
2904
2965
|
}
|
|
2966
|
+
function isFeePayerName(name) {
|
|
2967
|
+
const lower = name.toLowerCase();
|
|
2968
|
+
return lower.includes("fee") || lower.includes("payer");
|
|
2969
|
+
}
|
|
2905
2970
|
function isKeypairGenerate(node) {
|
|
2906
2971
|
if (node.type === "CallExpression") {
|
|
2907
2972
|
const callee = node.callee;
|
|
@@ -2933,10 +2998,15 @@ function findFeePayerArgument(node) {
|
|
|
2933
2998
|
for (const arg of node.arguments) {
|
|
2934
2999
|
if (arg.type === "ArrayExpression") {
|
|
2935
3000
|
const elements = arg.elements.filter((e) => e !== null);
|
|
3001
|
+
for (const el of elements) {
|
|
3002
|
+
if (el && el.type === "Identifier" && isFeePayerName(el.name)) {
|
|
3003
|
+
return el;
|
|
3004
|
+
}
|
|
3005
|
+
}
|
|
2936
3006
|
if (elements.length > 0) {
|
|
2937
|
-
const
|
|
2938
|
-
if (
|
|
2939
|
-
return
|
|
3007
|
+
const first = elements[0];
|
|
3008
|
+
if (first && first.type !== "SpreadElement") {
|
|
3009
|
+
return first;
|
|
2940
3010
|
}
|
|
2941
3011
|
}
|
|
2942
3012
|
}
|
|
@@ -2993,6 +3063,7 @@ var PII_PATTERNS = [
|
|
|
2993
3063
|
function detectMemoPII(content, filePath) {
|
|
2994
3064
|
const issues = [];
|
|
2995
3065
|
const lines = content.split("\n");
|
|
3066
|
+
const hasMemoProgram = /MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr/.test(content);
|
|
2996
3067
|
lines.forEach((line, index) => {
|
|
2997
3068
|
let memoValue = null;
|
|
2998
3069
|
let columnOffset = 0;
|
|
@@ -3002,6 +3073,9 @@ function detectMemoPII(content, filePath) {
|
|
|
3002
3073
|
} else if (line.includes("createMemoInstruction") || line.includes("MemoInstruction")) {
|
|
3003
3074
|
memoValue = extractMemoFromFunction(line);
|
|
3004
3075
|
columnOffset = line.indexOf("Instruction") + 1;
|
|
3076
|
+
} else if (hasMemoProgram && line.includes("Buffer.from(")) {
|
|
3077
|
+
memoValue = extractBufferFromValue(line);
|
|
3078
|
+
columnOffset = line.indexOf("Buffer.from") + 1;
|
|
3005
3079
|
}
|
|
3006
3080
|
if (memoValue) {
|
|
3007
3081
|
for (const pattern of PII_PATTERNS) {
|
|
@@ -3021,6 +3095,20 @@ function detectMemoPII(content, filePath) {
|
|
|
3021
3095
|
});
|
|
3022
3096
|
}
|
|
3023
3097
|
}
|
|
3098
|
+
const piiVarMatches = memoValue.matchAll(/\$\{(\w*(?:email|name|user|phone|ssn|address|identity|account)\w*)\}/gi);
|
|
3099
|
+
for (const match of piiVarMatches) {
|
|
3100
|
+
issues.push({
|
|
3101
|
+
type: "memo-pii",
|
|
3102
|
+
severity: "HIGH",
|
|
3103
|
+
file: filePath,
|
|
3104
|
+
line: index + 1,
|
|
3105
|
+
column: columnOffset,
|
|
3106
|
+
message: `Variable '${match[1]}' interpolated into memo likely contains PII`,
|
|
3107
|
+
suggestion: "Do not interpolate user-identifying variables into memo fields. Memos are permanently public on-chain.",
|
|
3108
|
+
codeSnippet: extractSnippet(content, index + 1),
|
|
3109
|
+
identifier: `pii-variable: ${match[1]}`
|
|
3110
|
+
});
|
|
3111
|
+
}
|
|
3024
3112
|
if (isDescriptiveContent(memoValue)) {
|
|
3025
3113
|
issues.push({
|
|
3026
3114
|
type: "memo-pii",
|
|
@@ -3071,6 +3159,19 @@ function extractMemoFromFunction(line) {
|
|
|
3071
3159
|
}
|
|
3072
3160
|
return null;
|
|
3073
3161
|
}
|
|
3162
|
+
function extractBufferFromValue(line) {
|
|
3163
|
+
const patterns = [
|
|
3164
|
+
/Buffer\.from\(\s*["']([^"']+)["']/,
|
|
3165
|
+
/Buffer\.from\(\s*`([^`]+)`/
|
|
3166
|
+
];
|
|
3167
|
+
for (const pattern of patterns) {
|
|
3168
|
+
const match = line.match(pattern);
|
|
3169
|
+
if (match) {
|
|
3170
|
+
return match[1];
|
|
3171
|
+
}
|
|
3172
|
+
}
|
|
3173
|
+
return null;
|
|
3174
|
+
}
|
|
3074
3175
|
function isDescriptiveContent(memo) {
|
|
3075
3176
|
if (memo.length > 20 && /\s/.test(memo)) {
|
|
3076
3177
|
return true;
|
|
@@ -3154,7 +3255,7 @@ var SolanaPrivacyAnalyzer = class {
|
|
|
3154
3255
|
async analyzeFile(filePath) {
|
|
3155
3256
|
const content = await readFile2(filePath);
|
|
3156
3257
|
const issues = [];
|
|
3157
|
-
issues.push(...
|
|
3258
|
+
issues.push(...detectFeePayerReuseInCode(content, filePath));
|
|
3158
3259
|
issues.push(...detectMemoPII(content, filePath));
|
|
3159
3260
|
return issues;
|
|
3160
3261
|
}
|
|
@@ -3581,6 +3682,8 @@ function getTestWallet(config) {
|
|
|
3581
3682
|
detectATALinkage,
|
|
3582
3683
|
detectAddressReuse,
|
|
3583
3684
|
detectCounterpartyReuse,
|
|
3685
|
+
detectFeePayerReuse,
|
|
3686
|
+
detectFeePayerReuseInCode,
|
|
3584
3687
|
detectIdentityMetadataExposure,
|
|
3585
3688
|
detectInstructionFingerprinting,
|
|
3586
3689
|
detectKnownEntityInteraction,
|