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 +1 -1
- package/plugin_manager.mjs +7 -3
- package/plugins/port_scanner.mjs +53 -1
- package/utils/validate.mjs +18 -4
package/package.json
CHANGED
package/plugin_manager.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
package/plugins/port_scanner.mjs
CHANGED
|
@@ -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
|
|
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 {
|
package/utils/validate.mjs
CHANGED
|
@@ -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(
|
|
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
|
};
|