nsauditor-ai 0.1.93 → 0.1.94

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/README.md CHANGED
@@ -17,8 +17,9 @@ NSAuditor AI is the open-source core of a privacy-first security intelligence pl
17
17
 
18
18
  ## What's New
19
19
 
20
- **Latest: CE 0.1.93 + Enterprise 0.16.2** (May 2026)
20
+ **Latest: CE 0.1.94 + Enterprise 0.16.3** (May 2026)
21
21
 
22
+ - ⚡ **`scan_cloud` runs cloud plugins in parallel** (CE 0.1.94 — paired EE 0.16.3) — the cloud-audit engine now runs the requested clouds' plugins **concurrently** (default up to 20 at once, 25s per-plugin timeout) so a full ~20-plugin AWS/GCP/Azure audit finishes within Claude Desktop's ~60s tool-call limit — previously the heaviest, most security-critical plugins (IAM, S3, EC2, Inspector/GuardDuty, decrypt-path) timed out at the 5s the budget forced under sequential execution. Tunable via `CLOUD_SCAN_CONCURRENCY` / `CLOUD_PLUGIN_TIMEOUT_MS`; the network `scan_host` path is unchanged; the anti-false-clean reporting is preserved. Plugin count UNCHANGED at 28; all six matrices UNCHANGED.
22
23
  - ☁️ **MCP `scan_cloud` tool: audit cloud accounts directly** (CE 0.1.93 — paired EE 0.16.2) — *(Enterprise)* a dedicated MCP tool so you can say *"Audit my AWS account"* or *"Audit my AWS and Azure accounts"* in Claude Desktop / Claude Code, with **no network host** — the server scopes the run to only the requested clouds' EE plugins. The MCP analog of the CLI `--host aws --plugins all` scoping. Enterprise-gated (`enterpriseMCP`) with plugin-layer defense-in-depth, and **anti-false-clean reporting** — a cloud is reported "audited" only if a plugin actually ran; a missing-credential / skipped cloud is surfaced as an explicit note + `audited:false` (R-HIGH caught + folded by the `audit-cloud-plugin-false-negatives` review). Plugin count UNCHANGED at 28; all six matrices UNCHANGED.
23
24
  - 🔌 **MCP `NSA_ENV_FILE`: per-environment `.env` for the MCP server** (CE 0.1.92 — paired EE 0.16.1) — the MCP analog of 0.16.0's CLI `--env`. Point the MCP server at a specific account/cloud by setting **`NSA_ENV_FILE`** to a dotenv path in your Claude Desktop / Claude Code config instead of inlining every credential; the server loads that environment's cloud credentials + `CLOUD_PROVIDER` at startup. Loaded **after** the MCP auth gate + license (scan-target vars only; the auth key + license are ignored if present in the file), **fail-fast** on a missing / INI / set-but-empty path, and treated as the **authoritative scan target** — leftover ambient provider credentials the file doesn't set are cleared, closing a false-clean (a partial file could otherwise silently scan a leftover account) caught + folded by the `audit-cloud-plugin-false-negatives` review. Plugin count UNCHANGED at 28; all six matrices UNCHANGED.
24
25
  - 🎯 **Per-account scanning: `--env` / `--aws-profile` + sentinel-host plugin scoping** (CE 0.1.91 — paired EE 0.16.0) — audit many cloud accounts one at a time without shell-export juggling. **`--env <path>`** loads a per-scan dotenv credentials file (override-on; fail-fast on a missing file; an INI/`~/.aws/credentials` file is detected and redirected to `--aws-profile`). **`--aws-profile <name>`** uses a named profile from the OS-default `~/.aws/credentials` (clears stale explicit keys, sets `AWS_SDK_LOAD_CONFIG=1`, implies `CLOUD_PROVIDER=aws`). On a cloud-sentinel host, **`--host aws|gcp|azure` + `--plugins all` auto-scopes** to only that cloud's plugins (other clouds + non-cloud plugins are skipped and logged); explicit `--plugins` lists are unaffected. A host↔`CLOUD_PROVIDER` conflict fails fast (no silent empty "clean" report), and a zero-match sentinel scope warns loudly. EE 0.16.0 adds a declarative `cloudProvider` field to all 27 cloud plugins. Plugin count UNCHANGED at 28; all six matrices UNCHANGED.
@@ -420,6 +421,11 @@ npx nsauditor-ai-mcp
420
421
  | `compliance_check` | Compliance mapping with gap analysis |
421
422
  | `export_report` | Generate formatted compliance report |
422
423
 
424
+ > `scan_cloud` runs the requested clouds' plugins **concurrently** (default up to 20 at once, 25s per-plugin
425
+ > timeout) so a full multi-service cloud audit completes within Claude Desktop's ~60s tool-call limit. Tune with
426
+ > `CLOUD_SCAN_CONCURRENCY` (default 20) and `CLOUD_PLUGIN_TIMEOUT_MS` (default 25000) in the server env. The
427
+ > network `PLUGIN_TIMEOUT_MS` still governs `scan_host` / network scans.
428
+
423
429
  Security: SSRF protection on all host inputs (blocks RFC 1918, loopback, fc00::/7, cloud metadata), port validation (1–65535), CPE format enforcement, dependency injection for test isolation. **Server-startup authentication is required** — see next section.
424
430
 
425
431
  ### Authentication (required)
@@ -503,6 +509,8 @@ The exact `NSA_MCP_AUTH_KEY` value to paste is printed by `nsauditor-ai mcp inst
503
509
  - `NSA_MCP_AUTH_KEY` — **required** (see Authentication section above)
504
510
  - `NSA_ALLOW_ALL_HOSTS=1` — required to scan private/RFC 1918 addresses (e.g., `192.168.x.x`)
505
511
  - `PLUGIN_TIMEOUT_MS=5000` — reduces per-plugin timeout to 5s so the full scan completes within Claude Desktop's 60s MCP limit
512
+ - `CLOUD_SCAN_CONCURRENCY` — max cloud plugins run at once by `scan_cloud` (default 20).
513
+ - `CLOUD_PLUGIN_TIMEOUT_MS` — per-plugin timeout for `scan_cloud` (default 25000; independent of the network `PLUGIN_TIMEOUT_MS`).
506
514
  - `AI_PROVIDER` and API key — optional, enables AI-powered analysis of scan results
507
515
 
508
516
  #### `NSA_ENV_FILE` — point the MCP server at an environment file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai",
3
- "version": "0.1.93",
3
+ "version": "0.1.94",
4
4
  "description": "Modular AI-assisted network security audit platform — Community Edition",
5
5
  "type": "module",
6
6
  "private": false,
@@ -21,6 +21,7 @@ import { discoverPlugins } from './utils/plugin_discovery.mjs';
21
21
  import { getTierFromEnv } from './utils/license.mjs';
22
22
  import { resolveCapabilities } from './utils/capabilities.mjs';
23
23
  import { scopeSelectionForHost, scopeSelectionForProviders } from './utils/sentinel_scope.mjs';
24
+ import { mapLimit } from './utils/concurrency.mjs';
24
25
 
25
26
  const __filename = fileURLToPath(import.meta.url);
26
27
 
@@ -145,9 +146,15 @@ async function callPlugin(mod, host, ctx, priorOutputs = null, cliOpts = {}) {
145
146
  // Forward CLI-derived opts (ports, etc.) so plugins can honor flags like --ports.
146
147
  // CLI opts come last so they don't override critical orchestration fields like
147
148
  // `context` if the CLI ever accidentally collides on those names.
148
- const pluginPromise = mod.run(host, port, { ...cliOpts, context: withBaseContext(ctx), ...extra });
149
-
150
- const timeoutMs = PLUGIN_TIMEOUT_MS;
149
+ // Promise.resolve().then(...) so a plugin that throws SYNCHRONOUSLY (before its
150
+ // first await) becomes a rejected promise the race below catches, instead of
151
+ // propagating out of callPlugin and aborting the whole (sequential or parallel) batch.
152
+ const pluginPromise = Promise.resolve().then(() => mod.run(host, port, { ...cliOpts, context: withBaseContext(ctx), ...extra }));
153
+
154
+ // Honor a per-run timeout (cloud path) but clamp to a positive number; an
155
+ // undefined (network path) or non-positive value falls back to PLUGIN_TIMEOUT_MS.
156
+ const reqTimeout = Number(cliOpts && cliOpts.timeoutMs);
157
+ const timeoutMs = Number.isFinite(reqTimeout) && reqTimeout > 0 ? reqTimeout : PLUGIN_TIMEOUT_MS;
151
158
  let timer;
152
159
  const timeoutPromise = new Promise((_, reject) => {
153
160
  timer = setTimeout(() => reject(new Error(`Plugin "${mod.name}" timed out after ${timeoutMs}ms`)), timeoutMs);
@@ -853,6 +860,46 @@ export class PluginManager {
853
860
  };
854
861
  }
855
862
 
863
+ /**
864
+ * Concurrent executor for cloud plugins. Mirrors _runOrchestrated's per-plugin
865
+ * logic (requirements + capability skip checks, callPlugin, manifest status)
866
+ * but runs with bounded parallelism and a per-run timeout — cloud plugins are
867
+ * independent (no sequential context chain), so each gets its OWN base ctx and
868
+ * there is no shared-mutation race. Returns { results, manifest } in plugin order.
869
+ */
870
+ async _runCloudPluginsParallel(selected, host, opts = {}) {
871
+ const concurrency = Number(process.env.CLOUD_SCAN_CONCURRENCY) || 20;
872
+ const rawTimeout = Number(process.env.CLOUD_PLUGIN_TIMEOUT_MS);
873
+ const timeoutMs = Number.isFinite(rawTimeout) && rawTimeout > 0 ? rawTimeout : 25000;
874
+ const toRun = selected.filter((p) => !isConcluder(p));
875
+
876
+ const entries = await mapLimit(toRun, concurrency, async (mod) => {
877
+ try {
878
+ const ctx = withBaseContext({ host, hostUp: false, tcpOpen: new Set(), udpOpen: new Set() });
879
+ if (!shouldRunPlugin(mod, ctx)) {
880
+ return { manifest: { id: String(mod.id || ''), name: mod.name || 'Plugin', status: 'skipped', reason: describeSkipReason(mod, ctx), duration_ms: 0 }, outputs: [] };
881
+ }
882
+ if (!this._hasCapabilities(mod, opts?.capabilities)) {
883
+ return { manifest: { id: String(mod.id || ''), name: mod.name || 'Plugin', status: 'skipped', reason: `missing capabilities: ${(mod.requiredCapabilities || []).join(',')}`, duration_ms: 0 }, outputs: [] };
884
+ }
885
+ const startMs = Date.now();
886
+ const wrappedRuns = await callPlugin(mod, host, ctx, [], { ...opts, timeoutMs });
887
+ const duration_ms = Date.now() - startMs;
888
+ let status = 'ran';
889
+ let reason = null;
890
+ for (const w of wrappedRuns) {
891
+ if (w.result?.timedOut) { status = 'timeout'; reason = w.result.error || `timed out after ${timeoutMs}ms`; }
892
+ else if (w.result?.error && status !== 'timeout') { status = 'error'; reason = w.result.error; }
893
+ }
894
+ return { manifest: { id: String(mod.id || ''), name: mod.name || 'Plugin', status, reason, duration_ms }, outputs: wrappedRuns };
895
+ } catch (err) {
896
+ return { manifest: { id: String(mod.id || ''), name: mod.name || 'Plugin', status: 'error', reason: String(err?.message || err), duration_ms: 0 }, outputs: [] };
897
+ }
898
+ });
899
+
900
+ return { results: entries.flatMap((e) => e.outputs), manifest: entries.map((e) => e.manifest) };
901
+ }
902
+
856
903
  /**
857
904
  * Cloud-account audit: scope the plugin run to the union of the given cloud
858
905
  * providers (by each plugin's `cloudProvider` field), with no network host.
@@ -886,7 +933,7 @@ export class PluginManager {
886
933
  ai: null, ai_meta: null, ai_error: null, ai_out_path: null };
887
934
  }
888
935
 
889
- const orch = await this._runOrchestrated(host, selected, opts);
936
+ const orch = await this._runCloudPluginsParallel(selected, host, opts);
890
937
  const conclusion = await this.runConcluder(orch.results);
891
938
 
892
939
  // Per-provider effectiveness from the manifest (ids match the selected set).
@@ -0,0 +1,21 @@
1
+ // utils/concurrency.mjs
2
+ //
3
+ // Minimal bounded-concurrency runner: run fn(item, index) over items with at
4
+ // most `limit` in flight, returning results in input order. A thrown fn rejects
5
+ // the whole call — callers that need per-item isolation should catch inside fn.
6
+
7
+ export async function mapLimit(items, limit, fn) {
8
+ const list = Array.isArray(items) ? items : [];
9
+ const n = list.length;
10
+ const out = new Array(n);
11
+ const cap = Math.max(1, Math.min((limit | 0) || 1, n || 1));
12
+ let next = 0;
13
+ async function worker() {
14
+ while (next < n) {
15
+ const i = next++;
16
+ out[i] = await fn(list[i], i);
17
+ }
18
+ }
19
+ await Promise.all(Array.from({ length: Math.min(cap, n) }, () => worker()));
20
+ return out;
21
+ }