nsauditor-ai 0.2.23 → 0.2.25
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 +33 -4
- package/cli.mjs +22 -3
- package/package.json +1 -1
- package/plugin_manager.mjs +28 -1
- package/utils/env_loader.mjs +65 -18
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ NSAuditor AI is the open-source core of a privacy-first security intelligence pl
|
|
|
17
17
|
|
|
18
18
|
## What's New
|
|
19
19
|
|
|
20
|
-
**
|
|
20
|
+
**Enterprise GRC push activation + AWS positive-substrate evidence uplift.** The Enterprise **Vanta** push connector is now wired for scan-time activation, a **Drata** connector library ships alongside it (Secureframe on the roadmap), and AWS PASS-tier findings now surface as positive substrate evidence in the compliance RoC. See the **[GRC Connectors](#grc-connectors-vanta--drata)** section below, and **[CHANGELOG.md](./CHANGELOG.md)** for the full per-release history.
|
|
21
21
|
|
|
22
22
|
→ 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)
|
|
23
23
|
|
|
@@ -58,7 +58,7 @@ If you're heading into a **SOC 2, HIPAA, NIST CSF 2.0, PCI DSS, ISO 27001, CIS C
|
|
|
58
58
|
- **GDPR Article 32 (Security of Processing)** (Regulation (EU) 2016/679) — **4 covered + 5 partial + 2 OOS across 11 Art. 32 sub-measure units**; **GDPR Article 32 infrastructure substrate only — NOT GDPR compliance** (GDPR is a 99-article legal regime; Art. 32 security-of-processing is the only article an infrastructure scanner can substrate-evidence; the rest is operator-side, out of scope by design). Four-factor proportionality (substrate *for* your "appropriate to the risk" determination, never an absolute pass/fail); personal-data-scope attestation (pair with your Art. 30 records of processing); **Art. 83(4) lower fine tier** (€10M/2%, not the €20M/4% headline tier); Art. 32(3)/Art. 42 cloud-provider certification-inheritance
|
|
59
59
|
- 🔐 **Cryptographically signed evidence** — SHA-256 chain-of-custody + RFC 3161 trusted timestamps + Ed25519 suppression signing. Non-repudiation, not just integrity. Auditors can verify offline.
|
|
60
60
|
- 🏛️ **Zero Data Exfiltration architecture** — your scan data never leaves your infrastructure. Air-gapped deployment supported. AI analysis happens locally (Ollama) or via your own API keys. Important for PCI DSS CDE-isolation threat models.
|
|
61
|
-
- 🔗 **
|
|
61
|
+
- 🔗 **GRC connectors — Vanta + Drata (Enterprise)** — map compliance findings to your GRC platform's evidence/test records and push them at **scan time** (opt-in). Suppression-aware outcome mapping, idempotent retries, rate-limit handling, token redaction, and Zero-Data-Exfiltration egress redaction. Secureframe on the roadmap; live validation against production tenants is in progress. See **[GRC Connectors](#grc-connectors-vanta--drata)** below.
|
|
62
62
|
- 🗄️ **WORM evidence storage** — S3 Object Lock COMPLIANCE-mode for SEC Rule 17a-4(f) / FINRA 4511 retention compliance
|
|
63
63
|
- 📊 **SLA / MTTR tracking + recurring-scan attestation** — the **Type II operating-effectiveness evidence** auditors actually demand (not just point-in-time snapshots)
|
|
64
64
|
- 🎯 **11 adversarial-audit Claude Code skills** authored per the Per-Framework Adversarial-Audit Skill Pairing institutional pattern — Phase-4 Compliance/GRC chain 8-of-8 COMPLETE for all shipped frameworks (SOC 2 + HIPAA + NIST CSF + PCI DSS + ISO 27001 + CIS Controls v8 + GDPR Article 32 + GRC connector)
|
|
@@ -110,7 +110,7 @@ How Marketplace fulfillment works (ZDE and air-gap preserved — no runtime AWS
|
|
|
110
110
|
| Recurring-scan attestation (Type II operating-effectiveness) | — | — | ✅ |
|
|
111
111
|
| WORM evidence storage (S3 Object Lock — SEC 17a-4 / FINRA 4511) | — | — | ✅ |
|
|
112
112
|
| **Enterprise — integration + deployment** | | | |
|
|
113
|
-
| GRC
|
|
113
|
+
| GRC connectors — Vanta + Drata push (scan-time, opt-in); Secureframe planned | — | — | ✅ |
|
|
114
114
|
| Tabletop simulation + SIEM correlation | — | — | ✅ |
|
|
115
115
|
| Docker per-scan isolation | — | — | ✅ |
|
|
116
116
|
| Air-gapped deployment | — | — | ✅ |
|
|
@@ -121,6 +121,35 @@ How Marketplace fulfillment works (ZDE and air-gap preserved — no runtime AWS
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
+
## GRC Connectors (Vanta & Drata)
|
|
125
|
+
|
|
126
|
+
*Enterprise feature. Requires `@nsasoft/nsauditor-ai-ee`.*
|
|
127
|
+
|
|
128
|
+
Every compliance scan already produces a GRC-ready JSON evidence artifact. The **GRC connectors** take the next step: they map each NSAuditor compliance finding to your GRC platform's own evidence/test records and **push them at scan time** — so your Vanta or Drata workspace reflects the latest cloud posture without a manual export/import round-trip.
|
|
129
|
+
|
|
130
|
+
**Opt-in, and Zero-Data-Exfiltration by default.** The push is off unless you set the environment variables below. When it runs, egress is redaction-gated: resource identifiers can be hashed or removed, the persisted audit log stores a body **fingerprint** (never the raw payload), and your API token is never written to any artifact. Nothing leaves your infrastructure that you didn't opt into.
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Enable the scan-time push (Enterprise)
|
|
134
|
+
COMPLIANCE_GRC_PROVIDER=vanta # or: drata
|
|
135
|
+
COMPLIANCE_GRC_TOKEN=<your API key> # never serialized to artifacts
|
|
136
|
+
# Optional:
|
|
137
|
+
# COMPLIANCE_GRC_REDACTION=hash # off | hash | remove (egress identifier redaction)
|
|
138
|
+
# COMPLIANCE_GRC_CONTROL_MAP=/path/to/config.json # provider config: Vanta control→test map, or Drata connection ({connectionId, resourceId, schemaMap})
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
| Platform | Status | Model |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| **Vanta** | Connector + scan-time activation shipped | Maps findings to Vanta test results; suppression-aware outcome mapping (pass / fail / passed-with-compensating-control), framework-dimensioned idempotency keys, retry with rate-limit backoff, circuit breaker |
|
|
144
|
+
| **Drata** | Connector library shipped | Pushes structured records via Drata **Custom Connections**; your Drata **Test Builder** rules (Advanced/Enterprise plans) evaluate them — the connector delivers evidence, your rules do the evaluation |
|
|
145
|
+
| **Secureframe** | On the roadmap | — |
|
|
146
|
+
|
|
147
|
+
**Reliability + audit-integrity built in:** idempotent retries (a network-timed-out push won't create duplicate records), per-attempt + total-duration timeout caps, a consecutive-failure circuit breaker, token redaction across every log and error path, and a durable per-control push audit log written next to your scan artifacts.
|
|
148
|
+
|
|
149
|
+
> **Honest status.** The Vanta and Drata connectors are shipped, opt-in, and covered by an extensive test suite. **Live validation against production Vanta / Drata tenants is in progress** as partner onboarding proceeds — until it completes, treat production use as early-access and validate against your own tenant first. This is a single-workspace, operator-configured connector; it is not a multi-tenant managed sync.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
124
153
|
## Quick Start
|
|
125
154
|
|
|
126
155
|
```bash
|
|
@@ -277,7 +306,7 @@ Results land in `./out/<host>_<timestamp>/`:
|
|
|
277
306
|
| — | **GDPR Article 32 Compliance Engine (NEW EE 0.20.0)** | Enterprise | **GDPR Article 32 (Security of Processing)** infrastructure substrate (Regulation (EU) 2016/679) — **4 covered + 5 partial + 2 OOS across 11 Art. 32 sub-measure units** (the 7th framework). **This is GDPR Article 32 infrastructure substrate ONLY — NOT GDPR compliance.** GDPR is a 99-article legal regime; Art. 32 security-of-processing is the only article whose evidence is technical infrastructure state, so the rest of GDPR (lawful basis, consent, DSARs, records of processing, DPIAs, transfers) is operator-side and out of scope by design. **Four-factor proportionality** — Art. 32 measures are "appropriate to the risk" taking into account state-of-the-art / cost / nature-scope-context-purposes / risk; nothing is an absolute pass/fail, the engine produces substrate *for* the operator's determination. **Personal-data-scope attestation** — the scanner reads configuration, not data classification; a finding is an Art. 32 concern only if the resource processes personal data (pair with your Art. 30 records of processing). Controller-vs-processor role applicability + Art. 28 processor agreements. **Art. 83(4) lower fine tier** — Art. 32 infringements cap at €10M or 2% of turnover, NOT the €20M / 4% Art. 83(5) headline tier (which is for the basic principles + data-subject rights). Art. 32(3)/Art. 42 cloud-provider certification-inheritance (ISO 27001 / SOC 2 / C5 / EU Cloud CoC adherence as an element of demonstrable compliance, not a substitute). Use `--compliance gdpr` or any combination for multi-framework reports from a single scan. |
|
|
278
307
|
| — | SLA & MTTR Tracking | Enterprise | Per-severity SLA targets, compensating-control flow, finding lifecycle, Type II rolling-quarter cadence. |
|
|
279
308
|
| — | Recurring-Scan Attestation | Enterprise | Multi-scan chronological matrix, cadence gap detection, scope-drift surface (CC8.1). |
|
|
280
|
-
| — | GRC Platform Connector | Enterprise |
|
|
309
|
+
| — | GRC Platform Connector | Enterprise | Vanta + Drata connectors (Secureframe planned) — scan-time push (opt-in), retry/backoff, deterministic idempotency, rate-limit handling, circuit breaker, foreign-token detection, ZDE egress redaction. See [GRC Connectors](#grc-connectors-vanta--drata). |
|
|
281
310
|
| — | WORM Evidence Storage | Enterprise | S3 Object Lock COMPLIANCE-mode + resource redaction + SHA-256 manifest. SEC 17a-4 / FINRA 4511 retention-compatible. |
|
|
282
311
|
| — | Tabletop Simulation | Enterprise | Probe-event manifest + SIEM detection correlation, configurable coverage bands (Type II / High-Assurance presets). |
|
|
283
312
|
|
package/cli.mjs
CHANGED
|
@@ -912,7 +912,8 @@ Usage:
|
|
|
912
912
|
|
|
913
913
|
Scan options:
|
|
914
914
|
--host, --ip, --target <h> Target host, IP, or CIDR
|
|
915
|
-
--host-file <path> File with one host per line
|
|
915
|
+
--host-file <path> File with one host per line (cloud sentinels in the
|
|
916
|
+
file reconcile CLOUD_PROVIDER the same as --host)
|
|
916
917
|
--env <path> Load a dotenv (KEY=value) file for this scan (per-account
|
|
917
918
|
credentials). Override-on; missing file = hard error.
|
|
918
919
|
--aws-profile <name> Use a named profile from the OS-default ~/.aws/credentials.
|
|
@@ -984,7 +985,11 @@ Cloud-scan hosts:
|
|
|
984
985
|
characters — your shell treats | as a pipe. Sentinels are
|
|
985
986
|
not DNS-resolved; they route the scan to the matching
|
|
986
987
|
cloud-scanner plugins via the provider's control-plane
|
|
987
|
-
API, and imply CLOUD_PROVIDER
|
|
988
|
+
API, and imply CLOUD_PROVIDER to the cloud leg(s) when
|
|
989
|
+
unset ('aws,gcp,azure' → CLOUD_PROVIDER=aws,gcp,azure).
|
|
990
|
+
A CLOUD_PROVIDER already pinned to a cloud that does NOT
|
|
991
|
+
cover every requested leg is a hard error (fail-fast),
|
|
992
|
+
not a silent skip of the uncovered legs. With
|
|
988
993
|
--plugins all the scan AUTO-SCOPES to only that cloud's
|
|
989
994
|
plugins (plugins not applicable to this host are skipped +
|
|
990
995
|
logged — other-cloud plugins run on their OWN --host pass,
|
|
@@ -1028,6 +1033,16 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
|
|
|
1028
1033
|
console.error('Error: --aws-profile requires a profile name (e.g. --aws-profile prod)');
|
|
1029
1034
|
process.exit(2);
|
|
1030
1035
|
}
|
|
1036
|
+
// Resolve the scan host list up-front (scan command only) so the CLOUD_PROVIDER
|
|
1037
|
+
// reconcile below (resolveScanEnv) sees --host-file cloud-sentinel legs too. With
|
|
1038
|
+
// --host-file the `host` arg is undefined, so without this a host-file of cloud
|
|
1039
|
+
// sentinels would bypass the fail-fast/imply reconcile entirely. Reused as the
|
|
1040
|
+
// scan target below (no double-parse); a parse failure is left to the scan-time
|
|
1041
|
+
// resolution so the existing error path + exit code are unchanged.
|
|
1042
|
+
let resolvedHosts = null;
|
|
1043
|
+
if (cmd === 'scan' && hostFile) {
|
|
1044
|
+
try { resolvedHosts = await parseHostFile(hostFile); } catch { /* defer to scan-time resolution */ }
|
|
1045
|
+
}
|
|
1031
1046
|
try {
|
|
1032
1047
|
const { resolveScanEnv } = await import('./utils/env_loader.mjs');
|
|
1033
1048
|
const fsm = await import('node:fs');
|
|
@@ -1035,6 +1050,7 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
|
|
|
1035
1050
|
envPath: typeof args.env === 'string' ? args.env : undefined,
|
|
1036
1051
|
awsProfile: typeof args.awsProfile === 'string' ? args.awsProfile : undefined,
|
|
1037
1052
|
host,
|
|
1053
|
+
hosts: resolvedHosts ?? undefined,
|
|
1038
1054
|
env: process.env,
|
|
1039
1055
|
fileExists: (p) => fsm.existsSync(p),
|
|
1040
1056
|
readFile: (p) => fsm.readFileSync(p, 'utf8'),
|
|
@@ -1912,7 +1928,10 @@ Docs: https://www.nsauditor.com/ai/ | Pricing: https://www.nsauditor.com/ai/
|
|
|
1912
1928
|
// Resolve host list
|
|
1913
1929
|
let hosts;
|
|
1914
1930
|
if (hostFile) {
|
|
1915
|
-
|
|
1931
|
+
// Reuse the up-front resolution done for the CLOUD_PROVIDER reconcile (avoids a
|
|
1932
|
+
// second parse + duplicate suspicious-line warnings); re-parse only if that was
|
|
1933
|
+
// skipped/failed so a genuine host-file error still surfaces here as before.
|
|
1934
|
+
hosts = resolvedHosts ?? await parseHostFile(hostFile);
|
|
1916
1935
|
} else if (host) {
|
|
1917
1936
|
hosts = await parseHostArg(host);
|
|
1918
1937
|
} else {
|
package/package.json
CHANGED
package/plugin_manager.mjs
CHANGED
|
@@ -728,9 +728,15 @@ export class PluginManager {
|
|
|
728
728
|
const wrappedRuns = await callPlugin(mod, host, ctx, outputs, opts);
|
|
729
729
|
const duration_ms = Date.now() - startMs;
|
|
730
730
|
|
|
731
|
-
// Determine manifest status from the plugin results
|
|
731
|
+
// Determine manifest status from the plugin results. A gate-skip envelope
|
|
732
|
+
// ({ up:false, skipped:true, ... }) carries neither `timedOut` nor `error`,
|
|
733
|
+
// so classify it as 'skipped' (not 'ran') iff nothing actually ran and there
|
|
734
|
+
// was no timeout/error — otherwise a self-skipped plugin would be miscounted
|
|
735
|
+
// as audited. Timeout/error still win over a skip; a skip+real-run mix is 'ran'.
|
|
732
736
|
let status = 'ran';
|
|
733
737
|
let reason = null;
|
|
738
|
+
let sawRealRun = false;
|
|
739
|
+
let skipReason = null;
|
|
734
740
|
for (const wrapped of wrappedRuns) {
|
|
735
741
|
if (wrapped.result?.timedOut) {
|
|
736
742
|
status = 'timeout';
|
|
@@ -738,8 +744,16 @@ export class PluginManager {
|
|
|
738
744
|
} else if (wrapped.result?.error && status !== 'timeout') {
|
|
739
745
|
status = 'error';
|
|
740
746
|
reason = wrapped.result.error;
|
|
747
|
+
} else if (wrapped.result?.skipped === true) {
|
|
748
|
+
skipReason = wrapped.result.reason || wrapped.result.skipReason || 'skipped by plugin gate';
|
|
749
|
+
} else {
|
|
750
|
+
sawRealRun = true;
|
|
741
751
|
}
|
|
742
752
|
}
|
|
753
|
+
if (status === 'ran' && skipReason != null && !sawRealRun) {
|
|
754
|
+
status = 'skipped';
|
|
755
|
+
reason = skipReason;
|
|
756
|
+
}
|
|
743
757
|
|
|
744
758
|
manifest.push({
|
|
745
759
|
id: String(mod.id || ''),
|
|
@@ -973,12 +987,25 @@ export class PluginManager {
|
|
|
973
987
|
const cloudHostKind = mod.cloudProvider ? `cloud:${mod.cloudProvider}` : opts.hostKind;
|
|
974
988
|
const wrappedRuns = await callPlugin(mod, host, ctx, [], { ...opts, timeoutMs, hostKind: cloudHostKind });
|
|
975
989
|
const duration_ms = Date.now() - startMs;
|
|
990
|
+
// Same classifier as _runOrchestrated: a gate-skip envelope
|
|
991
|
+
// ({ up:false, skipped:true, ... }) → 'skipped' (not 'ran') iff nothing ran
|
|
992
|
+
// and there was no timeout/error, so a self-skipped cloud is not counted as
|
|
993
|
+
// audited (auditedProviders derives from status==='ran' at :1034).
|
|
994
|
+
// NB: the `!sawRealRun` term is defensive-only on this path — cloud plugins
|
|
995
|
+
// are runStrategy:'single' (one wrappedRun), so a skip+real-run MIX cannot
|
|
996
|
+
// arise here (unlike the multi-port network path in _runOrchestrated, which
|
|
997
|
+
// has the mutation-pinned mix test). Kept for uniformity with that classifier.
|
|
976
998
|
let status = 'ran';
|
|
977
999
|
let reason = null;
|
|
1000
|
+
let sawRealRun = false;
|
|
1001
|
+
let skipReason = null;
|
|
978
1002
|
for (const w of wrappedRuns) {
|
|
979
1003
|
if (w.result?.timedOut) { status = 'timeout'; reason = w.result.error || `timed out after ${timeoutMs}ms`; }
|
|
980
1004
|
else if (w.result?.error && status !== 'timeout') { status = 'error'; reason = w.result.error; }
|
|
1005
|
+
else if (w.result?.skipped === true) { skipReason = w.result.reason || w.result.skipReason || 'skipped by plugin gate'; }
|
|
1006
|
+
else { sawRealRun = true; }
|
|
981
1007
|
}
|
|
1008
|
+
if (status === 'ran' && skipReason != null && !sawRealRun) { status = 'skipped'; reason = skipReason; }
|
|
982
1009
|
return { manifest: { id: String(mod.id || ''), name: mod.name || 'Plugin', status, reason, duration_ms }, outputs: wrappedRuns };
|
|
983
1010
|
} catch (err) {
|
|
984
1011
|
return { manifest: { id: String(mod.id || ''), name: mod.name || 'Plugin', status: 'error', reason: String(err?.message || err), duration_ms: 0 }, outputs: [] };
|
package/utils/env_loader.mjs
CHANGED
|
@@ -48,17 +48,43 @@ function providerMatchesHost(effective, hostProvider) {
|
|
|
48
48
|
return tokens.includes(hostProvider);
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// The distinct cloud-sentinel legs implied by the scan target, in first-appearance
|
|
52
|
+
// order, lowercased + deduped. Mirrors the CLI's host-file-XOR-host dispatch
|
|
53
|
+
// precedence: a pre-resolved `hosts` array (the --host-file path) is the
|
|
54
|
+
// authoritative source when present; otherwise the `--host` string (single 'aws'
|
|
55
|
+
// or a CSV 'aws,gcp,azure') is used. The two are NEVER unioned — the scan
|
|
56
|
+
// dispatches exactly one source, so reconciling both would over-widen
|
|
57
|
+
// CLOUD_PROVIDER and could false-throw on a leg that is never dispatched.
|
|
58
|
+
// Non-sentinel tokens (IPs / CIDRs / hostnames) are ignored; [] means no cloud
|
|
59
|
+
// leg (a pure network scan needs no CLOUD_PROVIDER).
|
|
60
|
+
function sentinelLegs(host, hosts) {
|
|
61
|
+
const tokens = Array.isArray(hosts)
|
|
62
|
+
? hosts.slice()
|
|
63
|
+
: (typeof host === 'string' ? host.split(',') : []);
|
|
64
|
+
const legs = [];
|
|
65
|
+
for (const raw of tokens) {
|
|
66
|
+
const t = String(raw).trim().toLowerCase();
|
|
67
|
+
if (CLOUD_SENTINELS.has(t) && !legs.includes(t)) legs.push(t);
|
|
68
|
+
}
|
|
69
|
+
return legs;
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
/**
|
|
52
73
|
* @param {object} args
|
|
53
74
|
* @param {string} [args.envPath] value of --env
|
|
54
75
|
* @param {string} [args.awsProfile] value of --aws-profile
|
|
55
|
-
* @param {string} [args.host] --host value
|
|
76
|
+
* @param {string} [args.host] --host value: a single sentinel ('aws'), a
|
|
77
|
+
* CSV of sentinels ('aws,gcp,azure'), or a
|
|
78
|
+
* network host — for CLOUD_PROVIDER implication
|
|
79
|
+
* @param {string[]} [args.hosts] pre-resolved host list (the --host-file path,
|
|
80
|
+
* where `host` is undefined); reconciled the
|
|
81
|
+
* same way as a CSV `host`
|
|
56
82
|
* @param {object} args.env snapshot of current process.env (read-only)
|
|
57
83
|
* @param {(p:string)=>boolean} args.fileExists
|
|
58
84
|
* @param {(p:string)=>string} args.readFile
|
|
59
85
|
* @returns {{set: Record<string,string>, unset: string[]}}
|
|
60
86
|
*/
|
|
61
|
-
export function resolveScanEnv({ envPath, awsProfile, host, env = {}, fileExists, readFile }) {
|
|
87
|
+
export function resolveScanEnv({ envPath, awsProfile, host, hosts, env = {}, fileExists, readFile }) {
|
|
62
88
|
const set = {};
|
|
63
89
|
const unset = [];
|
|
64
90
|
|
|
@@ -79,6 +105,16 @@ export function resolveScanEnv({ envPath, awsProfile, host, env = {}, fileExists
|
|
|
79
105
|
Object.assign(set, dotenv.parse(content));
|
|
80
106
|
}
|
|
81
107
|
|
|
108
|
+
// Capture whether the OPERATOR (not the tool) pinned CLOUD_PROVIDER — from the
|
|
109
|
+
// --env file (step 1) or the shell env — BEFORE the --aws-profile implication
|
|
110
|
+
// in step 2. This is what decides throw-vs-imply for sentinel legs (step 3):
|
|
111
|
+
// • operator-pinned provider that misses a host leg → conflict → fail-fast;
|
|
112
|
+
// • unpinned (or only tool/profile-implied) → imply the UNION of the legs.
|
|
113
|
+
// Without this snapshot, --aws-profile's implied bare 'aws' (step 2) would look
|
|
114
|
+
// like an operator pin and wrongly THROW on a `--host aws,gcp,azure` run.
|
|
115
|
+
const operatorPinned = providerAlreadySet(set, env);
|
|
116
|
+
const operatorProvider = operatorPinned ? effectiveProvider(set, env) : '';
|
|
117
|
+
|
|
82
118
|
// 2. --aws-profile: AWS_PROFILE wins; clear explicit keys; load ~/.aws/config; imply aws.
|
|
83
119
|
if (typeof awsProfile === 'string' && awsProfile.length) {
|
|
84
120
|
set.AWS_PROFILE = awsProfile;
|
|
@@ -90,23 +126,34 @@ export function resolveScanEnv({ envPath, awsProfile, host, env = {}, fileExists
|
|
|
90
126
|
if (!providerAlreadySet(set, env)) set.CLOUD_PROVIDER = 'aws';
|
|
91
127
|
}
|
|
92
128
|
|
|
93
|
-
// 3. Sentinel host
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
129
|
+
// 3. Sentinel host leg(s) imply their provider(s) when the operator hasn't
|
|
130
|
+
// pinned CLOUD_PROVIDER; fail-fast when a pinned provider does NOT cover
|
|
131
|
+
// every leg. Handles a single sentinel ('aws'), a CSV ('aws,gcp,azure'),
|
|
132
|
+
// AND a --host-file resolved list (`hosts`) uniformly — each is a set of
|
|
133
|
+
// legs. A leg that the effective provider does not cover would silently
|
|
134
|
+
// self-skip on the awsScanSkipReason/gcpScanSkipReason gate → ZERO plugins
|
|
135
|
+
// run on that leg → a silent "clean" report for a whole cloud.
|
|
136
|
+
const legs = sentinelLegs(host, hosts);
|
|
137
|
+
if (legs.length) {
|
|
138
|
+
if (operatorPinned) {
|
|
139
|
+
const uncovered = legs.filter((leg) => !providerMatchesHost(operatorProvider, leg));
|
|
140
|
+
if (uncovered.length) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`--host '${legs.join(',')}' conflicts with CLOUD_PROVIDER='${operatorProvider}': the host ` +
|
|
143
|
+
`leg(s) [${uncovered.join(', ')}] are not covered by the effective cloud provider, so every ` +
|
|
144
|
+
`plugin on ${uncovered.length > 1 ? 'those legs' : 'that leg'} would silently self-skip (an ` +
|
|
145
|
+
`empty "clean" report). Resolve by dropping the conflicting CLOUD_PROVIDER, or set ` +
|
|
146
|
+
`CLOUD_PROVIDER to include ${uncovered.join(',')} (e.g. CLOUD_PROVIDER=${legs.join(',')}).`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
// pinned provider covers every leg → leave the operator's value untouched
|
|
150
|
+
} else {
|
|
151
|
+
// Unpinned (or only tool/profile-implied): imply the UNION of the legs so
|
|
152
|
+
// each cloud leg runs. This overrides a bare 'aws' that step 2's
|
|
153
|
+
// --aws-profile implication may have written — 'aws' ⊆ the union, and the
|
|
154
|
+
// aws leg still resolves the profile via AWS_PROFILE.
|
|
155
|
+
set.CLOUD_PROVIDER = legs.join(',');
|
|
108
156
|
}
|
|
109
|
-
if (!providerAlreadySet(set, env)) set.CLOUD_PROVIDER = hostProvider;
|
|
110
157
|
}
|
|
111
158
|
|
|
112
159
|
return { set, unset };
|