@vainplex/shieldapi-cli 1.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/LICENSE +21 -0
- package/README.md +166 -0
- package/bin/shieldapi.js +5 -0
- package/package.json +43 -0
- package/src/commands/domain.js +35 -0
- package/src/commands/email.js +35 -0
- package/src/commands/hash.js +19 -0
- package/src/commands/health.js +29 -0
- package/src/commands/ip.js +35 -0
- package/src/commands/password.js +86 -0
- package/src/commands/scan.js +52 -0
- package/src/commands/url.js +35 -0
- package/src/index.js +109 -0
- package/src/lib/api.js +66 -0
- package/src/lib/exit.js +61 -0
- package/src/lib/formatter.js +345 -0
- package/src/lib/wallet.js +43 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Albert Hild
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# 🛡️ ShieldAPI CLI
|
|
2
|
+
|
|
3
|
+
**Security intelligence from your terminal. Pay-per-request with USDC.**
|
|
4
|
+
|
|
5
|
+
The first x402-powered security CLI. Check passwords, emails, domains, IPs, and URLs against breach databases, blacklists, and threat intelligence — no API keys, no subscriptions, just crypto micropayments.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install -g @vainplex/shieldapi-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or use directly with npx:
|
|
14
|
+
```bash
|
|
15
|
+
npx @vainplex/shieldapi-cli password "test123" --demo
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Quick Start
|
|
19
|
+
|
|
20
|
+
### Demo Mode (free, no wallet needed)
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
# Check if a password has been breached
|
|
24
|
+
shieldapi password "hunter2" --demo
|
|
25
|
+
|
|
26
|
+
# Check email for breaches
|
|
27
|
+
shieldapi email "test@example.com" --demo
|
|
28
|
+
|
|
29
|
+
# Check domain reputation
|
|
30
|
+
shieldapi domain "example.com" --demo
|
|
31
|
+
|
|
32
|
+
# Check IP reputation
|
|
33
|
+
shieldapi ip "8.8.8.8" --demo
|
|
34
|
+
|
|
35
|
+
# Check URL safety
|
|
36
|
+
shieldapi url "https://suspicious-site.com" --demo
|
|
37
|
+
|
|
38
|
+
# Full security scan
|
|
39
|
+
shieldapi scan --email "test@example.com" --domain "example.com" --demo
|
|
40
|
+
|
|
41
|
+
# Compute SHA-1 hash locally (offline, free)
|
|
42
|
+
shieldapi hash "mypassword"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Paid Mode (real data, USDC on Base)
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
# Set your wallet key
|
|
49
|
+
export SHIELDAPI_WALLET_KEY="0x..."
|
|
50
|
+
|
|
51
|
+
# Real breach check — costs $0.001 USDC
|
|
52
|
+
shieldapi password "hunter2"
|
|
53
|
+
|
|
54
|
+
# Or pass wallet inline
|
|
55
|
+
shieldapi email "ceo@company.com" --wallet "0x..."
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Commands
|
|
59
|
+
|
|
60
|
+
| Command | Description | Cost (USDC) |
|
|
61
|
+
|---------|-------------|-------------|
|
|
62
|
+
| `password <pw>` | Check password against 900M+ breach records | $0.001 |
|
|
63
|
+
| `email <addr>` | Email breach lookup with risk scoring | $0.005 |
|
|
64
|
+
| `domain <name>` | DNS, blacklists, SSL, SPF/DMARC analysis | $0.003 |
|
|
65
|
+
| `ip <addr>` | Blacklists, Tor exit node, reverse DNS | $0.002 |
|
|
66
|
+
| `url <url>` | Phishing, malware, brand impersonation | $0.003 |
|
|
67
|
+
| `scan` | Full scan (combine any targets) | $0.01 |
|
|
68
|
+
| `health` | API status and pricing | Free |
|
|
69
|
+
| `hash <pw>` | SHA-1 hash (offline, no API call) | Free |
|
|
70
|
+
|
|
71
|
+
## Global Options
|
|
72
|
+
|
|
73
|
+
| Flag | Description |
|
|
74
|
+
|------|-------------|
|
|
75
|
+
| `--wallet <key>` | Private key for x402 payments |
|
|
76
|
+
| `--demo` | Use demo mode (free, fake data) |
|
|
77
|
+
| `--json` | Output raw JSON (for CI/CD and agents) |
|
|
78
|
+
| `--yes, -y` | Skip payment confirmation prompts |
|
|
79
|
+
| `--quiet, -q` | Suppress spinners and warnings |
|
|
80
|
+
| `--no-color` | Disable ANSI colors |
|
|
81
|
+
| `--version, -V` | Show version |
|
|
82
|
+
| `--help, -h` | Show help |
|
|
83
|
+
|
|
84
|
+
### Password-specific Options
|
|
85
|
+
|
|
86
|
+
| Flag | Description |
|
|
87
|
+
|------|-------------|
|
|
88
|
+
| `--stdin` | Read password from stdin (avoids shell history) |
|
|
89
|
+
| `--hash` | Treat input as pre-computed SHA-1 hash |
|
|
90
|
+
|
|
91
|
+
## Exit Codes
|
|
92
|
+
|
|
93
|
+
Designed for CI/CD pipelines and AI agents:
|
|
94
|
+
|
|
95
|
+
| Code | Meaning |
|
|
96
|
+
|------|---------|
|
|
97
|
+
| `0` | Safe — no risk found |
|
|
98
|
+
| `1` | Risk — breaches, threats, or high risk detected |
|
|
99
|
+
| `2` | Usage error — invalid arguments |
|
|
100
|
+
| `3` | Network error — API unreachable |
|
|
101
|
+
| `4` | Payment error — insufficient USDC or wallet issue |
|
|
102
|
+
|
|
103
|
+
```bash
|
|
104
|
+
# Use in CI/CD
|
|
105
|
+
shieldapi password "$DB_PASSWORD" --json --quiet --yes
|
|
106
|
+
if [ $? -eq 1 ]; then
|
|
107
|
+
echo "COMPROMISED PASSWORD DETECTED"
|
|
108
|
+
exit 1
|
|
109
|
+
fi
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## Security
|
|
113
|
+
|
|
114
|
+
- **Passwords are hashed locally** with SHA-1 before any network request. Plaintext never leaves your machine.
|
|
115
|
+
- **Private keys are never persisted** to disk, logs, or displayed in output.
|
|
116
|
+
- **No telemetry** — zero phone-home, zero analytics.
|
|
117
|
+
- **Shell history warning** — the CLI warns when passwords are passed as arguments (use `--stdin` for sensitive passwords).
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
# Secure password checking (avoids shell history)
|
|
121
|
+
echo "mysecretpassword" | shieldapi password dummy --stdin
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## How x402 Works
|
|
125
|
+
|
|
126
|
+
[x402](https://x402.org) is an open protocol for HTTP payments. Instead of API keys:
|
|
127
|
+
|
|
128
|
+
1. You make a request → server returns `HTTP 402` with payment requirements
|
|
129
|
+
2. Your wallet signs a USDC payment authorization
|
|
130
|
+
3. Request is retried with payment proof in headers
|
|
131
|
+
4. Server verifies payment and returns data
|
|
132
|
+
|
|
133
|
+
All of this happens automatically. You just need a wallet with USDC on Base.
|
|
134
|
+
|
|
135
|
+
## For AI Agents
|
|
136
|
+
|
|
137
|
+
ShieldAPI CLI is designed for autonomous agent usage:
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
# JSON output for structured parsing
|
|
141
|
+
shieldapi password "test" --demo --json
|
|
142
|
+
|
|
143
|
+
# Quiet mode suppresses all stderr
|
|
144
|
+
shieldapi domain "example.com" --demo --json --quiet
|
|
145
|
+
|
|
146
|
+
# Exit codes for decision making
|
|
147
|
+
shieldapi ip "1.2.3.4" --demo --quiet
|
|
148
|
+
echo $? # 0 = safe, 1 = risk
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Environment Variables
|
|
152
|
+
|
|
153
|
+
| Variable | Description |
|
|
154
|
+
|----------|-------------|
|
|
155
|
+
| `SHIELDAPI_WALLET_KEY` | Private key (hex, with or without 0x prefix) |
|
|
156
|
+
| `NO_COLOR` | Disable colors (standard) |
|
|
157
|
+
|
|
158
|
+
## Links
|
|
159
|
+
|
|
160
|
+
- **API**: https://shield.vainplex.dev
|
|
161
|
+
- **x402 Protocol**: https://x402.org
|
|
162
|
+
- **GitHub**: https://github.com/vainplex/shieldapi-cli
|
|
163
|
+
|
|
164
|
+
## License
|
|
165
|
+
|
|
166
|
+
MIT © Albert Hild
|
package/bin/shieldapi.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vainplex/shieldapi-cli",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "Security intelligence from your terminal. Pay-per-request with USDC.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"shieldapi": "./bin/shieldapi.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"src/",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"security",
|
|
20
|
+
"breach",
|
|
21
|
+
"password",
|
|
22
|
+
"hibp",
|
|
23
|
+
"x402",
|
|
24
|
+
"usdc",
|
|
25
|
+
"crypto",
|
|
26
|
+
"cli"
|
|
27
|
+
],
|
|
28
|
+
"author": "Albert Hild",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/vainplex/shieldapi-cli.git"
|
|
33
|
+
},
|
|
34
|
+
"homepage": "https://shield.vainplex.dev",
|
|
35
|
+
"dependencies": {
|
|
36
|
+
"@x402/evm": "^2.5.0",
|
|
37
|
+
"@x402/fetch": "^2.5.0",
|
|
38
|
+
"chalk": "^5.3.0",
|
|
39
|
+
"commander": "^12.1.0",
|
|
40
|
+
"ora": "^8.1.0",
|
|
41
|
+
"viem": "^2.21.0"
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { apiRequest } from '../lib/api.js';
|
|
4
|
+
import { resolveWallet } from '../lib/wallet.js';
|
|
5
|
+
import { formatDomain } from '../lib/formatter.js';
|
|
6
|
+
import { exitCodeFromResult, exitCodeFromError } from '../lib/exit.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check domain reputation.
|
|
10
|
+
*/
|
|
11
|
+
export async function domainCommand(domain, opts) {
|
|
12
|
+
const spinner = opts.quiet ? null : ora({ text: `Checking domain: ${domain}`, stream: process.stderr }).start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
16
|
+
|
|
17
|
+
const data = await apiRequest('check-domain', { domain }, {
|
|
18
|
+
demo: opts.demo,
|
|
19
|
+
wallet,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
spinner?.stop();
|
|
23
|
+
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
console.log(JSON.stringify(data, null, 2));
|
|
26
|
+
} else {
|
|
27
|
+
formatDomain(data);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exitCode = exitCodeFromResult(data);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
spinner?.fail(err.message);
|
|
33
|
+
process.exitCode = exitCodeFromError(err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { apiRequest } from '../lib/api.js';
|
|
4
|
+
import { resolveWallet } from '../lib/wallet.js';
|
|
5
|
+
import { formatEmail } from '../lib/formatter.js';
|
|
6
|
+
import { exitCodeFromResult, exitCodeFromError } from '../lib/exit.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check an email address for breaches.
|
|
10
|
+
*/
|
|
11
|
+
export async function emailCommand(email, opts) {
|
|
12
|
+
const spinner = opts.quiet ? null : ora({ text: `Checking email: ${email}`, stream: process.stderr }).start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
16
|
+
|
|
17
|
+
const data = await apiRequest('check-email', { email }, {
|
|
18
|
+
demo: opts.demo,
|
|
19
|
+
wallet,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
spinner?.stop();
|
|
23
|
+
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
console.log(JSON.stringify(data, null, 2));
|
|
26
|
+
} else {
|
|
27
|
+
formatEmail(data, email);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exitCode = exitCodeFromResult(data);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
spinner?.fail(err.message);
|
|
33
|
+
process.exitCode = exitCodeFromError(err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Compute SHA-1 hash locally (offline, no API call).
|
|
6
|
+
*/
|
|
7
|
+
export async function hashCommand(password, opts) {
|
|
8
|
+
const hash = createHash('sha1').update(password).digest('hex').toUpperCase();
|
|
9
|
+
|
|
10
|
+
if (opts.json) {
|
|
11
|
+
console.log(JSON.stringify({ password_length: password.length, sha1: hash, prefix: hash.slice(0, 5) }));
|
|
12
|
+
} else {
|
|
13
|
+
console.log();
|
|
14
|
+
console.log(` SHA-1: ${chalk.bold(hash)}`);
|
|
15
|
+
console.log(` Prefix: ${chalk.cyan(hash.slice(0, 5))}`);
|
|
16
|
+
console.log(` Length: ${password.length} characters`);
|
|
17
|
+
console.log();
|
|
18
|
+
}
|
|
19
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { apiRequest } from '../lib/api.js';
|
|
4
|
+
import { formatHealth } from '../lib/formatter.js';
|
|
5
|
+
import { EXIT } from '../lib/exit.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Check API health status.
|
|
9
|
+
*/
|
|
10
|
+
export async function healthCommand(opts) {
|
|
11
|
+
const spinner = opts.quiet ? null : ora({ text: 'Checking API health...', stream: process.stderr }).start();
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const data = await apiRequest('health', {}, { demo: opts.demo });
|
|
15
|
+
|
|
16
|
+
spinner?.stop();
|
|
17
|
+
|
|
18
|
+
if (opts.json) {
|
|
19
|
+
console.log(JSON.stringify(data, null, 2));
|
|
20
|
+
} else {
|
|
21
|
+
formatHealth(data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
process.exitCode = EXIT.SAFE;
|
|
25
|
+
} catch (err) {
|
|
26
|
+
spinner?.fail(err.message);
|
|
27
|
+
process.exitCode = EXIT.NETWORK;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { apiRequest } from '../lib/api.js';
|
|
4
|
+
import { resolveWallet } from '../lib/wallet.js';
|
|
5
|
+
import { formatIp } from '../lib/formatter.js';
|
|
6
|
+
import { exitCodeFromResult, exitCodeFromError } from '../lib/exit.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check IP reputation.
|
|
10
|
+
*/
|
|
11
|
+
export async function ipCommand(ip, opts) {
|
|
12
|
+
const spinner = opts.quiet ? null : ora({ text: `Checking IP: ${ip}`, stream: process.stderr }).start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
16
|
+
|
|
17
|
+
const data = await apiRequest('check-ip', { ip }, {
|
|
18
|
+
demo: opts.demo,
|
|
19
|
+
wallet,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
spinner?.stop();
|
|
23
|
+
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
console.log(JSON.stringify(data, null, 2));
|
|
26
|
+
} else {
|
|
27
|
+
formatIp(data);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exitCode = exitCodeFromResult(data);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
spinner?.fail(err.message);
|
|
33
|
+
process.exitCode = exitCodeFromError(err);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { createInterface } from 'node:readline';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { apiRequest } from '../lib/api.js';
|
|
6
|
+
import { resolveWallet } from '../lib/wallet.js';
|
|
7
|
+
import { formatPassword } from '../lib/formatter.js';
|
|
8
|
+
import { exitCodeFromResult, exitCodeFromError, EXIT } from '../lib/exit.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Read a line from stdin (for --stdin mode).
|
|
12
|
+
*/
|
|
13
|
+
function readStdin() {
|
|
14
|
+
return new Promise((resolve, reject) => {
|
|
15
|
+
let data = '';
|
|
16
|
+
const rl = createInterface({ input: process.stdin });
|
|
17
|
+
rl.on('line', (line) => { data = line; rl.close(); });
|
|
18
|
+
rl.on('close', () => resolve(data.trim()));
|
|
19
|
+
rl.on('error', reject);
|
|
20
|
+
// Timeout after 10s
|
|
21
|
+
setTimeout(() => { rl.close(); reject(new Error('Stdin timeout — no input received')); }, 10000);
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Check a password against breach databases.
|
|
27
|
+
* Hashes locally with SHA-1, sends hash to API.
|
|
28
|
+
*/
|
|
29
|
+
export async function passwordCommand(password, opts) {
|
|
30
|
+
try {
|
|
31
|
+
let hash;
|
|
32
|
+
|
|
33
|
+
// --stdin: read password from stdin
|
|
34
|
+
if (opts.stdin) {
|
|
35
|
+
if (!opts.quiet) {
|
|
36
|
+
process.stderr.write(chalk.gray('Reading password from stdin...\n'));
|
|
37
|
+
}
|
|
38
|
+
const stdinPassword = await readStdin();
|
|
39
|
+
if (!stdinPassword) {
|
|
40
|
+
process.stderr.write(chalk.red('Error: No password received from stdin.\n'));
|
|
41
|
+
process.exitCode = EXIT.USAGE;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
hash = createHash('sha1').update(stdinPassword).digest('hex').toUpperCase();
|
|
45
|
+
} else if (opts.hash) {
|
|
46
|
+
// --hash: treat input as pre-computed SHA-1
|
|
47
|
+
hash = password.toUpperCase();
|
|
48
|
+
if (!/^[A-F0-9]{40}$/.test(hash)) {
|
|
49
|
+
process.stderr.write(chalk.red('Error: Invalid SHA-1 hash. Must be 40 hex characters.\n'));
|
|
50
|
+
process.exitCode = EXIT.USAGE;
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
} else {
|
|
54
|
+
// Normal mode: hash the password locally
|
|
55
|
+
hash = createHash('sha1').update(password).digest('hex').toUpperCase();
|
|
56
|
+
|
|
57
|
+
// Shell history warning (SR-5)
|
|
58
|
+
if (!opts.quiet && process.stdout.isTTY) {
|
|
59
|
+
process.stderr.write(
|
|
60
|
+
chalk.yellow('⚠ Password may appear in shell history. Use --stdin for sensitive passwords.\n')
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const spinner = opts.quiet ? null : ora({ text: 'Checking password...', stream: process.stderr }).start();
|
|
66
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
67
|
+
|
|
68
|
+
const data = await apiRequest('check-password', { hash }, {
|
|
69
|
+
demo: opts.demo,
|
|
70
|
+
wallet,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
spinner?.stop();
|
|
74
|
+
|
|
75
|
+
if (opts.json) {
|
|
76
|
+
console.log(JSON.stringify(data, null, 2));
|
|
77
|
+
} else {
|
|
78
|
+
formatPassword(data, hash);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
process.exitCode = exitCodeFromResult(data);
|
|
82
|
+
} catch (err) {
|
|
83
|
+
if (!opts.quiet) process.stderr.write(chalk.red(`✖ ${err.message}\n`));
|
|
84
|
+
process.exitCode = exitCodeFromError(err);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { apiRequest } from '../lib/api.js';
|
|
5
|
+
import { resolveWallet } from '../lib/wallet.js';
|
|
6
|
+
import { formatScan } from '../lib/formatter.js';
|
|
7
|
+
import { exitCodeFromResult, exitCodeFromError, EXIT } from '../lib/exit.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run a full security scan with multiple targets.
|
|
11
|
+
*/
|
|
12
|
+
export async function scanCommand(opts) {
|
|
13
|
+
const params = {};
|
|
14
|
+
|
|
15
|
+
if (opts.password) {
|
|
16
|
+
params.password_hash = createHash('sha1').update(opts.password).digest('hex').toUpperCase();
|
|
17
|
+
}
|
|
18
|
+
if (opts.email) params.email = opts.email;
|
|
19
|
+
if (opts.domain) params.domain = opts.domain;
|
|
20
|
+
if (opts.ip) params.ip = opts.ip;
|
|
21
|
+
if (opts.url) params.url = opts.url;
|
|
22
|
+
|
|
23
|
+
if (Object.keys(params).length === 0) {
|
|
24
|
+
process.stderr.write(chalk.red('Error: At least one target required. Use --email, --password, --domain, --ip, or --url.\n'));
|
|
25
|
+
process.exitCode = EXIT.USAGE;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const spinner = opts.quiet ? null : ora({ text: 'Running full security scan...', stream: process.stderr }).start();
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
33
|
+
|
|
34
|
+
const data = await apiRequest('full-scan', params, {
|
|
35
|
+
demo: opts.demo,
|
|
36
|
+
wallet,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
spinner?.stop();
|
|
40
|
+
|
|
41
|
+
if (opts.json) {
|
|
42
|
+
console.log(JSON.stringify(data, null, 2));
|
|
43
|
+
} else {
|
|
44
|
+
formatScan(data);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
process.exitCode = exitCodeFromResult(data);
|
|
48
|
+
} catch (err) {
|
|
49
|
+
spinner?.fail(err.message);
|
|
50
|
+
process.exitCode = exitCodeFromError(err);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { apiRequest } from '../lib/api.js';
|
|
4
|
+
import { resolveWallet } from '../lib/wallet.js';
|
|
5
|
+
import { formatUrl } from '../lib/formatter.js';
|
|
6
|
+
import { exitCodeFromResult, exitCodeFromError } from '../lib/exit.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check URL safety.
|
|
10
|
+
*/
|
|
11
|
+
export async function urlCommand(url, opts) {
|
|
12
|
+
const spinner = opts.quiet ? null : ora({ text: `Checking URL: ${url}`, stream: process.stderr }).start();
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
16
|
+
|
|
17
|
+
const data = await apiRequest('check-url', { url }, {
|
|
18
|
+
demo: opts.demo,
|
|
19
|
+
wallet,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
spinner?.stop();
|
|
23
|
+
|
|
24
|
+
if (opts.json) {
|
|
25
|
+
console.log(JSON.stringify(data, null, 2));
|
|
26
|
+
} else {
|
|
27
|
+
formatUrl(data);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
process.exitCode = exitCodeFromResult(data);
|
|
31
|
+
} catch (err) {
|
|
32
|
+
spinner?.fail(err.message);
|
|
33
|
+
process.exitCode = exitCodeFromError(err);
|
|
34
|
+
}
|
|
35
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Command } from 'commander';
|
|
2
|
+
import { passwordCommand } from './commands/password.js';
|
|
3
|
+
import { emailCommand } from './commands/email.js';
|
|
4
|
+
import { domainCommand } from './commands/domain.js';
|
|
5
|
+
import { ipCommand } from './commands/ip.js';
|
|
6
|
+
import { urlCommand } from './commands/url.js';
|
|
7
|
+
import { scanCommand } from './commands/scan.js';
|
|
8
|
+
import { healthCommand } from './commands/health.js';
|
|
9
|
+
import { hashCommand } from './commands/hash.js';
|
|
10
|
+
|
|
11
|
+
export function run(argv) {
|
|
12
|
+
const program = new Command();
|
|
13
|
+
|
|
14
|
+
program
|
|
15
|
+
.name('shieldapi')
|
|
16
|
+
.description('🛡️ ShieldAPI CLI — Security intelligence from your terminal. Pay-per-request with USDC.')
|
|
17
|
+
.version('1.1.0')
|
|
18
|
+
.option('--wallet <key>', 'Private key for x402 payments (or set SHIELDAPI_WALLET_KEY)')
|
|
19
|
+
.option('--json', 'Output raw JSON instead of formatted output')
|
|
20
|
+
.option('--no-color', 'Disable colors')
|
|
21
|
+
.option('-y, --yes', 'Skip payment confirmation prompts')
|
|
22
|
+
.option('-q, --quiet', 'Suppress non-essential output (spinners, warnings)');
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.command('password')
|
|
26
|
+
.description('Check a password against breach databases')
|
|
27
|
+
.argument('<password>', 'Password to check (hashed locally with SHA-1)')
|
|
28
|
+
.option('--demo', 'Use demo mode (free, no wallet needed)')
|
|
29
|
+
.option('--stdin', 'Read password from stdin (avoids shell history)')
|
|
30
|
+
.option('--hash', 'Treat input as pre-computed SHA-1 hash')
|
|
31
|
+
.action((password, cmdOpts, cmd) => {
|
|
32
|
+
const globalOpts = cmd.parent.opts();
|
|
33
|
+
passwordCommand(password, { ...globalOpts, ...cmdOpts });
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
program
|
|
37
|
+
.command('email')
|
|
38
|
+
.description('Check an email address for known breaches')
|
|
39
|
+
.argument('<email>', 'Email address to check')
|
|
40
|
+
.option('--demo', 'Use demo mode (free, no wallet needed)')
|
|
41
|
+
.action((email, cmdOpts, cmd) => {
|
|
42
|
+
const globalOpts = cmd.parent.opts();
|
|
43
|
+
emailCommand(email, { ...globalOpts, ...cmdOpts });
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
program
|
|
47
|
+
.command('domain')
|
|
48
|
+
.description('Check domain reputation (DNS, blacklists, SSL, SPF/DMARC)')
|
|
49
|
+
.argument('<domain>', 'Domain to check')
|
|
50
|
+
.option('--demo', 'Use demo mode (free, no wallet needed)')
|
|
51
|
+
.action((domain, cmdOpts, cmd) => {
|
|
52
|
+
const globalOpts = cmd.parent.opts();
|
|
53
|
+
domainCommand(domain, { ...globalOpts, ...cmdOpts });
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
program
|
|
57
|
+
.command('ip')
|
|
58
|
+
.description('Check IP reputation (blacklists, Tor, rDNS)')
|
|
59
|
+
.argument('<ip>', 'IPv4 address to check')
|
|
60
|
+
.option('--demo', 'Use demo mode (free, no wallet needed)')
|
|
61
|
+
.action((ip, cmdOpts, cmd) => {
|
|
62
|
+
const globalOpts = cmd.parent.opts();
|
|
63
|
+
ipCommand(ip, { ...globalOpts, ...cmdOpts });
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
program
|
|
67
|
+
.command('url')
|
|
68
|
+
.description('Check URL safety (phishing, malware, brand impersonation)')
|
|
69
|
+
.argument('<url>', 'URL to check')
|
|
70
|
+
.option('--demo', 'Use demo mode (free, no wallet needed)')
|
|
71
|
+
.action((url, cmdOpts, cmd) => {
|
|
72
|
+
const globalOpts = cmd.parent.opts();
|
|
73
|
+
urlCommand(url, { ...globalOpts, ...cmdOpts });
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
program
|
|
77
|
+
.command('scan')
|
|
78
|
+
.description('Run a full security scan with multiple targets')
|
|
79
|
+
.option('--email <email>', 'Email address to include')
|
|
80
|
+
.option('--password <password>', 'Password to include (hashed locally)')
|
|
81
|
+
.option('--domain <domain>', 'Domain to include')
|
|
82
|
+
.option('--ip <ip>', 'IP address to include')
|
|
83
|
+
.option('--url <url>', 'URL to include')
|
|
84
|
+
.option('--demo', 'Use demo mode (free, no wallet needed)')
|
|
85
|
+
.action((cmdOpts, cmd) => {
|
|
86
|
+
const globalOpts = cmd.parent.opts();
|
|
87
|
+
scanCommand({ ...globalOpts, ...cmdOpts });
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command('health')
|
|
92
|
+
.description('Check ShieldAPI health and available endpoints')
|
|
93
|
+
.option('--demo', 'Use demo mode')
|
|
94
|
+
.action((cmdOpts, cmd) => {
|
|
95
|
+
const globalOpts = cmd.parent.opts();
|
|
96
|
+
healthCommand({ ...globalOpts, ...cmdOpts });
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
program
|
|
100
|
+
.command('hash')
|
|
101
|
+
.description('Compute SHA-1 hash locally (offline, no API call)')
|
|
102
|
+
.argument('<password>', 'Password to hash')
|
|
103
|
+
.action((password, cmdOpts, cmd) => {
|
|
104
|
+
const globalOpts = cmd.parent.opts();
|
|
105
|
+
hashCommand(password, { ...globalOpts, ...cmdOpts });
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
program.parse(argv);
|
|
109
|
+
}
|
package/src/lib/api.js
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const BASE_URL = 'https://shield.vainplex.dev/api';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Make an API request. Uses @x402/fetch for paid requests, plain fetch for demo.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} endpoint - API endpoint path (e.g. 'check-password')
|
|
7
|
+
* @param {Record<string, string>} params - Query parameters
|
|
8
|
+
* @param {object} options
|
|
9
|
+
* @param {boolean} options.demo - Use demo mode
|
|
10
|
+
* @param {{ signer: object }|null} options.wallet - Wallet with x402 signer
|
|
11
|
+
* @returns {Promise<object>} Parsed JSON response
|
|
12
|
+
*/
|
|
13
|
+
export async function apiRequest(endpoint, params = {}, { demo = false, wallet = null } = {}) {
|
|
14
|
+
if (demo) {
|
|
15
|
+
params.demo = 'true';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const query = new URLSearchParams(params).toString();
|
|
19
|
+
const url = `${BASE_URL}/${endpoint}${query ? '?' + query : ''}`;
|
|
20
|
+
|
|
21
|
+
if (demo || endpoint === 'health') {
|
|
22
|
+
// Free endpoints — plain fetch
|
|
23
|
+
const res = await fetch(url);
|
|
24
|
+
if (!res.ok) {
|
|
25
|
+
const body = await res.text().catch(() => '');
|
|
26
|
+
throw new ApiError(res.status, body || res.statusText);
|
|
27
|
+
}
|
|
28
|
+
return res.json();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Paid endpoint — need wallet with signer
|
|
32
|
+
if (!wallet?.signer) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
'No wallet configured. Use --wallet <key> or set SHIELDAPI_WALLET_KEY environment variable.'
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Dynamic import to keep startup fast when not needed
|
|
39
|
+
const { wrapFetchWithPayment } = await import('@x402/fetch');
|
|
40
|
+
const paidFetch = wrapFetchWithPayment(fetch, wallet.signer);
|
|
41
|
+
|
|
42
|
+
const res = await paidFetch(url);
|
|
43
|
+
|
|
44
|
+
if (!res.ok) {
|
|
45
|
+
const body = await res.text().catch(() => '');
|
|
46
|
+
throw new ApiError(res.status, body || res.statusText);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return res.json();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export class ApiError extends Error {
|
|
53
|
+
constructor(status, body) {
|
|
54
|
+
let message = `API returned ${status}`;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = JSON.parse(body);
|
|
57
|
+
if (parsed.error) message += `: ${parsed.error}`;
|
|
58
|
+
if (parsed.details) message += ` (${parsed.details})`;
|
|
59
|
+
} catch {
|
|
60
|
+
if (body) message += `: ${body}`;
|
|
61
|
+
}
|
|
62
|
+
super(message);
|
|
63
|
+
this.name = 'ApiError';
|
|
64
|
+
this.status = status;
|
|
65
|
+
}
|
|
66
|
+
}
|
package/src/lib/exit.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit code constants per RFC-001.
|
|
3
|
+
*
|
|
4
|
+
* 0 = safe / low risk
|
|
5
|
+
* 1 = risk found (medium/high/critical)
|
|
6
|
+
* 2 = usage error (invalid args)
|
|
7
|
+
* 3 = network / API error
|
|
8
|
+
* 4 = payment error (insufficient balance, x402 failure)
|
|
9
|
+
*/
|
|
10
|
+
export const EXIT = {
|
|
11
|
+
SAFE: 0,
|
|
12
|
+
RISK: 1,
|
|
13
|
+
USAGE: 2,
|
|
14
|
+
NETWORK: 3,
|
|
15
|
+
PAYMENT: 4,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Determine exit code from API response data.
|
|
20
|
+
* Checks risk_level, found (password), breaches (email), threats (url).
|
|
21
|
+
*/
|
|
22
|
+
export function exitCodeFromResult(data) {
|
|
23
|
+
// Password check
|
|
24
|
+
if (data.found === true) return EXIT.RISK;
|
|
25
|
+
if (data.found === false) return EXIT.SAFE;
|
|
26
|
+
|
|
27
|
+
// Risk level based
|
|
28
|
+
const level = (data.risk_level || data.overall_risk_level || '').toLowerCase();
|
|
29
|
+
if (['critical', 'high', 'medium'].includes(level)) return EXIT.RISK;
|
|
30
|
+
if (['low', 'safe', 'none'].includes(level)) return EXIT.SAFE;
|
|
31
|
+
|
|
32
|
+
// Threats array
|
|
33
|
+
if (data.threats?.length > 0) return EXIT.RISK;
|
|
34
|
+
|
|
35
|
+
// Email breaches
|
|
36
|
+
if (data.breaches?.length > 0) return EXIT.RISK;
|
|
37
|
+
|
|
38
|
+
// Full scan — check nested
|
|
39
|
+
const checks = data.checks || {};
|
|
40
|
+
for (const val of Object.values(checks)) {
|
|
41
|
+
if (val?.found === true) return EXIT.RISK;
|
|
42
|
+
const rl = (val?.risk_level || '').toLowerCase();
|
|
43
|
+
if (['critical', 'high', 'medium'].includes(rl)) return EXIT.RISK;
|
|
44
|
+
}
|
|
45
|
+
if (data.password?.found) return EXIT.RISK;
|
|
46
|
+
|
|
47
|
+
return EXIT.SAFE;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Determine exit code from an error.
|
|
52
|
+
*/
|
|
53
|
+
export function exitCodeFromError(err) {
|
|
54
|
+
if (err?.status === 402 || err?.message?.includes('payment') || err?.message?.includes('x402') || err?.message?.includes('wallet')) {
|
|
55
|
+
return EXIT.PAYMENT;
|
|
56
|
+
}
|
|
57
|
+
if (err?.status >= 400 && err?.status < 500) {
|
|
58
|
+
return EXIT.USAGE;
|
|
59
|
+
}
|
|
60
|
+
return EXIT.NETWORK;
|
|
61
|
+
}
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Format a risk level badge with color.
|
|
5
|
+
*/
|
|
6
|
+
export function riskBadge(level, score) {
|
|
7
|
+
const displayScore = score != null ? (score > 10 ? `${score}/100` : `${score}/10`) : '?';
|
|
8
|
+
const label = `${level?.toUpperCase() || 'UNKNOWN'} (${displayScore})`;
|
|
9
|
+
switch (level?.toLowerCase()) {
|
|
10
|
+
case 'critical':
|
|
11
|
+
return chalk.bgRed.white.bold(` ${label} `);
|
|
12
|
+
case 'high':
|
|
13
|
+
return chalk.red.bold(label);
|
|
14
|
+
case 'medium':
|
|
15
|
+
return chalk.yellow.bold(label);
|
|
16
|
+
case 'low':
|
|
17
|
+
return chalk.green.bold(label);
|
|
18
|
+
case 'safe':
|
|
19
|
+
case 'none':
|
|
20
|
+
return chalk.green(label);
|
|
21
|
+
default:
|
|
22
|
+
return chalk.gray(label);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Format a number with commas.
|
|
28
|
+
*/
|
|
29
|
+
export function formatNumber(n) {
|
|
30
|
+
return Number(n).toLocaleString('en-US');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Print a section header.
|
|
35
|
+
*/
|
|
36
|
+
export function sectionHeader(title) {
|
|
37
|
+
console.log();
|
|
38
|
+
console.log(chalk.bold.underline(title));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Print key-value pair with indentation.
|
|
43
|
+
*/
|
|
44
|
+
export function kvLine(key, value, indent = 3) {
|
|
45
|
+
const pad = ' '.repeat(indent);
|
|
46
|
+
console.log(`${pad}${chalk.gray(key + ':')} ${value}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format password check result.
|
|
51
|
+
*/
|
|
52
|
+
export function formatPassword(data, hash) {
|
|
53
|
+
console.log();
|
|
54
|
+
if (data.found) {
|
|
55
|
+
console.log(chalk.red.bold('⚠️ PASSWORD COMPROMISED'));
|
|
56
|
+
console.log(` Found in ${chalk.red.bold(formatNumber(data.count))} breaches`);
|
|
57
|
+
console.log(` SHA-1: ${chalk.gray(hash)}`);
|
|
58
|
+
console.log();
|
|
59
|
+
console.log(chalk.red(' 🚨 Change this password immediately!'));
|
|
60
|
+
} else {
|
|
61
|
+
console.log(chalk.green.bold('✅ Password NOT found in breach databases'));
|
|
62
|
+
console.log(` SHA-1: ${chalk.gray(hash)}`);
|
|
63
|
+
}
|
|
64
|
+
console.log();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Format email breach result.
|
|
69
|
+
*/
|
|
70
|
+
export function formatEmail(data, email) {
|
|
71
|
+
console.log();
|
|
72
|
+
const breaches = data.breaches || [];
|
|
73
|
+
|
|
74
|
+
if (breaches.length > 0) {
|
|
75
|
+
console.log(
|
|
76
|
+
chalk.red.bold(`⚠️ ${breaches.length} breach${breaches.length > 1 ? 'es' : ''} found`) +
|
|
77
|
+
` | Risk: ${riskBadge(data.risk_level, data.risk_score)}`
|
|
78
|
+
);
|
|
79
|
+
console.log();
|
|
80
|
+
|
|
81
|
+
for (const breach of breaches) {
|
|
82
|
+
const name = breach.name || breach.title || 'Unknown';
|
|
83
|
+
const date = breach.date || breach.breach_date || '';
|
|
84
|
+
const count = breach.records || breach.pwn_count;
|
|
85
|
+
const exposed = breach.exposed_data || breach.data_classes;
|
|
86
|
+
|
|
87
|
+
let line = ` 📋 ${chalk.bold(name)}`;
|
|
88
|
+
if (date) line += ` (${date})`;
|
|
89
|
+
if (count) line += ` — ${formatNumber(count)} accounts`;
|
|
90
|
+
console.log(line);
|
|
91
|
+
|
|
92
|
+
if (exposed) {
|
|
93
|
+
const items = Array.isArray(exposed) ? exposed.join(', ') : exposed;
|
|
94
|
+
console.log(` Exposed: ${chalk.gray(items)}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (data.recommendations?.length) {
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(' 💡 Recommendations:');
|
|
101
|
+
for (const rec of data.recommendations) {
|
|
102
|
+
console.log(` • ${rec}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} else {
|
|
106
|
+
console.log(chalk.green.bold('✅ No breaches found') + ` for ${chalk.bold(email)}`);
|
|
107
|
+
}
|
|
108
|
+
console.log();
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Format domain reputation result.
|
|
113
|
+
*/
|
|
114
|
+
export function formatDomain(data) {
|
|
115
|
+
console.log();
|
|
116
|
+
console.log(
|
|
117
|
+
`🌐 Domain: ${chalk.bold(data.domain)} | Risk: ${riskBadge(data.risk_level, data.risk_score)}`
|
|
118
|
+
);
|
|
119
|
+
console.log();
|
|
120
|
+
|
|
121
|
+
if (data.dns) {
|
|
122
|
+
sectionHeader('DNS');
|
|
123
|
+
if (data.dns.a?.length) kvLine('A Records', data.dns.a.join(', '));
|
|
124
|
+
if (data.dns.mx?.length) kvLine('MX Records', data.dns.mx.join(', '));
|
|
125
|
+
if (data.dns.ns?.length) kvLine('NS Records', data.dns.ns.join(', '));
|
|
126
|
+
const hasSPF = data.dns.spf ?? data.dns.has_spf;
|
|
127
|
+
const hasDMARC = data.dns.dmarc ?? data.dns.has_dmarc;
|
|
128
|
+
if (hasSPF !== undefined) kvLine('SPF', hasSPF ? chalk.green('✓') : chalk.red('✗'));
|
|
129
|
+
if (hasDMARC !== undefined) kvLine('DMARC', hasDMARC ? chalk.green('✓') : chalk.red('✗'));
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (data.ssl) {
|
|
133
|
+
sectionHeader('SSL/TLS');
|
|
134
|
+
kvLine('Valid', data.ssl.valid ? chalk.green('✓') : chalk.red('✗'));
|
|
135
|
+
if (data.ssl.error) kvLine('Error', chalk.red(data.ssl.error));
|
|
136
|
+
if (data.ssl.issuer) kvLine('Issuer', data.ssl.issuer);
|
|
137
|
+
if (data.ssl.expires) kvLine('Expires', data.ssl.expires);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (data.blacklists) {
|
|
141
|
+
sectionHeader('Blacklists');
|
|
142
|
+
if (Array.isArray(data.blacklists)) {
|
|
143
|
+
const listed = data.blacklists.filter(b => b.listed);
|
|
144
|
+
if (listed.length > 0) {
|
|
145
|
+
kvLine('Listed on', chalk.red(listed.map(b => b.list).join(', ')));
|
|
146
|
+
} else {
|
|
147
|
+
kvLine('Status', chalk.green('Not blacklisted'));
|
|
148
|
+
}
|
|
149
|
+
} else {
|
|
150
|
+
const listed = data.blacklists.listed_on || [];
|
|
151
|
+
if (listed.length > 0) {
|
|
152
|
+
kvLine('Listed on', chalk.red(listed.join(', ')));
|
|
153
|
+
} else {
|
|
154
|
+
kvLine('Status', chalk.green('Not blacklisted'));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
console.log();
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Format IP reputation result.
|
|
163
|
+
*/
|
|
164
|
+
export function formatIp(data) {
|
|
165
|
+
console.log();
|
|
166
|
+
console.log(
|
|
167
|
+
`🖥 IP: ${chalk.bold(data.ip)} | Risk: ${riskBadge(data.risk_level, data.risk_score)}`
|
|
168
|
+
);
|
|
169
|
+
console.log();
|
|
170
|
+
|
|
171
|
+
if (data.reverse_dns) {
|
|
172
|
+
kvLine('Reverse DNS', data.reverse_dns);
|
|
173
|
+
}
|
|
174
|
+
if (data.is_tor_exit !== undefined) {
|
|
175
|
+
kvLine('Tor Exit Node', data.is_tor_exit ? chalk.red('Yes') : chalk.green('No'));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (data.blacklists) {
|
|
179
|
+
if (Array.isArray(data.blacklists)) {
|
|
180
|
+
const listed = data.blacklists.filter(b => b.listed);
|
|
181
|
+
if (listed.length > 0) {
|
|
182
|
+
kvLine('Blacklisted on', chalk.red(listed.map(b => b.name || b.list).join(', ')));
|
|
183
|
+
} else {
|
|
184
|
+
kvLine('Blacklists', chalk.green('Clean'));
|
|
185
|
+
}
|
|
186
|
+
} else if (data.blacklists.listed_on?.length) {
|
|
187
|
+
kvLine('Blacklisted on', chalk.red(data.blacklists.listed_on.join(', ')));
|
|
188
|
+
} else {
|
|
189
|
+
kvLine('Blacklists', chalk.green('Clean'));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
console.log();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Format URL safety result.
|
|
197
|
+
*/
|
|
198
|
+
export function formatUrl(data) {
|
|
199
|
+
console.log();
|
|
200
|
+
console.log(
|
|
201
|
+
`🔗 URL: ${chalk.bold(data.url)} | Risk: ${riskBadge(data.risk_level, data.risk_score)}`
|
|
202
|
+
);
|
|
203
|
+
console.log();
|
|
204
|
+
|
|
205
|
+
if (data.threats?.length) {
|
|
206
|
+
console.log(' ⚠️ Threats detected:');
|
|
207
|
+
for (const threat of data.threats) {
|
|
208
|
+
if (typeof threat === 'string') {
|
|
209
|
+
console.log(` • ${chalk.red(threat)}`);
|
|
210
|
+
} else {
|
|
211
|
+
const src = threat.source ? chalk.gray(`[${threat.source}]`) + ' ' : '';
|
|
212
|
+
console.log(` • ${src}${chalk.red(threat.detail || threat.type || JSON.stringify(threat))}`);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
} else {
|
|
216
|
+
console.log(` ${chalk.green('✅ No threats detected')}`);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (data.checks) {
|
|
220
|
+
sectionHeader('Checks');
|
|
221
|
+
for (const [key, value] of Object.entries(data.checks)) {
|
|
222
|
+
const label = key.replace(/_/g, ' ');
|
|
223
|
+
if (typeof value === 'object' && value !== null) {
|
|
224
|
+
// Complex check result — show found/reachable status or warnings
|
|
225
|
+
if (value.found !== undefined) {
|
|
226
|
+
kvLine(label, value.found ? chalk.red('Found in database') : chalk.green('Clean'));
|
|
227
|
+
} else if (value.reachable !== undefined) {
|
|
228
|
+
kvLine(label, value.reachable ? chalk.green('Reachable') : chalk.yellow('Unreachable'));
|
|
229
|
+
} else if (value.warnings?.length) {
|
|
230
|
+
kvLine(label, chalk.yellow(`${value.warnings.length} warning(s)`));
|
|
231
|
+
} else {
|
|
232
|
+
kvLine(label, chalk.green('OK'));
|
|
233
|
+
}
|
|
234
|
+
} else {
|
|
235
|
+
const status = value ? chalk.green('Passed') : chalk.red('Failed');
|
|
236
|
+
kvLine(label, status);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
console.log();
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Format full scan result.
|
|
245
|
+
*/
|
|
246
|
+
export function formatScan(data) {
|
|
247
|
+
console.log();
|
|
248
|
+
console.log(chalk.bold.underline('🛡️ Full Security Scan Results'));
|
|
249
|
+
|
|
250
|
+
// Support both flat (data.password) and nested (data.checks.password) structures
|
|
251
|
+
const checks = data.checks || {};
|
|
252
|
+
const pw = data.password || checks.password;
|
|
253
|
+
const emailData = data.email || checks.email_breaches || checks.email;
|
|
254
|
+
const domainData = data.domain || checks.domain;
|
|
255
|
+
const ipData = data.ip || checks.ip;
|
|
256
|
+
const urlData = data.url || checks.url;
|
|
257
|
+
|
|
258
|
+
if (pw) {
|
|
259
|
+
sectionHeader('🔑 Password');
|
|
260
|
+
if (pw.found) {
|
|
261
|
+
console.log(` ${chalk.red.bold('COMPROMISED')} — found in ${formatNumber(pw.count)} breaches`);
|
|
262
|
+
} else {
|
|
263
|
+
console.log(` ${chalk.green('Safe')} — not found in breach databases`);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (emailData) {
|
|
268
|
+
sectionHeader('📧 Email');
|
|
269
|
+
const breaches = emailData.breaches || [];
|
|
270
|
+
if (breaches.length > 0) {
|
|
271
|
+
console.log(` ${chalk.red.bold(breaches.length + ' breaches')} | Risk: ${riskBadge(emailData.risk_level, emailData.risk_score)}`);
|
|
272
|
+
for (const b of breaches) {
|
|
273
|
+
console.log(` • ${b.name || b.title || 'Unknown'}`);
|
|
274
|
+
}
|
|
275
|
+
} else if (emailData.domain_breach_count > 0) {
|
|
276
|
+
console.log(` ${chalk.yellow(emailData.domain_breach_count + ' domain breach(es)')} | Risk: ${riskBadge(emailData.risk_level, emailData.risk_score)}`);
|
|
277
|
+
} else {
|
|
278
|
+
console.log(` ${chalk.green('No breaches found')}`);
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (domainData) {
|
|
283
|
+
sectionHeader('🌐 Domain');
|
|
284
|
+
const label = domainData.domain ? `${domainData.domain} — ` : '';
|
|
285
|
+
console.log(` ${label}Risk: ${riskBadge(domainData.risk_level, domainData.risk_score)}`);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
if (ipData) {
|
|
289
|
+
sectionHeader('🖥 IP');
|
|
290
|
+
console.log(` Risk: ${riskBadge(ipData.risk_level, ipData.risk_score)}`);
|
|
291
|
+
if (ipData.is_tor_exit) console.log(` ${chalk.red('⚠️ Tor exit node')}`);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
if (urlData) {
|
|
295
|
+
sectionHeader('🔗 URL');
|
|
296
|
+
console.log(` Risk: ${riskBadge(urlData.risk_level, urlData.risk_score)}`);
|
|
297
|
+
if (urlData.threats?.length) {
|
|
298
|
+
for (const t of urlData.threats) {
|
|
299
|
+
const text = typeof t === 'string' ? t : (t.detail || t.type || JSON.stringify(t));
|
|
300
|
+
console.log(` • ${chalk.red(text)}`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Overall risk
|
|
306
|
+
if (data.overall_risk_level || data.overall_risk_score != null) {
|
|
307
|
+
console.log();
|
|
308
|
+
console.log(`Overall Risk: ${riskBadge(data.overall_risk_level, data.overall_risk_score)}`);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Summary lines
|
|
312
|
+
if (data.summary?.length) {
|
|
313
|
+
console.log();
|
|
314
|
+
for (const line of data.summary) {
|
|
315
|
+
console.log(` ${line}`);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
console.log();
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Format health check result.
|
|
324
|
+
*/
|
|
325
|
+
export function formatHealth(data) {
|
|
326
|
+
console.log();
|
|
327
|
+
console.log(chalk.green.bold('✅ ShieldAPI is healthy'));
|
|
328
|
+
console.log();
|
|
329
|
+
|
|
330
|
+
if (data.endpoints) {
|
|
331
|
+
console.log(chalk.bold('Available endpoints:'));
|
|
332
|
+
console.log();
|
|
333
|
+
for (const ep of data.endpoints) {
|
|
334
|
+
const name = ep.path || ep.endpoint || ep.name;
|
|
335
|
+
const price = ep.price || ep.cost;
|
|
336
|
+
console.log(` ${chalk.cyan(name)}${price ? ` — ${chalk.yellow(price + ' USDC')}` : ''}`);
|
|
337
|
+
}
|
|
338
|
+
} else if (typeof data === 'object') {
|
|
339
|
+
// Fallback: just show the keys
|
|
340
|
+
for (const [key, value] of Object.entries(data)) {
|
|
341
|
+
console.log(` ${chalk.gray(key + ':')} ${JSON.stringify(value)}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
console.log();
|
|
345
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createWalletClient, http, publicActions } from 'viem';
|
|
2
|
+
import { privateKeyToAccount } from 'viem/accounts';
|
|
3
|
+
import { base } from 'viem/chains';
|
|
4
|
+
import { toClientEvmSigner } from '@x402/evm';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Create a viem wallet client + x402 signer from a private key.
|
|
8
|
+
*
|
|
9
|
+
* @param {string} privateKey - Hex private key (with or without 0x prefix)
|
|
10
|
+
* @returns {{ client: import('viem').WalletClient, signer: object }}
|
|
11
|
+
*/
|
|
12
|
+
export function createWallet(privateKey) {
|
|
13
|
+
if (!privateKey) return null;
|
|
14
|
+
|
|
15
|
+
const key = privateKey.startsWith('0x') ? privateKey : `0x${privateKey}`;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const account = privateKeyToAccount(key);
|
|
19
|
+
const client = createWalletClient({
|
|
20
|
+
account,
|
|
21
|
+
chain: base,
|
|
22
|
+
transport: http(),
|
|
23
|
+
}).extend(publicActions);
|
|
24
|
+
|
|
25
|
+
const signer = toClientEvmSigner(client);
|
|
26
|
+
|
|
27
|
+
return { client, signer, address: account.address };
|
|
28
|
+
} catch (err) {
|
|
29
|
+
throw new Error(`Invalid private key: ${err.message}`);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Resolve wallet/signer from CLI option or environment variable.
|
|
35
|
+
*
|
|
36
|
+
* @param {object} opts - Commander options
|
|
37
|
+
* @returns {{ signer: object, address: string }|null}
|
|
38
|
+
*/
|
|
39
|
+
export function resolveWallet(opts) {
|
|
40
|
+
const key = opts?.wallet || process.env.SHIELDAPI_WALLET_KEY;
|
|
41
|
+
if (!key) return null;
|
|
42
|
+
return createWallet(key);
|
|
43
|
+
}
|