supascan 0.0.9 → 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/README.md +60 -110
- package/dist/supascan.js +66 -83
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,145 +1,88 @@
|
|
|
1
1
|
# supascan
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://github.com/abhishekg999/supascan/actions/workflows/tests.yml) [](https://raw.githubusercontent.com/abhishekg999/supascan/master/LICENCE)
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
**supascan** is an automated security scanner for Supabase databases. It detects exposed data, analyzes Row Level Security (RLS) policies, tests RPC functions, and generates comprehensive security reports.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
bun install -g supascan
|
|
9
|
-
```
|
|
7
|
+
## Features
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
- Automated schema and table discovery
|
|
10
|
+
- RLS policy effectiveness testing
|
|
11
|
+
- Exposed data detection with row count estimation
|
|
12
|
+
- RPC function parameter analysis and testing
|
|
13
|
+
- JWT token decoding and validation
|
|
14
|
+
- Multiple output formats (Console, JSON, HTML)
|
|
15
|
+
- Interactive HTML reports with live query interface
|
|
16
|
+
- Credential extraction from JavaScript files (experimental)
|
|
12
17
|
|
|
13
|
-
|
|
18
|
+
## Installation
|
|
14
19
|
|
|
15
|
-
|
|
20
|
+
**NPM:**
|
|
16
21
|
|
|
17
22
|
```bash
|
|
18
|
-
|
|
23
|
+
npm install -g supascan
|
|
19
24
|
```
|
|
20
25
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
#### Database Analysis
|
|
26
|
+
**Bun:**
|
|
24
27
|
|
|
25
28
|
```bash
|
|
26
|
-
|
|
27
|
-
supascan --url <url> --key <key>
|
|
28
|
-
|
|
29
|
-
# Analyze specific schema
|
|
30
|
-
supascan --url <url> --key <key> --schema public
|
|
31
|
-
|
|
32
|
-
# Generate HTML report
|
|
33
|
-
supascan --url <url> --key <key> --html
|
|
34
|
-
|
|
35
|
-
# JSON output
|
|
36
|
-
supascan --url <url> --key <key> --json
|
|
29
|
+
bun install -g supascan
|
|
37
30
|
```
|
|
38
31
|
|
|
39
|
-
|
|
32
|
+
**From source:**
|
|
40
33
|
|
|
41
34
|
```bash
|
|
42
|
-
|
|
43
|
-
supascan
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
supascan --url <url> --key <key> --dump public
|
|
35
|
+
git clone https://github.com/abhishekg999/supascan.git
|
|
36
|
+
cd supascan
|
|
37
|
+
bun install
|
|
38
|
+
bun run build
|
|
47
39
|
```
|
|
48
40
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
```bash
|
|
52
|
-
# Get RPC help
|
|
53
|
-
supascan --url <url> --key <key> --rpc public.get_user_stats
|
|
54
|
-
|
|
55
|
-
# Call RPC with parameters
|
|
56
|
-
supascan --url <url> --key <key> --rpc public.get_user_stats --args '{"user_id": "123"}'
|
|
57
|
-
|
|
58
|
-
# Show query execution plan
|
|
59
|
-
supascan --url <url> --key <key> --rpc public.get_user_stats --args '{"user_id": "123"}' --explain
|
|
60
|
-
```
|
|
41
|
+
## Usage
|
|
61
42
|
|
|
62
|
-
|
|
43
|
+
To get basic options and usage:
|
|
63
44
|
|
|
64
45
|
```bash
|
|
65
|
-
|
|
66
|
-
supascan --extract https://example.com/app.js --url <url> --key <key>
|
|
46
|
+
supascan --help
|
|
67
47
|
```
|
|
68
48
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
| Option | Description |
|
|
72
|
-
| ---------------------------------- | ---------------------------------------------------------------- |
|
|
73
|
-
| `-u, --url <url>` | Supabase URL |
|
|
74
|
-
| `-k, --key <key>` | Supabase anon key |
|
|
75
|
-
| `-s, --schema <schema>` | Schema to analyze (default: all schemas) |
|
|
76
|
-
| `-x, --extract <url>` | Extract credentials from JS file URL (experimental) |
|
|
77
|
-
| `--dump <schema.table\|schema>` | Dump data from specific table or swagger JSON from schema |
|
|
78
|
-
| `--limit <number>` | Limit rows for dump or RPC results (default: 10) |
|
|
79
|
-
| `--rpc <schema.rpc_name>` | Call an RPC function (read-only operations only) |
|
|
80
|
-
| `--args <json>` | JSON arguments for RPC call (use $VAR for environment variables) |
|
|
81
|
-
| `--json` | Output as JSON |
|
|
82
|
-
| `--html` | Generate HTML report |
|
|
83
|
-
| `-d, --debug` | Enable debug mode |
|
|
84
|
-
| `--explain` | Show query execution plan |
|
|
85
|
-
| `--suppress-experimental-warnings` | Suppress experimental warnings |
|
|
86
|
-
|
|
87
|
-
## What supascan Analyzes
|
|
49
|
+
### Quick Start
|
|
88
50
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
- **Table Access Analysis**: Identifies which tables are:
|
|
93
|
-
- ✅ **Readable** - Data is exposed and accessible
|
|
94
|
-
- ⚠️ **Empty/Protected** - No data or protected by RLS
|
|
95
|
-
- ❌ **Denied** - Access is explicitly denied
|
|
96
|
-
|
|
97
|
-
### JWT Token Analysis
|
|
98
|
-
|
|
99
|
-
- Parses and displays JWT token information
|
|
100
|
-
- Shows issuer, audience, expiration, and role information
|
|
101
|
-
|
|
102
|
-
### RPC Function Analysis
|
|
103
|
-
|
|
104
|
-
- Lists all available RPC functions
|
|
105
|
-
- Shows parameter requirements and types
|
|
106
|
-
- Validates parameters before execution
|
|
107
|
-
|
|
108
|
-
### Output Formats
|
|
109
|
-
|
|
110
|
-
- **Console**: Colorized terminal output with detailed analysis
|
|
111
|
-
- **JSON**: Machine-readable output for scripting
|
|
112
|
-
- **HTML**: Visual report that opens in your browser
|
|
113
|
-
|
|
114
|
-
## Examples
|
|
51
|
+
```bash
|
|
52
|
+
# Basic security scan
|
|
53
|
+
supascan --url https://your-project.supabase.co --key your-anon-key
|
|
115
54
|
|
|
116
|
-
|
|
55
|
+
# Generate HTML report
|
|
56
|
+
supascan --url https://your-project.supabase.co --key your-anon-key --html
|
|
117
57
|
|
|
118
|
-
|
|
119
|
-
supascan --url https://
|
|
120
|
-
```
|
|
58
|
+
# Analyze specific schema
|
|
59
|
+
supascan --url https://your-project.supabase.co --key your-anon-key --schema public
|
|
121
60
|
|
|
122
|
-
|
|
61
|
+
# Dump table data
|
|
62
|
+
supascan --url https://your-project.supabase.co --key your-anon-key --dump public.users --limit 100
|
|
123
63
|
|
|
124
|
-
|
|
125
|
-
supascan --url https://
|
|
64
|
+
# Test RPC function
|
|
65
|
+
supascan --url https://your-project.supabase.co --key your-anon-key --rpc public.my_function --args '{"param": "value"}'
|
|
126
66
|
```
|
|
127
67
|
|
|
128
|
-
|
|
68
|
+
## What supascan Detects
|
|
129
69
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
70
|
+
- **Exposed Tables**: Tables readable without authentication or with weak RLS
|
|
71
|
+
- **Data Leakage**: Estimated row counts for accessible tables
|
|
72
|
+
- **RPC Vulnerabilities**: Publicly callable functions and their parameters
|
|
73
|
+
- **JWT Issues**: Token expiration, role assignments, and claims
|
|
74
|
+
- **Schema Information**: Complete database structure visibility
|
|
133
75
|
|
|
134
76
|
## Security Considerations
|
|
135
77
|
|
|
136
|
-
⚠️ **Important**: This tool is
|
|
78
|
+
⚠️ **Important**: This tool is for authorized security testing only.
|
|
137
79
|
|
|
138
|
-
-
|
|
139
|
-
-
|
|
140
|
-
-
|
|
80
|
+
- Only scan databases you own or have explicit permission to test
|
|
81
|
+
- Use on staging/development environments when possible
|
|
82
|
+
- Never use on production databases without proper authorization
|
|
83
|
+
- Be aware that scanning may trigger rate limits or monitoring alerts
|
|
141
84
|
|
|
142
|
-
|
|
85
|
+
Unauthorized database scanning may be illegal in your jurisdiction.
|
|
143
86
|
|
|
144
87
|
## Development
|
|
145
88
|
|
|
@@ -150,12 +93,19 @@ bun install
|
|
|
150
93
|
# Run locally
|
|
151
94
|
bun run start
|
|
152
95
|
|
|
96
|
+
# Run tests
|
|
97
|
+
bun test
|
|
98
|
+
|
|
153
99
|
# Build
|
|
154
100
|
bun run build
|
|
101
|
+
```
|
|
155
102
|
|
|
156
|
-
|
|
157
|
-
bun test
|
|
103
|
+
## License
|
|
158
104
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
105
|
+
supascan is distributed under the [MIT License](LICENCE).
|
|
106
|
+
|
|
107
|
+
## Links
|
|
108
|
+
|
|
109
|
+
- **Homepage**: https://github.com/abhishekg999/supascan
|
|
110
|
+
- **Issues**: https://github.com/abhishekg999/supascan/issues
|
|
111
|
+
- **NPM**: https://www.npmjs.com/package/supascan
|
package/dist/supascan.js
CHANGED
|
@@ -13931,19 +13931,24 @@ var openInBrowser = (filePath) => {
|
|
|
13931
13931
|
const platform = process.platform;
|
|
13932
13932
|
let command;
|
|
13933
13933
|
let args;
|
|
13934
|
-
|
|
13935
|
-
|
|
13936
|
-
|
|
13937
|
-
|
|
13938
|
-
|
|
13939
|
-
|
|
13940
|
-
|
|
13941
|
-
|
|
13942
|
-
|
|
13943
|
-
|
|
13944
|
-
|
|
13945
|
-
|
|
13946
|
-
|
|
13934
|
+
if (Bun.env.BROWSER) {
|
|
13935
|
+
command = Bun.env.BROWSER;
|
|
13936
|
+
args = [filePath];
|
|
13937
|
+
} else {
|
|
13938
|
+
switch (platform) {
|
|
13939
|
+
case "darwin":
|
|
13940
|
+
command = "open";
|
|
13941
|
+
args = [filePath];
|
|
13942
|
+
break;
|
|
13943
|
+
case "win32":
|
|
13944
|
+
command = "start";
|
|
13945
|
+
args = [filePath];
|
|
13946
|
+
break;
|
|
13947
|
+
default:
|
|
13948
|
+
command = "xdg-open";
|
|
13949
|
+
args = [filePath];
|
|
13950
|
+
break;
|
|
13951
|
+
}
|
|
13947
13952
|
}
|
|
13948
13953
|
spawn(command, args, { detached: true, stdio: "ignore" });
|
|
13949
13954
|
};
|
|
@@ -14062,11 +14067,22 @@ class SupabaseService {
|
|
|
14062
14067
|
}
|
|
14063
14068
|
const hasData = data && data.length > 0;
|
|
14064
14069
|
if (hasData) {
|
|
14065
|
-
|
|
14066
|
-
|
|
14070
|
+
const { count } = await ctx.client.schema(schema).from(table).select("*", { count: "estimated", head: true });
|
|
14071
|
+
log.debug(ctx, `Table ${table} is readable with ~${count ?? "unknown"} rows (EXPOSED)`);
|
|
14072
|
+
return ok({
|
|
14073
|
+
status: "readable",
|
|
14074
|
+
accessible: true,
|
|
14075
|
+
hasData: true,
|
|
14076
|
+
rowCount: count ?? undefined
|
|
14077
|
+
});
|
|
14067
14078
|
}
|
|
14068
14079
|
log.debug(ctx, `Table ${table} returned 0 rows (empty or RLS blocked)`);
|
|
14069
|
-
return ok({
|
|
14080
|
+
return ok({
|
|
14081
|
+
status: "empty",
|
|
14082
|
+
accessible: true,
|
|
14083
|
+
hasData: false,
|
|
14084
|
+
rowCount: 0
|
|
14085
|
+
});
|
|
14070
14086
|
}
|
|
14071
14087
|
static async testTablesRead(ctx, schema, tables) {
|
|
14072
14088
|
log.debug(ctx, `Testing read access for ${tables.length} tables`);
|
|
@@ -14255,15 +14271,11 @@ function toggleApiKey() {
|
|
|
14255
14271
|
|
|
14256
14272
|
async function saveReport() {
|
|
14257
14273
|
try {
|
|
14258
|
-
// Generate filename with timestamp and domain
|
|
14259
14274
|
const domain = '${domain}';
|
|
14260
|
-
const timestamp = new Date().toISOString().split('T')[0];
|
|
14275
|
+
const timestamp = new Date().toISOString().split('T')[0];
|
|
14261
14276
|
const filename = \`supabase-analysis-\${domain}-\${timestamp}.html\`;
|
|
14262
|
-
|
|
14263
|
-
// Get the current HTML content
|
|
14264
14277
|
const htmlContent = document.documentElement.outerHTML;
|
|
14265
14278
|
|
|
14266
|
-
// Check if File System Access API is supported
|
|
14267
14279
|
if ('showSaveFilePicker' in window) {
|
|
14268
14280
|
const fileHandle = await window.showSaveFilePicker({
|
|
14269
14281
|
suggestedName: filename,
|
|
@@ -14278,11 +14290,8 @@ async function saveReport() {
|
|
|
14278
14290
|
const writable = await fileHandle.createWritable();
|
|
14279
14291
|
await writable.write(htmlContent);
|
|
14280
14292
|
await writable.close();
|
|
14281
|
-
|
|
14282
|
-
// Show success message
|
|
14283
14293
|
showNotification('Report saved successfully!', 'success');
|
|
14284
14294
|
} else {
|
|
14285
|
-
// Fallback for browsers that don't support File System Access API
|
|
14286
14295
|
const blob = new Blob([htmlContent], { type: 'text/html' });
|
|
14287
14296
|
const url = URL.createObjectURL(blob);
|
|
14288
14297
|
const a = document.createElement('a');
|
|
@@ -14296,17 +14305,13 @@ async function saveReport() {
|
|
|
14296
14305
|
showNotification('Report downloaded successfully!', 'success');
|
|
14297
14306
|
}
|
|
14298
14307
|
} catch (error) {
|
|
14299
|
-
if (error.name === 'AbortError')
|
|
14300
|
-
// User cancelled the save dialog
|
|
14301
|
-
return;
|
|
14302
|
-
}
|
|
14308
|
+
if (error.name === 'AbortError') return;
|
|
14303
14309
|
console.error('Error saving report:', error);
|
|
14304
14310
|
showNotification('Failed to save report: ' + error.message, 'error');
|
|
14305
14311
|
}
|
|
14306
14312
|
}
|
|
14307
14313
|
|
|
14308
14314
|
function showNotification(message, type = 'info') {
|
|
14309
|
-
// Create notification element
|
|
14310
14315
|
const notification = document.createElement('div');
|
|
14311
14316
|
notification.className = \`fixed top-4 right-4 px-4 py-3 rounded-lg shadow-lg z-50 font-mono text-sm transition-all duration-300 transform translate-x-full\`;
|
|
14312
14317
|
|
|
@@ -14321,17 +14326,10 @@ function showNotification(message, type = 'info') {
|
|
|
14321
14326
|
notification.textContent = message;
|
|
14322
14327
|
document.body.appendChild(notification);
|
|
14323
14328
|
|
|
14324
|
-
|
|
14325
|
-
setTimeout(() => {
|
|
14326
|
-
notification.classList.remove('translate-x-full');
|
|
14327
|
-
}, 100);
|
|
14328
|
-
|
|
14329
|
-
// Remove after 3 seconds
|
|
14329
|
+
setTimeout(() => notification.classList.remove('translate-x-full'), 100);
|
|
14330
14330
|
setTimeout(() => {
|
|
14331
14331
|
notification.classList.add('translate-x-full');
|
|
14332
|
-
setTimeout(() =>
|
|
14333
|
-
document.body.removeChild(notification);
|
|
14334
|
-
}, 300);
|
|
14332
|
+
setTimeout(() => document.body.removeChild(notification), 300);
|
|
14335
14333
|
}, 3000);
|
|
14336
14334
|
}
|
|
14337
14335
|
|
|
@@ -14342,18 +14340,13 @@ function escapeHtml(text) {
|
|
|
14342
14340
|
}
|
|
14343
14341
|
|
|
14344
14342
|
function renderSmartTable(data) {
|
|
14345
|
-
console.log('renderSmartTable called with data:', data);
|
|
14346
|
-
|
|
14347
14343
|
if (!data || data.length === 0) {
|
|
14348
|
-
console.log('No data to render');
|
|
14349
14344
|
return '<div class="p-8 text-center text-gray-400 text-sm font-mono">No data</div>';
|
|
14350
14345
|
}
|
|
14351
14346
|
|
|
14352
14347
|
const columns = Object.keys(data[0]);
|
|
14353
14348
|
const maxRows = Math.min(data.length, 100);
|
|
14354
14349
|
|
|
14355
|
-
console.log(\`Rendering table with \${columns.length} columns and \${maxRows} rows\`);
|
|
14356
|
-
|
|
14357
14350
|
let html = \`
|
|
14358
14351
|
<div class="overflow-x-auto scrollbar-thin">
|
|
14359
14352
|
<table class="w-full text-xs font-mono">
|
|
@@ -14406,7 +14399,6 @@ function renderSmartTable(data) {
|
|
|
14406
14399
|
html += \`<div class="px-3 py-2 text-xs text-slate-500 bg-slate-50 border-t border-slate-200 font-mono">Showing \${maxRows} of \${data.length} rows</div>\`;
|
|
14407
14400
|
}
|
|
14408
14401
|
|
|
14409
|
-
console.log('Table HTML generated successfully');
|
|
14410
14402
|
return html;
|
|
14411
14403
|
}
|
|
14412
14404
|
|
|
@@ -14414,7 +14406,6 @@ function executeQuery(uniqueId) {
|
|
|
14414
14406
|
const operation = document.getElementById(\`query-operation-\${uniqueId}\`).value;
|
|
14415
14407
|
const resultsDiv = document.getElementById(\`query-results-\${uniqueId}\`);
|
|
14416
14408
|
|
|
14417
|
-
// Better loading feedback
|
|
14418
14409
|
resultsDiv.innerHTML = \`
|
|
14419
14410
|
<div class="p-8 text-center">
|
|
14420
14411
|
<div class="inline-flex items-center gap-3 text-slate-600 font-mono text-sm">
|
|
@@ -14481,23 +14472,16 @@ function executeQuery(uniqueId) {
|
|
|
14481
14472
|
}
|
|
14482
14473
|
|
|
14483
14474
|
query.then(({ data, error }) => {
|
|
14484
|
-
console.log('Query result:', { data, error });
|
|
14485
|
-
|
|
14486
14475
|
if (error) {
|
|
14487
|
-
console.error('Query error:', error);
|
|
14488
14476
|
resultsDiv.innerHTML = \`<div class="p-4 text-red-600 text-sm font-mono bg-red-50 border border-red-200 rounded">Error: \${error.message}</div>\`;
|
|
14489
14477
|
} else {
|
|
14490
|
-
console.log('Query successful, data length:', data ? data.length : 0);
|
|
14491
14478
|
if (data && data.length > 0) {
|
|
14492
|
-
console.log('Calling renderSmartTable with data:', data);
|
|
14493
14479
|
resultsDiv.innerHTML = renderSmartTable(data);
|
|
14494
14480
|
} else {
|
|
14495
|
-
console.log('No data returned');
|
|
14496
14481
|
resultsDiv.innerHTML = '<div class="p-8 text-center text-gray-400 text-sm font-mono">No data returned</div>';
|
|
14497
14482
|
}
|
|
14498
14483
|
}
|
|
14499
14484
|
}).catch((err) => {
|
|
14500
|
-
console.error('Query execution error:', err);
|
|
14501
14485
|
resultsDiv.innerHTML = \`<div class="p-4 text-red-600 text-sm font-mono bg-red-50 border border-red-200 rounded">Execution error: \${err.message}</div>\`;
|
|
14502
14486
|
});
|
|
14503
14487
|
|
|
@@ -14509,7 +14493,6 @@ function executeQuery(uniqueId) {
|
|
|
14509
14493
|
function executeRPC(rpcName, uniqueId, schema) {
|
|
14510
14494
|
const resultsDiv = document.getElementById(\`rpc-results-\${uniqueId}\`);
|
|
14511
14495
|
|
|
14512
|
-
// Loading feedback
|
|
14513
14496
|
resultsDiv.innerHTML = \`
|
|
14514
14497
|
<div class="p-8 text-center">
|
|
14515
14498
|
<div class="inline-flex items-center gap-3 text-slate-600 font-mono text-sm">
|
|
@@ -14520,7 +14503,6 @@ function executeQuery(uniqueId) {
|
|
|
14520
14503
|
\`;
|
|
14521
14504
|
|
|
14522
14505
|
try {
|
|
14523
|
-
// Collect parameters from form inputs
|
|
14524
14506
|
const params = {};
|
|
14525
14507
|
const paramInputs = document.querySelectorAll(\`[id^="rpc-param-"][id$="-\${uniqueId}"]\`);
|
|
14526
14508
|
|
|
@@ -14529,41 +14511,29 @@ function executeQuery(uniqueId) {
|
|
|
14529
14511
|
const value = input.value.trim();
|
|
14530
14512
|
|
|
14531
14513
|
if (value !== '') {
|
|
14532
|
-
// Try to parse as JSON for complex types, otherwise use as string
|
|
14533
14514
|
try {
|
|
14534
14515
|
params[paramName] = JSON.parse(value);
|
|
14535
14516
|
} catch {
|
|
14536
|
-
// If JSON parsing fails, use the raw value
|
|
14537
14517
|
params[paramName] = value;
|
|
14538
14518
|
}
|
|
14539
14519
|
}
|
|
14540
14520
|
});
|
|
14541
14521
|
|
|
14542
|
-
console.log('Executing RPC:', rpcName, 'in schema:', schema, 'with params:', params);
|
|
14543
|
-
|
|
14544
14522
|
const cleanRpcName = rpcName.startsWith('rpc/') ? rpcName.slice(4) : rpcName;
|
|
14545
|
-
console.log('Clean RPC name:', cleanRpcName);
|
|
14546
|
-
|
|
14547
14523
|
const rpcCall = supabase.schema(schema).rpc(cleanRpcName, params);
|
|
14548
14524
|
|
|
14549
14525
|
rpcCall.then(({ data, error }) => {
|
|
14550
|
-
console.log('RPC result:', { data, error });
|
|
14551
|
-
|
|
14552
14526
|
if (error) {
|
|
14553
|
-
console.error('RPC error:', error);
|
|
14554
14527
|
resultsDiv.innerHTML = \`<div class="p-4 text-red-600 text-sm font-mono bg-red-50 border border-red-200 rounded">Error: \${error.message}</div>\`;
|
|
14555
14528
|
} else {
|
|
14556
|
-
console.log('RPC successful, data:', data);
|
|
14557
14529
|
if (data !== null && data !== undefined) {
|
|
14558
14530
|
if (Array.isArray(data)) {
|
|
14559
|
-
// If it's an array, render as table
|
|
14560
14531
|
if (data.length > 0) {
|
|
14561
14532
|
resultsDiv.innerHTML = renderSmartTable(data);
|
|
14562
14533
|
} else {
|
|
14563
14534
|
resultsDiv.innerHTML = '<div class="p-8 text-center text-gray-400 text-sm font-mono">RPC returned empty array</div>';
|
|
14564
14535
|
}
|
|
14565
14536
|
} else {
|
|
14566
|
-
// If it's a single value, display it nicely
|
|
14567
14537
|
resultsDiv.innerHTML = \`
|
|
14568
14538
|
<div class="p-6">
|
|
14569
14539
|
<div class="bg-slate-50 border border-slate-200 rounded-lg p-4">
|
|
@@ -14578,7 +14548,6 @@ function executeQuery(uniqueId) {
|
|
|
14578
14548
|
}
|
|
14579
14549
|
}
|
|
14580
14550
|
}).catch((err) => {
|
|
14581
|
-
console.error('RPC execution error:', err);
|
|
14582
14551
|
resultsDiv.innerHTML = \`<div class="p-4 text-red-600 text-sm font-mono bg-red-50 border border-red-200 rounded">Execution error: \${err.message}</div>\`;
|
|
14583
14552
|
});
|
|
14584
14553
|
|
|
@@ -14586,16 +14555,12 @@ function executeQuery(uniqueId) {
|
|
|
14586
14555
|
resultsDiv.innerHTML = \`<div class="text-red-600">Error: \${err.message}</div>\`;
|
|
14587
14556
|
}
|
|
14588
14557
|
}
|
|
14589
|
-
|
|
14590
|
-
// Show/hide query type sections
|
|
14558
|
+
|
|
14591
14559
|
document.addEventListener('DOMContentLoaded', function() {
|
|
14592
|
-
// Add event listeners to all operation selects
|
|
14593
14560
|
document.querySelectorAll('[id^="query-operation-"]').forEach(select => {
|
|
14594
14561
|
select.addEventListener('change', function() {
|
|
14595
14562
|
const uniqueId = this.id.replace('query-operation-', '');
|
|
14596
|
-
// Hide all query types for this specific interface
|
|
14597
14563
|
document.querySelectorAll(\`[id$="-\${uniqueId}"].query-type\`).forEach(el => el.classList.add('hidden'));
|
|
14598
|
-
// Show the selected query type
|
|
14599
14564
|
const targetId = this.value + '-query-' + uniqueId;
|
|
14600
14565
|
const target = document.getElementById(targetId);
|
|
14601
14566
|
if (target) target.classList.remove('hidden');
|
|
@@ -14738,10 +14703,13 @@ function APICredentialsDisplay({ url, key }) {
|
|
|
14738
14703
|
class: "flex items-center gap-1",
|
|
14739
14704
|
children: [
|
|
14740
14705
|
/* @__PURE__ */ $jsxDEV("span", {
|
|
14706
|
+
class: "flex-1 min-w-0",
|
|
14741
14707
|
children: [
|
|
14742
|
-
"Key:
|
|
14708
|
+
"Key:",
|
|
14709
|
+
" ",
|
|
14743
14710
|
/* @__PURE__ */ $jsxDEV("span", {
|
|
14744
14711
|
id: "api-key-display",
|
|
14712
|
+
class: "break-all",
|
|
14745
14713
|
children: [
|
|
14746
14714
|
key.substring(0, 20),
|
|
14747
14715
|
"..."
|
|
@@ -14751,7 +14719,7 @@ function APICredentialsDisplay({ url, key }) {
|
|
|
14751
14719
|
}, undefined, true, undefined, this),
|
|
14752
14720
|
/* @__PURE__ */ $jsxDEV("button", {
|
|
14753
14721
|
id: "api-key-toggle",
|
|
14754
|
-
class: "px-1 py-0.5 text-xs bg-blue-100 text-blue-800 rounded hover:bg-blue-200 transition-colors",
|
|
14722
|
+
class: "px-1 py-0.5 text-xs bg-blue-100 text-blue-800 rounded hover:bg-blue-200 transition-colors whitespace-nowrap flex-shrink-0",
|
|
14755
14723
|
onclick: "toggleApiKey()",
|
|
14756
14724
|
children: "Show Full Key"
|
|
14757
14725
|
}, undefined, false, undefined, this)
|
|
@@ -14881,12 +14849,12 @@ function TableRow({
|
|
|
14881
14849
|
switch (access?.status) {
|
|
14882
14850
|
case "readable":
|
|
14883
14851
|
statusClass = "bg-green-100 text-green-800 border-green-200";
|
|
14884
|
-
statusText = "
|
|
14852
|
+
statusText = `~${access.rowCount ?? "?"} rows exposed`;
|
|
14885
14853
|
statusIcon = "[+]";
|
|
14886
14854
|
break;
|
|
14887
14855
|
case "empty":
|
|
14888
14856
|
statusClass = "bg-yellow-100 text-yellow-800 border-yellow-200";
|
|
14889
|
-
statusText = "
|
|
14857
|
+
statusText = "0 rows - empty or RLS";
|
|
14890
14858
|
statusIcon = "[-]";
|
|
14891
14859
|
break;
|
|
14892
14860
|
case "denied":
|
|
@@ -15436,8 +15404,11 @@ class HtmlRendererService {
|
|
|
15436
15404
|
content: "width=device-width, initial-scale=1.0"
|
|
15437
15405
|
}, undefined, false, undefined, this),
|
|
15438
15406
|
/* @__PURE__ */ $jsxDEV("title", {
|
|
15439
|
-
children:
|
|
15440
|
-
|
|
15407
|
+
children: [
|
|
15408
|
+
result.summary.domain,
|
|
15409
|
+
" - Security Analysis"
|
|
15410
|
+
]
|
|
15411
|
+
}, undefined, true, undefined, this),
|
|
15441
15412
|
/* @__PURE__ */ $jsxDEV("script", {
|
|
15442
15413
|
src: "https://cdn.tailwindcss.com"
|
|
15443
15414
|
}, undefined, false, undefined, this),
|
|
@@ -15541,7 +15512,7 @@ class HtmlRendererService {
|
|
|
15541
15512
|
children: [
|
|
15542
15513
|
/* @__PURE__ */ $jsxDEV("h1", {
|
|
15543
15514
|
class: "text-2xl font-bold text-slate-900 mb-1 font-mono",
|
|
15544
|
-
children:
|
|
15515
|
+
children: result.summary.domain
|
|
15545
15516
|
}, undefined, false, undefined, this),
|
|
15546
15517
|
/* @__PURE__ */ $jsxDEV("p", {
|
|
15547
15518
|
class: "text-slate-600 font-mono text-sm",
|
|
@@ -15624,8 +15595,20 @@ class HtmlRendererService {
|
|
|
15624
15595
|
/* @__PURE__ */ $jsxDEV("footer", {
|
|
15625
15596
|
class: "mt-12 text-center text-slate-500 text-sm font-mono",
|
|
15626
15597
|
children: /* @__PURE__ */ $jsxDEV("p", {
|
|
15627
|
-
children:
|
|
15628
|
-
|
|
15598
|
+
children: [
|
|
15599
|
+
"Generated by",
|
|
15600
|
+
" ",
|
|
15601
|
+
/* @__PURE__ */ $jsxDEV("a", {
|
|
15602
|
+
href: "https://github.com/abhishekg999/supascan",
|
|
15603
|
+
target: "_blank",
|
|
15604
|
+
rel: "noopener noreferrer",
|
|
15605
|
+
class: "text-supabase-green hover:text-emerald-600 transition-colors underline",
|
|
15606
|
+
children: "supascan"
|
|
15607
|
+
}, undefined, false, undefined, this),
|
|
15608
|
+
" ",
|
|
15609
|
+
"- Security analysis tool for Supabase"
|
|
15610
|
+
]
|
|
15611
|
+
}, undefined, true, undefined, this)
|
|
15629
15612
|
}, undefined, false, undefined, this)
|
|
15630
15613
|
]
|
|
15631
15614
|
}, undefined, true, undefined, this)
|
|
@@ -15719,7 +15702,7 @@ function displayAnalysisResult(result) {
|
|
|
15719
15702
|
switch (access?.status) {
|
|
15720
15703
|
case "readable":
|
|
15721
15704
|
indicator = import_picocolors2.default.green("[+]");
|
|
15722
|
-
description = import_picocolors2.default.dim(
|
|
15705
|
+
description = import_picocolors2.default.dim(`(~${access.rowCount ?? "?"} rows exposed)`);
|
|
15723
15706
|
break;
|
|
15724
15707
|
case "empty":
|
|
15725
15708
|
indicator = import_picocolors2.default.yellow("[-]");
|
|
@@ -16184,7 +16167,7 @@ var experimentalWarning2 = onlyOnce2(() => {
|
|
|
16184
16167
|
}
|
|
16185
16168
|
});
|
|
16186
16169
|
// package.json
|
|
16187
|
-
var version = "0.0
|
|
16170
|
+
var version = "0.1.0";
|
|
16188
16171
|
|
|
16189
16172
|
// version.ts
|
|
16190
16173
|
var VERSION = version;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "supascan",
|
|
3
|
-
"version": "0.0
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Automated security scanner for Supabase databases - detect exposed data, analyze RLS policies, and test RPC functions",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Abhishek Govindarasu",
|
|
7
7
|
"main": "dist/supascan.js",
|