nsauditor-ai 0.2.7 → 0.2.8

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 ranges HIGH, 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 caveat WriteOnly 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.8 + Enterprise 0.19.3** (June 2026) — a **real CE bump** (MCP affordance): the `scan_cloud` tool description now enumerates the actual service coverage (AWS S3 / IAM / KMS / CloudTrail / CodePipeline-CodeBuild SoD / Lambda / API GW / DynamoDB / RDS / SQS-SNS / Secrets / Backup / VPC endpoints / SG perimeter / ElastiCache / SES / GuardDuty; Azure Key Vault / Storage / NSG / RBAC; GCP firewall / storage / impersonation) + all 6 compliance frameworks, so AI agents route service-named audit asks to the scanner; the evidence-gap list lines are badge-coherent (lead with the GAP clause) and carry the first ACTIONABLE clause as a companion that now survives findings-cap eviction; internal compliance-routing tags are stripped at the presentation layer so gap lines spend their budget on substance. Paired **EE 0.19.3** "MCP affordance + class-O truncation sweep": inline-credential Lambda env-var names + the `AWS_LAMBDA_` exclusion-prefix evasion bypass closed (1080); truncation/AccessDenied evidence-gaps across 8 AWS auditors now fail-close their native controls in all six frameworks instead of routing to zero (class-O sweep incl. the 1110 P-16 grant-bypass); a new Azure NSG dimension flags restricted-port exposure to the tenant-rentable `AzureCloud` service tags (1221); public-subnet Redis replication groups no longer silently downgrade (1180). Plugin count UNCHANGED at 28; all six coverage matrices UNCHANGED. **EE 0.19.3 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.7 + Enterprise 0.19.2** — paired pin for EE 0.19.2 "Confirmed false-negative tail" (6 more silent misses closed: 1222/1021/1070/1100/1024/1040). 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.8",
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
  }