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.
Files changed (2) hide show
  1. package/index.js +385 -0
  2. 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
+ }