magonai 1.0.7
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/index.js +385 -0
- package/package.json +21 -0
package/index.js
ADDED
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const https = require("https");
|
|
6
|
+
const os = require("os");
|
|
7
|
+
const AdmZip = require("adm-zip");
|
|
8
|
+
const { execSync } = require("child_process");
|
|
9
|
+
|
|
10
|
+
// -----------------------------
|
|
11
|
+
// Config
|
|
12
|
+
// -----------------------------
|
|
13
|
+
const SERVER_HOST = "cli.magonai.com";
|
|
14
|
+
const SERVER_PATH = "/scan";
|
|
15
|
+
const AUTH_HOST = "auth.magonai.com";
|
|
16
|
+
const CONFIG_DIR = path.join(os.homedir(), ".magonai");
|
|
17
|
+
const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
|
|
18
|
+
|
|
19
|
+
const EXCLUDE = new Set(["node_modules", ".git", ".env", ".env.local", ".env.production"]);
|
|
20
|
+
|
|
21
|
+
// -----------------------------
|
|
22
|
+
// Config helpers
|
|
23
|
+
// -----------------------------
|
|
24
|
+
function loadConfig() {
|
|
25
|
+
try {
|
|
26
|
+
return JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
|
|
27
|
+
} catch {
|
|
28
|
+
return {};
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function saveConfig(data) {
|
|
33
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
34
|
+
fs.writeFileSync(CONFIG_FILE, JSON.stringify(data, null, 2), "utf-8");
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// -----------------------------
|
|
38
|
+
// HTTPS helpers
|
|
39
|
+
// -----------------------------
|
|
40
|
+
function httpsGet(host, urlPath, headers = {}) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const req = https.request(
|
|
43
|
+
{ hostname: host, path: urlPath, method: "GET", headers },
|
|
44
|
+
(res) => {
|
|
45
|
+
let data = "";
|
|
46
|
+
res.on("data", c => data += c);
|
|
47
|
+
res.on("end", () => {
|
|
48
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
49
|
+
catch { resolve({ status: res.statusCode, body: data }); }
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
req.on("error", reject);
|
|
54
|
+
req.setTimeout(10000, () => { req.destroy(); reject(new Error("Request timed out")); });
|
|
55
|
+
req.end();
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function httpsPost(host, urlPath, payload, headers = {}) {
|
|
60
|
+
return new Promise((resolve, reject) => {
|
|
61
|
+
const body = JSON.stringify(payload);
|
|
62
|
+
const req = https.request(
|
|
63
|
+
{
|
|
64
|
+
hostname: host, path: urlPath, method: "POST",
|
|
65
|
+
headers: {
|
|
66
|
+
"Content-Type": "application/json",
|
|
67
|
+
"Content-Length": Buffer.byteLength(body),
|
|
68
|
+
...headers,
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
(res) => {
|
|
72
|
+
let data = "";
|
|
73
|
+
res.on("data", c => data += c);
|
|
74
|
+
res.on("end", () => {
|
|
75
|
+
try { resolve({ status: res.statusCode, body: JSON.parse(data) }); }
|
|
76
|
+
catch { resolve({ status: res.statusCode, body: data }); }
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
req.on("error", reject);
|
|
81
|
+
req.setTimeout(10000, () => { req.destroy(); reject(new Error("Request timed out")); });
|
|
82
|
+
req.write(body);
|
|
83
|
+
req.end();
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// -----------------------------
|
|
88
|
+
// Open URL in default browser
|
|
89
|
+
// -----------------------------
|
|
90
|
+
function openBrowser(url) {
|
|
91
|
+
try {
|
|
92
|
+
if (process.platform === "win32") {
|
|
93
|
+
execSync(`start "" "${url}"`, { stdio: "ignore" });
|
|
94
|
+
} else if (process.platform === "darwin") {
|
|
95
|
+
execSync(`open "${url}"`, { stdio: "ignore" });
|
|
96
|
+
} else {
|
|
97
|
+
try { execSync(`xdg-open "${url}"`, { stdio: "ignore" }); }
|
|
98
|
+
catch {
|
|
99
|
+
try { execSync(`gnome-open "${url}"`, { stdio: "ignore" }); }
|
|
100
|
+
catch { try { execSync(`firefox "${url}"`, { stdio: "ignore" }); } catch {} }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return true;
|
|
104
|
+
} catch {
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// -----------------------------
|
|
110
|
+
// Spinner
|
|
111
|
+
// -----------------------------
|
|
112
|
+
function startSpinner(msg) {
|
|
113
|
+
const frames = ["⠋","⠙","⠹","⠸","⠼","⠴","⠦","⠧","⠇","⠏"];
|
|
114
|
+
let i = 0;
|
|
115
|
+
return setInterval(() => {
|
|
116
|
+
process.stdout.write(`\r${frames[i++ % frames.length]} ${msg}`);
|
|
117
|
+
}, 100);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function stopSpinner(id) {
|
|
121
|
+
clearInterval(id);
|
|
122
|
+
process.stdout.write("\r\x1b[K");
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// =============================================================
|
|
126
|
+
// COMMANDS
|
|
127
|
+
// =============================================================
|
|
128
|
+
const command = process.argv[2];
|
|
129
|
+
|
|
130
|
+
// -----------------------------
|
|
131
|
+
// magonai login
|
|
132
|
+
// -----------------------------
|
|
133
|
+
if (command === "login") {
|
|
134
|
+
(async () => {
|
|
135
|
+
console.log("🔐 Authenticating with MagonAI...\n");
|
|
136
|
+
|
|
137
|
+
// Step 1: Request a one-time token from auth server
|
|
138
|
+
let token;
|
|
139
|
+
try {
|
|
140
|
+
const res = await httpsPost(AUTH_HOST, "/cli/auth/request", {});
|
|
141
|
+
if (res.status !== 200 || !res.body?.token) {
|
|
142
|
+
console.error("❌ Could not reach authentication server.");
|
|
143
|
+
console.error(" Make sure you have internet access and try again.");
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
token = res.body.token;
|
|
147
|
+
} catch (err) {
|
|
148
|
+
console.error("❌ Could not connect to authentication server:", err.message);
|
|
149
|
+
process.exit(1);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Step 2: Open browser with token embedded in the login URL
|
|
153
|
+
const loginUrl = `https://magonai.com/login?cli_token=${token}`;
|
|
154
|
+
console.log(" Opening your browser to log in...");
|
|
155
|
+
console.log(` ${loginUrl}\n`);
|
|
156
|
+
|
|
157
|
+
const opened = openBrowser(loginUrl);
|
|
158
|
+
if (!opened) {
|
|
159
|
+
console.log("⚠️ Could not open browser automatically.");
|
|
160
|
+
console.log(` Please visit this URL manually:\n ${loginUrl}\n`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Step 3: Poll auth server until login confirmed (max 5 minutes)
|
|
164
|
+
const spin = startSpinner("Waiting for authentication...");
|
|
165
|
+
|
|
166
|
+
const MAX_WAIT_MS = 5 * 60 * 1000;
|
|
167
|
+
const POLL_MS = 2000;
|
|
168
|
+
const started = Date.now();
|
|
169
|
+
let apiKey = null;
|
|
170
|
+
let userEmail = null;
|
|
171
|
+
let userName = null;
|
|
172
|
+
|
|
173
|
+
while (Date.now() - started < MAX_WAIT_MS) {
|
|
174
|
+
await new Promise(r => setTimeout(r, POLL_MS));
|
|
175
|
+
try {
|
|
176
|
+
const res = await httpsGet(AUTH_HOST, `/cli/auth/status?token=${token}`);
|
|
177
|
+
if (res.status === 200 && res.body?.authenticated) {
|
|
178
|
+
apiKey = res.body.apiKey;
|
|
179
|
+
userEmail = res.body.email || "";
|
|
180
|
+
userName = res.body.name || "";
|
|
181
|
+
break;
|
|
182
|
+
}
|
|
183
|
+
} catch {
|
|
184
|
+
// Network hiccup — keep polling
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
stopSpinner(spin);
|
|
189
|
+
|
|
190
|
+
if (!apiKey) {
|
|
191
|
+
console.error("❌ Authentication timed out. Please run `magonai login` again.");
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Step 4: Save credentials locally
|
|
196
|
+
saveConfig({
|
|
197
|
+
apiKey,
|
|
198
|
+
email: userEmail,
|
|
199
|
+
name: userName,
|
|
200
|
+
authenticatedAt: new Date().toISOString(),
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
console.log(`✅ Authenticated successfully${userEmail ? " as " + userEmail : ""}!`);
|
|
204
|
+
console.log(` Credentials saved to ${CONFIG_FILE}`);
|
|
205
|
+
process.exit(0);
|
|
206
|
+
})();
|
|
207
|
+
|
|
208
|
+
// -----------------------------
|
|
209
|
+
// magonai logout
|
|
210
|
+
// -----------------------------
|
|
211
|
+
} else if (command === "logout") {
|
|
212
|
+
(async () => {
|
|
213
|
+
const cfg = loadConfig();
|
|
214
|
+
if (!cfg.apiKey) {
|
|
215
|
+
console.log("⚠️ You are not logged in.");
|
|
216
|
+
process.exit(0);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Tell auth server to revoke the key in Redis + MongoDB
|
|
220
|
+
try {
|
|
221
|
+
const res = await httpsPost(
|
|
222
|
+
AUTH_HOST,
|
|
223
|
+
"/cli/auth/revoke",
|
|
224
|
+
{},
|
|
225
|
+
{ "Authorization": `Bearer ${cfg.apiKey}` }
|
|
226
|
+
);
|
|
227
|
+
if (res.status === 200) {
|
|
228
|
+
console.log(" API key revoked on server.");
|
|
229
|
+
} else {
|
|
230
|
+
console.warn(" Server revoke failed — clearing locally anyway.");
|
|
231
|
+
}
|
|
232
|
+
} catch {
|
|
233
|
+
console.warn(" Could not reach server — clearing locally only.");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
saveConfig({});
|
|
237
|
+
console.log("✅ Logged out. Credentials cleared.");
|
|
238
|
+
process.exit(0);
|
|
239
|
+
})();
|
|
240
|
+
|
|
241
|
+
// -----------------------------
|
|
242
|
+
// magonai whoami
|
|
243
|
+
// -----------------------------
|
|
244
|
+
} else if (command === "whoami") {
|
|
245
|
+
(async () => {
|
|
246
|
+
const cfg = loadConfig();
|
|
247
|
+
if (!cfg.apiKey) {
|
|
248
|
+
console.log("❌ Not logged in. Run `magonai login` first.");
|
|
249
|
+
process.exit(1);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Verify key is still valid against auth server (MongoDB-backed)
|
|
253
|
+
try {
|
|
254
|
+
const res = await httpsGet(
|
|
255
|
+
AUTH_HOST,
|
|
256
|
+
"/cli/auth/whoami",
|
|
257
|
+
{ "Authorization": `Bearer ${cfg.apiKey}` }
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
if (res.status === 200 && res.body?.valid) {
|
|
261
|
+
const { email, name, issuedAt } = res.body;
|
|
262
|
+
console.log(`👤 Logged in${email ? " as " + email : ""}${name ? " (" + name + ")" : ""}`);
|
|
263
|
+
console.log(` API key : ${cfg.apiKey.slice(0, 12)}${"*".repeat(20)}`);
|
|
264
|
+
console.log(` Since : ${issuedAt ? new Date(issuedAt).toLocaleString() : cfg.authenticatedAt || "unknown"}`);
|
|
265
|
+
} else {
|
|
266
|
+
console.log("❌ Your session has expired. Run `magonai login` again.");
|
|
267
|
+
saveConfig({});
|
|
268
|
+
process.exit(1);
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
// Offline fallback — show local info
|
|
272
|
+
console.log(`👤 Logged in${cfg.email ? " as " + cfg.email : ""} (offline)`);
|
|
273
|
+
console.log(` API key : ${cfg.apiKey.slice(0, 12)}${"*".repeat(20)}`);
|
|
274
|
+
console.log(` Since : ${cfg.authenticatedAt || "unknown"}`);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
process.exit(0);
|
|
278
|
+
})();
|
|
279
|
+
|
|
280
|
+
// -----------------------------
|
|
281
|
+
// magonai help
|
|
282
|
+
// -----------------------------
|
|
283
|
+
} else if (command === "help" || command === "--help" || command === "-h") {
|
|
284
|
+
console.log(`
|
|
285
|
+
MagonAI Security Scanner
|
|
286
|
+
|
|
287
|
+
Usage:
|
|
288
|
+
magonai Scan current project for vulnerabilities
|
|
289
|
+
magonai login Authenticate with your MagonAI account
|
|
290
|
+
magonai logout Clear saved credentials (revokes key on server)
|
|
291
|
+
magonai whoami Show currently logged-in account
|
|
292
|
+
magonai help Show this help message
|
|
293
|
+
`);
|
|
294
|
+
process.exit(0);
|
|
295
|
+
|
|
296
|
+
// -----------------------------
|
|
297
|
+
// magonai (default: scan)
|
|
298
|
+
// -----------------------------
|
|
299
|
+
} else {
|
|
300
|
+
(async () => {
|
|
301
|
+
const cfg = loadConfig();
|
|
302
|
+
if (!cfg.apiKey) {
|
|
303
|
+
console.log("❌ Not logged in. Please authenticate first:\n");
|
|
304
|
+
console.log(" magonai login\n");
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const projectPath = process.cwd();
|
|
309
|
+
|
|
310
|
+
const hasJS = fs.existsSync(path.join(projectPath, "package.json"));
|
|
311
|
+
const hasPython =
|
|
312
|
+
fs.existsSync(path.join(projectPath, "requirements.txt")) ||
|
|
313
|
+
fs.existsSync(path.join(projectPath, "Pipfile.lock")) ||
|
|
314
|
+
fs.existsSync(path.join(projectPath, "poetry.lock")) ||
|
|
315
|
+
fs.existsSync(path.join(projectPath, "pyproject.toml"));
|
|
316
|
+
|
|
317
|
+
if (!hasJS && !hasPython) {
|
|
318
|
+
console.log("❌ No package.json or Python dependency file found");
|
|
319
|
+
console.log(" Supported: package.json / requirements.txt / Pipfile.lock / poetry.lock / pyproject.toml");
|
|
320
|
+
process.exit(1);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const spin = startSpinner("Scanning for vulnerabilities...");
|
|
324
|
+
|
|
325
|
+
// Build zip of project — excluding noise directories
|
|
326
|
+
const zip = new AdmZip();
|
|
327
|
+
|
|
328
|
+
function addDir(dirPath, zipBase) {
|
|
329
|
+
let entries;
|
|
330
|
+
try { entries = fs.readdirSync(dirPath, { withFileTypes: true }); }
|
|
331
|
+
catch { return; }
|
|
332
|
+
for (const entry of entries) {
|
|
333
|
+
if (EXCLUDE.has(entry.name)) continue;
|
|
334
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
335
|
+
if (entry.isDirectory()) {
|
|
336
|
+
addDir(fullPath, zipBase ? `${zipBase}/${entry.name}` : entry.name);
|
|
337
|
+
} else {
|
|
338
|
+
zip.addLocalFile(fullPath, zipBase || "");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
addDir(projectPath, "");
|
|
344
|
+
const zipBuffer = zip.toBuffer();
|
|
345
|
+
|
|
346
|
+
const chunks = [];
|
|
347
|
+
try {
|
|
348
|
+
await new Promise((resolve, reject) => {
|
|
349
|
+
const options = {
|
|
350
|
+
hostname: SERVER_HOST,
|
|
351
|
+
path: SERVER_PATH,
|
|
352
|
+
method: "POST",
|
|
353
|
+
headers: {
|
|
354
|
+
"Content-Type": "application/zip",
|
|
355
|
+
"Content-Length": zipBuffer.length,
|
|
356
|
+
"Authorization": `Bearer ${cfg.apiKey}`,
|
|
357
|
+
},
|
|
358
|
+
};
|
|
359
|
+
|
|
360
|
+
const req = https.request(options, (res) => {
|
|
361
|
+
if (res.statusCode === 401) {
|
|
362
|
+
stopSpinner(spin);
|
|
363
|
+
console.error("❌ Invalid or expired credentials. Please run `magonai login` again.");
|
|
364
|
+
process.exit(1);
|
|
365
|
+
}
|
|
366
|
+
res.on("data", chunk => chunks.push(chunk));
|
|
367
|
+
res.on("end", resolve);
|
|
368
|
+
res.on("error", reject);
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
req.on("error", reject);
|
|
372
|
+
req.write(zipBuffer);
|
|
373
|
+
req.end();
|
|
374
|
+
});
|
|
375
|
+
} catch (err) {
|
|
376
|
+
stopSpinner(spin);
|
|
377
|
+
console.error("❌ Could not connect to server:", err.message);
|
|
378
|
+
process.exit(1);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
stopSpinner(spin);
|
|
382
|
+
console.log(Buffer.concat(chunks).toString("utf-8"));
|
|
383
|
+
process.exit(0);
|
|
384
|
+
})();
|
|
385
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "magonai",
|
|
3
|
+
"version": "1.0.7",
|
|
4
|
+
"description": "MagonAI vulnerability scanner CLI",
|
|
5
|
+
"bin": {
|
|
6
|
+
"magonai": "index.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"adm-zip": "^0.5.17",
|
|
10
|
+
"archiver": "^7.0.1",
|
|
11
|
+
"form-data": "^4.0.0",
|
|
12
|
+
"node-fetch": "^2.7.0"
|
|
13
|
+
},
|
|
14
|
+
"main": "index.js",
|
|
15
|
+
"devDependencies": {},
|
|
16
|
+
"scripts": {
|
|
17
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
18
|
+
},
|
|
19
|
+
"author": "",
|
|
20
|
+
"license": "ISC"
|
|
21
|
+
}
|