supascan 0.0.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/.bun-version +1 -0
- package/.github/workflows/release-github.yml +70 -0
- package/.github/workflows/release-npm.yml +45 -0
- package/.github/workflows/tests.yml +36 -0
- package/LICENCE +22 -0
- package/README.md +115 -0
- package/apps/cli/build.ts +37 -0
- package/apps/cli/package.json +29 -0
- package/apps/cli/src/commands/analyze.ts +213 -0
- package/apps/cli/src/commands/dump.ts +68 -0
- package/apps/cli/src/commands/rpc.ts +67 -0
- package/apps/cli/src/context.ts +96 -0
- package/apps/cli/src/embedded-report.ts +1 -0
- package/apps/cli/src/formatters/console.ts +39 -0
- package/apps/cli/src/formatters/events.ts +95 -0
- package/apps/cli/src/index.ts +105 -0
- package/apps/cli/src/types.ts +9 -0
- package/apps/cli/src/utils/args.ts +46 -0
- package/apps/cli/src/utils/browser.ts +29 -0
- package/apps/cli/src/utils/files.ts +12 -0
- package/apps/cli/src/version.ts +3 -0
- package/apps/web/build.ts +68 -0
- package/apps/web/dev.ts +5 -0
- package/apps/web/index.html +75 -0
- package/apps/web/package.json +23 -0
- package/apps/web/src/App.tsx +129 -0
- package/apps/web/src/components/QueryBuilder.tsx +174 -0
- package/apps/web/src/components/QueryWindow.tsx +133 -0
- package/apps/web/src/components/RPCExecutor.tsx +176 -0
- package/apps/web/src/components/SchemaBrowser.tsx +269 -0
- package/apps/web/src/components/SmartTable.tsx +129 -0
- package/apps/web/src/components/TargetConfig.tsx +130 -0
- package/apps/web/src/components/TargetSummary.tsx +105 -0
- package/apps/web/src/hooks/useAnalysis.ts +54 -0
- package/apps/web/src/hooks/useNotification.ts +28 -0
- package/apps/web/src/hooks/useRPC.ts +53 -0
- package/apps/web/src/hooks/useSupabase.ts +46 -0
- package/apps/web/src/hooks/useTableQuery.ts +148 -0
- package/apps/web/src/index.tsx +18 -0
- package/apps/web/src/types.ts +16 -0
- package/apps/web/src/utils/hash.ts +27 -0
- package/context.test.ts +93 -0
- package/package.json +48 -0
- package/package.publish.json +18 -0
- package/packages/core/package.json +22 -0
- package/packages/core/src/analyzer.ts +212 -0
- package/packages/core/src/extractor.ts +233 -0
- package/packages/core/src/index.ts +9 -0
- package/packages/core/src/supabase.ts +316 -0
- package/packages/core/src/types/analyzer.types.ts +72 -0
- package/packages/core/src/types/event.types.ts +4 -0
- package/packages/core/src/types/events.types.ts +5 -0
- package/packages/core/src/types/extractor.types.ts +54 -0
- package/packages/core/src/types/result.types.ts +17 -0
- package/packages/core/src/types/supabase.types.ts +98 -0
- package/tsconfig.json +23 -0
- package/turbo.json +19 -0
- package/utils.test.ts +68 -0
- package/version.ts +3 -0
package/.bun-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.3.2
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
name: Create GitHub Release
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [master]
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
tests:
|
|
9
|
+
uses: ./.github/workflows/tests.yml
|
|
10
|
+
|
|
11
|
+
release:
|
|
12
|
+
needs: tests
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: write
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
fetch-depth: 0
|
|
22
|
+
|
|
23
|
+
- uses: oven-sh/setup-bun@v2
|
|
24
|
+
with:
|
|
25
|
+
bun-version-file: ".bun-version"
|
|
26
|
+
|
|
27
|
+
- run: bun install --frozen-lockfile
|
|
28
|
+
- run: bun run build:binary
|
|
29
|
+
|
|
30
|
+
- name: Get version from package.json
|
|
31
|
+
id: pkg
|
|
32
|
+
run: echo "version=$(bun -e "console.log(require('./package.json').version)")" >> $GITHUB_OUTPUT
|
|
33
|
+
|
|
34
|
+
- name: Check if tag exists
|
|
35
|
+
id: tag-check
|
|
36
|
+
run: |
|
|
37
|
+
if git rev-parse "v${{ steps.pkg.outputs.version }}" >/dev/null 2>&1; then
|
|
38
|
+
echo "exists=true" >> $GITHUB_OUTPUT
|
|
39
|
+
else
|
|
40
|
+
echo "exists=false" >> $GITHUB_OUTPUT
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
- name: Create and push tag
|
|
44
|
+
if: steps.tag-check.outputs.exists == 'false'
|
|
45
|
+
run: |
|
|
46
|
+
git config user.name "github-actions"
|
|
47
|
+
git config user.email "github-actions@github.com"
|
|
48
|
+
git tag v${{ steps.pkg.outputs.version }}
|
|
49
|
+
git push origin v${{ steps.pkg.outputs.version }}
|
|
50
|
+
|
|
51
|
+
- name: Check if release exists
|
|
52
|
+
id: release-check
|
|
53
|
+
run: |
|
|
54
|
+
if gh release view "v${{ steps.pkg.outputs.version }}" >/dev/null 2>&1; then
|
|
55
|
+
echo "exists=true" >> $GITHUB_OUTPUT
|
|
56
|
+
else
|
|
57
|
+
echo "exists=false" >> $GITHUB_OUTPUT
|
|
58
|
+
fi
|
|
59
|
+
env:
|
|
60
|
+
GH_TOKEN: ${{ github.token }}
|
|
61
|
+
|
|
62
|
+
- name: Create GitHub Release
|
|
63
|
+
if: steps.release-check.outputs.exists == 'false'
|
|
64
|
+
uses: softprops/action-gh-release@v1
|
|
65
|
+
with:
|
|
66
|
+
tag_name: v${{ steps.pkg.outputs.version }}
|
|
67
|
+
name: Release v${{ steps.pkg.outputs.version }}
|
|
68
|
+
generate_release_notes: true
|
|
69
|
+
files: |
|
|
70
|
+
dist/supascan
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [master]
|
|
5
|
+
workflow_dispatch:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
tests:
|
|
9
|
+
uses: ./.github/workflows/tests.yml
|
|
10
|
+
|
|
11
|
+
publish:
|
|
12
|
+
needs: tests
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
permissions:
|
|
15
|
+
contents: read
|
|
16
|
+
id-token: write
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- uses: oven-sh/setup-bun@v2
|
|
22
|
+
with:
|
|
23
|
+
bun-version-file: ".bun-version"
|
|
24
|
+
|
|
25
|
+
- run: bun install --frozen-lockfile
|
|
26
|
+
- run: bun run build:bundle
|
|
27
|
+
|
|
28
|
+
- name: Get version from package.json
|
|
29
|
+
id: pkg
|
|
30
|
+
run: echo "version=$(bun -e "console.log(require('./package.json').version)")" >> $GITHUB_OUTPUT
|
|
31
|
+
|
|
32
|
+
- name: Check if version exists on npm
|
|
33
|
+
id: npm-check
|
|
34
|
+
run: |
|
|
35
|
+
if npm view supascan@${{ steps.pkg.outputs.version }} version 2>/dev/null; then
|
|
36
|
+
echo "exists=true" >> $GITHUB_OUTPUT
|
|
37
|
+
else
|
|
38
|
+
echo "exists=false" >> $GITHUB_OUTPUT
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
- name: Publish to npm
|
|
42
|
+
if: steps.npm-check.outputs.exists == 'false'
|
|
43
|
+
run: bun publish --access public
|
|
44
|
+
env:
|
|
45
|
+
NPM_CONFIG_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [master]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [master]
|
|
8
|
+
workflow_call:
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
test:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
permissions:
|
|
14
|
+
contents: read
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- uses: oven-sh/setup-bun@v2
|
|
20
|
+
with:
|
|
21
|
+
bun-version-file: ".bun-version"
|
|
22
|
+
|
|
23
|
+
- name: Install dependencies
|
|
24
|
+
run: bun install --frozen-lockfile
|
|
25
|
+
|
|
26
|
+
- name: Lint
|
|
27
|
+
run: bun run lint
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: bun run test
|
|
31
|
+
|
|
32
|
+
- name: Build bundle
|
|
33
|
+
run: bun run build:bundle
|
|
34
|
+
|
|
35
|
+
- name: Build binary
|
|
36
|
+
run: bun run build:binary
|
package/LICENCE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025-present Abhishek Govindarasu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
+
a copy of this software and associated documentation files (the
|
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
+
the following conditions:
|
|
12
|
+
|
|
13
|
+
The above copyright notice and this permission notice shall be
|
|
14
|
+
included in all copies or substantial portions of the Software.
|
|
15
|
+
|
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# supascan
|
|
2
|
+
|
|
3
|
+
[](https://github.com/abhishekg999/supascan/actions/workflows/tests.yml) [](https://raw.githubusercontent.com/abhishekg999/supascan/master/LICENCE)
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
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)
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
**Note:**
|
|
21
|
+
|
|
22
|
+
- Primarily tested with [Bun](https://bun.sh) runtime (Node.js support is experimental)
|
|
23
|
+
|
|
24
|
+
**Bun:**
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bun install -g supascan
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**NPM:**
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
npm install -g supascan
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
**From source:**
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
git clone https://github.com/abhishekg999/supascan.git
|
|
40
|
+
cd supascan
|
|
41
|
+
bun install
|
|
42
|
+
bun run build
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
To get basic options and usage:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
supascan --help
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Quick Start
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Basic security scan
|
|
57
|
+
supascan --url https://your-project.supabase.co --key your-anon-key
|
|
58
|
+
|
|
59
|
+
# Generate HTML report
|
|
60
|
+
supascan --url https://your-project.supabase.co --key your-anon-key --html
|
|
61
|
+
|
|
62
|
+
# Analyze specific schema
|
|
63
|
+
supascan --url https://your-project.supabase.co --key your-anon-key --schema public
|
|
64
|
+
|
|
65
|
+
# Dump table data
|
|
66
|
+
supascan --url https://your-project.supabase.co --key your-anon-key --dump public.users --limit 100
|
|
67
|
+
|
|
68
|
+
# Test RPC function
|
|
69
|
+
supascan --url https://your-project.supabase.co --key your-anon-key --rpc public.my_function --args '{"param": "value"}'
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## What supascan Detects
|
|
73
|
+
|
|
74
|
+
- **Exposed Tables**: Tables readable without authentication or with weak RLS
|
|
75
|
+
- **Data Leakage**: Estimated row counts for accessible tables
|
|
76
|
+
- **RPC Vulnerabilities**: Publicly callable functions and their parameters
|
|
77
|
+
- **JWT Issues**: Token expiration, role assignments, and claims
|
|
78
|
+
- **Schema Information**: Complete database structure visibility
|
|
79
|
+
|
|
80
|
+
## Security Considerations
|
|
81
|
+
|
|
82
|
+
⚠️ **Important**: This tool is for authorized security testing only.
|
|
83
|
+
|
|
84
|
+
- Only scan databases you own or have explicit permission to test
|
|
85
|
+
- Use on staging/development environments when possible
|
|
86
|
+
- Never use on production databases without proper authorization
|
|
87
|
+
- Be aware that scanning may trigger rate limits or monitoring alerts
|
|
88
|
+
|
|
89
|
+
Unauthorized database scanning may be illegal in your jurisdiction.
|
|
90
|
+
|
|
91
|
+
## Development
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Install dependencies
|
|
95
|
+
bun install
|
|
96
|
+
|
|
97
|
+
# Run locally
|
|
98
|
+
bun run start
|
|
99
|
+
|
|
100
|
+
# Run tests
|
|
101
|
+
bun test
|
|
102
|
+
|
|
103
|
+
# Build
|
|
104
|
+
bun run build
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## License
|
|
108
|
+
|
|
109
|
+
supascan is distributed under the [MIT License](LICENCE).
|
|
110
|
+
|
|
111
|
+
## Links
|
|
112
|
+
|
|
113
|
+
- **Homepage**: https://github.com/abhishekg999/supascan
|
|
114
|
+
- **Issues**: https://github.com/abhishekg999/supascan/issues
|
|
115
|
+
- **NPM**: https://www.npmjs.com/package/supascan
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import { mkdir, readFile } from "fs/promises";
|
|
3
|
+
|
|
4
|
+
console.log("Building CLI...");
|
|
5
|
+
|
|
6
|
+
await $`tsc --noEmit`;
|
|
7
|
+
|
|
8
|
+
console.log("Building web app for embedding...");
|
|
9
|
+
await $`cd ../web && bun run build`;
|
|
10
|
+
|
|
11
|
+
const webHtml = await readFile("../web/dist/index.html", "utf-8");
|
|
12
|
+
|
|
13
|
+
await Bun.write(
|
|
14
|
+
"./src/embedded-report.ts",
|
|
15
|
+
`export const reportTemplate = ${JSON.stringify(webHtml)};`,
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
await mkdir("./dist", { recursive: true });
|
|
19
|
+
|
|
20
|
+
const nodeBuild = await Bun.build({
|
|
21
|
+
entrypoints: ["./src/index.ts"],
|
|
22
|
+
outdir: "./dist",
|
|
23
|
+
minify: true,
|
|
24
|
+
target: "node",
|
|
25
|
+
banner: "#!/usr/bin/env node",
|
|
26
|
+
naming: {
|
|
27
|
+
entry: "supascan.js",
|
|
28
|
+
},
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
if (!nodeBuild.success) {
|
|
32
|
+
console.error("Failed to bundle CLI");
|
|
33
|
+
for (const log of nodeBuild.logs) console.error(log);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
console.log("✓ CLI built: dist/supascan.js");
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "supascan",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/supascan.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"supascan": "./dist/supascan.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist/**"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"build": "bun run build.ts",
|
|
15
|
+
"dev": "bun run src/index.ts",
|
|
16
|
+
"lint": "tsc --noEmit"
|
|
17
|
+
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"@commander-js/extra-typings": "^14.0.0",
|
|
20
|
+
"@supabase/supabase-js": "^2.75.0",
|
|
21
|
+
"@supascan/core": "workspace:*",
|
|
22
|
+
"commander": "^14.0.1",
|
|
23
|
+
"picocolors": "^1.1.1"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@types/bun": "latest",
|
|
27
|
+
"typescript": "^5.9.3"
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import {
|
|
2
|
+
analyze,
|
|
3
|
+
type AnalysisResult,
|
|
4
|
+
type RPCParameter,
|
|
5
|
+
} from "@supascan/core";
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
import type { CLIContext } from "../context";
|
|
8
|
+
import { log } from "../formatters/console";
|
|
9
|
+
import { handleEvent } from "../formatters/events";
|
|
10
|
+
|
|
11
|
+
export async function executeAnalyzeCommand(
|
|
12
|
+
ctx: CLIContext,
|
|
13
|
+
options: { schema?: string },
|
|
14
|
+
): Promise<void> {
|
|
15
|
+
if (ctx.html) {
|
|
16
|
+
const { reportTemplate } = await import("../embedded-report");
|
|
17
|
+
const { generateTempFilePath, writeHtmlFile } = await import(
|
|
18
|
+
"../utils/files"
|
|
19
|
+
);
|
|
20
|
+
const { openInBrowser } = await import("../utils/browser");
|
|
21
|
+
|
|
22
|
+
const config = {
|
|
23
|
+
url: ctx.url,
|
|
24
|
+
key: ctx.key,
|
|
25
|
+
headers: ctx.headers,
|
|
26
|
+
autorun: true,
|
|
27
|
+
};
|
|
28
|
+
const encoded = Buffer.from(JSON.stringify(config)).toString("base64");
|
|
29
|
+
|
|
30
|
+
const filePath = generateTempFilePath();
|
|
31
|
+
const htmlContent = reportTemplate.replace(
|
|
32
|
+
"</head>",
|
|
33
|
+
`<script>window.__SUPASCAN_CONFIG__ = "${encoded}";</script></head>`,
|
|
34
|
+
);
|
|
35
|
+
writeHtmlFile(filePath, htmlContent);
|
|
36
|
+
|
|
37
|
+
openInBrowser(filePath);
|
|
38
|
+
log.success(`HTML report generated: ${filePath}`);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const analysisGen = analyze(ctx.client, ctx.url, ctx.key, options);
|
|
43
|
+
let analysisResult;
|
|
44
|
+
|
|
45
|
+
while (true) {
|
|
46
|
+
const next = await analysisGen.next();
|
|
47
|
+
if (next.done) {
|
|
48
|
+
analysisResult = next.value;
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
handleEvent(ctx, next.value);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!analysisResult || !analysisResult.success) {
|
|
55
|
+
log.error(
|
|
56
|
+
"Analysis failed",
|
|
57
|
+
analysisResult?.error.message ?? "Unknown error",
|
|
58
|
+
);
|
|
59
|
+
process.exit(1);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (ctx.json) {
|
|
63
|
+
console.log(JSON.stringify(analysisResult.value, null, 2));
|
|
64
|
+
} else {
|
|
65
|
+
displayAnalysisResult(analysisResult.value);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function displayAnalysisResult(result: AnalysisResult): void {
|
|
70
|
+
console.log();
|
|
71
|
+
console.log(pc.bold(pc.cyan("=".repeat(60))));
|
|
72
|
+
console.log(pc.bold(pc.cyan(" SUPABASE DATABASE ANALYSIS")));
|
|
73
|
+
console.log(pc.bold(pc.cyan("=".repeat(60))));
|
|
74
|
+
console.log();
|
|
75
|
+
|
|
76
|
+
console.log(pc.bold(pc.yellow("TARGET SUMMARY")));
|
|
77
|
+
console.log(pc.dim("-".repeat(20)));
|
|
78
|
+
console.log(pc.bold("Domain:"), pc.white(result.summary.domain));
|
|
79
|
+
|
|
80
|
+
if (result.summary.metadata?.service) {
|
|
81
|
+
console.log(pc.bold("Service:"), pc.white(result.summary.metadata.service));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (result.summary.metadata?.region) {
|
|
85
|
+
console.log(
|
|
86
|
+
pc.bold("Project ID:"),
|
|
87
|
+
pc.white(result.summary.metadata.region),
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (result.summary.metadata?.title) {
|
|
92
|
+
console.log(pc.bold("Title:"), pc.white(result.summary.metadata.title));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (result.summary.metadata?.version) {
|
|
96
|
+
console.log(pc.bold("Version:"), pc.white(result.summary.metadata.version));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (result.summary.jwtInfo) {
|
|
100
|
+
console.log();
|
|
101
|
+
console.log(pc.bold(pc.yellow("JWT TOKEN INFO")));
|
|
102
|
+
console.log(pc.dim("-".repeat(20)));
|
|
103
|
+
|
|
104
|
+
if (result.summary.jwtInfo.iss) {
|
|
105
|
+
console.log(pc.bold("Issuer:"), pc.white(result.summary.jwtInfo.iss));
|
|
106
|
+
}
|
|
107
|
+
if (result.summary.jwtInfo.aud) {
|
|
108
|
+
console.log(pc.bold("Audience:"), pc.white(result.summary.jwtInfo.aud));
|
|
109
|
+
}
|
|
110
|
+
if (result.summary.jwtInfo.role) {
|
|
111
|
+
console.log(pc.bold("Role:"), pc.white(result.summary.jwtInfo.role));
|
|
112
|
+
}
|
|
113
|
+
if (result.summary.jwtInfo.exp) {
|
|
114
|
+
const expDate = new Date(result.summary.jwtInfo.exp * 1000);
|
|
115
|
+
console.log(pc.bold("Expires:"), pc.white(expDate.toISOString()));
|
|
116
|
+
}
|
|
117
|
+
if (result.summary.jwtInfo.iat) {
|
|
118
|
+
const iatDate = new Date(result.summary.jwtInfo.iat * 1000);
|
|
119
|
+
console.log(pc.bold("Issued:"), pc.white(iatDate.toISOString()));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
console.log();
|
|
124
|
+
console.log(pc.bold(pc.cyan("DATABASE ANALYSIS")));
|
|
125
|
+
console.log(pc.dim("-".repeat(20)));
|
|
126
|
+
console.log(
|
|
127
|
+
pc.bold("Schemas discovered:"),
|
|
128
|
+
pc.green(result.schemas.length.toString()),
|
|
129
|
+
);
|
|
130
|
+
console.log();
|
|
131
|
+
|
|
132
|
+
Object.entries(result.schemaDetails).forEach(([schema, analysis]) => {
|
|
133
|
+
console.log(pc.bold(pc.cyan(`Schema: ${schema}`)));
|
|
134
|
+
console.log();
|
|
135
|
+
|
|
136
|
+
const exposedCount = Object.values(analysis.tableAccess).filter(
|
|
137
|
+
(a) => a.status === "readable",
|
|
138
|
+
).length;
|
|
139
|
+
const deniedCount = Object.values(analysis.tableAccess).filter(
|
|
140
|
+
(a) => a.status === "denied",
|
|
141
|
+
).length;
|
|
142
|
+
const emptyCount = Object.values(analysis.tableAccess).filter(
|
|
143
|
+
(a) => a.status === "empty",
|
|
144
|
+
).length;
|
|
145
|
+
|
|
146
|
+
console.log(
|
|
147
|
+
pc.bold("Tables:"),
|
|
148
|
+
pc.green(analysis.tables.length.toString()),
|
|
149
|
+
);
|
|
150
|
+
console.log(
|
|
151
|
+
pc.dim(
|
|
152
|
+
` ${exposedCount} exposed | ${emptyCount} empty/protected | ${deniedCount} denied`,
|
|
153
|
+
),
|
|
154
|
+
);
|
|
155
|
+
console.log();
|
|
156
|
+
|
|
157
|
+
if (analysis.tables.length > 0) {
|
|
158
|
+
analysis.tables.forEach((table) => {
|
|
159
|
+
const access = analysis.tableAccess[table];
|
|
160
|
+
let indicator = "";
|
|
161
|
+
let description = "";
|
|
162
|
+
|
|
163
|
+
switch (access?.status) {
|
|
164
|
+
case "readable":
|
|
165
|
+
indicator = pc.green("[+]");
|
|
166
|
+
description = pc.dim(`(~${access.rowCount ?? "?"} rows exposed)`);
|
|
167
|
+
break;
|
|
168
|
+
case "empty":
|
|
169
|
+
indicator = pc.yellow("[-]");
|
|
170
|
+
description = pc.dim("(0 rows - empty or RLS)");
|
|
171
|
+
break;
|
|
172
|
+
case "denied":
|
|
173
|
+
indicator = pc.red("[X]");
|
|
174
|
+
description = pc.dim("(access denied)");
|
|
175
|
+
break;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
console.log(` ${indicator} ${pc.white(table)} ${description}`);
|
|
179
|
+
});
|
|
180
|
+
} else {
|
|
181
|
+
console.log(pc.dim(" No tables found"));
|
|
182
|
+
}
|
|
183
|
+
console.log();
|
|
184
|
+
|
|
185
|
+
console.log(pc.bold("RPCs:"), pc.green(analysis.rpcs.length.toString()));
|
|
186
|
+
if (analysis.rpcFunctions.length > 0) {
|
|
187
|
+
analysis.rpcFunctions.forEach((rpc) => {
|
|
188
|
+
console.log(` * ${pc.white(rpc.name)}`);
|
|
189
|
+
if (rpc.parameters.length > 0) {
|
|
190
|
+
rpc.parameters.forEach((param: RPCParameter) => {
|
|
191
|
+
const required = param.required
|
|
192
|
+
? pc.red("(required)")
|
|
193
|
+
: pc.dim("(optional)");
|
|
194
|
+
const type = param.format
|
|
195
|
+
? `${param.type} (${param.format})`
|
|
196
|
+
: param.type;
|
|
197
|
+
console.log(
|
|
198
|
+
` - ${pc.cyan(param.name)}: ${pc.yellow(type)} ${required}`,
|
|
199
|
+
);
|
|
200
|
+
});
|
|
201
|
+
} else {
|
|
202
|
+
console.log(pc.dim(" No parameters"));
|
|
203
|
+
}
|
|
204
|
+
});
|
|
205
|
+
} else {
|
|
206
|
+
console.log(pc.dim(" No RPCs found"));
|
|
207
|
+
}
|
|
208
|
+
console.log();
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
console.log(pc.bold(pc.cyan("=".repeat(60))));
|
|
212
|
+
console.log();
|
|
213
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { dumpTable } from "@supascan/core";
|
|
2
|
+
import type { CLIContext } from "../context";
|
|
3
|
+
import { log } from "../formatters/console";
|
|
4
|
+
|
|
5
|
+
export async function executeDumpCommand(
|
|
6
|
+
ctx: CLIContext,
|
|
7
|
+
options: { dump: string; limit: string },
|
|
8
|
+
): Promise<void> {
|
|
9
|
+
const parts = options.dump.split(".");
|
|
10
|
+
|
|
11
|
+
if (parts.length === 1) {
|
|
12
|
+
const schema = parts[0];
|
|
13
|
+
if (!schema) {
|
|
14
|
+
log.error("Invalid dump format. Use schema.table or schema");
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
log.info(`Dumping swagger for schema: ${schema}`);
|
|
19
|
+
|
|
20
|
+
const { data, error } = await ctx.client.schema(schema).from("").select();
|
|
21
|
+
|
|
22
|
+
if (error) {
|
|
23
|
+
log.error("Failed to fetch swagger", error.message);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
console.log(JSON.stringify(data, null, 2));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (parts.length === 2) {
|
|
32
|
+
const schema = parts[0];
|
|
33
|
+
const table = parts[1];
|
|
34
|
+
|
|
35
|
+
if (!schema || !table) {
|
|
36
|
+
log.error("Invalid dump format. Use schema.table");
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const limit = parseInt(options.limit);
|
|
41
|
+
if (isNaN(limit) || limit <= 0) {
|
|
42
|
+
log.error("Invalid limit value");
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
log.info(`Dumping table: ${schema}.${table} (limit: ${limit})`);
|
|
47
|
+
|
|
48
|
+
const result = await dumpTable(ctx.client, schema, table, limit);
|
|
49
|
+
|
|
50
|
+
if (!result.success) {
|
|
51
|
+
log.error("Failed to dump table", result.error.message);
|
|
52
|
+
process.exit(1);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (ctx.json) {
|
|
56
|
+
console.log(JSON.stringify(result.value, null, 2));
|
|
57
|
+
} else {
|
|
58
|
+
console.log(
|
|
59
|
+
`\nTable: ${schema}.${table} (${result.value.count} total rows, showing ${result.value.rows.length})\n`,
|
|
60
|
+
);
|
|
61
|
+
console.table(result.value.rows);
|
|
62
|
+
}
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
log.error("Invalid dump format. Use schema.table or schema");
|
|
67
|
+
process.exit(1);
|
|
68
|
+
}
|