nsauditor-ai 0.1.20 → 0.1.22

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai",
3
- "version": "0.1.20",
3
+ "version": "0.1.22",
4
4
  "description": "Modular AI-assisted network security audit platform — Community Edition",
5
5
  "type": "module",
6
6
  "private": false,
@@ -123,7 +123,7 @@ function withBaseContext(ctxLike) {
123
123
  return { ...base, ...live };
124
124
  }
125
125
 
126
- async function callPlugin(mod, host, ctx, priorOutputs = null) {
126
+ async function callPlugin(mod, host, ctx, priorOutputs = null, cliOpts = {}) {
127
127
  // Decide if we run once per matching open port, or once total.
128
128
  const req = mod?.requirements || {};
129
129
  const runs = [];
@@ -141,7 +141,10 @@ async function callPlugin(mod, host, ctx, priorOutputs = null) {
141
141
 
142
142
  const runWithCtx = (port) => {
143
143
  const extra = isOsDetector && Array.isArray(priorOutputs) ? { results: priorOutputs } : {};
144
- const pluginPromise = mod.run(host, port, { context: withBaseContext(ctx), ...extra });
144
+ // Forward CLI-derived opts (ports, etc.) so plugins can honor flags like --ports.
145
+ // CLI opts come last so they don't override critical orchestration fields like
146
+ // `context` if the CLI ever accidentally collides on those names.
147
+ const pluginPromise = mod.run(host, port, { ...cliOpts, context: withBaseContext(ctx), ...extra });
145
148
 
146
149
  const timeoutMs = PLUGIN_TIMEOUT_MS;
147
150
  let timer;
@@ -694,8 +697,9 @@ export class PluginManager {
694
697
 
695
698
  vlog(`Running ${mod.name} (priority ${getPriority(mod)}) on ${host}`);
696
699
  // **FIX**: pass prior outputs into OS Detector via callPlugin(..., priorOutputs)
700
+ // **N.27 FIX**: forward CLI-derived opts (ports, etc.) so plugins can honor CLI flags
697
701
  const startMs = Date.now();
698
- const wrappedRuns = await callPlugin(mod, host, ctx, outputs);
702
+ const wrappedRuns = await callPlugin(mod, host, ctx, outputs, opts);
699
703
  const duration_ms = Date.now() - startMs;
700
704
 
701
705
  // Determine manifest status from the plugin results
@@ -18,6 +18,45 @@ function uniqInts(arr = []) {
18
18
  return [...new Set((arr || []).map((x) => Number(x)).filter(Number.isFinite))];
19
19
  }
20
20
 
21
+ /**
22
+ * Parse a CLI-style ports spec string into TCP/UDP port arrays.
23
+ *
24
+ * Accepted formats (entries comma-separated, whitespace tolerated):
25
+ * "8090" → { tcp: [8090], udp: [] }
26
+ * "8090,9090" → { tcp: [8090, 9090], udp: [] }
27
+ * "8090/tcp" → { tcp: [8090], udp: [] }
28
+ * "8090/udp" → { tcp: [], udp: [8090] }
29
+ * "8090,9090/udp" → { tcp: [8090], udp: [9090] }
30
+ * "8090/tcp,9090/udp" → { tcp: [8090], udp: [9090] }
31
+ *
32
+ * Default protocol when not specified: TCP.
33
+ *
34
+ * Malformed entries (non-numeric, out-of-range 1–65535, empty, unknown
35
+ * protocol suffix) are silently skipped — defensive for sloppy CLI input.
36
+ *
37
+ * @param {string} spec
38
+ * @returns {{ tcp: number[], udp: number[] }}
39
+ */
40
+ export function parsePortsSpec(spec) {
41
+ const out = { tcp: [], udp: [] };
42
+ if (typeof spec !== 'string') return out;
43
+ const entries = spec.split(',').map(s => s.trim()).filter(Boolean);
44
+ for (const entry of entries) {
45
+ // Reject entries with more than one '/' separator (e.g. "8090/tcp/extra")
46
+ const parts = entry.split('/');
47
+ if (parts.length > 2) continue;
48
+ const portStr = parts[0];
49
+ const proto = (parts[1] || 'tcp').toLowerCase();
50
+ if (proto !== 'tcp' && proto !== 'udp') continue;
51
+ const port = Number(portStr);
52
+ if (!Number.isInteger(port) || port < 1 || port > 65535) continue;
53
+ out[proto].push(port);
54
+ }
55
+ out.tcp = uniqInts(out.tcp);
56
+ out.udp = uniqInts(out.udp);
57
+ return out;
58
+ }
59
+
21
60
  async function loadConfigPortsFromServicesJson(cwd = process.cwd()) {
22
61
  // Supports the "array schema" used by tests:
23
62
  // { "services": [ { port, protocol }, ... ] }
@@ -179,7 +218,13 @@ export default {
179
218
  const maxBannerBytes = toInt(process.env.TCP_BANNER_MAX_BYTES, 350);
180
219
  const udpPayload = Buffer.from("hi");
181
220
 
182
- // Port sources: opts first, else config/services.json, else empty (tests supply what they need)
221
+ // Port sources, in priority order:
222
+ // 1. Explicit opts.tcpPorts / opts.udpPorts arrays (tests, programmatic API)
223
+ // 2. config/services.json (default well-known port set)
224
+ // 3. Empty
225
+ // Then ADDITIVELY merge opts.ports (CLI --ports flag, comma-separated string with
226
+ // optional /tcp /udp suffix). Additive semantics so that --ports adds extras to the
227
+ // default scan rather than silently replacing it (Task N.27, fixed in v0.1.22).
183
228
  let tcpPorts = Array.isArray(opts.tcpPorts) ? uniqInts(opts.tcpPorts) : [];
184
229
  let udpPorts = Array.isArray(opts.udpPorts) ? uniqInts(opts.udpPorts) : [];
185
230
 
@@ -189,6 +234,13 @@ export default {
189
234
  udpPorts = cfg.udp;
190
235
  }
191
236
 
237
+ // Additive merge of CLI --ports flag (string spec)
238
+ if (typeof opts.ports === 'string' && opts.ports.trim()) {
239
+ const extra = parsePortsSpec(opts.ports);
240
+ tcpPorts = uniqInts([...tcpPorts, ...extra.tcp]);
241
+ udpPorts = uniqInts([...udpPorts, ...extra.udp]);
242
+ }
243
+
192
244
  // If still nothing, just return empty structure
193
245
  if (!tcpPorts.length && !udpPorts.length) {
194
246
  return {
@@ -20,8 +20,15 @@
20
20
  import fsp from 'node:fs/promises';
21
21
  import path from 'node:path';
22
22
  import dnsP from 'node:dns/promises';
23
+ import { fileURLToPath } from 'node:url';
23
24
  import { resolveBaseOutDir } from './output_dir.mjs';
24
25
 
26
+ // Package root — derived from THIS file's location (utils/validate.mjs) by
27
+ // going up one directory. Used as the default plugin-discovery base so that
28
+ // `nsauditor-ai validate` finds the plugins shipped with the package, NOT
29
+ // whatever happens to be in the user's cwd. Fixed in v0.1.21 (Task N.25).
30
+ const PKG_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
31
+
25
32
  export const STATUSES = Object.freeze({
26
33
  OK: 'ok',
27
34
  WARN: 'warn',
@@ -36,26 +43,32 @@ const DEFAULT_NETWORK_HOST = 'localhost'; // hermetic — no external dependency
36
43
  /**
37
44
  * Verify all installed plugins load without error.
38
45
  *
46
+ * Discovery base is the **package root** (derived from this file's location),
47
+ * not `process.cwd()`. Critical: when the bin shim is invoked from anywhere
48
+ * other than the install dir, `process.cwd()` is the user's working
49
+ * directory — which has no plugins. v0.1.20 had this bug; fixed in v0.1.21.
50
+ *
39
51
  * @param {object} [opts]
40
52
  * @param {Function} [opts.discover] - Override for testing.
53
+ * @param {string} [opts.pkgRoot] - Override package root path (test injection).
41
54
  */
42
- export async function checkPlugins({ discover } = {}) {
55
+ export async function checkPlugins({ discover, pkgRoot = PKG_ROOT } = {}) {
43
56
  try {
44
57
  const fn = discover || (await import('./plugin_discovery.mjs')).discoverPlugins;
45
- const result = await fn(process.cwd());
58
+ const result = await fn(pkgRoot);
46
59
  const plugins = Array.isArray(result) ? result : (result?.plugins ?? []);
47
60
  return {
48
61
  name: 'plugins',
49
62
  status: STATUSES.OK,
50
63
  message: `${plugins.length} plugin${plugins.length === 1 ? '' : 's'} loaded`,
51
- details: { count: plugins.length },
64
+ details: { count: plugins.length, basePath: pkgRoot },
52
65
  };
53
66
  } catch (err) {
54
67
  return {
55
68
  name: 'plugins',
56
69
  status: STATUSES.ERROR,
57
70
  message: `Plugin discovery failed: ${err.message}`,
58
- details: { error: err.message },
71
+ details: { error: err.message, basePath: pkgRoot },
59
72
  };
60
73
  }
61
74
  }
@@ -276,4 +289,5 @@ export const _internals = {
276
289
  FREE_SPACE_WARN_MB,
277
290
  DEFAULT_NETWORK_TIMEOUT_MS,
278
291
  DEFAULT_NETWORK_HOST,
292
+ PKG_ROOT,
279
293
  };