@ayurak/aribot-cli 1.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/README.md +66 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +290 -0
- package/package.json +53 -0
- package/src/cli.ts +333 -0
- package/tsconfig.json +16 -0
package/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Aribot CLI
|
|
2
|
+
|
|
3
|
+
AI-powered threat modeling from the command line.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g aribot-cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
# Authenticate
|
|
15
|
+
aribot login
|
|
16
|
+
|
|
17
|
+
# Upload and analyze a diagram
|
|
18
|
+
aribot analyze architecture.drawio
|
|
19
|
+
|
|
20
|
+
# List your diagrams
|
|
21
|
+
aribot diagrams
|
|
22
|
+
|
|
23
|
+
# View threats
|
|
24
|
+
aribot threats <diagram-id>
|
|
25
|
+
|
|
26
|
+
# Export report
|
|
27
|
+
aribot export <diagram-id> --format pdf
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Commands
|
|
31
|
+
|
|
32
|
+
| Command | Description |
|
|
33
|
+
|---------|-------------|
|
|
34
|
+
| `aribot login` | Authenticate with your API key |
|
|
35
|
+
| `aribot logout` | Remove stored credentials |
|
|
36
|
+
| `aribot whoami` | Show current auth status |
|
|
37
|
+
| `aribot diagrams` | List all your diagrams |
|
|
38
|
+
| `aribot analyze <file>` | Upload and analyze a diagram |
|
|
39
|
+
| `aribot threats <id>` | List threats for a diagram |
|
|
40
|
+
| `aribot generate-threats <id>` | Generate AI threats |
|
|
41
|
+
| `aribot export <id>` | Export report (pdf/json/csv) |
|
|
42
|
+
|
|
43
|
+
## Options
|
|
44
|
+
|
|
45
|
+
### analyze
|
|
46
|
+
- `-n, --name <name>` - Set diagram name
|
|
47
|
+
- `--auto-threats` - Auto-generate AI threats (default: true)
|
|
48
|
+
|
|
49
|
+
### threats
|
|
50
|
+
- `-s, --severity <level>` - Filter by severity
|
|
51
|
+
|
|
52
|
+
### export
|
|
53
|
+
- `-f, --format <format>` - Export format (pdf, json, csv)
|
|
54
|
+
- `-o, --output <file>` - Output file path
|
|
55
|
+
|
|
56
|
+
## Get API Key
|
|
57
|
+
|
|
58
|
+
Get your API key at [developer.ayurak.com](https://developer.ayurak.com)
|
|
59
|
+
|
|
60
|
+
## Documentation
|
|
61
|
+
|
|
62
|
+
Full documentation: [developer.ayurak.com/docs](https://developer.ayurak.com/docs)
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
MIT
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Aribot CLI - AI-Powered Threat Modeling
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* aribot login # Authenticate with API key
|
|
7
|
+
* aribot analyze <diagram> # Analyze a diagram file
|
|
8
|
+
* aribot threats <diagram-id> # List threats for a diagram
|
|
9
|
+
* aribot export <diagram-id> # Export report
|
|
10
|
+
* aribot diagrams # List all diagrams
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/**
|
|
4
|
+
* Aribot CLI - AI-Powered Threat Modeling
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* aribot login # Authenticate with API key
|
|
8
|
+
* aribot analyze <diagram> # Analyze a diagram file
|
|
9
|
+
* aribot threats <diagram-id> # List threats for a diagram
|
|
10
|
+
* aribot export <diagram-id> # Export report
|
|
11
|
+
* aribot diagrams # List all diagrams
|
|
12
|
+
*/
|
|
13
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
14
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
const commander_1 = require("commander");
|
|
18
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
19
|
+
const ora_1 = __importDefault(require("ora"));
|
|
20
|
+
const conf_1 = __importDefault(require("conf"));
|
|
21
|
+
const inquirer_1 = __importDefault(require("inquirer"));
|
|
22
|
+
const form_data_1 = __importDefault(require("form-data"));
|
|
23
|
+
const fs_1 = __importDefault(require("fs"));
|
|
24
|
+
const path_1 = __importDefault(require("path"));
|
|
25
|
+
const config = new conf_1.default({ projectName: 'aribot-cli' });
|
|
26
|
+
const API_BASE = 'https://api.aribot.ayurak.com';
|
|
27
|
+
const program = new commander_1.Command();
|
|
28
|
+
program
|
|
29
|
+
.name('aribot')
|
|
30
|
+
.description('AI-powered threat modeling CLI by Ayurak')
|
|
31
|
+
.version('1.0.0');
|
|
32
|
+
// Helper to get auth headers
|
|
33
|
+
function getHeaders() {
|
|
34
|
+
const apiKey = config.get('apiKey');
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
console.error(chalk_1.default.red('Not authenticated. Run: aribot login'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
41
|
+
'Content-Type': 'application/json'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
// API request helper
|
|
45
|
+
async function apiRequest(endpoint, options = {}) {
|
|
46
|
+
const fetch = (await import('node-fetch')).default;
|
|
47
|
+
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
48
|
+
...options,
|
|
49
|
+
headers: { ...getHeaders(), ...(options.headers || {}) }
|
|
50
|
+
});
|
|
51
|
+
if (!response.ok) {
|
|
52
|
+
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
|
53
|
+
}
|
|
54
|
+
return response.json();
|
|
55
|
+
}
|
|
56
|
+
// Login command
|
|
57
|
+
program
|
|
58
|
+
.command('login')
|
|
59
|
+
.description('Authenticate with your Aribot API key')
|
|
60
|
+
.action(async () => {
|
|
61
|
+
const { apiKey } = await inquirer_1.default.prompt([{
|
|
62
|
+
type: 'password',
|
|
63
|
+
name: 'apiKey',
|
|
64
|
+
message: 'Enter your Aribot API key:',
|
|
65
|
+
mask: '*'
|
|
66
|
+
}]);
|
|
67
|
+
const spinner = (0, ora_1.default)('Validating API key...').start();
|
|
68
|
+
try {
|
|
69
|
+
const fetch = (await import('node-fetch')).default;
|
|
70
|
+
const response = await fetch(`${API_BASE}/v2/auth/validate/`, {
|
|
71
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
72
|
+
});
|
|
73
|
+
if (response.ok) {
|
|
74
|
+
config.set('apiKey', apiKey);
|
|
75
|
+
spinner.succeed(chalk_1.default.green('Authenticated successfully!'));
|
|
76
|
+
console.log(chalk_1.default.dim('API key stored securely.'));
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
spinner.fail(chalk_1.default.red('Invalid API key'));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
spinner.fail(chalk_1.default.red('Authentication failed'));
|
|
84
|
+
console.error(error);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
// Logout command
|
|
88
|
+
program
|
|
89
|
+
.command('logout')
|
|
90
|
+
.description('Remove stored credentials')
|
|
91
|
+
.action(() => {
|
|
92
|
+
config.delete('apiKey');
|
|
93
|
+
console.log(chalk_1.default.green('Logged out successfully.'));
|
|
94
|
+
});
|
|
95
|
+
// List diagrams
|
|
96
|
+
program
|
|
97
|
+
.command('diagrams')
|
|
98
|
+
.description('List all your diagrams')
|
|
99
|
+
.option('-l, --limit <number>', 'Number of diagrams to show', '10')
|
|
100
|
+
.action(async (options) => {
|
|
101
|
+
const spinner = (0, ora_1.default)('Fetching diagrams...').start();
|
|
102
|
+
try {
|
|
103
|
+
const data = await apiRequest(`/v2/diagrams/?limit=${options.limit}`);
|
|
104
|
+
spinner.stop();
|
|
105
|
+
console.log(chalk_1.default.bold('\nYour Diagrams:\n'));
|
|
106
|
+
if (!data.results?.length) {
|
|
107
|
+
console.log(chalk_1.default.dim('No diagrams found. Create one at https://portal.aribot.ayurak.com'));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
data.results.forEach((d) => {
|
|
111
|
+
const status = d.status === 'completed' ? chalk_1.default.green('✓') : chalk_1.default.yellow('⋯');
|
|
112
|
+
console.log(`${status} ${chalk_1.default.cyan(d.id.slice(0, 8))} ${d.name} ${chalk_1.default.dim(d.threats_count + ' threats')}`);
|
|
113
|
+
});
|
|
114
|
+
console.log(chalk_1.default.dim(`\nShowing ${data.results.length} of ${data.count} diagrams`));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
spinner.fail('Failed to fetch diagrams');
|
|
118
|
+
console.error(error);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
// Analyze diagram
|
|
122
|
+
program
|
|
123
|
+
.command('analyze <file>')
|
|
124
|
+
.description('Upload and analyze a diagram file')
|
|
125
|
+
.option('-n, --name <name>', 'Diagram name')
|
|
126
|
+
.option('--auto-threats', 'Automatically generate AI threats', true)
|
|
127
|
+
.action(async (file, options) => {
|
|
128
|
+
if (!fs_1.default.existsSync(file)) {
|
|
129
|
+
console.error(chalk_1.default.red(`File not found: ${file}`));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const spinner = (0, ora_1.default)('Uploading diagram...').start();
|
|
133
|
+
try {
|
|
134
|
+
const fetch = (await import('node-fetch')).default;
|
|
135
|
+
const form = new form_data_1.default();
|
|
136
|
+
form.append('file', fs_1.default.createReadStream(file));
|
|
137
|
+
form.append('name', options.name || path_1.default.basename(file, path_1.default.extname(file)));
|
|
138
|
+
form.append('auto_generate_threats', options.autoThreats ? 'true' : 'false');
|
|
139
|
+
const apiKey = config.get('apiKey');
|
|
140
|
+
const response = await fetch(`${API_BASE}/v2/diagrams/upload-analyze/`, {
|
|
141
|
+
method: 'POST',
|
|
142
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
143
|
+
body: form
|
|
144
|
+
});
|
|
145
|
+
if (!response.ok) {
|
|
146
|
+
throw new Error(`Upload failed: ${response.status}`);
|
|
147
|
+
}
|
|
148
|
+
const data = await response.json();
|
|
149
|
+
spinner.succeed('Diagram uploaded!');
|
|
150
|
+
console.log(chalk_1.default.bold('\nDiagram Created:'));
|
|
151
|
+
console.log(` ID: ${chalk_1.default.cyan(data.id)}`);
|
|
152
|
+
console.log(` Name: ${data.name}`);
|
|
153
|
+
console.log(` Status: ${data.status}`);
|
|
154
|
+
if (options.autoThreats) {
|
|
155
|
+
const threatSpinner = (0, ora_1.default)('Generating AI threats...').start();
|
|
156
|
+
// Poll for completion
|
|
157
|
+
let attempts = 0;
|
|
158
|
+
while (attempts < 30) {
|
|
159
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
160
|
+
const status = await apiRequest(`/v2/diagrams/${data.id}/`);
|
|
161
|
+
if (status.status === 'completed') {
|
|
162
|
+
threatSpinner.succeed(`Generated ${status.threats_count} threats`);
|
|
163
|
+
break;
|
|
164
|
+
}
|
|
165
|
+
attempts++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
console.log(chalk_1.default.dim(`\nView at: https://portal.aribot.ayurak.com/diagrams/${data.id}`));
|
|
169
|
+
}
|
|
170
|
+
catch (error) {
|
|
171
|
+
spinner.fail('Analysis failed');
|
|
172
|
+
console.error(error);
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
// List threats
|
|
176
|
+
program
|
|
177
|
+
.command('threats <diagram-id>')
|
|
178
|
+
.description('List threats for a diagram')
|
|
179
|
+
.option('-s, --severity <level>', 'Filter by severity (critical, high, medium, low)')
|
|
180
|
+
.action(async (diagramId, options) => {
|
|
181
|
+
const spinner = (0, ora_1.default)('Fetching threats...').start();
|
|
182
|
+
try {
|
|
183
|
+
let url = `/v2/diagrams/${diagramId}/threats/`;
|
|
184
|
+
if (options.severity) {
|
|
185
|
+
url += `?severity=${options.severity}`;
|
|
186
|
+
}
|
|
187
|
+
const data = await apiRequest(url);
|
|
188
|
+
spinner.stop();
|
|
189
|
+
console.log(chalk_1.default.bold('\nThreats:\n'));
|
|
190
|
+
const severityColors = {
|
|
191
|
+
critical: chalk_1.default.red,
|
|
192
|
+
high: chalk_1.default.yellow,
|
|
193
|
+
medium: chalk_1.default.blue,
|
|
194
|
+
low: chalk_1.default.dim
|
|
195
|
+
};
|
|
196
|
+
data.results?.forEach((t) => {
|
|
197
|
+
const color = severityColors[t.severity] || chalk_1.default.white;
|
|
198
|
+
console.log(`${color(`[${t.severity.toUpperCase()}]`)} ${t.title}`);
|
|
199
|
+
console.log(chalk_1.default.dim(` STRIDE: ${t.stride_category} | ${t.id.slice(0, 8)}`));
|
|
200
|
+
console.log();
|
|
201
|
+
});
|
|
202
|
+
console.log(chalk_1.default.dim(`Total: ${data.count} threats`));
|
|
203
|
+
}
|
|
204
|
+
catch (error) {
|
|
205
|
+
spinner.fail('Failed to fetch threats');
|
|
206
|
+
console.error(error);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
209
|
+
// Export report
|
|
210
|
+
program
|
|
211
|
+
.command('export <diagram-id>')
|
|
212
|
+
.description('Export diagram report')
|
|
213
|
+
.option('-f, --format <format>', 'Export format (pdf, json, csv)', 'json')
|
|
214
|
+
.option('-o, --output <file>', 'Output file path')
|
|
215
|
+
.action(async (diagramId, options) => {
|
|
216
|
+
const spinner = (0, ora_1.default)(`Exporting ${options.format.toUpperCase()} report...`).start();
|
|
217
|
+
try {
|
|
218
|
+
const fetch = (await import('node-fetch')).default;
|
|
219
|
+
const apiKey = config.get('apiKey');
|
|
220
|
+
const response = await fetch(`${API_BASE}/v2/diagrams/${diagramId}/export/?format=${options.format}`, { headers: { 'Authorization': `Bearer ${apiKey}` } });
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
throw new Error(`Export failed: ${response.status}`);
|
|
223
|
+
}
|
|
224
|
+
const outputPath = options.output || `aribot-report-${diagramId.slice(0, 8)}.${options.format}`;
|
|
225
|
+
if (options.format === 'json') {
|
|
226
|
+
const data = await response.json();
|
|
227
|
+
fs_1.default.writeFileSync(outputPath, JSON.stringify(data, null, 2));
|
|
228
|
+
}
|
|
229
|
+
else {
|
|
230
|
+
const buffer = await response.arrayBuffer();
|
|
231
|
+
fs_1.default.writeFileSync(outputPath, Buffer.from(buffer));
|
|
232
|
+
}
|
|
233
|
+
spinner.succeed(`Report saved to ${chalk_1.default.cyan(outputPath)}`);
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
spinner.fail('Export failed');
|
|
237
|
+
console.error(error);
|
|
238
|
+
}
|
|
239
|
+
});
|
|
240
|
+
// Generate threats command
|
|
241
|
+
program
|
|
242
|
+
.command('generate-threats <diagram-id>')
|
|
243
|
+
.description('Generate AI threats for an existing diagram')
|
|
244
|
+
.action(async (diagramId) => {
|
|
245
|
+
const spinner = (0, ora_1.default)('Generating AI threats...').start();
|
|
246
|
+
try {
|
|
247
|
+
await apiRequest(`/v2/diagrams/${diagramId}/generate-threats/`, {
|
|
248
|
+
method: 'POST'
|
|
249
|
+
});
|
|
250
|
+
spinner.text = 'Processing...';
|
|
251
|
+
// Poll for completion
|
|
252
|
+
let attempts = 0;
|
|
253
|
+
while (attempts < 30) {
|
|
254
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
255
|
+
const status = await apiRequest(`/v2/diagrams/${diagramId}/`);
|
|
256
|
+
if (status.ai_threats_generated) {
|
|
257
|
+
spinner.succeed(`Generated ${status.threats_count} threats`);
|
|
258
|
+
return;
|
|
259
|
+
}
|
|
260
|
+
attempts++;
|
|
261
|
+
}
|
|
262
|
+
spinner.succeed('Threat generation initiated');
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
spinner.fail('Failed to generate threats');
|
|
266
|
+
console.error(error);
|
|
267
|
+
}
|
|
268
|
+
});
|
|
269
|
+
// Whoami command
|
|
270
|
+
program
|
|
271
|
+
.command('whoami')
|
|
272
|
+
.description('Show current authentication status')
|
|
273
|
+
.action(async () => {
|
|
274
|
+
const apiKey = config.get('apiKey');
|
|
275
|
+
if (!apiKey) {
|
|
276
|
+
console.log(chalk_1.default.yellow('Not authenticated'));
|
|
277
|
+
console.log(chalk_1.default.dim('Run: aribot login'));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
try {
|
|
281
|
+
const data = await apiRequest('/v2/auth/me/');
|
|
282
|
+
console.log(chalk_1.default.green('Authenticated as:'));
|
|
283
|
+
console.log(` Email: ${data.email}`);
|
|
284
|
+
console.log(` Company: ${data.company || 'N/A'}`);
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
console.log(chalk_1.default.yellow('API key stored but validation failed'));
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ayurak/aribot-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI-powered threat modeling CLI by Ayurak. Automatically analyze diagrams, generate STRIDE threats, and get security recommendations.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"aribot": "./dist/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"build": "tsc",
|
|
11
|
+
"start": "node dist/cli.js",
|
|
12
|
+
"prepublishOnly": "npm run build"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"threat-modeling",
|
|
16
|
+
"security",
|
|
17
|
+
"stride",
|
|
18
|
+
"devsecops",
|
|
19
|
+
"cli",
|
|
20
|
+
"ai",
|
|
21
|
+
"appsec",
|
|
22
|
+
"diagram-analysis",
|
|
23
|
+
"vulnerability",
|
|
24
|
+
"risk-assessment"
|
|
25
|
+
],
|
|
26
|
+
"author": "Ayurak AI <support@ayurak.com>",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/ayurak/aribot-cli"
|
|
31
|
+
},
|
|
32
|
+
"homepage": "https://developer.ayurak.com",
|
|
33
|
+
"bugs": {
|
|
34
|
+
"url": "https://github.com/ayurak/aribot-cli/issues"
|
|
35
|
+
},
|
|
36
|
+
"engines": {
|
|
37
|
+
"node": ">=18.0.0"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"chalk": "^5.3.0",
|
|
41
|
+
"commander": "^12.0.0",
|
|
42
|
+
"conf": "^12.0.0",
|
|
43
|
+
"form-data": "^4.0.0",
|
|
44
|
+
"inquirer": "^9.2.0",
|
|
45
|
+
"node-fetch": "^3.3.0",
|
|
46
|
+
"ora": "^8.0.0"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@types/inquirer": "^9.0.9",
|
|
50
|
+
"@types/node": "^20.0.0",
|
|
51
|
+
"typescript": "^5.3.0"
|
|
52
|
+
}
|
|
53
|
+
}
|
package/src/cli.ts
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Aribot CLI - AI-Powered Threat Modeling
|
|
4
|
+
*
|
|
5
|
+
* Usage:
|
|
6
|
+
* aribot login # Authenticate with API key
|
|
7
|
+
* aribot analyze <diagram> # Analyze a diagram file
|
|
8
|
+
* aribot threats <diagram-id> # List threats for a diagram
|
|
9
|
+
* aribot export <diagram-id> # Export report
|
|
10
|
+
* aribot diagrams # List all diagrams
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { Command } from 'commander';
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import ora from 'ora';
|
|
16
|
+
import Conf from 'conf';
|
|
17
|
+
import inquirer from 'inquirer';
|
|
18
|
+
import FormData from 'form-data';
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import path from 'path';
|
|
21
|
+
|
|
22
|
+
const config = new Conf({ projectName: 'aribot-cli' });
|
|
23
|
+
const API_BASE = 'https://api.aribot.ayurak.com';
|
|
24
|
+
|
|
25
|
+
const program = new Command();
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.name('aribot')
|
|
29
|
+
.description('AI-powered threat modeling CLI by Ayurak')
|
|
30
|
+
.version('1.0.0');
|
|
31
|
+
|
|
32
|
+
// Helper to get auth headers
|
|
33
|
+
function getHeaders(): Record<string, string> {
|
|
34
|
+
const apiKey = config.get('apiKey') as string;
|
|
35
|
+
if (!apiKey) {
|
|
36
|
+
console.error(chalk.red('Not authenticated. Run: aribot login'));
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
41
|
+
'Content-Type': 'application/json'
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// API request helper
|
|
46
|
+
async function apiRequest(endpoint: string, options: any = {}): Promise<any> {
|
|
47
|
+
const fetch = (await import('node-fetch')).default;
|
|
48
|
+
const response = await fetch(`${API_BASE}${endpoint}`, {
|
|
49
|
+
...options,
|
|
50
|
+
headers: { ...getHeaders(), ...(options.headers || {}) }
|
|
51
|
+
} as any);
|
|
52
|
+
|
|
53
|
+
if (!response.ok) {
|
|
54
|
+
throw new Error(`API Error: ${response.status} ${response.statusText}`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return response.json();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Login command
|
|
61
|
+
program
|
|
62
|
+
.command('login')
|
|
63
|
+
.description('Authenticate with your Aribot API key')
|
|
64
|
+
.action(async () => {
|
|
65
|
+
const { apiKey } = await inquirer.prompt([{
|
|
66
|
+
type: 'password',
|
|
67
|
+
name: 'apiKey',
|
|
68
|
+
message: 'Enter your Aribot API key:',
|
|
69
|
+
mask: '*'
|
|
70
|
+
}]);
|
|
71
|
+
|
|
72
|
+
const spinner = ora('Validating API key...').start();
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const fetch = (await import('node-fetch')).default;
|
|
76
|
+
const response = await fetch(`${API_BASE}/v2/auth/validate/`, {
|
|
77
|
+
headers: { 'Authorization': `Bearer ${apiKey}` }
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
if (response.ok) {
|
|
81
|
+
config.set('apiKey', apiKey);
|
|
82
|
+
spinner.succeed(chalk.green('Authenticated successfully!'));
|
|
83
|
+
console.log(chalk.dim('API key stored securely.'));
|
|
84
|
+
} else {
|
|
85
|
+
spinner.fail(chalk.red('Invalid API key'));
|
|
86
|
+
}
|
|
87
|
+
} catch (error) {
|
|
88
|
+
spinner.fail(chalk.red('Authentication failed'));
|
|
89
|
+
console.error(error);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Logout command
|
|
94
|
+
program
|
|
95
|
+
.command('logout')
|
|
96
|
+
.description('Remove stored credentials')
|
|
97
|
+
.action(() => {
|
|
98
|
+
config.delete('apiKey');
|
|
99
|
+
console.log(chalk.green('Logged out successfully.'));
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
// List diagrams
|
|
103
|
+
program
|
|
104
|
+
.command('diagrams')
|
|
105
|
+
.description('List all your diagrams')
|
|
106
|
+
.option('-l, --limit <number>', 'Number of diagrams to show', '10')
|
|
107
|
+
.action(async (options) => {
|
|
108
|
+
const spinner = ora('Fetching diagrams...').start();
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const data = await apiRequest(`/v2/diagrams/?limit=${options.limit}`);
|
|
112
|
+
spinner.stop();
|
|
113
|
+
|
|
114
|
+
console.log(chalk.bold('\nYour Diagrams:\n'));
|
|
115
|
+
|
|
116
|
+
if (!data.results?.length) {
|
|
117
|
+
console.log(chalk.dim('No diagrams found. Create one at https://portal.aribot.ayurak.com'));
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
data.results.forEach((d: any) => {
|
|
122
|
+
const status = d.status === 'completed' ? chalk.green('✓') : chalk.yellow('⋯');
|
|
123
|
+
console.log(`${status} ${chalk.cyan(d.id.slice(0, 8))} ${d.name} ${chalk.dim(d.threats_count + ' threats')}`);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
console.log(chalk.dim(`\nShowing ${data.results.length} of ${data.count} diagrams`));
|
|
127
|
+
} catch (error) {
|
|
128
|
+
spinner.fail('Failed to fetch diagrams');
|
|
129
|
+
console.error(error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Analyze diagram
|
|
134
|
+
program
|
|
135
|
+
.command('analyze <file>')
|
|
136
|
+
.description('Upload and analyze a diagram file')
|
|
137
|
+
.option('-n, --name <name>', 'Diagram name')
|
|
138
|
+
.option('--auto-threats', 'Automatically generate AI threats', true)
|
|
139
|
+
.action(async (file, options) => {
|
|
140
|
+
if (!fs.existsSync(file)) {
|
|
141
|
+
console.error(chalk.red(`File not found: ${file}`));
|
|
142
|
+
process.exit(1);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const spinner = ora('Uploading diagram...').start();
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const fetch = (await import('node-fetch')).default;
|
|
149
|
+
const form = new FormData();
|
|
150
|
+
form.append('file', fs.createReadStream(file));
|
|
151
|
+
form.append('name', options.name || path.basename(file, path.extname(file)));
|
|
152
|
+
form.append('auto_generate_threats', options.autoThreats ? 'true' : 'false');
|
|
153
|
+
|
|
154
|
+
const apiKey = config.get('apiKey') as string;
|
|
155
|
+
const response = await fetch(`${API_BASE}/v2/diagrams/upload-analyze/`, {
|
|
156
|
+
method: 'POST',
|
|
157
|
+
headers: { 'Authorization': `Bearer ${apiKey}` },
|
|
158
|
+
body: form as any
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (!response.ok) {
|
|
162
|
+
throw new Error(`Upload failed: ${response.status}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const data = await response.json() as any;
|
|
166
|
+
spinner.succeed('Diagram uploaded!');
|
|
167
|
+
|
|
168
|
+
console.log(chalk.bold('\nDiagram Created:'));
|
|
169
|
+
console.log(` ID: ${chalk.cyan(data.id)}`);
|
|
170
|
+
console.log(` Name: ${data.name}`);
|
|
171
|
+
console.log(` Status: ${data.status}`);
|
|
172
|
+
|
|
173
|
+
if (options.autoThreats) {
|
|
174
|
+
const threatSpinner = ora('Generating AI threats...').start();
|
|
175
|
+
|
|
176
|
+
// Poll for completion
|
|
177
|
+
let attempts = 0;
|
|
178
|
+
while (attempts < 30) {
|
|
179
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
180
|
+
const status = await apiRequest(`/v2/diagrams/${data.id}/`);
|
|
181
|
+
|
|
182
|
+
if (status.status === 'completed') {
|
|
183
|
+
threatSpinner.succeed(`Generated ${status.threats_count} threats`);
|
|
184
|
+
break;
|
|
185
|
+
}
|
|
186
|
+
attempts++;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
console.log(chalk.dim(`\nView at: https://portal.aribot.ayurak.com/diagrams/${data.id}`));
|
|
191
|
+
} catch (error) {
|
|
192
|
+
spinner.fail('Analysis failed');
|
|
193
|
+
console.error(error);
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
// List threats
|
|
198
|
+
program
|
|
199
|
+
.command('threats <diagram-id>')
|
|
200
|
+
.description('List threats for a diagram')
|
|
201
|
+
.option('-s, --severity <level>', 'Filter by severity (critical, high, medium, low)')
|
|
202
|
+
.action(async (diagramId, options) => {
|
|
203
|
+
const spinner = ora('Fetching threats...').start();
|
|
204
|
+
|
|
205
|
+
try {
|
|
206
|
+
let url = `/v2/diagrams/${diagramId}/threats/`;
|
|
207
|
+
if (options.severity) {
|
|
208
|
+
url += `?severity=${options.severity}`;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const data = await apiRequest(url);
|
|
212
|
+
spinner.stop();
|
|
213
|
+
|
|
214
|
+
console.log(chalk.bold('\nThreats:\n'));
|
|
215
|
+
|
|
216
|
+
const severityColors: Record<string, any> = {
|
|
217
|
+
critical: chalk.red,
|
|
218
|
+
high: chalk.yellow,
|
|
219
|
+
medium: chalk.blue,
|
|
220
|
+
low: chalk.dim
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
data.results?.forEach((t: any) => {
|
|
224
|
+
const color = severityColors[t.severity] || chalk.white;
|
|
225
|
+
console.log(`${color(`[${t.severity.toUpperCase()}]`)} ${t.title}`);
|
|
226
|
+
console.log(chalk.dim(` STRIDE: ${t.stride_category} | ${t.id.slice(0, 8)}`));
|
|
227
|
+
console.log();
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
console.log(chalk.dim(`Total: ${data.count} threats`));
|
|
231
|
+
} catch (error) {
|
|
232
|
+
spinner.fail('Failed to fetch threats');
|
|
233
|
+
console.error(error);
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// Export report
|
|
238
|
+
program
|
|
239
|
+
.command('export <diagram-id>')
|
|
240
|
+
.description('Export diagram report')
|
|
241
|
+
.option('-f, --format <format>', 'Export format (pdf, json, csv)', 'json')
|
|
242
|
+
.option('-o, --output <file>', 'Output file path')
|
|
243
|
+
.action(async (diagramId, options) => {
|
|
244
|
+
const spinner = ora(`Exporting ${options.format.toUpperCase()} report...`).start();
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
const fetch = (await import('node-fetch')).default;
|
|
248
|
+
const apiKey = config.get('apiKey') as string;
|
|
249
|
+
|
|
250
|
+
const response = await fetch(
|
|
251
|
+
`${API_BASE}/v2/diagrams/${diagramId}/export/?format=${options.format}`,
|
|
252
|
+
{ headers: { 'Authorization': `Bearer ${apiKey}` } }
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
if (!response.ok) {
|
|
256
|
+
throw new Error(`Export failed: ${response.status}`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const outputPath = options.output || `aribot-report-${diagramId.slice(0, 8)}.${options.format}`;
|
|
260
|
+
|
|
261
|
+
if (options.format === 'json') {
|
|
262
|
+
const data = await response.json();
|
|
263
|
+
fs.writeFileSync(outputPath, JSON.stringify(data, null, 2));
|
|
264
|
+
} else {
|
|
265
|
+
const buffer = await response.arrayBuffer();
|
|
266
|
+
fs.writeFileSync(outputPath, Buffer.from(buffer));
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
spinner.succeed(`Report saved to ${chalk.cyan(outputPath)}`);
|
|
270
|
+
} catch (error) {
|
|
271
|
+
spinner.fail('Export failed');
|
|
272
|
+
console.error(error);
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
// Generate threats command
|
|
277
|
+
program
|
|
278
|
+
.command('generate-threats <diagram-id>')
|
|
279
|
+
.description('Generate AI threats for an existing diagram')
|
|
280
|
+
.action(async (diagramId) => {
|
|
281
|
+
const spinner = ora('Generating AI threats...').start();
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
await apiRequest(`/v2/diagrams/${diagramId}/generate-threats/`, {
|
|
285
|
+
method: 'POST'
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
spinner.text = 'Processing...';
|
|
289
|
+
|
|
290
|
+
// Poll for completion
|
|
291
|
+
let attempts = 0;
|
|
292
|
+
while (attempts < 30) {
|
|
293
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
294
|
+
const status = await apiRequest(`/v2/diagrams/${diagramId}/`);
|
|
295
|
+
|
|
296
|
+
if (status.ai_threats_generated) {
|
|
297
|
+
spinner.succeed(`Generated ${status.threats_count} threats`);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
attempts++;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
spinner.succeed('Threat generation initiated');
|
|
304
|
+
} catch (error) {
|
|
305
|
+
spinner.fail('Failed to generate threats');
|
|
306
|
+
console.error(error);
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Whoami command
|
|
311
|
+
program
|
|
312
|
+
.command('whoami')
|
|
313
|
+
.description('Show current authentication status')
|
|
314
|
+
.action(async () => {
|
|
315
|
+
const apiKey = config.get('apiKey') as string;
|
|
316
|
+
|
|
317
|
+
if (!apiKey) {
|
|
318
|
+
console.log(chalk.yellow('Not authenticated'));
|
|
319
|
+
console.log(chalk.dim('Run: aribot login'));
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const data = await apiRequest('/v2/auth/me/');
|
|
325
|
+
console.log(chalk.green('Authenticated as:'));
|
|
326
|
+
console.log(` Email: ${data.email}`);
|
|
327
|
+
console.log(` Company: ${data.company || 'N/A'}`);
|
|
328
|
+
} catch {
|
|
329
|
+
console.log(chalk.yellow('API key stored but validation failed'));
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
program.parse();
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "NodeNext",
|
|
5
|
+
"moduleResolution": "NodeNext",
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"declaration": true
|
|
13
|
+
},
|
|
14
|
+
"include": ["src/**/*"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|