iamsecure 0.1.2 → 0.1.3

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/bin/iamsecure.js CHANGED
@@ -121,13 +121,18 @@ try {
121
121
  }
122
122
  }
123
123
  if (values.ui) {
124
- const { url } = await serveDashboard(report, {
124
+ const { url, replaced } = await serveDashboard(report, {
125
125
  port: Number(values.port),
126
126
  open: !values['no-open'],
127
127
  rescan: runScan,
128
128
  aiProvider: values.ai ?? null,
129
129
  analyze: values.ai ? makeAnalyzer({ provider: values.ai, model: values['ai-model'] }) : undefined,
130
130
  });
131
+ if (replaced) console.log(c.dim(`\nStopped the previous dashboard (${replaced.root}) and took over port ${replaced.port}.`));
132
+ const port = Number(new URL(url).port);
133
+ if (port !== Number(values.port)) {
134
+ console.log(c.yellow(`\n⚠ Port ${values.port} is busy by another app, using ${port}.`));
135
+ }
131
136
  console.log(`\n${c.bold('Dashboard:')} ${c.cyan(url)} ${c.dim('(Ctrl+C to stop)')}`);
132
137
  }
133
138
  } catch (err) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iamsecure",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Scan a codebase for vulnerable dependencies and insecure code, ranked by real-world risk \u2014 with an optional AI second opinion.",
5
5
  "author": "Ravindra Bisht",
6
6
  "repository": {
package/src/scan.js CHANGED
@@ -7,7 +7,7 @@ import { scanSecrets } from "./code/secrets.js";
7
7
  import { loadExploitData } from "./risk/exploit.js";
8
8
  import { scoreFindings } from "./risk/score.js";
9
9
 
10
- export const VERSION = "0.1.2";
10
+ export const VERSION = "0.1.3";
11
11
 
12
12
  /**
13
13
  * Run a full scan and return a JSON-serialisable report.
package/src/ui/index.html CHANGED
@@ -409,6 +409,7 @@ function groupRow(name, list) {
409
409
  function header(r) {
410
410
  const s = r.summary;
411
411
  const p = r.stats.packages;
412
+ document.title = `${r.project.name} — iamsecure`;
412
413
  $('#crumb').innerHTML = `<span>Projects</span><span class="sep">›</span><span>${esc(r.project.name)}</span>`;
413
414
  $('#counters').innerHTML = LEVELS.map((l) => `
414
415
  <button class="counter ${l} ${s.byRisk[l] ? 'has' : ''} ${state.level === l ? 'on' : ''}" data-level="${l}" title="Show only ${l} (real risk)">
package/src/ui/server.js CHANGED
@@ -1,8 +1,11 @@
1
1
  import http from 'node:http';
2
+ import net from 'node:net';
2
3
  import fs from 'node:fs';
4
+ import path from 'node:path';
3
5
  import crypto from 'node:crypto';
4
6
  import { spawn } from 'node:child_process';
5
7
  import { fileURLToPath } from 'node:url';
8
+ import { cacheDir } from '../util/cache.js';
6
9
 
7
10
  const PAGE = fileURLToPath(new URL('./index.html', import.meta.url));
8
11
 
@@ -14,16 +17,64 @@ function openBrowser(url) {
14
17
  } catch {}
15
18
  }
16
19
 
20
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
21
+
22
+ // Each running dashboard records {pid, port, secret, root} in a user-only file, so a newer run can
23
+ // ask it to stop. Only someone who can read that file knows the secret, so websites can't do it.
24
+ const lockFile = (port) => path.join(cacheDir, `dashboard-${port}.json`);
25
+
26
+ function portFree(port) {
27
+ return new Promise((resolve) => {
28
+ const probe = net.createServer();
29
+ probe.once('error', () => resolve(false));
30
+ probe.listen(port, '127.0.0.1', () => probe.close(() => resolve(true)));
31
+ });
32
+ }
33
+
34
+ /** Ask the dashboard holding `port` to shut down. Returns its lock info if it stopped, else null. */
35
+ async function stopPrevious(port) {
36
+ let info;
37
+ try {
38
+ info = JSON.parse(fs.readFileSync(lockFile(port), 'utf8'));
39
+ } catch {
40
+ return null; // not one of ours (or from a version without this feature)
41
+ }
42
+ try {
43
+ const res = await fetch(`http://127.0.0.1:${port}/api/shutdown`, {
44
+ method: 'POST',
45
+ headers: { 'x-iamsecure-shutdown': info.secret },
46
+ signal: AbortSignal.timeout(2000),
47
+ });
48
+ if (!res.ok) return null;
49
+ } catch {
50
+ return null;
51
+ }
52
+ for (let i = 0; i < 40; i++) {
53
+ if (await portFree(port)) return info;
54
+ await sleep(100);
55
+ }
56
+ return null;
57
+ }
58
+
17
59
  function listen(server, port, attempts = 20) {
60
+ // One 'listening' handler that reads the port actually bound. (A callback per attempt would
61
+ // also fire for the failed attempts and report the busy port — opening another dashboard.)
18
62
  return new Promise((resolve, reject) => {
19
- const tryPort = (p, left) => {
20
- server.once('error', (err) => {
21
- if (err.code === 'EADDRINUSE' && left > 0) tryPort(p + 1, left - 1);
22
- else reject(err);
23
- });
24
- server.listen(p, '127.0.0.1', () => resolve(p));
63
+ let p = port;
64
+ const onError = (err) => {
65
+ if (err.code === 'EADDRINUSE' && attempts-- > 0) server.listen(++p, '127.0.0.1');
66
+ else {
67
+ server.off('listening', onListening);
68
+ reject(err);
69
+ }
70
+ };
71
+ const onListening = () => {
72
+ server.off('error', onError);
73
+ resolve(server.address().port);
25
74
  };
26
- tryPort(port, attempts);
75
+ server.on('error', onError);
76
+ server.once('listening', onListening);
77
+ server.listen(p, '127.0.0.1');
27
78
  });
28
79
  }
29
80
 
@@ -36,7 +87,14 @@ function listen(server, port, attempts = 20) {
36
87
  export async function serveDashboard(initialReport, { port = 4477, open = true, rescan, analyze, aiProvider = null } = {}) {
37
88
  let report = initialReport;
38
89
  const token = crypto.randomBytes(16).toString('hex');
90
+ const shutdownSecret = crypto.randomBytes(24).toString('hex');
39
91
  let busy = false;
92
+ let lockPath = null;
93
+ const removeLock = () => {
94
+ try {
95
+ if (lockPath && JSON.parse(fs.readFileSync(lockPath, 'utf8')).pid === process.pid) fs.unlinkSync(lockPath);
96
+ } catch {}
97
+ };
40
98
 
41
99
  const server = http.createServer(async (req, res) => {
42
100
  const host = (req.headers.host ?? '').split(':')[0];
@@ -62,6 +120,15 @@ export async function serveDashboard(initialReport, { port = 4477, open = true,
62
120
  .replace('__IAMSECURE_AI__', JSON.stringify(aiProvider));
63
121
  return send(200, html, 'text/html; charset=utf-8');
64
122
  }
123
+ if (req.method === 'POST' && url.pathname === '/api/shutdown') {
124
+ if (req.headers['x-iamsecure-shutdown'] !== shutdownSecret) return send(403, { error: 'forbidden' });
125
+ send(200, { ok: true });
126
+ console.log('\nThis dashboard was replaced by a newer `iamsecure --ui` run. Stopping.');
127
+ removeLock();
128
+ server.close();
129
+ setTimeout(() => process.exit(0), 100);
130
+ return;
131
+ }
65
132
  if (!url.pathname.startsWith('/api/')) return send(404, { error: 'not found' });
66
133
  if (req.headers['x-iamsecure-token'] !== token) return send(401, { error: 'bad token' });
67
134
 
@@ -90,8 +157,24 @@ export async function serveDashboard(initialReport, { port = 4477, open = true,
90
157
  }
91
158
  });
92
159
 
160
+ // Replace an older iamsecure dashboard on the same port instead of piling up new ones.
161
+ const replaced = (await portFree(port)) ? null : await stopPrevious(port);
162
+
93
163
  const actual = await listen(server, port);
164
+ lockPath = lockFile(actual);
165
+ try {
166
+ fs.mkdirSync(cacheDir, { recursive: true });
167
+ fs.writeFileSync(lockPath, JSON.stringify({ pid: process.pid, port: actual, secret: shutdownSecret, root: report.project.root, startedAt: new Date().toISOString() }), { mode: 0o600 });
168
+ } catch {}
169
+ process.once('exit', removeLock);
170
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
171
+ process.once(sig, () => {
172
+ removeLock();
173
+ process.exit(sig === 'SIGINT' ? 130 : 0);
174
+ });
175
+ }
176
+
94
177
  const url = `http://localhost:${actual}/`;
95
178
  if (open) openBrowser(url);
96
- return { url, server };
179
+ return { url, server, replaced };
97
180
  }