nsauditor-ai 0.2.6 → 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 +2 -2
- package/mcp_server.mjs +4 -2
- package/package.json +1 -1
- package/utils/cloud_finding_summary.mjs +64 -5
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.
|
|
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.
|
|
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
|
-
|
|
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
|
@@ -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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|