nsauditor-ai 0.2.7 → 0.2.9

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,9 +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.2.7 + Enterprise 0.19.2** (June 2026) — paired pin for **EE 0.19.2** "Confirmed false-negative tail": six more gauntlet-confirmed Tier-B false-negatives across the Pro/Enterprise cloud auditors, each TDD'd and independently adversarially reviewed Azure Key Vault legacy access-policy per-verb breadth (a 2-verb decrypt+unwrapKey envelope-decryption grant now flags) + two surfaced titlePattern anchor-drifts + drift-detector closure (1222); broad-but-not-full **public** GCP firewall source rangesHIGH, RFC1918 discounted (1021); AWS KMS PendingDeletion keys now policy-audited the deletion is reversible (1070); a CodePipeline sticky approval-latch each production stage now needs its own gate (1100); GCP Storage bucket-enumeration truncation evidence-gap, class-O-routed (1024); and a CloudTrail data-events read-coverage caveatWriteOnly drops S3 GetObject exfiltration events (1040). Plugin count UNCHANGED at 28; all six coverage matrices UNCHANGED; **CE has no code change (paired pin)**.
20
+ **Latest: CE 0.2.9 + Enterprise 0.19.4** (June 2026) — paired README-refresh bump (no Community Edition code change this cycle). **EE 0.19.4 "Routing-Integrity Hardening"** closes the routing false-clean class a real finding (or a visible evidence-gap) that mapped to **zero** compliance controls while the verdict stayed green: a build-time **routing guard** (a complete-partition `nativeFrameworks` allowlist) so no marked gap routes nowhere + GuardDuty (1200) dedupe + class-O routing; a **deferred-scope unmark** across 8 plugins (a capability boundary is not an evidence-gap the MCP "unverified" list shrinks); the 1160 AWS-default VPC-endpoint full-access policy down-rated **CRITICAL→MEDIUM** with cross-framework routing (PCI 1.4.1 / ISO A.8.22 / CIS 12.2 + SOC 2 CC6.6 / HIPAA 164.312(a)(1)); and 1150 SQS/SNS **alarm-independence** (alarm posture survives a `Get*Attributes` deny; all four `alarm-coverage-unverifiable` causes fail-close soc2{A1.2,CC7.2}+hipaa{164.312(b)}). **PCI DSS matrix shifts 20/8/39 19/9/39** (Req 7.2.2 covered→partial an honest down-rate of a QSA-flagged overclaim, backed by 1030 over-privilege detection). Plugin count UNCHANGED at 28; the other five matrices UNCHANGED. **EE 0.19.4 requires CE 0.2.8+.**
21
21
 
22
- **Prior: CE 0.2.6 + Enterprise 0.19.1** — paired pin for EE 0.19.1 "Confirmed false-negative batch" (7 cloud plugins hardened: 1030/1150/1130/1120/1080/1025/1160). See [CHANGELOG.md](./CHANGELOG.md).
22
+ **Prior: CE 0.2.8 + Enterprise 0.19.3** — real CE bump (MCP `scan_cloud` routing description + actionable gap-list visibility) paired with EE 0.19.3 "MCP affordance + class-O truncation sweep" (1080/1221/1180 + class-O sweep across 8 AWS auditors). See [CHANGELOG.md](./CHANGELOG.md).
23
23
 
24
24
  → Full release history: **[CHANGELOG.md](./CHANGELOG.md)**
25
25
  → See a sample EE scan output: **[walk-through with synthetic Acme Corp AWS account](https://www.nsauditor.com/ai/docs/sample-scan/)** (no signup required)
package/mcp_server.mjs CHANGED
@@ -193,7 +193,9 @@ function appendCallSentinel(text, callId) {
193
193
  // Tool definitions (JSON Schema for input validation)
194
194
  // ---------------------------------------------------------------------------
195
195
 
196
- const TOOLS = [
196
+ // Exported for direct testing (the scan_cloud description is a routing surface —
197
+ // tests pin its service-coverage enumeration + contract clauses).
198
+ export const TOOLS = [
197
199
  {
198
200
  name: 'scan_host',
199
201
  description:
@@ -216,7 +218,7 @@ const TOOLS = [
216
218
  {
217
219
  name: 'scan_cloud',
218
220
  description:
219
- '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. findingsSummary[provider].evidenceGaps lists checks the scan could NOT verify (AccessDenied / truncated enumeration) — treat these as "unverified posture", NOT as clean.',
221
+ 'Audit one or more cloud accounts (AWS / GCP / Azure) for security & compliance posture using the credentials configured in the server environment. Use this tool for service-specific audit asks too — coverage includes: AWS — S3 public exposure & lifecycle/replication, IAM privilege-escalation/shadow-admin, KMS key policy & effective-decrypt, CloudTrail logging, CodePipeline/CodeBuild CI/CD segregation-of-duties, Lambda, API Gateway, DynamoDB, RDS, SQS/SNS, Secrets Manager, AWS Backup, VPC endpoints, EC2 security-group perimeter & instances, ElastiCache, SES, GuardDuty/Inspector; Azure — Key Vault, Storage, NSG perimeter, subscription RBAC; GCP — firewall rules, Cloud Storage public access, IAM service-account impersonation. Findings map to SOC 2, HIPAA, NIST CSF 2.0, PCI DSS, ISO 27001, and CIS v8 controls. 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. findingsSummary[provider].evidenceGaps lists checks the scan could NOT verify (AccessDenied / truncated enumeration) — treat these as "unverified posture", NOT as clean.',
220
222
  inputSchema: {
221
223
  type: 'object',
222
224
  properties: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
4
4
  "description": "Modular AI-assisted network security audit platform — Community Edition",
5
5
  "type": "module",
6
6
  "private": false,
@@ -12,15 +12,50 @@ const RANK = { CRITICAL: 5, HIGH: 4, MEDIUM: 3, LOW: 2, INFO: 1, PASS: 0 };
12
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
13
  const REASON_KEYS = ['classification', 'title', 'message', 'reason', 'finding', 'detail'];
14
14
  const UNKNOWN_PROVIDER = 'unknown';
15
+ // Scan-coverage gap prose — an EXACT 1:1 mirror of the EE evidence-gap anchor
16
+ // (nsauditor-ai-ee utils/evidence_gap_contract.mjs EVIDENCE_GAP_ANCHOR; an EE meta-test
17
+ // drift-pins source equality). Used ONLY to order clauses WITHIN one finding — a rollup
18
+ // mixing a gap clause with an actionable clause (e.g. Key Vault key-enum gap +
19
+ // over-privilege broad-grant) must put each clause on its proper channel. NEVER used to
20
+ // classify findings — that is the details.evidenceGap marker's job.
21
+ export const GAP_CLAUSE_RE =
22
+ /evidence[ -]?gap|\bUNVERIFIED\b|posture[^.]{0,60}\bunverified\b|posture[^.]{0,40}\bindeterminate\b|\bindeterminate,\s*manual verification|could not (?:be )?(?:read|verified?|verify|enumerated?|enumerate|complete[d]?|parse[d]?|determine[d]?|retrieve[d]?|scoped?)|did not complete|not a clean result|scan coverage is partial|provenance is partial|failed to (?:fetch|list|retrieve)|\bnot assessed\b|manual verification required/i;
23
+ // Substrate-evidence PASS prose (1220/1222 push these into the same issues[] as
24
+ // violations) — informational; never a lead clause while any alternative exists.
25
+ const PASS_CLAUSE_RE = /substrate evidence:/i;
26
+ // EE compliance-routing tag (e.g. 'EE-RT.1.2 multi-region-enumeration-incomplete: ')
27
+ // leading many gap emissions — anchor plumbing for the EE framework JSONs, not
28
+ // operator prose. Stripped at THIS presentation layer only (routing matches the
29
+ // RAW issue strings EE-side) so the 160-char slice spends its budget on substance.
30
+ const ROUTING_PREFIX_RE = /^EE-RT\.[\dx.]+ [a-z][a-z-]*: /;
31
+
32
+ function classifyClause(s) {
33
+ if (PASS_CLAUSE_RE.test(s)) return 'pass';
34
+ if (GAP_CLAUSE_RE.test(s)) return 'gap';
35
+ return 'actionable';
36
+ }
37
+
38
+ // Lead-clause pick: actionable > gap > pass by default; the evidence-gap list flips to
39
+ // gap > actionable > pass so the [⚠ EVIDENCE GAP] badge always badges a GAP clause,
40
+ // never a verified fact. Falls back to issues[0] (never empty).
41
+ function pickClause(issues, prefer) {
42
+ const order = prefer === 'gap' ? ['gap', 'actionable', 'pass'] : ['actionable', 'gap', 'pass'];
43
+ for (const cls of order) {
44
+ const hit = issues.find((i) => i && classifyClause(String(i)) === cls);
45
+ if (hit !== undefined) return String(hit);
46
+ }
47
+ return String(issues[0]);
48
+ }
15
49
 
16
50
  /** Robust one-line descriptor from any plugin finding shape. Never empty, never a raw object dump. */
17
- export function describeFinding(x) {
51
+ export function describeFinding(x, opts = {}) {
18
52
  if (!x || typeof x !== 'object') return String(x ?? '').slice(0, 160);
19
53
  let res = '';
20
54
  for (const k of RESOURCE_KEYS) { if (x[k]) { res = String(x[k]); break; } }
21
55
  let why = '';
22
- if (Array.isArray(x.issues) && x.issues.length) why = String(x.issues[0]);
56
+ if (Array.isArray(x.issues) && x.issues.length) why = pickClause(x.issues, opts.prefer);
23
57
  else for (const k of REASON_KEYS) { if (x[k]) { why = String(x[k]); break; } }
58
+ why = why.replace(ROUTING_PREFIX_RE, '');
24
59
  const s = ((res ? res + ' — ' : '') + why).trim();
25
60
  if (s) return s.slice(0, 160);
26
61
  const sev = String(x.severity || x.level || '').toUpperCase();
@@ -66,11 +101,28 @@ export function summarizeCloudFindings(results, providerOf, cap = Number(process
66
101
  for (const x of found) {
67
102
  const sev = String(x?.severity || x?.level || 'INFO').toUpperCase();
68
103
  bucket.counts[sev] = (bucket.counts[sev] || 0) + 1;
104
+ let findingEntry = null;
69
105
  if (sev === 'CRITICAL' || sev === 'HIGH') {
70
- bucket.findings.push({ severity: sev, plugin: String(r?.id ?? ''), title: describeFinding(x) });
106
+ findingEntry = { severity: sev, plugin: String(r?.id ?? ''), title: describeFinding(x) };
107
+ bucket.findings.push(findingEntry);
71
108
  }
72
109
  if (x && typeof x === 'object' && x.details && x.details.evidenceGap === true) {
73
- bucket.evidenceGaps.push({ severity: sev, plugin: String(r?.id ?? ''), title: describeFinding(x) });
110
+ // Lead with the GAP clause (the badge says "unverified" badging a verified
111
+ // fact is incoherent); carry the first actionable clause as a companion so a
112
+ // mixed rollup's actionable content still reaches the caller (review fold D3).
113
+ const gapEntry = { severity: sev, plugin: String(r?.id ?? ''), title: describeFinding(x, { prefer: 'gap' }) };
114
+ const clauses = Array.isArray(x.issues) ? x.issues.filter(Boolean).map(String) : [];
115
+ const actionable = clauses.find((i) => classifyClause(i) === 'actionable');
116
+ if (actionable && clauses.some((i) => classifyClause(i) === 'gap')) {
117
+ gapEntry.action = actionable.replace(ROUTING_PREFIX_RE, '').slice(0, 160);
118
+ }
119
+ // For CRITICAL/HIGH the actionable clause is normally itemized in the findings
120
+ // list above, so the companion would duplicate it — but the findings list is
121
+ // capped AFTER this loop, and an evicted entry would lose its actionable clause
122
+ // everywhere. Keep a ref; the post-cap pass below drops the companion only when
123
+ // the findings-list entry actually survived (0.19.3 batch-review fold).
124
+ if (findingEntry) gapEntry._findingRef = findingEntry;
125
+ bucket.evidenceGaps.push(gapEntry);
74
126
  }
75
127
  }
76
128
  }
@@ -82,6 +134,13 @@ export function summarizeCloudFindings(results, providerOf, cap = Number(process
82
134
  b.findings.sort((a, c) => (RANK[c.severity] || 0) - (RANK[a.severity] || 0));
83
135
  if (b.findings.length > cap) { b.truncated = true; b.findings = b.findings.slice(0, cap); }
84
136
  if (b.evidenceGaps.length > cap) { b.evidenceGapsTruncated = true; b.evidenceGaps = b.evidenceGaps.slice(0, cap); }
137
+ const kept = new Set(b.findings);
138
+ for (const g of b.evidenceGaps) {
139
+ if (g._findingRef) {
140
+ if (kept.has(g._findingRef)) delete g.action; // itemized above — avoid the D3 duplicate
141
+ delete g._findingRef; // internal ref must never reach the MCP payload
142
+ }
143
+ }
85
144
  }
86
145
  return out;
87
146
  }
@@ -126,7 +185,7 @@ export function renderCloudFindingsMarkdown(summary, providers) {
126
185
  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`);
127
186
  for (const f of (b.findings || [])) lines.push(`- **[${f.severity}]** ${f.plugin}: ${f.title}`);
128
187
  if (b.truncated) lines.push(`- _…CRITICAL/HIGH list truncated; see counts above for totals._`);
129
- for (const g of (b.evidenceGaps || [])) lines.push(`- **[⚠ EVIDENCE GAP — unverified]** ${g.plugin}: ${g.title}`);
188
+ for (const g of (b.evidenceGaps || [])) lines.push(`- **[⚠ EVIDENCE GAP — unverified]** ${g.plugin}: ${g.title}${g.action ? ` · actionable: ${g.action}` : ''}`);
130
189
  if (b.evidenceGapsTruncated) lines.push(`- _…evidence-gap list truncated; see LOW count for totals._`);
131
190
  lines.push('');
132
191
  }