nsauditor-ai 0.1.94 β 0.1.95
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 +4 -1
- package/mcp_server.mjs +12 -6
- package/package.json +1 -1
- package/utils/cloud_finding_summary.mjs +83 -0
package/README.md
CHANGED
|
@@ -19,6 +19,7 @@ NSAuditor AI is the open-source core of a privacy-first security intelligence pl
|
|
|
19
19
|
|
|
20
20
|
**Latest: CE 0.1.94 + Enterprise 0.16.3** (May 2026)
|
|
21
21
|
|
|
22
|
+
- π‘οΈ **`scan_cloud` now surfaces cloud findings reliably** (CE 0.1.95 β paired EE 0.16.4) β *(false-clean fix)* the tool now builds a `findingsSummary` (per-provider severity counts + the CRITICAL/HIGH list) **directly from the scan results**, instead of the network-host concluder β which is built for port/service scans, doesn't understand cloud compliance findings, and silently **dropped** them. A scan could previously report `audited:true` with **0 findings over real CRITICALs**; now every finding is counted and every CRITICAL/HIGH is listed. Distinct from the 0.1.93 not-audited fold (caught by the `audit-cloud-plugin-false-negatives` review). Plugin count UNCHANGED at 28; all six matrices UNCHANGED.
|
|
22
23
|
- β‘ **`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.
|
|
23
24
|
- βοΈ **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.
|
|
24
25
|
- π **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.
|
|
@@ -424,7 +425,9 @@ npx nsauditor-ai-mcp
|
|
|
424
425
|
> `scan_cloud` runs the requested clouds' plugins **concurrently** (default up to 20 at once, 25s per-plugin
|
|
425
426
|
> timeout) so a full multi-service cloud audit completes within Claude Desktop's ~60s tool-call limit. Tune with
|
|
426
427
|
> `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
|
+
> network `PLUGIN_TIMEOUT_MS` still governs `scan_host` / network scans. Read the result's **`findingsSummary`**
|
|
429
|
+
> (per-provider severity counts + a CRITICAL/HIGH list) for the findings; `audited:false` / `notes` / `pluginsRan:0`
|
|
430
|
+
> still mean a cloud was NOT audited (never a clean pass). Pass `providers:["aws"]` to audit only the cloud named.
|
|
428
431
|
|
|
429
432
|
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.
|
|
430
433
|
|
package/mcp_server.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
import { getTierFromEnv, loadLicense } from './utils/license.mjs';
|
|
26
26
|
import { resolveCapabilities } from './utils/capabilities.mjs';
|
|
27
27
|
import { buildMarkdownReport } from './utils/report_md.mjs';
|
|
28
|
+
import { summarizeCloudFindings, renderCloudFindingsMarkdown } from './utils/cloud_finding_summary.mjs';
|
|
28
29
|
import { authorizeMcpServerStartup, getMcpAuthKeyAge, getRotationWarningDays, reportMcpAuthSource } from './utils/mcp_auth.mjs';
|
|
29
30
|
|
|
30
31
|
const _require = createRequire(import.meta.url);
|
|
@@ -196,7 +197,7 @@ const TOOLS = [
|
|
|
196
197
|
{
|
|
197
198
|
name: 'scan_cloud',
|
|
198
199
|
description:
|
|
199
|
-
'Audit one or more cloud accounts (AWS / GCP / Azure) for security & compliance posture using the credentials configured in the server environment. No network host required. Requires an Enterprise license.
|
|
200
|
+
'Audit one or more cloud accounts (AWS / GCP / Azure) for security & compliance posture using the credentials configured in the server environment. No network host required. Requires an Enterprise license. Audit ONLY the cloud(s) the user names β pass providers:["aws"] for "audit my AWS account"; omit providers only when the user asks to audit ALL clouds. Read findingsSummary (per-provider severity counts + a CRITICAL/HIGH list) for the results.',
|
|
200
201
|
inputSchema: {
|
|
201
202
|
type: 'object',
|
|
202
203
|
properties: {
|
|
@@ -379,12 +380,17 @@ export async function handleScanCloud(args) {
|
|
|
379
380
|
else process.env.CLOUD_PROVIDER = savedProvider;
|
|
380
381
|
}
|
|
381
382
|
|
|
382
|
-
//
|
|
383
|
+
// Build the caller-visible findings surface DIRECTLY from the raw results β the
|
|
384
|
+
// network-host concluder (result_concluder.mjs) doesn't understand cloud
|
|
385
|
+
// compliance findings and silently drops them (the false-clean this fixes).
|
|
386
|
+
const providerOf = (id) => (pm.plugins || []).find((p) => String(p.id) === String(id))?.cloudProvider || null;
|
|
387
|
+
const findingsSummary = summarizeCloudFindings(output.results || [], providerOf);
|
|
388
|
+
|
|
389
|
+
// Render Markdown FROM the findings summary (NOT the host renderer, which is
|
|
390
|
+
// empty for a cloud conclusion). Best-effort.
|
|
383
391
|
let markdown = null;
|
|
384
392
|
try {
|
|
385
|
-
|
|
386
|
-
markdown = buildMarkdownReport({ host: output.host, conclusion: output.conclusion, toolVersion: TOOL_VERSION });
|
|
387
|
-
}
|
|
393
|
+
markdown = renderCloudFindingsMarkdown(findingsSummary, providers);
|
|
388
394
|
} catch { /* swallow β markdown is best-effort */ }
|
|
389
395
|
|
|
390
396
|
// Anti-false-clean: a requested cloud is "audited" ONLY if >=1 of its plugins
|
|
@@ -412,7 +418,7 @@ export async function handleScanCloud(args) {
|
|
|
412
418
|
providers,
|
|
413
419
|
audited: auditedProviders.length > 0,
|
|
414
420
|
auditedProviders,
|
|
415
|
-
|
|
421
|
+
findingsSummary,
|
|
416
422
|
manifest: output.manifest ?? [],
|
|
417
423
|
pluginsRan,
|
|
418
424
|
markdown,
|
package/package.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// utils/cloud_finding_summary.mjs
|
|
2
|
+
//
|
|
3
|
+
// Builds the caller-visible findings surface for scan_cloud DIRECTLY from the raw
|
|
4
|
+
// plugin results β the network-host concluder (result_concluder.mjs) doesn't
|
|
5
|
+
// understand cloud compliance findings and silently drops them. Robust across the
|
|
6
|
+
// plugin-specific finding shapes (AWS bucket/userName/issues, GCP/Azure title).
|
|
7
|
+
|
|
8
|
+
const RANK = { CRITICAL: 5, HIGH: 4, MEDIUM: 3, LOW: 2, INFO: 1, PASS: 0 };
|
|
9
|
+
// Resource-identifying keys, most-specific first so a precise label wins over the
|
|
10
|
+
// generic resource/name/arn fallbacks. Grounded in the REAL EE plugin emissions
|
|
11
|
+
// (S3 1020 emits `bucket`, Key Vault `vault`, DynamoDB `table`, KMS `key`, β¦).
|
|
12
|
+
const RESOURCE_KEYS = ['userName', 'bucket', 'bucketName', 'function', 'functionName', 'table', 'tableName', 'instanceId', 'group', 'groupId', 'key', 'keyId', 'vault', 'vaultName', 'pipeline', 'topic', 'queue', 'projectId', 'domain', 'secretName', 'roleName', 'accountName', 'resource', 'resourceId', 'name', 'arn'];
|
|
13
|
+
const REASON_KEYS = ['classification', 'title', 'message', 'reason', 'finding', 'detail'];
|
|
14
|
+
const UNKNOWN_PROVIDER = 'unknown';
|
|
15
|
+
|
|
16
|
+
/** Robust one-line descriptor from any plugin finding shape. Never empty, never a raw object dump. */
|
|
17
|
+
export function describeFinding(x) {
|
|
18
|
+
if (!x || typeof x !== 'object') return String(x ?? '').slice(0, 160);
|
|
19
|
+
let res = '';
|
|
20
|
+
for (const k of RESOURCE_KEYS) { if (x[k]) { res = String(x[k]); break; } }
|
|
21
|
+
let why = '';
|
|
22
|
+
if (Array.isArray(x.issues) && x.issues.length) why = String(x.issues[0]);
|
|
23
|
+
else for (const k of REASON_KEYS) { if (x[k]) { why = String(x[k]); break; } }
|
|
24
|
+
const s = ((res ? res + ' β ' : '') + why).trim();
|
|
25
|
+
if (s) return s.slice(0, 160);
|
|
26
|
+
const sev = String(x.severity || x.level || '').toUpperCase();
|
|
27
|
+
return sev ? sev + ' finding (no description)' : 'finding (no description)';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function findingsOf(r) {
|
|
31
|
+
const f = (r && (r.findings ?? r.result?.findings ?? r.result?.data)) ?? [];
|
|
32
|
+
return Array.isArray(f) ? f : [];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {Array} results out.results (each {id, result:{findings|data}})
|
|
37
|
+
* @param {(id:any)=>string|null} providerOf plugin id -> cloudProvider
|
|
38
|
+
* @param {number} [cap] max CRITICAL/HIGH entries listed per provider
|
|
39
|
+
* @returns {Object} { [provider]: { counts:{SEV:n}, findings:[{severity,plugin,title}], truncated:boolean } }
|
|
40
|
+
*/
|
|
41
|
+
export function summarizeCloudFindings(results, providerOf, cap = Number(process.env.CLOUD_FINDINGS_CAP) || 60) {
|
|
42
|
+
const out = {};
|
|
43
|
+
for (const r of (Array.isArray(results) ? results : [])) {
|
|
44
|
+
const found = findingsOf(r);
|
|
45
|
+
if (!found.length) continue;
|
|
46
|
+
// Attribute to the plugin's cloud; an unresolved id that carries REAL findings is
|
|
47
|
+
// bucketed under 'unknown' rather than silently dropped (defense-in-depth against a
|
|
48
|
+
// future id collision / cloudProvider drift β never let a real finding vanish).
|
|
49
|
+
const prov = providerOf(r?.id ?? r?.result?.id) || UNKNOWN_PROVIDER;
|
|
50
|
+
const bucket = (out[prov] ||= { counts: {}, findings: [], truncated: false });
|
|
51
|
+
for (const x of found) {
|
|
52
|
+
const sev = String(x?.severity || x?.level || 'INFO').toUpperCase();
|
|
53
|
+
bucket.counts[sev] = (bucket.counts[sev] || 0) + 1;
|
|
54
|
+
if (sev === 'CRITICAL' || sev === 'HIGH') {
|
|
55
|
+
bucket.findings.push({ severity: sev, plugin: String(r?.id ?? ''), title: describeFinding(x) });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Sort by severity (CRITICAL first) THEN truncate β so a CRITICAL is never evicted
|
|
60
|
+
// from the displayed list by a HIGH. Counts above are always complete (pre-cap).
|
|
61
|
+
for (const prov of Object.keys(out)) {
|
|
62
|
+
const b = out[prov];
|
|
63
|
+
b.findings.sort((a, c) => (RANK[c.severity] || 0) - (RANK[a.severity] || 0));
|
|
64
|
+
if (b.findings.length > cap) { b.truncated = true; b.findings = b.findings.slice(0, cap); }
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Compact markdown from a summary. Named providers first, then any extras (e.g. 'unknown') so nothing is hidden. */
|
|
70
|
+
export function renderCloudFindingsMarkdown(summary, providers) {
|
|
71
|
+
const named = providers && providers.length ? providers.slice() : [];
|
|
72
|
+
const order = [...named, ...Object.keys(summary).filter((p) => !named.includes(p))];
|
|
73
|
+
const lines = [];
|
|
74
|
+
for (const prov of order) {
|
|
75
|
+
const b = summary[prov]; if (!b) continue;
|
|
76
|
+
const c = b.counts || {};
|
|
77
|
+
lines.push(`## ${String(prov).toUpperCase()} β ${c.CRITICAL || 0} CRITICAL Β· ${c.HIGH || 0} HIGH Β· ${c.MEDIUM || 0} MEDIUM Β· ${c.LOW || 0} LOW Β· ${c.PASS || 0} PASS`);
|
|
78
|
+
for (const f of (b.findings || [])) lines.push(`- **[${f.severity}]** ${f.plugin}: ${f.title}`);
|
|
79
|
+
if (b.truncated) lines.push(`- _β¦CRITICAL/HIGH list truncated; see counts above for totals._`);
|
|
80
|
+
lines.push('');
|
|
81
|
+
}
|
|
82
|
+
return lines.join('\n').trim();
|
|
83
|
+
}
|