nsauditor-ai 0.1.85 โ†’ 0.1.87

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,7 +17,12 @@ 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.85 + Enterprise 0.15.4** (May 2026)
20
+ **Latest: CE 0.1.87 + Enterprise 0.15.6** (May 2026)
21
+
22
+ - ๐Ÿงญ **Compliance-mapping correctness** (CE 0.1.87 โ€” paired EE 0.15.6) โ€” EE 0.15.6 closes two cross-framework defects in how S3 public-exposure findings route to the compliance frameworks: a publicly-accessible bucket now correctly maps to **NIST CSF PR.AA-05 + PR.DS-01** and **PCI DSS 7.2.1** (it previously showed CLEAN on those two), and a missing-Public-Access-Block guardrail gap (not a confirmed exposure) no longer false-FAILs the confidentiality-exposure controls. CE is a paired no-op bump (no CE code change). Plugin count UNCHANGED at 28; all six coverage matrices UNCHANGED.
23
+ - ๐Ÿงน **Dependency-hygiene / institutional-trust patch** (CE 0.1.86 โ€” paired EE 0.15.5) โ€” removes deprecation warnings + advisories from `npm install`. The abandoned `simple-wappalyzer` (โ†’ deprecated, now-commercial `wappalyzer-core`) is replaced by an in-house **zero-dependency** web tech fingerprinter (`utils/tech_fingerprint.mjs` โ€” header/HTML/script-src/cookie/meta signatures; same `{name,categories,confidence,version}` output). `@anthropic-ai/sdk` bumped `^0.82`โ†’`^0.100` (exits the GHSA-p7fg-763f-g4gf range; the Filesystem Memory Tool is never used). The direct `uuid` dependency is dropped in favor of native `crypto.randomUUID()`. NEW `SECURITY.md` documents remaining transitive notices. No feature/behavior change.
24
+
25
+
21
26
 
22
27
  - ๐Ÿชฃ **Non-current-version ACL sampling + public WRITE-vs-READ differentiation** (EE 0.15.4 โ€” paired) โ€” closes the two residuals the 0.15.3 spec carried as deferred. Plugin 1020 gains NEW step 2c-v: on versioning-Enabled/Suspended buckets it samples **non-current** object versions (`ListObjectVersions` first-page, cap-bounded โ†’ per-version `GetObjectAcl({VersionId})`, delete-markers skipped) โ€” catching a public ACL that survives on an **overwritten** version still downloadable via `?versionId=` after the current version is made private (CRITICAL via the existing `"publicly accessible"` anchor; PAB `IgnorePublicAcls` โ†’ LOW; skipped on `BucketOwnerEnforced`). NEW sibling helper `extractPublicWriteGroups` flags public WRITE/WRITE_ACP/FULL_CONTROL grants (anyone-can-overwrite) distinctly from READ-only, as an enrichment line on the already-CRITICAL finding at bucket/object/version ACL sites. `ListObjectVersions AccessDenied` (distinct `s3:ListBucketVersions` action) + a `GetBucketVersioning AccessDenied` review-fold both degrade to routed LOW evidence-gaps via the existing `"S3 object-ACL evidence-gap"` anchor โ€” never a silent PASS. **Enterprise plugin count UNCHANGED (28); all six coverage matrices UNCHANGED; ZERO framework-JSON edits.** TDD-first, +27 tests; EE regression 6628/6628 GREEN. _(Staged on `main`; awaiting live smoke + trio publish.)_
23
28
  - ๐Ÿชฃ **Object-level ACL enumeration + BucketOwnerEnforced short-circuit** (EE 0.15.3 โ€” paired) โ€” closes the 4th and final S3 public-exposure vector (object-level ACLs) documented as a residual in the 0.15.2 closure. Plugin 1020 gains NEW step 2c sampled `GetObjectAcl` enumeration over first-page objects (default cap 10; per-object throttle default 50ms) + NEW step 2a `GetBucketOwnershipControls` upstream short-circuit that skips both 2b (bucket-ACL) and 2c (object-ACL) on `BucketOwnerEnforced` buckets (the default since April 2023; saves 11+ API calls per BOE bucket on modern estates AND closes a false-positive class where BOE buckets with legacy stored public ACL grants previously emitted CRITICAL โ€” they now emit informational because S3 structurally ignores ACL grants under BOE). **INTENTIONAL MATRIX DELTA from 0.15.2** on BOE buckets with pre-BOE legacy grants: CRITICAL โ†’ informational. NEW shared `extractPublicGroups` helper used by BOTH step 2b (refactored byte-identical) AND step 2c. 4 LOW evidence-gap emissions via NEW `"S3 object-ACL evidence-gap"` substring anchor on SOC 2 CC7.1 + HIPAA ยง164.312(b) (substrate-depth on already-covered controls; matrices unchanged). Live AWS smoke all 4 spot-checks PASS (BOE detection; E1 CRITICAL en-dash bytes preserved; cap clamping; objectRateMs throttling). Enterprise plugin count unchanged (28); all six coverage matrices unchanged.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nsauditor-ai",
3
- "version": "0.1.85",
3
+ "version": "0.1.87",
4
4
  "description": "Modular AI-assisted network security audit platform โ€” Community Edition",
5
5
  "type": "module",
6
6
  "private": false,
@@ -27,7 +27,7 @@
27
27
  "plugin_manager.mjs"
28
28
  ],
29
29
  "dependencies": {
30
- "@anthropic-ai/sdk": "^0.82.0",
30
+ "@anthropic-ai/sdk": "^0.100.0",
31
31
  "@modelcontextprotocol/sdk": "^1.29.0",
32
32
  "dnssd": "^0.4.1",
33
33
  "dotenv": "^17.2.1",
@@ -36,10 +36,8 @@
36
36
  "node-upnp-utils": "^1.0.3",
37
37
  "openai": "^4.104.0",
38
38
  "oui-data": "^1.1.427",
39
- "simple-wappalyzer": "^1.1.75",
40
39
  "snmp-native": "^1.2.0",
41
40
  "jose": "^6.2.0",
42
- "uuid": "^13.0.0",
43
41
  "xml2js": "^0.6.2"
44
42
  },
45
43
  "optionalDependencies": {
@@ -1,13 +1,11 @@
1
1
  // plugins/webapp_detector.mjs
2
2
  // New plugin: Webapp Detector
3
- // Uses `simple-wappalyzer` to fingerprint web applications present on a host.
3
+ // Uses the in-house zero-dep fingerprinter (utils/tech_fingerprint.mjs) to identify
4
+ // web applications present on a host.
4
5
  // Tries HTTPS first (port 443), then HTTP (port 80), and can also try custom ports via opts.ports.
5
6
  // NOTE: Unlike http_probe, undici/fetch cannot ignore TLS easily per-request, so self-signed
6
7
  // HTTPS will usually fail and the plugin will fall back to HTTP.
7
8
  //
8
- // Add to package.json (dependencies):
9
- // "simple-wappalyzer": "^1.14.0" // or latest
10
- //
11
9
  // Example use (plugin manager):
12
10
  // webappDetector.run("192.168.1.1")
13
11
  //
@@ -29,7 +27,7 @@
29
27
  // apps: [ { name, categories, confidence, version?, slug, ... }, ... ]
30
28
  // }
31
29
 
32
- import wappalyzer from 'simple-wappalyzer';
30
+ import { fingerprint } from '../utils/tech_fingerprint.mjs';
33
31
 
34
32
  const DEBUG =
35
33
  String(process.env.DEBUG_MODE || '').toLowerCase() === '1' ||
@@ -107,18 +105,18 @@ async function tryDetectAt(url) {
107
105
  }
108
106
  }
109
107
 
110
- /** Run wappalyzer on provided HTML/headers. */
108
+ /** Run in-house fingerprinter on provided HTML/headers. */
111
109
  async function detectFromHtml(url, html, statusCode, headers) {
112
110
  try {
113
- const result = await wappalyzer({ url, html, statusCode, headers });
111
+ const result = fingerprint({ url, html, statusCode, headers });
114
112
  if (Array.isArray(result) && result.length) {
115
- log('wappalyzer apps=', result.map(a => a.name).join(', '));
113
+ log('fingerprint apps=', result.map(a => a.name).join(', '));
116
114
  } else {
117
- log('wappalyzer apps=โˆ…');
115
+ log('fingerprint apps=โˆ…');
118
116
  }
119
117
  return result || [];
120
118
  } catch (e) {
121
- log('wappalyzer error:', e?.message || e);
119
+ log('fingerprint error:', e?.message || e);
122
120
  return [];
123
121
  }
124
122
  }
@@ -137,7 +135,7 @@ function summarizeApps(apps) {
137
135
  export default {
138
136
  id: '010',
139
137
  name: 'Webapp Detector',
140
- description: 'Identifies web applications and frameworks using simple-wappalyzer (tries HTTPS then HTTP).',
138
+ description: 'Identifies web applications and frameworks using the in-house fingerprinter (tries HTTPS then HTTP).',
141
139
  priority: 55, // run near HTTP probe
142
140
  requirements: { host: 'up', tcp_open: [80, 443] }, // heuristic gate; still attempts both
143
141
  protocols: ['tcp'],
@@ -156,7 +154,7 @@ export default {
156
154
  os: null,
157
155
  type: 'webapp',
158
156
  data: [],
159
- apps: [], // raw simple-wappalyzer results
157
+ apps: [], // raw fingerprinter results
160
158
  };
161
159
 
162
160
  let target = normalizeTarget(host);
@@ -244,3 +242,5 @@ export async function conclude({ host, result }) {
244
242
  authoritative: false,
245
243
  }));
246
244
  }
245
+
246
+ export { detectFromHtml, summarizeApps };
@@ -5,7 +5,6 @@
5
5
 
6
6
  import dgram from 'dgram';
7
7
  import { parseString } from 'xml2js';
8
- import { v4 as uuidv4 } from 'uuid';
9
8
  import { isPrivateLike } from '../utils/net_validation.mjs';
10
9
 
11
10
  const DEBUG = /^(1|true|yes|on)$/i.test(String(process.env.DEBUG_MODE || process.env.WSD_DEBUG || ""));
@@ -28,7 +27,7 @@ function discoverWsDiscoveryDevices(targetHost, timeout = 5000) {
28
27
  const client = dgram.createSocket('udp4');
29
28
  const devices = [];
30
29
  const knownAddresses = new Set();
31
- const probeMessageId = `urn:uuid:${uuidv4()}`;
30
+ const probeMessageId = `urn:uuid:${crypto.randomUUID()}`;
32
31
  const includeNonMatched = /^(1|true|yes|on)$/i.test(String(process.env.WSD_INCLUDE_NON_MATCHED || ""));
33
32
 
34
33
  // The multicast address and port for WS-Discovery
@@ -1,5 +1,4 @@
1
1
  // utils/finding_schema.mjs
2
- import { v4 as uuidv4 } from 'uuid';
3
2
 
4
3
  export const FINDING_CATEGORIES = ['AUTH', 'CRYPTO', 'CONFIG', 'SERVICE', 'EXPOSURE', 'CVE'];
5
4
  export const FINDING_STATUSES = ['UNVERIFIED', 'VERIFIED', 'POTENTIAL', 'FALSE_POSITIVE'];
@@ -62,5 +61,5 @@ export function validateFinding(f) {
62
61
  * UUID-based โ€” no counter to reset, no collision risk across restarts.
63
62
  */
64
63
  export function generateFindingId() {
65
- return `F-${uuidv4()}`;
64
+ return `F-${crypto.randomUUID()}`;
66
65
  }
@@ -0,0 +1,249 @@
1
+ /**
2
+ * tech_fingerprint.mjs โ€” zero-dependency web-technology fingerprinter.
3
+ * Replaces the abandoned `simple-wappalyzer` / deprecated `wappalyzer-core`.
4
+ * Returns wappalyzer-shaped results: [{ name, categories, confidence, version }].
5
+ */
6
+
7
+ // ---------------------------------------------------------------------------
8
+ // Signature table
9
+ // Each entry defines detection surfaces (OR semantics โ€” any one match detects).
10
+ // Surfaces:
11
+ // header โ€” [headerName, regex] tested against that header's value
12
+ // altHeader โ€” [headerName, regex] secondary header (also OR'd in)
13
+ // meta โ€” [metaName, regex] tested against <meta name=โ€ฆ content=โ€ฆ>
14
+ // script โ€” [regex, โ€ฆ] tested against each <script src="โ€ฆ"> URL
15
+ // cookie โ€” [regex, โ€ฆ] tested against Set-Cookie header value
16
+ // versionFrom โ€” { surface, key?, regex } โ†’ capture group 1 becomes version
17
+ // implies โ€” [name, โ€ฆ] โ†’ inject those techs at confidence 50
18
+ // ---------------------------------------------------------------------------
19
+ const SIGS = [
20
+ {
21
+ name: 'Nginx',
22
+ categories: ['Web servers'],
23
+ header: ['server', /nginx/i],
24
+ versionFrom: { surface: 'header', key: 'server', regex: /nginx\/([\d.]+)/i },
25
+ },
26
+ {
27
+ name: 'Apache',
28
+ categories: ['Web servers'],
29
+ header: ['server', /apache/i],
30
+ versionFrom: { surface: 'header', key: 'server', regex: /apache\/([\d.]+)/i },
31
+ },
32
+ {
33
+ name: 'IIS',
34
+ categories: ['Web servers'],
35
+ header: ['server', /(?:microsoft-)?iis/i],
36
+ },
37
+ {
38
+ name: 'PHP',
39
+ categories: ['Programming languages'],
40
+ header: ['x-powered-by', /php/i],
41
+ versionFrom: { surface: 'header', key: 'x-powered-by', regex: /php\/([\d.]+)/i },
42
+ },
43
+ {
44
+ name: 'ASP.NET',
45
+ categories: ['Web frameworks'],
46
+ header: ['x-powered-by', /asp\.net/i],
47
+ altHeader: ['x-aspnet-version', /.+/],
48
+ versionFrom: { surface: 'header', key: 'x-aspnet-version', regex: /([\d.]+)/ },
49
+ },
50
+ {
51
+ name: 'Express',
52
+ categories: ['Web frameworks'],
53
+ header: ['x-powered-by', /express/i],
54
+ },
55
+ {
56
+ name: 'WordPress',
57
+ categories: ['CMS'],
58
+ meta: ['generator', /wordpress/i],
59
+ script: [/\/wp-(?:content|includes)\//i],
60
+ versionFrom: { surface: 'meta', key: 'generator', regex: /wordpress\s+([\d.]+)/i },
61
+ implies: ['PHP'],
62
+ },
63
+ {
64
+ name: 'Drupal',
65
+ categories: ['CMS'],
66
+ meta: ['generator', /drupal/i],
67
+ altHeader: ['x-generator', /drupal/i],
68
+ },
69
+ {
70
+ name: 'Joomla',
71
+ categories: ['CMS'],
72
+ meta: ['generator', /joomla/i],
73
+ },
74
+ {
75
+ name: 'jQuery',
76
+ categories: ['JavaScript libraries'],
77
+ script: [/jquery[.-]([\d.]+)(?:\.min)?\.js/i],
78
+ versionFrom: { surface: 'script', regex: /jquery[.-]([\d]+(?:\.[\d]+)*)/i },
79
+ },
80
+ {
81
+ name: 'React',
82
+ categories: ['JavaScript frameworks'],
83
+ script: [/\breact(?:\.production|\.development)?(?:\.min)?\.js/i],
84
+ },
85
+ {
86
+ name: 'Vue.js',
87
+ categories: ['JavaScript frameworks'],
88
+ script: [/\bvue(?:@[\d.]+)?(?:\.global|\.runtime)?(?:\.min)?\.js/i],
89
+ },
90
+ {
91
+ name: 'Cloudflare',
92
+ categories: ['CDN'],
93
+ header: ['server', /cloudflare/i],
94
+ altHeader: ['cf-ray', /.+/],
95
+ },
96
+ ];
97
+
98
+ // Build a lookup map from name โ†’ sig for implies resolution.
99
+ const SIG_BY_NAME = new Map(SIGS.map(s => [s.name, s]));
100
+
101
+ // ---------------------------------------------------------------------------
102
+ // Helpers
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /** Normalise headers: lowercase keys, join array values. */
106
+ function normaliseHeaders(raw = {}) {
107
+ const out = {};
108
+ for (const [k, v] of Object.entries(raw)) {
109
+ out[k.toLowerCase()] = Array.isArray(v) ? v.join('; ') : String(v);
110
+ }
111
+ return out;
112
+ }
113
+
114
+ /** Extract all <script src="โ€ฆ"> URLs from HTML (case-insensitive). */
115
+ function extractScriptSrcs(html) {
116
+ const srcs = [];
117
+ const re = /<script[^>]+\bsrc\s*=\s*["']([^"']+)["']/gi;
118
+ let m;
119
+ while ((m = re.exec(html)) !== null) srcs.push(m[1]);
120
+ return srcs;
121
+ }
122
+
123
+ /** Extract <meta name="โ€ฆ" content="โ€ฆ"> into a lowercased-name โ†’ content map. */
124
+ function extractMetas(html) {
125
+ const map = {};
126
+ const re = /<meta[^>]+>/gi; // NOTE: stops at first '>'; meta content containing '>' is not handled (no current signature needs it)
127
+ let m;
128
+ while ((m = re.exec(html)) !== null) {
129
+ const tag = m[0];
130
+ const nameM = /\bname\s*=\s*["']([^"']+)["']/i.exec(tag);
131
+ const contM = /\bcontent\s*=\s*["']([^"']*)["']/i.exec(tag);
132
+ if (nameM && contM) map[nameM[1].toLowerCase()] = contM[1];
133
+ }
134
+ return map;
135
+ }
136
+
137
+ // ---------------------------------------------------------------------------
138
+ // Core export
139
+ // ---------------------------------------------------------------------------
140
+
141
+ /**
142
+ * Fingerprint a web response.
143
+ * @param {{ url?, html?, statusCode?, headers? }} probe
144
+ * @returns {{ name, categories, confidence, version }[]}
145
+ */
146
+ export function fingerprint({ url, html = '', statusCode, headers = {} } = {}) {
147
+ const hdrs = normaliseHeaders(headers);
148
+ const srcs = extractScriptSrcs(html);
149
+ const metas = extractMetas(html);
150
+ // url + statusCode reserved for future per-URL / status-code rules
151
+ const cookie = hdrs['set-cookie'] ?? ''; // set-cookie is multi-valued; joined for regex matching โ€” cookie patterns should match anywhere in the joined string.
152
+
153
+ /** Resolve version for a detected sig given the surfaces already parsed. */
154
+ function resolveVersion(sig) {
155
+ const vf = sig.versionFrom;
156
+ if (!vf) return undefined;
157
+ let haystack = '';
158
+ if (vf.surface === 'header') {
159
+ haystack = hdrs[vf.key ?? ''] ?? '';
160
+ } else if (vf.surface === 'meta') {
161
+ haystack = metas[vf.key ?? ''] ?? '';
162
+ } else if (vf.surface === 'script') {
163
+ // Test every src URL; return the first capture.
164
+ for (const src of srcs) {
165
+ const vm = vf.regex.exec(src);
166
+ if (vm) return vm[1];
167
+ }
168
+ return undefined;
169
+ }
170
+ const vm = vf.regex.exec(haystack);
171
+ return vm ? vm[1] : undefined;
172
+ }
173
+
174
+ /** Count how many distinct surfaces match for a sig. */
175
+ function countSurfaces(sig) {
176
+ let count = 0;
177
+
178
+ // header
179
+ if (sig.header) {
180
+ const [hName, hRe] = sig.header;
181
+ if (hdrs[hName] && hRe.test(hdrs[hName])) count++;
182
+ }
183
+
184
+ // altHeader โ€” only counts if it's a different surface from header
185
+ if (sig.altHeader) {
186
+ const [hName, hRe] = sig.altHeader;
187
+ if (hdrs[hName] && hRe.test(hdrs[hName])) count++;
188
+ }
189
+
190
+ // meta
191
+ if (sig.meta) {
192
+ const [mName, mRe] = sig.meta;
193
+ const val = metas[mName.toLowerCase()];
194
+ if (val && mRe.test(val)) count++;
195
+ }
196
+
197
+ // script โ€” counts as 1 surface if any src URL matches any pattern
198
+ if (sig.script) {
199
+ const scriptHit = sig.script.some(re => srcs.some(src => re.test(src)));
200
+ if (scriptHit) count++;
201
+ }
202
+
203
+ // cookie โ€” counts as 1 surface if any pattern matches
204
+ if (sig.cookie) {
205
+ const cookieHit = sig.cookie.some(re => re.test(cookie));
206
+ if (cookieHit) count++;
207
+ }
208
+
209
+ return count;
210
+ }
211
+
212
+ // --- Detect ---
213
+ const detected = new Map(); // name โ†’ result object
214
+
215
+ for (const sig of SIGS) {
216
+ const surfaces = countSurfaces(sig);
217
+ if (surfaces < 1) continue;
218
+
219
+ const confidence = Math.min(100, 25 + 25 * surfaces);
220
+ const version = resolveVersion(sig);
221
+
222
+ detected.set(sig.name, {
223
+ name: sig.name,
224
+ categories: [...sig.categories],
225
+ confidence,
226
+ version,
227
+ });
228
+ }
229
+
230
+ // --- Resolve implies ---
231
+ for (const sig of SIGS) {
232
+ if (!sig.implies || !detected.has(sig.name)) continue;
233
+ for (const impliedName of sig.implies) {
234
+ if (detected.has(impliedName)) continue;
235
+ const impliedSig = SIG_BY_NAME.get(impliedName);
236
+ if (!impliedSig) continue;
237
+ detected.set(impliedName, {
238
+ name: impliedName,
239
+ categories: [...impliedSig.categories],
240
+ confidence: 50,
241
+ version: undefined,
242
+ });
243
+ }
244
+ }
245
+
246
+ return Array.from(detected.values());
247
+ }
248
+
249
+ export default fingerprint;