nsauditor-ai 0.1.29 → 0.1.30
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 +27 -1
- package/cli.mjs +204 -7
- package/package.json +1 -1
- package/utils/capabilities.mjs +26 -0
- package/utils/license.mjs +236 -2
- package/utils/oui.mjs +11 -3
package/README.md
CHANGED
|
@@ -15,6 +15,17 @@ NSAuditor AI is the open-source core of a privacy-first security intelligence pl
|
|
|
15
15
|
|
|
16
16
|
**Zero Data Exfiltration by design.** NSAuditor AI works fully offline. AI analysis, CVE correlation, and continuous monitoring all happen locally. External calls (to AI APIs, NVD, etc.) are opt-in and use your own API keys. We never see your scan data.
|
|
17
17
|
|
|
18
|
+
## What's New (0.1.30)
|
|
19
|
+
|
|
20
|
+
The 0.1.30 line is a paired release with `@nsasoft/nsauditor-ai-ee@0.3.2` that closes the customer-onboarding gap and a critical false-clean SOC 2 reporting bug:
|
|
21
|
+
|
|
22
|
+
- **`nsauditor-ai license install <KEY>`** — verifies the JWT *before* persisting; writes to macOS Keychain or `~/.nsauditor/.env` (mode 0600) depending on platform. Three-line install flow: `npm install -g` → `license install` → `license --status`. No more shell-rc edits.
|
|
23
|
+
- **Multi-source license loader** — `loadLicense()` resolves keys from env var → platform Keychain → `~/.nsauditor/.env`, in that order. CI/CD env-var override still wins.
|
|
24
|
+
- **`nsauditor-ai license --plugins`** — real enumeration of discovered plugins, grouped by source (CE / EE / custom), with active-or-required-tier status.
|
|
25
|
+
- **`nsauditor-ai --version` / `-v`** — prints version and exits 0 (parallel to `--help`'s discovery-flag UX).
|
|
26
|
+
- **Cloud-sentinel SSRF bypass** — `--host aws|gcp|azure` no longer requires `NSA_ALLOW_ALL_HOSTS=1`. The sentinel literals route to EE cloud-scanner plugins via the provider's API; the SSRF guard's RFC 1918 / loopback protection is preserved for real network targets.
|
|
27
|
+
- **EE-0.3.2.1 hard dep** — CE forwards the per-plugin `results` array to `enrichScan()`. Without this, EE 0.3.2's cloud-finding harvester sees nothing and produces false-clean SOC 2 reports against AWS accounts. EE emits a runtime version-skew warning when this opt is missing.
|
|
28
|
+
|
|
18
29
|
## What It Does
|
|
19
30
|
|
|
20
31
|
```
|
|
@@ -49,7 +60,7 @@ NSAuditor AI is available in three editions:
|
|
|
49
60
|
| Advanced CTEM + trend analysis | — | ✅ | ✅ |
|
|
50
61
|
| Cloud scanners (AWS/GCP/Azure) | — | — | ✅ |
|
|
51
62
|
| Zero Trust assessment | — | — | ✅ |
|
|
52
|
-
| SOC 2 compliance (
|
|
63
|
+
| SOC 2 compliance (8 covered + 5 partial controls) | — | — | ✅ |
|
|
53
64
|
| SLA/MTTR tracking + compensating controls | — | — | ✅ |
|
|
54
65
|
| Recurring-scan attestation (Type II evidence) | — | — | ✅ |
|
|
55
66
|
| GRC platform connector (Vanta) | — | — | ✅ |
|
|
@@ -70,6 +81,9 @@ NSAuditor AI is available in three editions:
|
|
|
70
81
|
# Install globally
|
|
71
82
|
npm install -g nsauditor-ai
|
|
72
83
|
|
|
84
|
+
# See all flags, subcommands, and worked examples
|
|
85
|
+
nsauditor-ai --help
|
|
86
|
+
|
|
73
87
|
# Configure (optional — scans work fully offline without AI)
|
|
74
88
|
cat > .env << 'EOF'
|
|
75
89
|
AI_ENABLED=true
|
|
@@ -357,8 +371,16 @@ The `keychain:` prefix works anywhere an API key is read — CLI, MCP server, or
|
|
|
357
371
|
|
|
358
372
|
```
|
|
359
373
|
nsauditor-ai scan [options]
|
|
374
|
+
nsauditor-ai license install <KEY>
|
|
375
|
+
nsauditor-ai license <--status | --capabilities | --plugins>
|
|
376
|
+
nsauditor-ai security <set|delete|list|get> <KEY>
|
|
377
|
+
nsauditor-ai validate
|
|
378
|
+
nsauditor-ai --help (or -h, or `help`)
|
|
379
|
+
nsauditor-ai --version (or -v, or `version`)
|
|
360
380
|
```
|
|
361
381
|
|
|
382
|
+
> Run `nsauditor-ai --help` (or `-h`, or just `nsauditor-ai help`) for a quick reference of subcommands, flags, env vars, and worked examples — works without a license key configured. `--version` / `-v` (CE 0.1.30+) prints `nsauditor-ai <version>` and exits 0.
|
|
383
|
+
|
|
362
384
|
| Flag | Description | Default |
|
|
363
385
|
|---|---|---|
|
|
364
386
|
| `--host <target>` | Target: IP, hostname, CIDR, dash range. Aliases: `--ip`, `--target` | *required*\* |
|
|
@@ -374,6 +396,10 @@ nsauditor-ai scan [options]
|
|
|
374
396
|
| `--interval <min>` | Rescan interval in minutes (requires `--watch`) | `60` |
|
|
375
397
|
| `--webhook-url <url>` | Webhook URL for delta alerts | — |
|
|
376
398
|
| `--alert-severity <sev>` | Minimum severity for webhook alerts | `high` |
|
|
399
|
+
| `--compliance <fw>` | Compliance framework to map findings into (e.g. `soc2`). **Enterprise license required.** See `@nsasoft/nsauditor-ai-ee` README for supported frameworks | — |
|
|
400
|
+
| `--compliance-scope <path>` | Optional JSON file describing the assessment scope (passed to the compliance engine for cover-page attestation) | — |
|
|
401
|
+
| `--help`, `-h` | Print usage block (subcommands, flags, env vars, examples) and exit 0 | — |
|
|
402
|
+
| `--version`, `-v` | Print `nsauditor-ai <version>` and exit 0 (CE 0.1.30+) | — |
|
|
377
403
|
|
|
378
404
|
\* Either `--host` or `--host-file` is required.
|
|
379
405
|
|
package/cli.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import { buildCsv } from './utils/export_csv.mjs';
|
|
|
15
15
|
import { buildMarkdownReport } from './utils/report_md.mjs';
|
|
16
16
|
import { recordScan, getLastScan, computeDiff, formatDiffReport, pruneForCE, HISTORY_FILE } from './utils/scan_history.mjs';
|
|
17
17
|
import { getTierFromEnv, loadLicense } from './utils/license.mjs';
|
|
18
|
-
import { resolveCapabilities, hasCapability } from './utils/capabilities.mjs';
|
|
18
|
+
import { resolveCapabilities, hasCapability, inferRequiredTier } from './utils/capabilities.mjs';
|
|
19
19
|
import { createScheduler } from './utils/scheduler.mjs';
|
|
20
20
|
import { buildDeltaReport, formatDeltaSummary, hasSignificantChanges } from './utils/delta_reporter.mjs';
|
|
21
21
|
import { sendWebhook, buildAlertPayload, isSafeWebhookUrl } from './utils/webhook.mjs';
|
|
@@ -525,11 +525,27 @@ async function parseArgs(argv) {
|
|
|
525
525
|
// Help: bare `--help`/`-h`/`help` or completely empty invocation.
|
|
526
526
|
// Recognized before the scan-default so it doesn't crash with
|
|
527
527
|
// "--host or --host-file is required" on a help request.
|
|
528
|
+
//
|
|
529
|
+
// CE-0.1.30.1 reviewer M1: short flag `-h` only matches at `a[0]`. The
|
|
530
|
+
// long-flag `--help` matches anywhere in argv (typing `nsauditor-ai
|
|
531
|
+
// scan --help` should still print help). Pre-fix the parser had
|
|
532
|
+
// `a.includes('-h')` which would match `-h` as a value of another
|
|
533
|
+
// flag (e.g., `--alert-severity -h` would silently fire help instead
|
|
534
|
+
// of failing argv validation). Same fix mirrored in the version branch
|
|
535
|
+
// below.
|
|
528
536
|
if (a.length === 0 || a[0] === '--help' || a[0] === '-h' || a[0] === 'help' ||
|
|
529
|
-
a.includes('--help')
|
|
537
|
+
a.includes('--help')) {
|
|
530
538
|
args.cmd = 'help';
|
|
531
539
|
return args;
|
|
532
540
|
}
|
|
541
|
+
// Version: bare `--version`/`-v`/`version`. CE-0.1.30.1 — same pre-license
|
|
542
|
+
// dispatch as help so a discovery flag never errors with
|
|
543
|
+
// "--host or --host-file is required" or with a missing-key fatal.
|
|
544
|
+
if (a[0] === '--version' || a[0] === '-v' || a[0] === 'version' ||
|
|
545
|
+
a.includes('--version')) {
|
|
546
|
+
args.cmd = 'version';
|
|
547
|
+
return args;
|
|
548
|
+
}
|
|
533
549
|
if (a.length && !a[0].startsWith('--')) args.cmd = a[0];
|
|
534
550
|
|
|
535
551
|
const get = (name) => {
|
|
@@ -588,10 +604,28 @@ async function parseArgs(argv) {
|
|
|
588
604
|
return args;
|
|
589
605
|
}
|
|
590
606
|
|
|
607
|
+
// EE-0.3.2.5 (CE side): cloud-provider sentinel hosts ('aws' / 'gcp' /
|
|
608
|
+
// 'azure', case-insensitive) are NOT real DNS names — they're scoping
|
|
609
|
+
// tokens that EE cloud-scanner plugins (020/021/022/023/030) interpret
|
|
610
|
+
// as "this run targets the cloud provider via its API, not a network
|
|
611
|
+
// address." resolveAndValidate() returns ENOTFOUND for these, so pre-
|
|
612
|
+
// 0.3.2.5 customers had to set NSA_ALLOW_ALL_HOSTS=1 to bypass — which
|
|
613
|
+
// is undocumented AND disables the guard for legitimate RFC 1918 / IP
|
|
614
|
+
// targets in the same scan. Whitelist the sentinels in scanSingleHost
|
|
615
|
+
// so cloud scans Just Work and the env-var bypass remains scoped to
|
|
616
|
+
// its documented use case (local-network auditing).
|
|
617
|
+
//
|
|
618
|
+
// Hoisted to module scope so the Set is built once at module load
|
|
619
|
+
// rather than per scanSingleHost() call (reviewer L1 fold).
|
|
620
|
+
const CLOUD_SENTINEL_HOSTS = new Set(['aws', 'gcp', 'azure']);
|
|
621
|
+
|
|
591
622
|
async function scanSingleHost(pm, host, plugins, opts, promptMode) {
|
|
592
623
|
// SSRF guard — block loopback, private ranges, cloud metadata endpoints.
|
|
593
624
|
// Set NSA_ALLOW_ALL_HOSTS=1 to scan RFC 1918 / private ranges (local network auditing).
|
|
594
|
-
|
|
625
|
+
// Cloud-sentinel hosts (see CLOUD_SENTINEL_HOSTS above) skip the guard.
|
|
626
|
+
const isCloudSentinel = typeof host === 'string' && CLOUD_SENTINEL_HOSTS.has(host.toLowerCase());
|
|
627
|
+
|
|
628
|
+
if (!process.env.NSA_ALLOW_ALL_HOSTS && !isCloudSentinel) {
|
|
595
629
|
if (isBlockedIp(host)) {
|
|
596
630
|
throw new Error(`Scanning blocked address range is not allowed: ${host}`);
|
|
597
631
|
}
|
|
@@ -626,6 +660,17 @@ async function scanSingleHost(pm, host, plugins, opts, promptMode) {
|
|
|
626
660
|
// EE enrichment hook — no-op if @nsasoft/nsauditor-ai-ee is not installed
|
|
627
661
|
// or the license tier doesn't grant intelligenceEngine. Compliance + outDir
|
|
628
662
|
// are forwarded so EE can write scan_finding_queue.json and SOC 2 artifacts.
|
|
663
|
+
//
|
|
664
|
+
// CE-0.1.30.5: forward the per-plugin `results` array. This is the hard
|
|
665
|
+
// dependency that makes EE-0.3.2.1's cloud-finding harvester actually
|
|
666
|
+
// work in production — pre-CE-0.1.30, EE only saw the concluder object
|
|
667
|
+
// (single plugin output) and could not harvest findings from cloud
|
|
668
|
+
// plugins (020/030/etc.) that live in pm.run().results[]. Without this
|
|
669
|
+
// line, EE 0.3.2 emits a runtime warning ("CE 0.1.30+ … running with
|
|
670
|
+
// older CE") AND continues to produce false-clean SOC 2 reports against
|
|
671
|
+
// AWS accounts. EE 0.3.2 + CE 0.1.30 ship as a paired release; mixing
|
|
672
|
+
// versions is supported but the EE-side cloud-harvest behavior requires
|
|
673
|
+
// CE-0.1.30+ to take effect.
|
|
629
674
|
try {
|
|
630
675
|
const { enrichScan } = await import('@nsasoft/nsauditor-ai-ee');
|
|
631
676
|
const eeEnrichment = await enrichScan(conclusion, {
|
|
@@ -633,6 +678,7 @@ async function scanSingleHost(pm, host, plugins, opts, promptMode) {
|
|
|
633
678
|
outDir,
|
|
634
679
|
compliance: opts.compliance ?? process.env.COMPLIANCE_FRAMEWORKS ?? null,
|
|
635
680
|
complianceScope: opts.complianceScope ?? null,
|
|
681
|
+
results,
|
|
636
682
|
onWarn: (msg) => console.warn(`[EE] ${msg}`),
|
|
637
683
|
});
|
|
638
684
|
if (eeEnrichment?.enrichedPrompt) {
|
|
@@ -758,6 +804,16 @@ function maxSeverityInConclusion(conclusion) {
|
|
|
758
804
|
async function main() {
|
|
759
805
|
const { cmd, host, plugins, insecureHttps, hostFile, parallel, failOn, outputFormat, watch, intervalMinutes, webhookUrl, alertSeverity, ports, compliance, complianceScope } = await parseArgs(process.argv);
|
|
760
806
|
|
|
807
|
+
// Version: handled before license verification so it works without a key.
|
|
808
|
+
// CE-0.1.30.1 — closes the discovery-flag UX gap where pre-fix
|
|
809
|
+
// `nsauditor-ai --version` errored with "Fatal: --host or --host-file
|
|
810
|
+
// is required". Output format mirrors GNU `--version` convention:
|
|
811
|
+
// "<tool> <version>" on a single line, then exit 0.
|
|
812
|
+
if (cmd === 'version') {
|
|
813
|
+
console.log(`nsauditor-ai ${TOOL_VERSION}`);
|
|
814
|
+
process.exit(0);
|
|
815
|
+
}
|
|
816
|
+
|
|
761
817
|
// Help: handled before license verification so it works without a key.
|
|
762
818
|
if (cmd === 'help') {
|
|
763
819
|
console.log(`nsauditor-ai — Modular AI-assisted network security audit platform
|
|
@@ -768,7 +824,8 @@ Usage:
|
|
|
768
824
|
nsauditor-ai license <subcommand>
|
|
769
825
|
nsauditor-ai security <subcommand>
|
|
770
826
|
nsauditor-ai validate
|
|
771
|
-
nsauditor-ai
|
|
827
|
+
nsauditor-ai version (or --version / -v)
|
|
828
|
+
nsauditor-ai help (or --help / -h)
|
|
772
829
|
|
|
773
830
|
Scan options:
|
|
774
831
|
--host, --ip, --target <h> Target host, IP, or CIDR
|
|
@@ -788,8 +845,13 @@ Scan options:
|
|
|
788
845
|
--compliance-scope <path> JSON file describing the assessment scope
|
|
789
846
|
|
|
790
847
|
License subcommands:
|
|
848
|
+
nsauditor-ai license install <KEY> Verify and persist a license key (Keychain
|
|
849
|
+
on macOS, ~/.nsauditor/.env on Linux/Windows
|
|
850
|
+
with mode 0600). Rejects invalid/expired keys.
|
|
791
851
|
nsauditor-ai license --status Show active tier, org, seats, expiry
|
|
792
852
|
nsauditor-ai license --capabilities List active capabilities for current tier
|
|
853
|
+
nsauditor-ai license --plugins List discovered plugins grouped by source
|
|
854
|
+
(CE / EE / custom) with active-or-required-tier
|
|
793
855
|
|
|
794
856
|
Security subcommands (macOS Keychain):
|
|
795
857
|
nsauditor-ai security set <KEY> Store a secret (read from stdin)
|
|
@@ -800,15 +862,24 @@ Security subcommands (macOS Keychain):
|
|
|
800
862
|
Environment:
|
|
801
863
|
NSAUDITOR_LICENSE_KEY Pro/Enterprise license JWT (env var; takes precedence)
|
|
802
864
|
NSA_ALLOW_ALL_HOSTS=1 Permit RFC1918 / loopback (local-network auditing)
|
|
803
|
-
CLOUD_PROVIDER=aws|gcp|azure Required for cloud scanner plugins (020/021/022)
|
|
865
|
+
CLOUD_PROVIDER=aws|gcp|azure Required for cloud scanner plugins (020/021/022/023/030)
|
|
804
866
|
AI_PROVIDER=openai|claude|ollama AI provider for report generation
|
|
805
867
|
COMPLIANCE_TSA_URL RFC 3161 timestamp authority for SOC 2 attestation
|
|
806
868
|
|
|
869
|
+
Cloud-scan hosts:
|
|
870
|
+
--host aws | gcp | azure Sentinel literals (case-insensitive). These are not
|
|
871
|
+
DNS-resolved; they route the scan to the matching
|
|
872
|
+
cloud-scanner plugin via the provider's control-plane
|
|
873
|
+
API. Pair with --plugins 020,030 (or similar) — using
|
|
874
|
+
--plugins all on a sentinel host produces noisy
|
|
875
|
+
unreachable-host findings from non-cloud plugins.
|
|
876
|
+
|
|
807
877
|
Examples:
|
|
808
878
|
nsauditor-ai scan --host 10.0.0.1 --plugins all
|
|
809
879
|
CLOUD_PROVIDER=aws AWS_PROFILE=default \\
|
|
810
|
-
nsauditor-ai scan --host aws --plugins 020
|
|
880
|
+
nsauditor-ai scan --host aws --plugins 020 --compliance soc2
|
|
811
881
|
nsauditor-ai scan --host 10.0.0.0/24 --plugins all --compliance soc2
|
|
882
|
+
nsauditor-ai license install enterprise_eyJhbGciOiJFUzI1NiIs...
|
|
812
883
|
nsauditor-ai license --status
|
|
813
884
|
|
|
814
885
|
Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/pricing/`);
|
|
@@ -848,8 +919,134 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
|
|
|
848
919
|
for (const [name, enabled] of Object.entries(caps)) {
|
|
849
920
|
console.log(` ${enabled ? '✓' : '✗'} ${name}`);
|
|
850
921
|
}
|
|
922
|
+
} else if (rawArgs.includes('--plugins')) {
|
|
923
|
+
// CE-0.1.30.3 — real enumeration of discovered plugins, grouped by
|
|
924
|
+
// source (CE / EE / custom NSAUDITOR_PLUGIN_PATH). Pre-fix this
|
|
925
|
+
// branch crashed with `TypeError: p.toLowerCase is not a function`
|
|
926
|
+
// (since hotfixed in 0.1.28 to a Usage fallback). Now: discover
|
|
927
|
+
// plugins, group by `_source`, format with active/required-tier
|
|
928
|
+
// status. Output format matches the EE README "Quick Start" example.
|
|
929
|
+
const tier = getTierFromEnv();
|
|
930
|
+
const caps = resolveCapabilities(tier);
|
|
931
|
+
const pm = await PluginManager.create(`${__dirname}/plugins`);
|
|
932
|
+
|
|
933
|
+
const groups = { ce: [], ee: [], custom: [] };
|
|
934
|
+
for (const plugin of pm.plugins) {
|
|
935
|
+
const source = plugin._source ?? 'ce';
|
|
936
|
+
if (!groups[source]) groups[source] = [];
|
|
937
|
+
groups[source].push(plugin);
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
const sourceLabels = {
|
|
941
|
+
ce: 'CE plugins (from nsauditor-ai)',
|
|
942
|
+
ee: 'EE plugins (from @nsasoft/nsauditor-ai-ee)',
|
|
943
|
+
custom: 'Custom plugins (from NSAUDITOR_PLUGIN_PATH)',
|
|
944
|
+
};
|
|
945
|
+
|
|
946
|
+
// Render in fixed order: ce → ee → custom (then any unknown sources alphabetical).
|
|
947
|
+
const renderOrder = ['ce', 'ee', 'custom', ...Object.keys(groups).filter((k) => !['ce','ee','custom'].includes(k)).sort()];
|
|
948
|
+
let totalRendered = 0;
|
|
949
|
+
for (const source of renderOrder) {
|
|
950
|
+
const plugins = groups[source];
|
|
951
|
+
if (!plugins || plugins.length === 0) continue;
|
|
952
|
+
if (totalRendered > 0) console.log('');
|
|
953
|
+
console.log(`${sourceLabels[source] ?? `${source} plugins`}:`);
|
|
954
|
+
|
|
955
|
+
// Sort by id (string-compare keeps zero-padded ids in numeric order).
|
|
956
|
+
const sorted = [...plugins].sort((a, b) =>
|
|
957
|
+
String(a.id ?? '').localeCompare(String(b.id ?? ''))
|
|
958
|
+
);
|
|
959
|
+
for (const plugin of sorted) {
|
|
960
|
+
const required = Array.isArray(plugin.requiredCapabilities) ? plugin.requiredCapabilities : [];
|
|
961
|
+
const allMet = required.length === 0 || required.every((c) => Boolean(caps[c]));
|
|
962
|
+
// Reviewer M2 fold: derive the required tier from the unmet
|
|
963
|
+
// capability set so the "requires: …" label is accurate even
|
|
964
|
+
// when the plugin doesn't declare a `tier` field. EE plugins
|
|
965
|
+
// 021/022/023 (no `tier` declaration) require `cloudScanners`
|
|
966
|
+
// which is enterprise-gated — pre-fold they showed
|
|
967
|
+
// "requires: pro" misleadingly. Now they show "requires:
|
|
968
|
+
// enterprise" via inferRequiredTier(). plugin.tier is the
|
|
969
|
+
// operator-declared override; fall back to inference.
|
|
970
|
+
const inferredTier = inferRequiredTier(required);
|
|
971
|
+
const requiresLabel = plugin.tier ?? inferredTier ?? 'pro';
|
|
972
|
+
const status = allMet ? '✓ active' : `✗ requires: ${requiresLabel}`;
|
|
973
|
+
// Layout matches the EE README example:
|
|
974
|
+
// " 003 SSH Scanner ✓ active"
|
|
975
|
+
const idStr = String(plugin.id ?? '?').padEnd(3);
|
|
976
|
+
const nameStr = String(plugin.name ?? '<unnamed>').padEnd(28);
|
|
977
|
+
console.log(` ${idStr} ${nameStr} ${status}`);
|
|
978
|
+
}
|
|
979
|
+
totalRendered += sorted.length;
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (totalRendered === 0) {
|
|
983
|
+
console.log('No plugins discovered. Re-install nsauditor-ai or check NSAUDITOR_PLUGIN_PATH.');
|
|
984
|
+
} else {
|
|
985
|
+
console.log('');
|
|
986
|
+
console.log(` ${totalRendered} plugin${totalRendered === 1 ? '' : 's'} total · current tier: ${tier}`);
|
|
987
|
+
}
|
|
988
|
+
} else if (rawArgs.includes('install')) {
|
|
989
|
+
// CE-0.1.30.4 — install command. Verify the JWT FIRST, then persist
|
|
990
|
+
// to a platform-appropriate location (macOS Keychain / file).
|
|
991
|
+
// Closes the customer-onboarding gap where new customers had no
|
|
992
|
+
// friendly way to register a license without manually editing
|
|
993
|
+
// shell-rc / .env files.
|
|
994
|
+
const installIdx = rawArgs.indexOf('install');
|
|
995
|
+
const rawKey = rawArgs[installIdx + 1];
|
|
996
|
+
const installKey = typeof rawKey === 'string' ? rawKey.trim() : '';
|
|
997
|
+
|
|
998
|
+
if (!installKey || installKey.startsWith('-')) {
|
|
999
|
+
console.error('Usage: nsauditor-ai license install <KEY>');
|
|
1000
|
+
console.error(' KEY is the JWT from your purchase confirmation (starts with `pro_` or `enterprise_`).');
|
|
1001
|
+
console.error(' Verifies the signature before persisting; invalid keys are rejected.');
|
|
1002
|
+
console.error('');
|
|
1003
|
+
console.error('Storage locations (chosen by platform):');
|
|
1004
|
+
console.error(' macOS: Keychain — service "nsauditor-ai", account "NSAUDITOR_LICENSE_KEY"');
|
|
1005
|
+
console.error(' Linux: $XDG_CONFIG_HOME/nsauditor/.env (or ~/.nsauditor/.env), mode 0600');
|
|
1006
|
+
console.error(' Windows: %USERPROFILE%\\.nsauditor\\.env (DPAPI integration on roadmap)');
|
|
1007
|
+
process.exit(2);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// 1. Verify the key BEFORE persisting. Bypasses the resolver chain
|
|
1011
|
+
// by passing the key explicitly to loadLicense().
|
|
1012
|
+
const verified = await loadLicense(installKey);
|
|
1013
|
+
if (!verified.valid) {
|
|
1014
|
+
console.error(`✗ License key rejected: ${verified.reason}`);
|
|
1015
|
+
console.error(' No changes made. Confirm the key matches your purchase email exactly.');
|
|
1016
|
+
process.exit(1);
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
// 2. Persist (Keychain on macOS / file elsewhere). Lazy import to
|
|
1020
|
+
// avoid loading the persistor unless install is actually invoked.
|
|
1021
|
+
const { persistLicenseKey } = await import('./utils/license.mjs');
|
|
1022
|
+
const persisted = await persistLicenseKey(installKey);
|
|
1023
|
+
if (!persisted.ok) {
|
|
1024
|
+
console.error(`✗ Verification succeeded but storage failed: ${persisted.error}`);
|
|
1025
|
+
console.error(' Fall-back: set NSAUDITOR_LICENSE_KEY env var manually:');
|
|
1026
|
+
console.error(' export NSAUDITOR_LICENSE_KEY="<your-key>"');
|
|
1027
|
+
process.exit(1);
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// 3. Confirm. Same shape as `license --status` so customers see
|
|
1031
|
+
// the persisted key reflected back. NEVER print the key value
|
|
1032
|
+
// itself — it's a secret. Reviewer M1 fold: surface the
|
|
1033
|
+
// Keychain-fallback warning if the persistor returned one
|
|
1034
|
+
// (silent fallback would leave macOS users believing they
|
|
1035
|
+
// have Keychain protection when they don't).
|
|
1036
|
+
const tierLabel = { ce: 'Community Edition (CE)', pro: 'Pro', enterprise: 'Enterprise' };
|
|
1037
|
+
if (persisted.warning) {
|
|
1038
|
+
console.warn(`⚠ ${persisted.warning}`);
|
|
1039
|
+
}
|
|
1040
|
+
console.log(`✓ ${tierLabel[verified.tier]} license installed`);
|
|
1041
|
+
console.log(` Stored at: ${persisted.location}`);
|
|
1042
|
+
console.log(` Org: ${verified.org}`);
|
|
1043
|
+
console.log(` Seats: ${verified.seats}`);
|
|
1044
|
+
console.log(` License ID: ${verified.licenseId}`);
|
|
1045
|
+
console.log(` Expires: ${verified.expiresAt}`);
|
|
1046
|
+
console.log('');
|
|
1047
|
+
console.log(' Verify with: nsauditor-ai license --status');
|
|
851
1048
|
} else {
|
|
852
|
-
console.log('Usage: nsauditor-ai license --status | --capabilities');
|
|
1049
|
+
console.log('Usage: nsauditor-ai license --status | --capabilities | --plugins | install <KEY>');
|
|
853
1050
|
}
|
|
854
1051
|
process.exit(0);
|
|
855
1052
|
}
|
package/package.json
CHANGED
package/utils/capabilities.mjs
CHANGED
|
@@ -51,3 +51,29 @@ export function resolveCapabilities(tier = 'ce') {
|
|
|
51
51
|
export function hasCapability(capabilities, cap) {
|
|
52
52
|
return Boolean(capabilities?.[cap]);
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
// CE-0.1.30.3 reviewer M2 fold: derive the highest tier among a plugin's
|
|
56
|
+
// required capabilities. Used by `license --plugins` to render
|
|
57
|
+
// "✗ requires: <tier>" accurately when a plugin doesn't declare a `tier`
|
|
58
|
+
// field. Pre-fold the CLI fell back to `'pro'` for plugins like 021/022/023
|
|
59
|
+
// (no `tier` field, but require `cloudScanners` which is enterprise-gated)
|
|
60
|
+
// — which misled customers about what license they needed.
|
|
61
|
+
//
|
|
62
|
+
// Returns 'ce' / 'pro' / 'enterprise' (the highest tier among all required
|
|
63
|
+
// caps), or null when the plugin has no requiredCapabilities.
|
|
64
|
+
const _TIER_RANK = { ce: 0, pro: 1, enterprise: 2 };
|
|
65
|
+
const _RANK_TO_TIER = ['ce', 'pro', 'enterprise'];
|
|
66
|
+
|
|
67
|
+
export function inferRequiredTier(requiredCapabilities) {
|
|
68
|
+
if (!Array.isArray(requiredCapabilities) || requiredCapabilities.length === 0) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
let maxRank = 0;
|
|
72
|
+
for (const cap of requiredCapabilities) {
|
|
73
|
+
const def = CAPABILITIES[cap];
|
|
74
|
+
if (!def) continue;
|
|
75
|
+
const rank = _TIER_RANK[def.tier] ?? 0;
|
|
76
|
+
if (rank > maxRank) maxRank = rank;
|
|
77
|
+
}
|
|
78
|
+
return _RANK_TO_TIER[maxRank];
|
|
79
|
+
}
|
package/utils/license.mjs
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
// become invalid. See license-manager docs/architecture.md for full procedure.
|
|
8
8
|
|
|
9
9
|
import { jwtVerify, importSPKI } from 'jose';
|
|
10
|
+
import { promises as fsp } from 'node:fs';
|
|
11
|
+
import { homedir, platform } from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import dotenv from 'dotenv';
|
|
14
|
+
import { keychainGet, keychainSet } from './keychain.mjs';
|
|
10
15
|
|
|
11
16
|
// ES256 public key — embedded directly so it works in npm package (no file read).
|
|
12
17
|
// Corresponding private key is in the license-manager service (NEVER shipped here).
|
|
@@ -41,6 +46,229 @@ export function getTierFromEnv() {
|
|
|
41
46
|
return 'ce';
|
|
42
47
|
}
|
|
43
48
|
|
|
49
|
+
/**
|
|
50
|
+
* CE-0.1.30.2 — multi-source license-key resolution chain.
|
|
51
|
+
*
|
|
52
|
+
* Resolution order (first non-empty wins):
|
|
53
|
+
* 1. process.env.NSAUDITOR_LICENSE_KEY — CI/CD takes precedence
|
|
54
|
+
* 2. macOS Keychain (service=nsauditor-ai, account=NSAUDITOR_LICENSE_KEY)
|
|
55
|
+
* — set by `nsauditor-ai license install <KEY>` on macOS
|
|
56
|
+
* 3. $XDG_CONFIG_HOME/nsauditor/.env (or ~/.nsauditor/.env) — universal
|
|
57
|
+
* file fallback; set by `license install` on Linux/Windows OR
|
|
58
|
+
* manually edited by the operator. Mode 0600 expected; permissive
|
|
59
|
+
* mode triggers a warning (still loaded — operator's choice).
|
|
60
|
+
*
|
|
61
|
+
* @param {object} [opts]
|
|
62
|
+
* @param {string} [opts._homeFileOverride] — test seam. Path to a .env-format
|
|
63
|
+
* file to read instead of ~/.nsauditor/.env. Bypasses XDG resolution.
|
|
64
|
+
* @param {Function} [opts._keychainGet] — test seam. Replaces the macOS
|
|
65
|
+
* Keychain reader.
|
|
66
|
+
* @returns {Promise<string|null>} The license key string, or null if no
|
|
67
|
+
* source had one.
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveLicenseKey(opts = {}) {
|
|
70
|
+
// 1. env var
|
|
71
|
+
if (process.env.NSAUDITOR_LICENSE_KEY) return process.env.NSAUDITOR_LICENSE_KEY;
|
|
72
|
+
|
|
73
|
+
// 2. platform secret store (macOS Keychain today; Linux/Windows skip)
|
|
74
|
+
const kget = opts._keychainGet ?? keychainGet;
|
|
75
|
+
try {
|
|
76
|
+
const fromKeychain = await kget('NSAUDITOR_LICENSE_KEY');
|
|
77
|
+
if (fromKeychain) return fromKeychain;
|
|
78
|
+
} catch { /* keychain unavailable — fall through */ }
|
|
79
|
+
|
|
80
|
+
// 3. ~/.nsauditor/.env (or $XDG_CONFIG_HOME/nsauditor/.env)
|
|
81
|
+
const filePath = opts._homeFileOverride ?? defaultLicenseFilePath();
|
|
82
|
+
try {
|
|
83
|
+
const stat = await fsp.stat(filePath);
|
|
84
|
+
// Warn (non-fatal) if mode is more permissive than 0600 on POSIX.
|
|
85
|
+
// Windows has no concept of POSIX file mode; skip the check there.
|
|
86
|
+
// Reviewer M10 fold: only warn once per path per process to avoid
|
|
87
|
+
// spamming the console under repeated CLI invocations.
|
|
88
|
+
if (platform() !== 'win32' && stat.isFile() && (stat.mode & 0o077) !== 0) {
|
|
89
|
+
if (!_permissiveWarnedPaths.has(filePath)) {
|
|
90
|
+
const modeStr = (stat.mode & 0o777).toString(8).padStart(3, '0');
|
|
91
|
+
console.warn(`⚠ ${filePath} has permissive mode ${modeStr} — recommend chmod 0600`);
|
|
92
|
+
_permissiveWarnedPaths.add(filePath);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const buf = await fsp.readFile(filePath, 'utf8');
|
|
96
|
+
const parsed = dotenv.parse(buf);
|
|
97
|
+
if (parsed.NSAUDITOR_LICENSE_KEY) return parsed.NSAUDITOR_LICENSE_KEY;
|
|
98
|
+
} catch { /* file missing / unreadable — fall through */ }
|
|
99
|
+
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// CE-0.1.30.2 reviewer M10: one-shot permissive-mode warning per path per
|
|
104
|
+
// process. Without this, every `loadLicense()` call against a 0644 file
|
|
105
|
+
// emits a console.warn — and CE re-resolves on every CLI invocation
|
|
106
|
+
// (plus the `cmd === 'license'` branch double-calls today, see reviewer
|
|
107
|
+
// M7 follow-up). Module-scoped Set keeps memory bounded (~1 path per
|
|
108
|
+
// install) and silences the noise without hiding the message from
|
|
109
|
+
// operators who haven't seen it yet.
|
|
110
|
+
const _permissiveWarnedPaths = new Set();
|
|
111
|
+
|
|
112
|
+
function defaultLicenseFilePath() {
|
|
113
|
+
// Honor $XDG_CONFIG_HOME (Linux convention; some macOS users set it).
|
|
114
|
+
// Falls back to ~/.nsauditor/.env which is what the existing README
|
|
115
|
+
// method-2 docs already promise.
|
|
116
|
+
if (process.env.XDG_CONFIG_HOME) {
|
|
117
|
+
return join(process.env.XDG_CONFIG_HOME, 'nsauditor', '.env');
|
|
118
|
+
}
|
|
119
|
+
return join(homedir(), '.nsauditor', '.env');
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* CE-0.1.30.4 — persist a verified license key to a platform-appropriate
|
|
124
|
+
* location so subsequent `loadLicense()` calls find it via the resolver
|
|
125
|
+
* chain (CE-0.1.30.2).
|
|
126
|
+
*
|
|
127
|
+
* Platform routing:
|
|
128
|
+
* - macOS: keychainSet('NSAUDITOR_LICENSE_KEY', key) via the existing
|
|
129
|
+
* utils/keychain.mjs helper. If Keychain is unavailable
|
|
130
|
+
* (e.g., headless mac without `security` daemon, user
|
|
131
|
+
* denied the prompt), falls back to the file path so
|
|
132
|
+
* install does not silently fail.
|
|
133
|
+
* - Linux: write $XDG_CONFIG_HOME/nsauditor/.env (or ~/.nsauditor/.env)
|
|
134
|
+
* with mode 0600 (parent dir 0700). Preserves any OTHER
|
|
135
|
+
* env-vars the operator has in the file — only the
|
|
136
|
+
* NSAUDITOR_LICENSE_KEY line is replaced/added.
|
|
137
|
+
* - Windows: same file path (%USERPROFILE%\.nsauditor\.env). No DPAPI
|
|
138
|
+
* yet — defer to a future release; the file ACL inherits
|
|
139
|
+
* from the user profile which is typically restrictive
|
|
140
|
+
* enough on Windows 10+.
|
|
141
|
+
*
|
|
142
|
+
* The caller MUST verify the key via loadLicense() BEFORE calling
|
|
143
|
+
* persistLicenseKey() — this function does not validate. Persisting an
|
|
144
|
+
* invalid or expired key would store garbage and the next loadLicense()
|
|
145
|
+
* call would just reject it again, but doing so silently undermines the
|
|
146
|
+
* `install` command's contract ("we only persist verified keys").
|
|
147
|
+
*
|
|
148
|
+
* @param {string} key - The full prefixed key (`pro_eyJ...` or `enterprise_eyJ...`).
|
|
149
|
+
* @param {object} [opts]
|
|
150
|
+
* @param {string} [opts._platform] - Test seam (override Node's platform()).
|
|
151
|
+
* @param {Function} [opts._keychainSet] - Test seam (replace Keychain writer).
|
|
152
|
+
* @param {string} [opts._homeFileOverride] - Test seam (override the file path).
|
|
153
|
+
* @returns {Promise<{ok: true, location: string} | {ok: false, error: string}>}
|
|
154
|
+
* On success, `location` is a human-readable string ("macOS Keychain
|
|
155
|
+
* (service=nsauditor-ai)" or the filesystem path). The `install`
|
|
156
|
+
* command surfaces this so the operator knows where the key landed.
|
|
157
|
+
*/
|
|
158
|
+
export async function persistLicenseKey(key, opts = {}) {
|
|
159
|
+
if (typeof key !== 'string' || key.length === 0) {
|
|
160
|
+
return { ok: false, error: 'persistLicenseKey: key must be a non-empty string' };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const plat = opts._platform ?? platform();
|
|
164
|
+
const kset = opts._keychainSet ?? keychainSet;
|
|
165
|
+
|
|
166
|
+
// 1. macOS: try Keychain first.
|
|
167
|
+
// Reviewer M1 fold: track Keychain-fallback reason so the caller can
|
|
168
|
+
// surface it to the operator. Pre-fix, the fallback was silent — the
|
|
169
|
+
// operator only learned about it implicitly via the `location` line
|
|
170
|
+
// showing a filesystem path instead of "macOS Keychain (...)".
|
|
171
|
+
let keychainFallbackReason = null;
|
|
172
|
+
if (plat === 'darwin') {
|
|
173
|
+
try {
|
|
174
|
+
await kset('NSAUDITOR_LICENSE_KEY', key);
|
|
175
|
+
return { ok: true, location: 'macOS Keychain (service=nsauditor-ai)' };
|
|
176
|
+
} catch (err) {
|
|
177
|
+
// Fall through to file-based storage. Examples: `security` daemon
|
|
178
|
+
// unavailable on headless mac; user denied the Keychain prompt;
|
|
179
|
+
// SIP-restricted environment.
|
|
180
|
+
keychainFallbackReason = err && err.message ? err.message : String(err);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 2. File-based storage (Linux, Windows, macOS Keychain fallback).
|
|
185
|
+
try {
|
|
186
|
+
const filePath = opts._homeFileOverride ?? defaultLicenseFilePath();
|
|
187
|
+
const dir = dirname(filePath);
|
|
188
|
+
// Create dir with 0700 if missing (recursive=true is a no-op if it
|
|
189
|
+
// already exists; mode is only applied to NEW dirs along the path).
|
|
190
|
+
await fsp.mkdir(dir, { recursive: true, mode: 0o700 });
|
|
191
|
+
|
|
192
|
+
// Preserve other env-vars in an existing file. Read-modify-write
|
|
193
|
+
// pattern: parse current contents, replace/add the
|
|
194
|
+
// NSAUDITOR_LICENSE_KEY line, write back.
|
|
195
|
+
let existingContent = '';
|
|
196
|
+
try {
|
|
197
|
+
existingContent = await fsp.readFile(filePath, 'utf8');
|
|
198
|
+
} catch { /* missing file — fine, we'll create one */ }
|
|
199
|
+
|
|
200
|
+
const newContent = mergeLicenseIntoEnvFile(existingContent, key);
|
|
201
|
+
await fsp.writeFile(filePath, newContent, { mode: 0o600 });
|
|
202
|
+
|
|
203
|
+
// Re-chmod in case the file pre-existed with a more permissive mode
|
|
204
|
+
// (writeFile only sets mode on CREATE, not overwrite).
|
|
205
|
+
if (plat !== 'win32') {
|
|
206
|
+
await fsp.chmod(filePath, 0o600);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const result = { ok: true, location: filePath };
|
|
210
|
+
if (keychainFallbackReason !== null) {
|
|
211
|
+
result.warning =
|
|
212
|
+
`macOS Keychain unavailable (${keychainFallbackReason}); fell back to file storage. ` +
|
|
213
|
+
`Re-run after granting Keychain access for stronger protection.`;
|
|
214
|
+
}
|
|
215
|
+
return result;
|
|
216
|
+
} catch (err) {
|
|
217
|
+
return { ok: false, error: err.message };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Merge a license key into the existing dotenv-format file content,
|
|
223
|
+
* preserving every OTHER line. If a NSAUDITOR_LICENSE_KEY line already
|
|
224
|
+
* exists, replace it; otherwise append. If the file was empty/new,
|
|
225
|
+
* write a header comment.
|
|
226
|
+
*
|
|
227
|
+
* Reviewer M2 / M2b folds:
|
|
228
|
+
* - **Multi-occurrence safety**: a corrupted file with TWO+ existing
|
|
229
|
+
* `NSAUDITOR_LICENSE_KEY=` lines previously had only the first
|
|
230
|
+
* replaced. dotenv parses last-wins, so `--status` would show the
|
|
231
|
+
* OLD value while install reported success. Now we replace the first
|
|
232
|
+
* occurrence and remove the rest, collapsing blank lines.
|
|
233
|
+
* - **CRLF preservation**: the regex anchors on `[ \t]*` (not `\s*`,
|
|
234
|
+
* which matches `\r`) and the value-tail uses `[^\r\n]*` so Windows-
|
|
235
|
+
* style line endings are not mangled. The replacement line itself
|
|
236
|
+
* uses `\n` regardless — Notepad and dotenv both accept mixed
|
|
237
|
+
* endings, but this avoids producing them from `\r`-stripped tails.
|
|
238
|
+
*
|
|
239
|
+
* Exported for test coverage of the merge semantics specifically.
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
export function mergeLicenseIntoEnvFile(existingContent, key) {
|
|
243
|
+
const KEY_LINE_RE = /^[ \t]*NSAUDITOR_LICENSE_KEY[ \t]*=[^\r\n]*$/gm;
|
|
244
|
+
const newLine = `NSAUDITOR_LICENSE_KEY=${key}`;
|
|
245
|
+
|
|
246
|
+
// Count matches (regex needs the global flag for matchAll-equivalence).
|
|
247
|
+
const matches = existingContent.match(KEY_LINE_RE);
|
|
248
|
+
if (matches && matches.length > 0) {
|
|
249
|
+
// Replace the first occurrence in place; remove any duplicates
|
|
250
|
+
// (corrupted-file defense). Collapse the blank lines that result.
|
|
251
|
+
let firstReplaced = false;
|
|
252
|
+
let merged = existingContent.replace(KEY_LINE_RE, () => {
|
|
253
|
+
if (firstReplaced) return '__NSAUDITOR_PURGE__'; // sentinel for removal
|
|
254
|
+
firstReplaced = true;
|
|
255
|
+
return newLine;
|
|
256
|
+
});
|
|
257
|
+
// Drop sentinel lines + their trailing newline.
|
|
258
|
+
merged = merged.replace(/__NSAUDITOR_PURGE__\r?\n?/g, '');
|
|
259
|
+
return merged;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (existingContent.trim().length === 0) {
|
|
263
|
+
// Empty/new file — write a header.
|
|
264
|
+
return `# NSAuditor AI license — set via \`nsauditor-ai license install\`\n${newLine}\n`;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// Append to existing content (with a separating newline if needed).
|
|
268
|
+
const sep = existingContent.endsWith('\n') ? '' : '\n';
|
|
269
|
+
return `${existingContent}${sep}${newLine}\n`;
|
|
270
|
+
}
|
|
271
|
+
|
|
44
272
|
/**
|
|
45
273
|
* Full async JWT verification. Call once at startup.
|
|
46
274
|
* On success, caches verified tier so subsequent getTierFromEnv() calls
|
|
@@ -48,12 +276,17 @@ export function getTierFromEnv() {
|
|
|
48
276
|
*
|
|
49
277
|
* Never throws — degrades to 'ce' on any failure.
|
|
50
278
|
*
|
|
51
|
-
* @param {string} [keyStr] - License key;
|
|
279
|
+
* @param {string} [keyStr] - License key; if omitted, runs the multi-source
|
|
280
|
+
* resolution chain (env var → Keychain → ~/.nsauditor/.env). See
|
|
281
|
+
* resolveLicenseKey() above.
|
|
52
282
|
* @returns {Promise<{valid: boolean, tier: string, org?: string, seats?: number,
|
|
53
283
|
* licenseId?: string, capabilities?: string[], expiresAt?: string, reason?: string}>}
|
|
54
284
|
*/
|
|
55
285
|
export async function loadLicense(keyStr) {
|
|
56
|
-
|
|
286
|
+
// Explicit keyStr argument wins (preserves the existing behavior for
|
|
287
|
+
// callers like the `license --status` subcommand which passes the env
|
|
288
|
+
// var directly). When omitted, run the multi-source resolution chain.
|
|
289
|
+
const raw = keyStr ?? (await resolveLicenseKey());
|
|
57
290
|
if (!raw) return { valid: false, tier: 'ce', reason: 'no key provided' };
|
|
58
291
|
|
|
59
292
|
// Strip tier prefix
|
|
@@ -124,4 +357,5 @@ export function _resetCache() {
|
|
|
124
357
|
throw new Error('_resetCache is test-only and disabled in production');
|
|
125
358
|
}
|
|
126
359
|
_verifiedTier = null;
|
|
360
|
+
_permissiveWarnedPaths.clear();
|
|
127
361
|
}
|
package/utils/oui.mjs
CHANGED
|
@@ -6,11 +6,19 @@ import { fileURLToPath } from "url";
|
|
|
6
6
|
|
|
7
7
|
let OUI_DB = null;
|
|
8
8
|
|
|
9
|
+
// CE-0.1.30.3 reviewer M1 fold: gate startup chatter behind NSA_VERBOSE so
|
|
10
|
+
// operator-facing flags like `nsauditor-ai license --plugins` and
|
|
11
|
+
// `nsauditor-ai --version` do not pollute stdout with debug banners.
|
|
12
|
+
// Errors remain unconditional (console.error, stderr) — the operator
|
|
13
|
+
// should still see real failure modes.
|
|
14
|
+
const VERBOSE = /^(1|true|yes|on)$/i.test(String(process.env.NSA_VERBOSE || ''));
|
|
15
|
+
const vlog = VERBOSE ? (...a) => console.log(...a) : () => {};
|
|
16
|
+
|
|
9
17
|
// Try to import oui-data module, fall back to local file if it fails
|
|
10
18
|
async function loadOuiData() {
|
|
11
19
|
try {
|
|
12
20
|
const ouiData = await import("oui-data", { with: { type: "json" } }).then(module => module.default);
|
|
13
|
-
|
|
21
|
+
vlog("[oui.mjs] Successfully loaded oui-data module with", Object.keys(ouiData).length, "entries");
|
|
14
22
|
return ouiData;
|
|
15
23
|
} catch (e) {
|
|
16
24
|
console.error("[oui.mjs] Failed to load oui-data module:", e.message);
|
|
@@ -19,7 +27,7 @@ async function loadOuiData() {
|
|
|
19
27
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
20
28
|
const localPath = path.join(__dirname, "oui-data.json");
|
|
21
29
|
const data = JSON.parse(await fs.readFile(localPath, "utf8"));
|
|
22
|
-
|
|
30
|
+
vlog("[oui.mjs] Loaded fallback oui-data.json from", localPath, "with", Object.keys(data).length, "entries");
|
|
23
31
|
return data;
|
|
24
32
|
} catch (fallbackError) {
|
|
25
33
|
console.error("[oui.mjs] Failed to load fallback oui-data.json:", fallbackError.message);
|
|
@@ -65,7 +73,7 @@ export function lookupVendor(mac) {
|
|
|
65
73
|
const oui = macToOUI(mac);
|
|
66
74
|
const entry = OUI_DB[oui];
|
|
67
75
|
const vendor = pickOrgName(entry);
|
|
68
|
-
|
|
76
|
+
vlog(`[oui.mjs] Lookup for MAC ${mac} (OUI: ${oui}) -> Vendor: ${vendor || 'Not found'}`);
|
|
69
77
|
return vendor;
|
|
70
78
|
} catch (e) {
|
|
71
79
|
console.error("[oui.mjs] Lookup error:", e.message);
|