nsauditor-ai 0.1.3 → 0.1.5
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 +44 -4
- package/cli.mjs +60 -2
- package/package.json +1 -1
- package/utils/keychain.mjs +120 -0
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ A modular, AI-assisted network security audit platform that scans, understands,
|
|
|
7
7
|
[](https://www.npmjs.com/package/nsauditor-ai)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
[](https://nodejs.org)
|
|
10
|
-
[](#tests)
|
|
11
11
|
|
|
12
12
|
---
|
|
13
13
|
|
|
@@ -264,14 +264,20 @@ Security: SSRF protection on all host inputs (blocks RFC 1918, loopback, fc00::/
|
|
|
264
264
|
|
|
265
265
|
### Claude Desktop Setup
|
|
266
266
|
|
|
267
|
-
|
|
267
|
+
First install the package globally:
|
|
268
|
+
|
|
269
|
+
```bash
|
|
270
|
+
npm install -g nsauditor-ai
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Then add this to your `claude_desktop_config.json` (Settings → Developer → Edit Config):
|
|
268
274
|
|
|
269
275
|
```json
|
|
270
276
|
{
|
|
271
277
|
"mcpServers": {
|
|
272
278
|
"nsauditor-ai": {
|
|
273
|
-
"command": "
|
|
274
|
-
"args": ["
|
|
279
|
+
"command": "node",
|
|
280
|
+
"args": ["/path/to/global/node_modules/nsauditor-ai/mcp_server.mjs"],
|
|
275
281
|
"env": {
|
|
276
282
|
"AI_PROVIDER": "claude",
|
|
277
283
|
"ANTHROPIC_API_KEY": "your-key-here",
|
|
@@ -283,6 +289,8 @@ Add this to your `claude_desktop_config.json` (Settings → Developer → Edit C
|
|
|
283
289
|
}
|
|
284
290
|
```
|
|
285
291
|
|
|
292
|
+
Find your global install path with `npm root -g`, then append `/nsauditor-ai/mcp_server.mjs`.
|
|
293
|
+
|
|
286
294
|
- `NSA_ALLOW_ALL_HOSTS=1` — required to scan private/RFC 1918 addresses (e.g., `192.168.x.x`)
|
|
287
295
|
- `PLUGIN_TIMEOUT_MS=5000` — reduces per-plugin timeout to 5s so the full scan completes within Claude Desktop's 60s MCP limit
|
|
288
296
|
- `AI_PROVIDER` and API key — optional, enables AI-powered analysis of scan results
|
|
@@ -295,6 +303,38 @@ claude mcp add nsauditor-ai -- npx nsauditor-ai-mcp
|
|
|
295
303
|
|
|
296
304
|
---
|
|
297
305
|
|
|
306
|
+
## Secure Credential Storage
|
|
307
|
+
|
|
308
|
+
Store API keys in the macOS Keychain instead of plaintext `.env` files:
|
|
309
|
+
|
|
310
|
+
```bash
|
|
311
|
+
# Store keys
|
|
312
|
+
nsauditor-ai security set ANTHROPIC_API_KEY
|
|
313
|
+
nsauditor-ai security set OPENAI_API_KEY
|
|
314
|
+
|
|
315
|
+
# List stored keys (masked)
|
|
316
|
+
nsauditor-ai security list
|
|
317
|
+
|
|
318
|
+
# Delete a key
|
|
319
|
+
nsauditor-ai security delete OPENAI_API_KEY
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
Then reference them with the `keychain:` prefix in `.env` or Claude Desktop config:
|
|
323
|
+
|
|
324
|
+
```env
|
|
325
|
+
ANTHROPIC_API_KEY=keychain:ANTHROPIC_API_KEY
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
```json
|
|
329
|
+
"env": {
|
|
330
|
+
"ANTHROPIC_API_KEY": "keychain:ANTHROPIC_API_KEY"
|
|
331
|
+
}
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
The `keychain:` prefix works anywhere an API key is read — CLI, MCP server, or programmatic API.
|
|
335
|
+
|
|
336
|
+
---
|
|
337
|
+
|
|
298
338
|
## CLI Reference
|
|
299
339
|
|
|
300
340
|
```
|
package/cli.mjs
CHANGED
|
@@ -92,11 +92,12 @@ async function maybeSendToOpenAI({ host, results, conclusion, promptMode = 'basi
|
|
|
92
92
|
: aiProvider === 'ollama'
|
|
93
93
|
? toCleanPath(process.env.OLLAMA_MODEL || 'llama3')
|
|
94
94
|
: toCleanPath(process.env.OPENAI_MODEL || 'gpt-4o-mini');
|
|
95
|
+
const { resolveSecret } = await import('./utils/keychain.mjs');
|
|
95
96
|
const keyRaw = aiProvider === 'claude'
|
|
96
|
-
? process.env.ANTHROPIC_API_KEY
|
|
97
|
+
? await resolveSecret(process.env.ANTHROPIC_API_KEY)
|
|
97
98
|
: aiProvider === 'ollama'
|
|
98
99
|
? 'ollama' // Ollama needs no real key; OpenAI SDK requires a non-empty string
|
|
99
|
-
: process.env.OPENAI_API_KEY;
|
|
100
|
+
: await resolveSecret(process.env.OPENAI_API_KEY);
|
|
100
101
|
const key = keyRaw ? String(keyRaw).trim() : null;
|
|
101
102
|
|
|
102
103
|
// Base output folder (directory ONLY; if a file path is given, take its dir)
|
|
@@ -650,6 +651,27 @@ const SEVERITY_RANK = { critical: 4, high: 3, medium: 2, low: 1, info: 0 };
|
|
|
650
651
|
* @param {object} conclusion
|
|
651
652
|
* @returns {number} highest severity rank found (0-4)
|
|
652
653
|
*/
|
|
654
|
+
async function readSecretFromStdin(keyName) {
|
|
655
|
+
if (!process.stdin.isTTY) {
|
|
656
|
+
// Piped input
|
|
657
|
+
return new Promise((resolve) => {
|
|
658
|
+
let data = '';
|
|
659
|
+
process.stdin.setEncoding('utf8');
|
|
660
|
+
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
661
|
+
process.stdin.on('end', () => resolve(data.trim() || null));
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
// Interactive prompt
|
|
665
|
+
const { createInterface } = await import('node:readline');
|
|
666
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
667
|
+
return new Promise((resolve) => {
|
|
668
|
+
rl.question(`Enter value for ${keyName}: `, (answer) => {
|
|
669
|
+
rl.close();
|
|
670
|
+
resolve(answer.trim() || null);
|
|
671
|
+
});
|
|
672
|
+
});
|
|
673
|
+
}
|
|
674
|
+
|
|
653
675
|
function maxSeverityInConclusion(conclusion) {
|
|
654
676
|
const services = conclusion?.result?.services || [];
|
|
655
677
|
let max = 0;
|
|
@@ -709,6 +731,42 @@ async function main() {
|
|
|
709
731
|
process.exit(0);
|
|
710
732
|
}
|
|
711
733
|
|
|
734
|
+
if (cmd === 'security') {
|
|
735
|
+
const { keychainSet, keychainDelete, keychainList, keychainGet } = await import('./utils/keychain.mjs');
|
|
736
|
+
const rawArgs = process.argv.slice(2);
|
|
737
|
+
const subCmd = rawArgs[1]; // set | delete | list | get
|
|
738
|
+
const keyName = rawArgs[2];
|
|
739
|
+
|
|
740
|
+
if (subCmd === 'set' && keyName) {
|
|
741
|
+
// Read secret from stdin (piped) or prompt
|
|
742
|
+
const secret = await readSecretFromStdin(keyName);
|
|
743
|
+
if (!secret) { console.error('No secret provided.'); process.exit(1); }
|
|
744
|
+
await keychainSet(keyName, secret);
|
|
745
|
+
console.log(`Stored "${keyName}" in macOS Keychain (service: nsauditor-ai)`);
|
|
746
|
+
} else if (subCmd === 'delete' && keyName) {
|
|
747
|
+
const ok = await keychainDelete(keyName);
|
|
748
|
+
console.log(ok ? `Deleted "${keyName}" from Keychain` : `"${keyName}" not found in Keychain`);
|
|
749
|
+
} else if (subCmd === 'list') {
|
|
750
|
+
const entries = await keychainList();
|
|
751
|
+
if (entries.length === 0) {
|
|
752
|
+
console.log('No nsauditor-ai keys stored in Keychain.');
|
|
753
|
+
} else {
|
|
754
|
+
console.log('Stored keys (service: nsauditor-ai):\n');
|
|
755
|
+
for (const name of entries) {
|
|
756
|
+
const val = await keychainGet(name);
|
|
757
|
+
const masked = val ? `${val.slice(0, 8)}...(${val.length} chars)` : '(empty)';
|
|
758
|
+
console.log(` ${name} = ${masked}`);
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
} else {
|
|
762
|
+
console.log(`Usage:
|
|
763
|
+
nsauditor-ai security set <KEY_NAME> Store a secret in macOS Keychain
|
|
764
|
+
nsauditor-ai security delete <KEY_NAME> Remove a secret from Keychain
|
|
765
|
+
nsauditor-ai security list List stored secrets (masked)`);
|
|
766
|
+
}
|
|
767
|
+
process.exit(0);
|
|
768
|
+
}
|
|
769
|
+
|
|
712
770
|
if (cmd !== 'scan') {
|
|
713
771
|
console.error(`Unknown command: ${cmd}`);
|
|
714
772
|
process.exit(2);
|
package/package.json
CHANGED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// utils/keychain.mjs
|
|
2
|
+
// macOS Keychain integration for secure credential storage.
|
|
3
|
+
// Falls back gracefully on non-macOS platforms.
|
|
4
|
+
|
|
5
|
+
import { execFile } from 'node:child_process';
|
|
6
|
+
import { platform } from 'node:os';
|
|
7
|
+
|
|
8
|
+
const SERVICE = 'nsauditor-ai';
|
|
9
|
+
const isMac = platform() === 'darwin';
|
|
10
|
+
|
|
11
|
+
function exec(cmd, args) {
|
|
12
|
+
return new Promise((resolve, reject) => {
|
|
13
|
+
execFile(cmd, args, { timeout: 5000 }, (err, stdout, stderr) => {
|
|
14
|
+
if (err) return reject(new Error(stderr?.trim() || err.message));
|
|
15
|
+
resolve(stdout.trim());
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Read a secret from the macOS Keychain.
|
|
22
|
+
* @param {string} account - Key name (e.g., 'ANTHROPIC_API_KEY')
|
|
23
|
+
* @returns {Promise<string|null>} The secret value, or null if not found
|
|
24
|
+
*/
|
|
25
|
+
export async function keychainGet(account) {
|
|
26
|
+
if (!isMac) return null;
|
|
27
|
+
try {
|
|
28
|
+
return await exec('security', [
|
|
29
|
+
'find-generic-password', '-s', SERVICE, '-a', account, '-w'
|
|
30
|
+
]);
|
|
31
|
+
} catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Store a secret in the macOS Keychain.
|
|
38
|
+
* Updates existing entry if present.
|
|
39
|
+
* @param {string} account - Key name
|
|
40
|
+
* @param {string} secret - The secret value
|
|
41
|
+
*/
|
|
42
|
+
export async function keychainSet(account, secret) {
|
|
43
|
+
if (!isMac) throw new Error('Keychain storage is only supported on macOS');
|
|
44
|
+
// Delete existing entry first (ignore errors if not found)
|
|
45
|
+
try {
|
|
46
|
+
await exec('security', ['delete-generic-password', '-s', SERVICE, '-a', account]);
|
|
47
|
+
} catch { /* not found — fine */ }
|
|
48
|
+
await exec('security', [
|
|
49
|
+
'add-generic-password', '-s', SERVICE, '-a', account, '-w', secret
|
|
50
|
+
]);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Delete a secret from the macOS Keychain.
|
|
55
|
+
* @param {string} account - Key name
|
|
56
|
+
* @returns {Promise<boolean>} true if deleted, false if not found
|
|
57
|
+
*/
|
|
58
|
+
export async function keychainDelete(account) {
|
|
59
|
+
if (!isMac) throw new Error('Keychain storage is only supported on macOS');
|
|
60
|
+
try {
|
|
61
|
+
await exec('security', ['delete-generic-password', '-s', SERVICE, '-a', account]);
|
|
62
|
+
return true;
|
|
63
|
+
} catch {
|
|
64
|
+
return false;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* List all nsauditor-ai entries in the Keychain.
|
|
70
|
+
* @returns {Promise<string[]>} Array of account names
|
|
71
|
+
*/
|
|
72
|
+
export async function keychainList() {
|
|
73
|
+
if (!isMac) return [];
|
|
74
|
+
try {
|
|
75
|
+
const raw = await exec('security', ['dump-keychain']);
|
|
76
|
+
const entries = [];
|
|
77
|
+
const lines = raw.split('\n');
|
|
78
|
+
let inOurService = false;
|
|
79
|
+
for (const line of lines) {
|
|
80
|
+
const trimmed = line.trim();
|
|
81
|
+
// Match both formats: 0x00000007 <blob>="nsauditor-ai" and "svce"<blob>="nsauditor-ai"
|
|
82
|
+
if (trimmed.includes(`="${SERVICE}"`)) {
|
|
83
|
+
inOurService = true;
|
|
84
|
+
} else if (inOurService && trimmed.includes('"acct"<blob>="')) {
|
|
85
|
+
const m = trimmed.match(/"acct"<blob>="([^"]+)"/);
|
|
86
|
+
if (m) entries.push(m[1]);
|
|
87
|
+
inOurService = false;
|
|
88
|
+
} else if (trimmed.startsWith('keychain:') || trimmed.startsWith('class:')) {
|
|
89
|
+
inOurService = false;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return [...new Set(entries)];
|
|
93
|
+
} catch {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve a value that may be a keychain reference.
|
|
100
|
+
* If the value starts with 'keychain:', read from Keychain.
|
|
101
|
+
* Otherwise return the value as-is.
|
|
102
|
+
* @param {string|undefined} value - Raw env var value
|
|
103
|
+
* @returns {Promise<string|null>} Resolved secret
|
|
104
|
+
*/
|
|
105
|
+
export async function resolveSecret(value) {
|
|
106
|
+
if (!value) return null;
|
|
107
|
+
const str = String(value).trim();
|
|
108
|
+
if (!str) return null;
|
|
109
|
+
if (str.startsWith('keychain:')) {
|
|
110
|
+
const label = str.slice('keychain:'.length).trim();
|
|
111
|
+
if (!label) return null;
|
|
112
|
+
const secret = await keychainGet(label);
|
|
113
|
+
if (!secret) {
|
|
114
|
+
console.error(`[keychain] No entry found for "${label}" in Keychain (service: ${SERVICE})`);
|
|
115
|
+
console.error(`[keychain] Store it with: nsauditor-ai security set ${label}`);
|
|
116
|
+
}
|
|
117
|
+
return secret;
|
|
118
|
+
}
|
|
119
|
+
return str;
|
|
120
|
+
}
|