@vainplex/shieldapi-cli 1.3.1 → 2.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/package.json +4 -1
- package/src/commands/checkPrompt.js +101 -0
- package/src/commands/domain.js +11 -28
- package/src/commands/email.js +11 -28
- package/src/commands/hash.js +3 -0
- package/src/commands/health.js +0 -1
- package/src/commands/ip.js +11 -28
- package/src/commands/password.js +19 -5
- package/src/commands/scan.js +6 -0
- package/src/commands/scanSkill.js +157 -0
- package/src/commands/url.js +11 -28
- package/src/index.js +32 -2
- package/src/lib/api.js +47 -0
- package/src/lib/command.js +54 -0
- package/src/lib/validator.js +47 -0
- package/src/lib/wallet.js +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vainplex/shieldapi-cli",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"description": "Security intelligence from your terminal. Pay-per-request with USDC.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -12,6 +12,9 @@
|
|
|
12
12
|
"README.md",
|
|
13
13
|
"LICENSE"
|
|
14
14
|
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"test": "node --test tests/*.test.js"
|
|
17
|
+
},
|
|
15
18
|
"engines": {
|
|
16
19
|
"node": ">=18"
|
|
17
20
|
},
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import ora from 'ora';
|
|
3
|
+
import { apiRequestPost } from '../lib/api.js';
|
|
4
|
+
import { resolveWallet } from '../lib/wallet.js';
|
|
5
|
+
import { riskBadge, sectionHeader } from '../lib/formatter.js';
|
|
6
|
+
import { EXIT } from '../lib/exit.js';
|
|
7
|
+
|
|
8
|
+
export async function checkPromptCommand(prompt, opts) {
|
|
9
|
+
const spinner = opts.quiet ? null : ora({ text: 'Analyzing prompt for injection patterns...', stream: process.stderr }).start();
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
let inputPrompt = prompt;
|
|
13
|
+
|
|
14
|
+
// Read from stdin if --stdin flag
|
|
15
|
+
if (opts.stdin || !prompt) {
|
|
16
|
+
const chunks = [];
|
|
17
|
+
for await (const chunk of process.stdin) {
|
|
18
|
+
chunks.push(chunk);
|
|
19
|
+
}
|
|
20
|
+
inputPrompt = Buffer.concat(chunks).toString('utf-8').trim();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (!inputPrompt) {
|
|
24
|
+
if (spinner) spinner.fail('No prompt provided');
|
|
25
|
+
process.exitCode = EXIT.USAGE;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const body = {
|
|
30
|
+
prompt: inputPrompt,
|
|
31
|
+
...(opts.context && { context: opts.context }),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
35
|
+
const data = await apiRequestPost('check-prompt', body, {
|
|
36
|
+
demo: opts.demo,
|
|
37
|
+
wallet,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
if (spinner) spinner.stop();
|
|
41
|
+
|
|
42
|
+
if (opts.json) {
|
|
43
|
+
console.log(JSON.stringify(data, null, 2));
|
|
44
|
+
} else {
|
|
45
|
+
formatCheckPrompt(data, inputPrompt);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Exit code: 0=clean, 1=injection detected
|
|
49
|
+
process.exitCode = data.isInjection ? 1 : 0;
|
|
50
|
+
} catch (err) {
|
|
51
|
+
if (spinner) spinner.fail(err.message);
|
|
52
|
+
process.exitCode = EXIT.API_ERROR;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function formatCheckPrompt(data, prompt) {
|
|
57
|
+
console.log();
|
|
58
|
+
|
|
59
|
+
if (data.isInjection) {
|
|
60
|
+
console.log(chalk.red.bold('⚠️ PROMPT INJECTION DETECTED'));
|
|
61
|
+
console.log();
|
|
62
|
+
console.log(` Confidence: ${chalk.red.bold(Math.round(data.confidence * 100) + '%')}`);
|
|
63
|
+
console.log(` Category: ${chalk.yellow(data.category)}`);
|
|
64
|
+
console.log(` Patterns Checked: ${data.patternsChecked}`);
|
|
65
|
+
console.log(` Scan Duration: ${data.scanDuration}ms`);
|
|
66
|
+
|
|
67
|
+
if (data.patterns?.length > 0) {
|
|
68
|
+
sectionHeader('Detected Patterns');
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
for (const p of data.patterns) {
|
|
72
|
+
console.log(` 🔴 ${chalk.bold(p.type)}`);
|
|
73
|
+
console.log(` ${p.description}`);
|
|
74
|
+
if (p.evidence) {
|
|
75
|
+
const truncated = p.evidence.length > 100
|
|
76
|
+
? p.evidence.slice(0, 100) + '...'
|
|
77
|
+
: p.evidence;
|
|
78
|
+
console.log(` ${chalk.gray('Evidence:')} ${chalk.dim(truncated)}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (data.decodedContent) {
|
|
84
|
+
sectionHeader('Decoded Content');
|
|
85
|
+
console.log(` ${chalk.dim(data.decodedContent.slice(0, 200))}`);
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
console.log(chalk.green.bold('✅ No injection detected'));
|
|
89
|
+
console.log();
|
|
90
|
+
console.log(` Confidence: ${chalk.green('0%')}`);
|
|
91
|
+
console.log(` Patterns Checked: ${data.patternsChecked}`);
|
|
92
|
+
console.log(` Scan Duration: ${data.scanDuration}ms`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (data.demo) {
|
|
96
|
+
console.log();
|
|
97
|
+
console.log(chalk.yellow(' ⚠️ Demo mode — results limited. Pay with x402 for full analysis.'));
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
console.log();
|
|
101
|
+
}
|
package/src/commands/domain.js
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { apiRequest } from '../lib/api.js';
|
|
4
|
-
import { resolveWallet } from '../lib/wallet.js';
|
|
1
|
+
import { runCommand } from '../lib/command.js';
|
|
5
2
|
import { formatDomain } from '../lib/formatter.js';
|
|
6
|
-
import {
|
|
3
|
+
import { isValidDomain } from '../lib/validator.js';
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Check domain reputation.
|
|
10
7
|
*/
|
|
11
8
|
export async function domainCommand(domain, opts) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
}
|
|
9
|
+
await runCommand({
|
|
10
|
+
endpoint: 'check-domain',
|
|
11
|
+
paramName: 'domain',
|
|
12
|
+
paramValue: domain,
|
|
13
|
+
formatFn: formatDomain,
|
|
14
|
+
spinnerText: `Checking domain: ${domain}`,
|
|
15
|
+
validateFn: isValidDomain,
|
|
16
|
+
validateMsg: `Invalid domain: "${domain}". Provide a domain without protocol (e.g. example.com).`,
|
|
17
|
+
}, opts);
|
|
35
18
|
}
|
package/src/commands/email.js
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { apiRequest } from '../lib/api.js';
|
|
4
|
-
import { resolveWallet } from '../lib/wallet.js';
|
|
1
|
+
import { runCommand } from '../lib/command.js';
|
|
5
2
|
import { formatEmail } from '../lib/formatter.js';
|
|
6
|
-
import {
|
|
3
|
+
import { isValidEmail } from '../lib/validator.js';
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Check an email address for breaches.
|
|
10
7
|
*/
|
|
11
8
|
export async function emailCommand(email, opts) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
}
|
|
9
|
+
await runCommand({
|
|
10
|
+
endpoint: 'check-email',
|
|
11
|
+
paramName: 'email',
|
|
12
|
+
paramValue: email,
|
|
13
|
+
formatFn: formatEmail,
|
|
14
|
+
spinnerText: `Checking email: ${email}`,
|
|
15
|
+
validateFn: isValidEmail,
|
|
16
|
+
validateMsg: `Invalid email: "${email}". Expected format: user@example.com.`,
|
|
17
|
+
}, opts);
|
|
35
18
|
}
|
package/src/commands/hash.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
|
+
import { EXIT } from '../lib/exit.js';
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Compute SHA-1 hash locally (offline, no API call).
|
|
@@ -16,4 +17,6 @@ export async function hashCommand(password, opts) {
|
|
|
16
17
|
console.log(` Length: ${password.length} characters`);
|
|
17
18
|
console.log();
|
|
18
19
|
}
|
|
20
|
+
|
|
21
|
+
process.exitCode = EXIT.SAFE;
|
|
19
22
|
}
|
package/src/commands/health.js
CHANGED
package/src/commands/ip.js
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { apiRequest } from '../lib/api.js';
|
|
4
|
-
import { resolveWallet } from '../lib/wallet.js';
|
|
1
|
+
import { runCommand } from '../lib/command.js';
|
|
5
2
|
import { formatIp } from '../lib/formatter.js';
|
|
6
|
-
import {
|
|
3
|
+
import { isValidIPv4 } from '../lib/validator.js';
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Check IP reputation.
|
|
10
7
|
*/
|
|
11
8
|
export async function ipCommand(ip, opts) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
}
|
|
9
|
+
await runCommand({
|
|
10
|
+
endpoint: 'check-ip',
|
|
11
|
+
paramName: 'ip',
|
|
12
|
+
paramValue: ip,
|
|
13
|
+
formatFn: formatIp,
|
|
14
|
+
spinnerText: `Checking IP: ${ip}`,
|
|
15
|
+
validateFn: isValidIPv4,
|
|
16
|
+
validateMsg: `Invalid IPv4 address: "${ip}". Expected format: 1.2.3.4.`,
|
|
17
|
+
}, opts);
|
|
35
18
|
}
|
package/src/commands/password.js
CHANGED
|
@@ -51,14 +51,28 @@ function readStdin() {
|
|
|
51
51
|
} else {
|
|
52
52
|
// Piped mode: read all data from stdin
|
|
53
53
|
let data = '';
|
|
54
|
+
let resolved = false;
|
|
54
55
|
process.stdin.setEncoding('utf8');
|
|
55
56
|
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
56
|
-
process.stdin.on('end', () =>
|
|
57
|
-
|
|
57
|
+
process.stdin.on('end', () => {
|
|
58
|
+
if (!resolved) {
|
|
59
|
+
resolved = true;
|
|
60
|
+
resolve(data.trim());
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
process.stdin.on('error', (err) => {
|
|
64
|
+
if (!resolved) {
|
|
65
|
+
resolved = true;
|
|
66
|
+
reject(err);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
58
69
|
setTimeout(() => {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
if (!resolved) {
|
|
71
|
+
resolved = true;
|
|
72
|
+
process.stdin.destroy();
|
|
73
|
+
if (data) resolve(data.trim());
|
|
74
|
+
else reject(new Error('Stdin timeout — no input received'));
|
|
75
|
+
}
|
|
62
76
|
}, 10000);
|
|
63
77
|
}
|
|
64
78
|
});
|
package/src/commands/scan.js
CHANGED
|
@@ -13,6 +13,12 @@ export async function scanCommand(opts) {
|
|
|
13
13
|
const params = {};
|
|
14
14
|
|
|
15
15
|
if (opts.password) {
|
|
16
|
+
// Shell history warning (C2: scan --password leak)
|
|
17
|
+
if (!opts.quiet && process.stderr.isTTY) {
|
|
18
|
+
process.stderr.write(
|
|
19
|
+
chalk.yellow('⚠ Password may appear in shell history. Use `shieldapi password --stdin` for sensitive passwords.\n')
|
|
20
|
+
);
|
|
21
|
+
}
|
|
16
22
|
params.password_hash = createHash('sha1').update(opts.password).digest('hex').toUpperCase();
|
|
17
23
|
}
|
|
18
24
|
if (opts.email) params.email = opts.email;
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { join, extname, basename } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { apiRequestPost } from '../lib/api.js';
|
|
6
|
+
import { resolveWallet } from '../lib/wallet.js';
|
|
7
|
+
import { riskBadge, sectionHeader, kvLine, formatNumber } from '../lib/formatter.js';
|
|
8
|
+
import { EXIT } from '../lib/exit.js';
|
|
9
|
+
|
|
10
|
+
const SCANNABLE_EXTENSIONS = new Set([
|
|
11
|
+
'.js', '.mjs', '.cjs', '.ts', '.tsx', '.jsx', '.py', '.sh', '.bash',
|
|
12
|
+
'.md', '.markdown', '.json', '.yml', '.yaml', '.toml', '.env',
|
|
13
|
+
'.bat', '.ps1', '.rb', '.go', '.rs', '.java',
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
function loadSkillFiles(path) {
|
|
17
|
+
const files = [];
|
|
18
|
+
const stat = statSync(path);
|
|
19
|
+
|
|
20
|
+
if (stat.isFile()) {
|
|
21
|
+
const content = readFileSync(path, 'utf-8');
|
|
22
|
+
return [{ name: basename(path), content }];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (stat.isDirectory()) {
|
|
26
|
+
const entries = readdirSync(path, { withFileTypes: true });
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
29
|
+
const fullPath = join(path, entry.name);
|
|
30
|
+
if (entry.isFile()) {
|
|
31
|
+
const ext = extname(entry.name).toLowerCase();
|
|
32
|
+
if (SCANNABLE_EXTENSIONS.has(ext) || entry.name === 'Dockerfile' || entry.name === 'Makefile') {
|
|
33
|
+
try {
|
|
34
|
+
const content = readFileSync(fullPath, 'utf-8');
|
|
35
|
+
if (content.length <= 100 * 1024) { // 100KB per file max
|
|
36
|
+
files.push({ name: entry.name, content });
|
|
37
|
+
}
|
|
38
|
+
} catch { /* skip unreadable files */ }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return files;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function scanSkillCommand(target, opts) {
|
|
48
|
+
const spinner = opts.quiet ? null : ora({ text: `Scanning skill: ${target || 'stdin'}`, stream: process.stderr }).start();
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
let body = {};
|
|
52
|
+
|
|
53
|
+
if (target && existsSync(target)) {
|
|
54
|
+
// Local file or directory
|
|
55
|
+
const files = loadSkillFiles(target);
|
|
56
|
+
if (files.length === 0) {
|
|
57
|
+
if (spinner) spinner.fail('No scannable files found');
|
|
58
|
+
process.exitCode = EXIT.USAGE;
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// If there's a SKILL.md, use it as the skill field
|
|
63
|
+
const skillMd = files.find(f => f.name.toLowerCase() === 'skill.md');
|
|
64
|
+
if (skillMd) {
|
|
65
|
+
body.skill = skillMd.content;
|
|
66
|
+
}
|
|
67
|
+
body.files = files;
|
|
68
|
+
if (spinner) spinner.text = `Scanning ${files.length} files from ${target}`;
|
|
69
|
+
} else if (target) {
|
|
70
|
+
// Assume it's raw skill content or a skill name
|
|
71
|
+
body.skill = target;
|
|
72
|
+
} else {
|
|
73
|
+
if (spinner) spinner.fail('No target specified');
|
|
74
|
+
process.exitCode = EXIT.USAGE;
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
79
|
+
const data = await apiRequestPost('scan-skill', body, {
|
|
80
|
+
demo: opts.demo,
|
|
81
|
+
wallet,
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
if (spinner) spinner.stop();
|
|
85
|
+
|
|
86
|
+
if (opts.json) {
|
|
87
|
+
console.log(JSON.stringify(data, null, 2));
|
|
88
|
+
} else {
|
|
89
|
+
formatScanSkill(data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Exit code: 0=clean, 1=findings, 2=critical
|
|
93
|
+
if (data.riskLevel === 'CLEAN') {
|
|
94
|
+
process.exitCode = 0;
|
|
95
|
+
} else if (data.riskLevel === 'CRITICAL') {
|
|
96
|
+
process.exitCode = 2;
|
|
97
|
+
} else {
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
}
|
|
100
|
+
} catch (err) {
|
|
101
|
+
if (spinner) spinner.fail(err.message);
|
|
102
|
+
process.exitCode = EXIT.API_ERROR;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function formatScanSkill(data) {
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(chalk.bold.underline('🛡️ Skill Safety Scan Results'));
|
|
109
|
+
console.log();
|
|
110
|
+
|
|
111
|
+
// Risk badge
|
|
112
|
+
const badge = riskBadge(data.riskLevel?.toLowerCase(), data.riskScore);
|
|
113
|
+
console.log(` Risk Level: ${badge}`);
|
|
114
|
+
console.log(` Files Analyzed: ${data.filesAnalyzed || '?'}`);
|
|
115
|
+
console.log(` Patterns Checked: ${formatNumber(data.totalPatterns || 0)}`);
|
|
116
|
+
console.log(` Scan Duration: ${data.scanDuration}ms`);
|
|
117
|
+
|
|
118
|
+
if (data.findings?.length > 0) {
|
|
119
|
+
sectionHeader(`Findings (${data.findings.length})`);
|
|
120
|
+
console.log();
|
|
121
|
+
|
|
122
|
+
const bySeverity = { CRITICAL: [], HIGH: [], MEDIUM: [], LOW: [] };
|
|
123
|
+
for (const f of data.findings) {
|
|
124
|
+
(bySeverity[f.severity] || bySeverity.LOW).push(f);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
for (const [sev, findings] of Object.entries(bySeverity)) {
|
|
128
|
+
if (findings.length === 0) continue;
|
|
129
|
+
const color = sev === 'CRITICAL' ? chalk.red.bold
|
|
130
|
+
: sev === 'HIGH' ? chalk.red
|
|
131
|
+
: sev === 'MEDIUM' ? chalk.yellow
|
|
132
|
+
: chalk.gray;
|
|
133
|
+
|
|
134
|
+
for (const f of findings) {
|
|
135
|
+
console.log(` ${color(`[${sev}]`)} ${f.description}`);
|
|
136
|
+
if (f.location) console.log(` 📍 ${chalk.gray(f.location)}`);
|
|
137
|
+
if (f.category) console.log(` 📂 ${chalk.gray(f.category)}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} else {
|
|
141
|
+
console.log();
|
|
142
|
+
console.log(chalk.green.bold(' ✅ No security issues detected'));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// Summary
|
|
146
|
+
if (data.summary) {
|
|
147
|
+
console.log();
|
|
148
|
+
console.log(` ${chalk.gray(data.summary)}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (data.demo) {
|
|
152
|
+
console.log();
|
|
153
|
+
console.log(chalk.yellow(' ⚠️ Demo mode — results limited. Pay with x402 for full scan.'));
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
console.log();
|
|
157
|
+
}
|
package/src/commands/url.js
CHANGED
|
@@ -1,35 +1,18 @@
|
|
|
1
|
-
import
|
|
2
|
-
import chalk from 'chalk';
|
|
3
|
-
import { apiRequest } from '../lib/api.js';
|
|
4
|
-
import { resolveWallet } from '../lib/wallet.js';
|
|
1
|
+
import { runCommand } from '../lib/command.js';
|
|
5
2
|
import { formatUrl } from '../lib/formatter.js';
|
|
6
|
-
import {
|
|
3
|
+
import { isValidUrl } from '../lib/validator.js';
|
|
7
4
|
|
|
8
5
|
/**
|
|
9
6
|
* Check URL safety.
|
|
10
7
|
*/
|
|
11
8
|
export async function urlCommand(url, opts) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
}
|
|
9
|
+
await runCommand({
|
|
10
|
+
endpoint: 'check-url',
|
|
11
|
+
paramName: 'url',
|
|
12
|
+
paramValue: url,
|
|
13
|
+
formatFn: formatUrl,
|
|
14
|
+
spinnerText: `Checking URL: ${url}`,
|
|
15
|
+
validateFn: isValidUrl,
|
|
16
|
+
validateMsg: `Invalid URL: "${url}". Expected format: https://example.com.`,
|
|
17
|
+
}, opts);
|
|
35
18
|
}
|
package/src/index.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
|
+
import { dirname, join } from 'node:path';
|
|
1
4
|
import { Command } from 'commander';
|
|
2
5
|
import { passwordCommand } from './commands/password.js';
|
|
3
6
|
import { emailCommand } from './commands/email.js';
|
|
@@ -7,6 +10,11 @@ import { urlCommand } from './commands/url.js';
|
|
|
7
10
|
import { scanCommand } from './commands/scan.js';
|
|
8
11
|
import { healthCommand } from './commands/health.js';
|
|
9
12
|
import { hashCommand } from './commands/hash.js';
|
|
13
|
+
import { scanSkillCommand } from './commands/scanSkill.js';
|
|
14
|
+
import { checkPromptCommand } from './commands/checkPrompt.js';
|
|
15
|
+
|
|
16
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
17
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../package.json'), 'utf8'));
|
|
10
18
|
|
|
11
19
|
export function run(argv) {
|
|
12
20
|
const program = new Command();
|
|
@@ -14,11 +22,10 @@ export function run(argv) {
|
|
|
14
22
|
program
|
|
15
23
|
.name('shieldapi')
|
|
16
24
|
.description('🛡️ ShieldAPI CLI — Security intelligence from your terminal. Pay-per-request with USDC.')
|
|
17
|
-
.version(
|
|
25
|
+
.version(pkg.version)
|
|
18
26
|
.option('--wallet <key>', 'Private key for x402 payments (or set SHIELDAPI_WALLET_KEY)')
|
|
19
27
|
.option('--json', 'Output raw JSON instead of formatted output')
|
|
20
28
|
.option('--no-color', 'Disable colors')
|
|
21
|
-
.option('-y, --yes', 'Skip payment confirmation prompts')
|
|
22
29
|
.option('-q, --quiet', 'Suppress non-essential output (spinners, warnings)');
|
|
23
30
|
|
|
24
31
|
program
|
|
@@ -105,5 +112,28 @@ export function run(argv) {
|
|
|
105
112
|
hashCommand(password, { ...globalOpts, ...cmdOpts });
|
|
106
113
|
});
|
|
107
114
|
|
|
115
|
+
program
|
|
116
|
+
.command('scan-skill')
|
|
117
|
+
.description('Scan an AI skill or plugin for security issues (8 risk categories)')
|
|
118
|
+
.argument('[target]', 'Path to SKILL.md, directory, or raw skill content')
|
|
119
|
+
.option('--demo', 'Use demo mode (free, no wallet needed)')
|
|
120
|
+
.action((target, cmdOpts, cmd) => {
|
|
121
|
+
const globalOpts = cmd.parent.opts();
|
|
122
|
+
scanSkillCommand(target, { ...globalOpts, ...cmdOpts });
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
program
|
|
126
|
+
.command('check-prompt')
|
|
127
|
+
.description('Check text for prompt injection patterns (200+ detectors)')
|
|
128
|
+
.argument('[prompt]', 'Text to analyze (or use --stdin)')
|
|
129
|
+
.option('--demo', 'Use demo mode (free, no wallet needed)')
|
|
130
|
+
.option('--stdin', 'Read prompt from stdin')
|
|
131
|
+
.option('--context <context>', 'Context hint: user-input, skill-prompt, system-prompt', 'user-input')
|
|
132
|
+
.action((prompt, cmdOpts, cmd) => {
|
|
133
|
+
const globalOpts = cmd.parent.opts();
|
|
134
|
+
checkPromptCommand(prompt, { ...globalOpts, ...cmdOpts });
|
|
135
|
+
});
|
|
136
|
+
|
|
108
137
|
program.parse(argv);
|
|
138
|
+
|
|
109
139
|
}
|
package/src/lib/api.js
CHANGED
|
@@ -73,3 +73,50 @@ export class ApiError extends Error {
|
|
|
73
73
|
this.status = status;
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* POST request for Phase 2 endpoints (scan-skill, check-prompt).
|
|
79
|
+
* Uses @x402/fetch for paid requests, plain fetch for demo.
|
|
80
|
+
*/
|
|
81
|
+
export async function apiRequestPost(endpoint, body = {}, { demo = false, wallet = null } = {}) {
|
|
82
|
+
const query = demo ? '?demo=true' : '';
|
|
83
|
+
const url = `${BASE_URL}/${endpoint}${query}`;
|
|
84
|
+
|
|
85
|
+
const fetchOptions = {
|
|
86
|
+
method: 'POST',
|
|
87
|
+
headers: { 'Content-Type': 'application/json' },
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if (demo || endpoint === 'health') {
|
|
92
|
+
const res = await fetch(url, fetchOptions);
|
|
93
|
+
if (!res.ok) {
|
|
94
|
+
const body = await res.text().catch(() => '');
|
|
95
|
+
throw new ApiError(res.status, body || res.statusText);
|
|
96
|
+
}
|
|
97
|
+
return res.json();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (!wallet?.signer) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
'No wallet configured. Use --wallet <key> or set SHIELDAPI_WALLET_KEY environment variable.'
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const { x402Client } = await import('@x402/core/client');
|
|
107
|
+
const { registerExactEvmScheme } = await import('@x402/evm/exact/client');
|
|
108
|
+
const { wrapFetchWithPayment } = await import('@x402/fetch');
|
|
109
|
+
|
|
110
|
+
const client = new x402Client();
|
|
111
|
+
registerExactEvmScheme(client, { signer: wallet.signer });
|
|
112
|
+
|
|
113
|
+
const paidFetch = wrapFetchWithPayment(fetch, client);
|
|
114
|
+
const res = await paidFetch(url, fetchOptions);
|
|
115
|
+
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
const body = await res.text().catch(() => '');
|
|
118
|
+
throw new ApiError(res.status, body || res.statusText);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return res.json();
|
|
122
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import ora from 'ora';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import { apiRequest } from './api.js';
|
|
4
|
+
import { resolveWallet } from './wallet.js';
|
|
5
|
+
import { exitCodeFromResult, exitCodeFromError, EXIT } from './exit.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Generic command runner that extracts common boilerplate from
|
|
9
|
+
* domain, email, ip, and url commands.
|
|
10
|
+
*
|
|
11
|
+
* @param {object} config
|
|
12
|
+
* @param {string} config.endpoint - API endpoint (e.g. 'check-domain')
|
|
13
|
+
* @param {string} config.paramName - Query parameter name (e.g. 'domain')
|
|
14
|
+
* @param {string} config.paramValue - The user-supplied value
|
|
15
|
+
* @param {function} config.formatFn - Formatter function (data, paramValue) => void
|
|
16
|
+
* @param {string} config.spinnerText - Text for the spinner
|
|
17
|
+
* @param {function} [config.validateFn] - Optional validator (value) => boolean
|
|
18
|
+
* @param {string} [config.validateMsg] - Error message if validation fails
|
|
19
|
+
* @param {object} opts - Merged global + command options
|
|
20
|
+
*/
|
|
21
|
+
export async function runCommand(config, opts) {
|
|
22
|
+
const { endpoint, paramName, paramValue, formatFn, spinnerText, validateFn, validateMsg } = config;
|
|
23
|
+
|
|
24
|
+
// Input validation (S2)
|
|
25
|
+
if (validateFn && !validateFn(paramValue)) {
|
|
26
|
+
process.stderr.write(chalk.red(`Error: ${validateMsg || 'Invalid input.'}\n`));
|
|
27
|
+
process.exitCode = EXIT.USAGE;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const spinner = opts.quiet ? null : ora({ text: spinnerText, stream: process.stderr }).start();
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const wallet = opts.demo ? null : resolveWallet(opts);
|
|
35
|
+
|
|
36
|
+
const data = await apiRequest(endpoint, { [paramName]: paramValue }, {
|
|
37
|
+
demo: opts.demo,
|
|
38
|
+
wallet,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
spinner?.stop();
|
|
42
|
+
|
|
43
|
+
if (opts.json) {
|
|
44
|
+
console.log(JSON.stringify(data, null, 2));
|
|
45
|
+
} else {
|
|
46
|
+
formatFn(data, paramValue);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
process.exitCode = exitCodeFromResult(data);
|
|
50
|
+
} catch (err) {
|
|
51
|
+
spinner?.fail(err.message);
|
|
52
|
+
process.exitCode = exitCodeFromError(err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Input validators for fast-fail before API calls.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Check if string is a plausible email address.
|
|
7
|
+
* @param {string} s
|
|
8
|
+
* @returns {boolean}
|
|
9
|
+
*/
|
|
10
|
+
export function isValidEmail(s) {
|
|
11
|
+
if (!s || typeof s !== 'string') return false;
|
|
12
|
+
const parts = s.split('@');
|
|
13
|
+
if (parts.length !== 2) return false;
|
|
14
|
+
const [local, domain] = parts;
|
|
15
|
+
return local.length > 0 && domain.includes('.') && domain.length > 2;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if string is a valid IPv4 address.
|
|
20
|
+
* @param {string} s
|
|
21
|
+
* @returns {boolean}
|
|
22
|
+
*/
|
|
23
|
+
export function isValidIPv4(s) {
|
|
24
|
+
if (!s || typeof s !== 'string') return false;
|
|
25
|
+
return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(s);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Check if string is a valid domain (no protocol prefix, contains a dot).
|
|
30
|
+
* @param {string} s
|
|
31
|
+
* @returns {boolean}
|
|
32
|
+
*/
|
|
33
|
+
export function isValidDomain(s) {
|
|
34
|
+
if (!s || typeof s !== 'string') return false;
|
|
35
|
+
if (s.startsWith('http://') || s.startsWith('https://')) return false;
|
|
36
|
+
return s.includes('.') && s.length > 3 && !s.includes(' ');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Check if string is a valid URL (starts with http:// or https://).
|
|
41
|
+
* @param {string} s
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function isValidUrl(s) {
|
|
45
|
+
if (!s || typeof s !== 'string') return false;
|
|
46
|
+
return s.startsWith('http://') || s.startsWith('https://');
|
|
47
|
+
}
|
package/src/lib/wallet.js
CHANGED
|
@@ -31,7 +31,7 @@ export function createWallet(privateKey) {
|
|
|
31
31
|
|
|
32
32
|
return { signer, address: account.address };
|
|
33
33
|
} catch (err) {
|
|
34
|
-
throw new Error(
|
|
34
|
+
throw new Error('Invalid private key. Expected 64 hex characters (with or without 0x prefix).');
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
|