infracensus-collector 0.1.0

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.
Files changed (91) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +323 -0
  3. package/dist/a10-index.js +91 -0
  4. package/dist/a10-index.js.map +7 -0
  5. package/dist/azure-index.js +114 -0
  6. package/dist/azure-index.js.map +7 -0
  7. package/dist/chunk-2VVO5SGB.js +443 -0
  8. package/dist/chunk-2VVO5SGB.js.map +7 -0
  9. package/dist/chunk-3JF5HO4Y.js +372 -0
  10. package/dist/chunk-3JF5HO4Y.js.map +7 -0
  11. package/dist/chunk-3MIZN7SP.js +5160 -0
  12. package/dist/chunk-3MIZN7SP.js.map +7 -0
  13. package/dist/chunk-42NDGVLF.js +78 -0
  14. package/dist/chunk-42NDGVLF.js.map +7 -0
  15. package/dist/chunk-7EGPKFND.js +349 -0
  16. package/dist/chunk-7EGPKFND.js.map +7 -0
  17. package/dist/chunk-7ESBA5C5.js +31 -0
  18. package/dist/chunk-7ESBA5C5.js.map +7 -0
  19. package/dist/chunk-7HFPVDT3.js +1237 -0
  20. package/dist/chunk-7HFPVDT3.js.map +7 -0
  21. package/dist/chunk-DBJNP764.js +774 -0
  22. package/dist/chunk-DBJNP764.js.map +7 -0
  23. package/dist/chunk-IQU5HMXO.js +1837 -0
  24. package/dist/chunk-IQU5HMXO.js.map +7 -0
  25. package/dist/chunk-LNYJAPA2.js +28 -0
  26. package/dist/chunk-LNYJAPA2.js.map +7 -0
  27. package/dist/chunk-U2UV5MMR.js +18 -0
  28. package/dist/chunk-U2UV5MMR.js.map +7 -0
  29. package/dist/chunk-VEMGGUZ6.js +528 -0
  30. package/dist/chunk-VEMGGUZ6.js.map +7 -0
  31. package/dist/chunk-VXDH6AVJ.js +1047 -0
  32. package/dist/chunk-VXDH6AVJ.js.map +7 -0
  33. package/dist/chunk-VZFXWTU2.js +267 -0
  34. package/dist/chunk-VZFXWTU2.js.map +7 -0
  35. package/dist/chunk-XSHYWQAH.js +20 -0
  36. package/dist/chunk-XSHYWQAH.js.map +7 -0
  37. package/dist/chunk-ZCZOUDBC.js +225 -0
  38. package/dist/chunk-ZCZOUDBC.js.map +7 -0
  39. package/dist/chunk-ZIUWFLKI.js +11945 -0
  40. package/dist/chunk-ZIUWFLKI.js.map +7 -0
  41. package/dist/cli.js +193 -0
  42. package/dist/cli.js.map +7 -0
  43. package/dist/config-AA7OSIVF.js +23 -0
  44. package/dist/config-AA7OSIVF.js.map +7 -0
  45. package/dist/configimport-index.js +286 -0
  46. package/dist/configimport-index.js.map +7 -0
  47. package/dist/hyperv-index.js +105 -0
  48. package/dist/hyperv-index.js.map +7 -0
  49. package/dist/identity-index.js +105 -0
  50. package/dist/identity-index.js.map +7 -0
  51. package/dist/index.js +11444 -0
  52. package/dist/index.js.map +7 -0
  53. package/dist/linux-QMB5WPDK.js +247 -0
  54. package/dist/linux-QMB5WPDK.js.map +7 -0
  55. package/dist/pkgRoot-RFOX7XXU.js +7 -0
  56. package/dist/pkgRoot-RFOX7XXU.js.map +7 -0
  57. package/dist/sensitive-data-index.js +383 -0
  58. package/dist/sensitive-data-index.js.map +7 -0
  59. package/dist/service-U6Y7MN75.js +11 -0
  60. package/dist/service-U6Y7MN75.js.map +7 -0
  61. package/dist/sql-index.js +106 -0
  62. package/dist/sql-index.js.map +7 -0
  63. package/dist/status-SBN5WZZX.js +26 -0
  64. package/dist/status-SBN5WZZX.js.map +7 -0
  65. package/dist/tools-KT77LANU.js +93 -0
  66. package/dist/tools-KT77LANU.js.map +7 -0
  67. package/dist/vmware-index.js +118 -0
  68. package/dist/vmware-index.js.map +7 -0
  69. package/dist/voice-index.js +181 -0
  70. package/dist/voice-index.js.map +7 -0
  71. package/dist/windows-ZUCEMMDI.js +323 -0
  72. package/dist/windows-ZUCEMMDI.js.map +7 -0
  73. package/package.json +95 -0
  74. package/scripts/bundle.mjs +113 -0
  75. package/scripts/collector-ctl.mjs +143 -0
  76. package/scripts/collector.env.example +48 -0
  77. package/scripts/doctor.mjs +199 -0
  78. package/scripts/enrol.mjs +215 -0
  79. package/scripts/generate-ssh-key-fixtures.mjs +84 -0
  80. package/scripts/install-record.mjs +65 -0
  81. package/scripts/install.sh +203 -0
  82. package/scripts/lib/daemon-log.mjs +132 -0
  83. package/scripts/lib/deregister.mjs +81 -0
  84. package/scripts/lib/enrolment.mjs +266 -0
  85. package/scripts/lib/install-manifest.mjs +160 -0
  86. package/scripts/lib/plane-probe.mjs +100 -0
  87. package/scripts/lib/secure-file.mjs +71 -0
  88. package/scripts/lib/service-state.mjs +100 -0
  89. package/scripts/uninstall-local.mjs +133 -0
  90. package/scripts/uninstall.sh +84 -0
  91. package/scripts/uselog.mjs +83 -0
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ InfraCensus Collector
2
+ Copyright (c) 2026 Zentrosoft. All rights reserved.
3
+
4
+ This software is proprietary. It is published to the npm registry so that
5
+ licensed InfraCensus tenants can install and update the collector with
6
+ standard tooling; publication is not a grant of rights.
7
+
8
+ You may install and run this software only to connect infrastructure you
9
+ control to an InfraCensus deployment you are licensed to use. You may not
10
+ copy (except as required to install and run it), modify, translate, reverse
11
+ engineer, decompile, disassemble, sublicense, sell, lease, or redistribute
12
+ this software, in whole or in part, except as permitted by applicable law
13
+ notwithstanding this restriction, or with prior written permission.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20
+ FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
21
+ DEALINGS IN THE SOFTWARE.
22
+
23
+ Contact: info@infracensus.com
package/README.md ADDED
@@ -0,0 +1,323 @@
1
+ # InfraCensus Collector
2
+
3
+ **This is the operator's page.** It is written for the person who runs the
4
+ collector on their own hardware, inside their own network, holding their own
5
+ device credentials — not for us.
6
+
7
+ The collector is **outbound-only on your network**. It polls the control plane;
8
+ the control plane never connects in, and nothing here listens on a network
9
+ interface. There is exactly one listener anywhere in the product and it is
10
+ pinned to `127.0.0.1` — the local status UI, described below, unreachable from
11
+ any other machine by construction and asserted at startup, not assumed.
12
+
13
+ ---
14
+
15
+ ## How it is organized — three planes
16
+
17
+ ### 1. The data plane
18
+
19
+ ```
20
+ your network this host InfraCensus cloud
21
+ ──────────── ───────── ─────────────────
22
+
23
+ config: /etc/netdoc-collector/collector.env (0600)
24
+ identity + the collector key + device credentials
25
+
26
+
27
+ switches, hosts, ◄── scan ── scan modules poll (outbound) ──► control plane
28
+ hypervisors, network · vmware · hyperv ◄── scan jobs, (never connects
29
+ vCenter, SQL, sql · lb · voice · identity cancellations, into your
30
+ Entra ID, Azure azure · config-import restart signal) network)
31
+
32
+ │ raw facts — device credentials are used
33
+ │ HERE and go no further
34
+
35
+ canonical snapshot / inventory
36
+ (normalized, redacted, stamped
37
+ with tenant + site)
38
+
39
+
40
+ authenticated push ── Bearer collector key ──► ingest
41
+
42
+
43
+ use log (JSONL ledger on this host —
44
+ your evidence of what was touched)
45
+
46
+ browser ON THIS HOST ──► http://127.0.0.1:9477 the local UI (status, log tail,
47
+ run-a-scan) — loopback only
48
+ ```
49
+
50
+ Configuration is resolved once, locally. Scan modules read it, talk to your
51
+ equipment, and hand back raw facts. Those facts are normalized into the
52
+ canonical model, redacted, and pushed outbound with the collector's key. In
53
+ daemon mode the same loop runs in reverse order: the collector polls for a job,
54
+ the job carries the targets and credentials, and the snapshot is stamped from
55
+ the job. Nothing is ever pushed that was not first normalized and redacted.
56
+
57
+ ### 2. The security boundaries
58
+
59
+ **The collector key** authenticates this host to the control plane.
60
+
61
+ - At rest it lives in exactly one file (`collector.env` /
62
+ `collector.config.json`), created empty and locked to mode `0600` (Linux) or
63
+ an explicit three-SID ACL (Windows) *before* the key is written into it.
64
+ - It is **never a process argument** — there is no `--api-key` flag, and a test
65
+ asserts there never will be. Enrolment takes it at a muted prompt or from a
66
+ file whose *path* (not contents) was the argument.
67
+ - It is **never logged**. Every log line and every ledger record passes through
68
+ a redactor that knows the key's value before the first line is written.
69
+
70
+ **Device credentials** (SSH, SNMP, WMI, vCenter, SQL, Graph, Azure) are
71
+ resolved on this host and used from this process. They are never placed in a
72
+ snapshot, never logged (the redactor learns per-job credentials the moment a
73
+ job is claimed, before the job is logged at all), and never sent anywhere. What
74
+ leaves the building is the *result* of using them — and device configuration
75
+ text is redacted here, before it leaves.
76
+
77
+ **The local UI** is loopback-only with cloud-verified login:
78
+
79
+ - It binds `127.0.0.1` explicitly and the daemon *checks the address the OS
80
+ actually bound* after listen, refusing to serve on anything else.
81
+ - There are **no local accounts**. Signing in forwards your email and password
82
+ (with the collector's own key as the Bearer) to the control plane, which
83
+ accepts only an **active account of this collector's own tenant**. Disable
84
+ the account in the portal and the UI closes with it. No password is ever
85
+ stored — or logged — on this host.
86
+ - Sessions are cookies signed with a **secret generated fresh at every daemon
87
+ start**, so restarting the daemon invalidates every session, and there is no
88
+ session secret at rest to protect. Cookies are HttpOnly, SameSite=Strict,
89
+ and expire after 8 hours. Five failed logins in fifteen minutes lock the
90
+ login for everyone until the oldest failure ages out — one limiter across
91
+ all sessions, because on loopback there is only one address.
92
+
93
+ ### 3. The UI plane — what it serves, and what it refuses to
94
+
95
+ Serves, to a signed-in operator on this host:
96
+
97
+ - daemon status: version, when it started, when it last polled the control
98
+ plane, when data last left this host;
99
+ - a per-module table of last runs (read from the use-log ledger — the same
100
+ file that is your evidence, so the UI can never claim more than the ledger
101
+ holds);
102
+ - the tail of the daemon log;
103
+ - a **Run scan** button per module (spawns the same one-shot
104
+ `netdoc-collector scan <module>` you could type) and a **Restart** button
105
+ (the same restart path the portal's restart signal uses).
106
+
107
+ Refuses, by construction:
108
+
109
+ - to be reachable from any other machine (loopback bind, asserted);
110
+ - to display the collector key, or any stored credential, anywhere — the
111
+ status payload is built from an allowlist that does not contain them, the
112
+ page is a static string with nothing interpolated into it, and a test holds
113
+ both;
114
+ - to accept credentials for storage — there are no device-credential fields in
115
+ the UI; credentials are configured in the config file, on disk, under its
116
+ permissions;
117
+ - to answer anything stateful without the session cookie;
118
+ - to serve at all when `COLLECTOR_UI=0` — disabled means no listener exists,
119
+ not a listener that says no.
120
+
121
+ ---
122
+
123
+ ## Install
124
+
125
+ The collector is distributed on the npm registry as **`infracensus-collector`**.
126
+ The package installs one command, `netdoc-collector` (package name and command
127
+ name differ on purpose, the way `typescript` installs `tsc`). Node.js 20 or
128
+ newer is the only prerequisite.
129
+
130
+ If Node/npm is not on the host yet — Linux: install Node.js 20+ from your
131
+ distro or nodejs.org; e.g. RHEL/Fedora: `sudo dnf install nodejs npm` —
132
+ Debian/Ubuntu: `sudo apt install nodejs npm` — or nvm. Windows:
133
+ `winget install OpenJS.NodeJS.LTS` (or the nodejs.org installer).
134
+
135
+ ### Linux
136
+
137
+ ```bash
138
+ sudo npm install -g infracensus-collector
139
+ sudo netdoc-collector enrol # prompts for the enrolment token; never an argument
140
+ sudo netdoc-collector service install # systemd unit, enabled and started
141
+ ```
142
+
143
+ The service runs as the `netdoc-collector` system account, keeps its identity
144
+ at `/etc/netdoc-collector/collector.env` (mode `0600` root:root, read by
145
+ systemd before privileges drop), and writes everything — logs, ledger, PID —
146
+ to `/var/lib/netdoc-collector`. An `npm update -g infracensus-collector`
147
+ replaces the code without touching either.
148
+
149
+ ### Windows
150
+
151
+ From an **ADMIN PowerShell**:
152
+
153
+ ```powershell
154
+ npm install -g infracensus-collector
155
+ netdoc-collector enrol # prompts; the key is typed hidden, never an argument
156
+ netdoc-collector service install # registers and starts the Windows service
157
+ ```
158
+
159
+ This registers the **InfraCensus Collector** Windows service (the SCM-level id
160
+ is `infracensuscollector.exe`, derived by the service host — check it with
161
+ `netdoc-collector service status` or
162
+ `Get-Service -DisplayName "InfraCensus Collector"`). The identity lives under
163
+ `C:\ProgramData\InfraCensus Collector` — locked to SYSTEM and Administrators,
164
+ and deliberately outside the npm package so an uninstall or update of the code
165
+ cannot destroy a key that was shown exactly once.
166
+
167
+ ### From a checkout (development / air-gap)
168
+
169
+ `pnpm install` in this directory, then the `pnpm` aliases below; `pnpm build`
170
+ bundles `dist/`. The CLI is the same code either way: `npx tsx src/cli.ts
171
+ <command>` in a checkout is `netdoc-collector <command>` on an installed host.
172
+ On an air-gapped Linux host, `sudo ./scripts/install.sh` registers the
173
+ from-source checkout as a service without touching a registry.
174
+
175
+ ### Enrolment
176
+
177
+ An enrolment is six values shown in the portal under **Admin → Collectors**,
178
+ wrapped in one `enrol_…` token beside the key. The token **contains the key**:
179
+ it is shown once, belongs in a password manager, and the prompt that takes it
180
+ does not echo. An all-sites collector's token carries an empty org and site on
181
+ purpose — such a collector runs as a daemon and takes tenant, site and targets
182
+ from each job.
183
+
184
+ ---
185
+
186
+ ## Command reference
187
+
188
+ One installed command, `netdoc-collector` (npm's `bin` shim onto `dist/cli.js`).
189
+ `enrol` / `doctor` / `uselog` are standalone scripts underneath — they must
190
+ work before, or without, a built collector — and the CLI routes those three
191
+ subcommands to them, wiring the platform's config paths on the way.
192
+
193
+ | Command | What it does |
194
+ |---|---|
195
+ | `netdoc-collector run` | The daemon: poll the control plane for scan jobs, serve the local UI. This is what the service unit runs. |
196
+ | `netdoc-collector scan [module]` | ONE scan in the foreground, then exit. Default module: `network`. `--dry-run` builds the result and prints a summary without pushing. |
197
+ | `netdoc-collector status` | Local status: enrolment, daemon liveness, last run per module. |
198
+ | `netdoc-collector enrol` | Enrol this host (muted prompt; `--replace` re-keys an existing install). |
199
+ | `netdoc-collector doctor` | Is it running, and is it talking to the cloud. Prints no credential, ever. |
200
+ | `netdoc-collector uselog` | What this collector did on your network, from your own ledger. |
201
+ | `netdoc-collector service install` | Register and start the background service (Linux: `sudo`, systemd — Windows: elevated, SCM). Refuses politely until the host is enrolled. |
202
+ | `netdoc-collector service uninstall` | Stop and deregister the service. The identity and ledgers stay on disk, and it says so. |
203
+ | `netdoc-collector service status` | Registered/active? Exit `0` running · `1` registered but stopped · `2` not installed. |
204
+ | `netdoc-collector --version` | Print the version. |
205
+ | `netdoc-collector --help` | Print usage. |
206
+
207
+ Anything else prints the help and exits `2`.
208
+
209
+ Scan modules: `network` (ICMP/ARP/SNMP/SSH sweep), `vmware`, `hyperv`, `sql`,
210
+ `lb` (A10), `voice` (CUCM/Unity/Webex), `identity` (Microsoft Entra ID),
211
+ `azure` (CSPM), `config-import` (offline device configs). Each reads its own
212
+ section of the config; a module with nothing configured says what is missing
213
+ and exits non-zero.
214
+
215
+ Checkout equivalents: `pnpm scan`, `pnpm scan:dry`, `pnpm scan:daemon`,
216
+ `pnpm vmware-scan`, `pnpm hyperv-scan`, `pnpm sql-scan`, `pnpm lb-scan`,
217
+ `pnpm voice-scan`, `pnpm identity-scan`, `pnpm azure-scan`,
218
+ `pnpm config-import`, `pnpm enrol`, `pnpm doctor`, `pnpm uselog`.
219
+
220
+ ---
221
+
222
+ ## The local UI
223
+
224
+ ```
225
+ http://localhost:9477 # from a browser ON the collector host
226
+ ```
227
+
228
+ Sign in with your InfraCensus account (an active account of this collector's
229
+ tenant — there is nothing local to sign in to). The page shows the daemon's
230
+ status, the per-module last-run table with Run-scan buttons, a Restart button,
231
+ and the daemon log tail, refreshing every five seconds. A scan started from the
232
+ UI runs as a separate process and narrates itself into the same log tail the
233
+ page is already showing.
234
+
235
+ Two environment variables control it — the only two the UI has:
236
+
237
+ | Variable | Effect |
238
+ |---|---|
239
+ | `COLLECTOR_UI_PORT` | Port for the UI (default `9477`; always `127.0.0.1`). An unusable value falls back to the default rather than stopping the daemon. |
240
+ | `COLLECTOR_UI=0` | Disable the UI entirely. The daemon then opens no port at all. |
241
+
242
+ On an installed host, set them in `/etc/netdoc-collector/collector.env` (Linux)
243
+ or the service environment (Windows) and restart the service.
244
+
245
+ If the UI fails to start — a squatted port, say — the daemon logs a warning and
246
+ keeps scanning. The status page is a convenience; the scans are the job.
247
+
248
+ ---
249
+
250
+ ## Check it, and see what it did
251
+
252
+ ```bash
253
+ netdoc-collector doctor # elevated on Windows: the config is ACL'd
254
+ netdoc-collector status
255
+ netdoc-collector uselog # your evidence ledger, rendered
256
+ ```
257
+
258
+ `doctor` answers "is it running, and is it talking to the cloud" entirely
259
+ locally, printing values only from an allowlist of non-secret config keys; the
260
+ key itself appears only as a short fingerprint. `uselog` renders the collector's
261
+ own ledger — a rotated, redacted JSONL file (`collector-use.log`, ~25 MB hard
262
+ ceiling) recording what was scanned, what was contacted, and what was sent
263
+ where. It is your record, not our telemetry: nothing in it is transmitted, and
264
+ deleting it costs only your own evidence.
265
+
266
+ ---
267
+
268
+ ## Uninstall
269
+
270
+ ```bash
271
+ sudo netdoc-collector service uninstall && sudo npm uninstall -g infracensus-collector
272
+ # full purge of config + ledgers afterwards (the uninstall prints this too):
273
+ sudo rm -rf /etc/netdoc-collector /var/lib/netdoc-collector
274
+ ```
275
+
276
+ ```powershell
277
+ # elevated:
278
+ netdoc-collector service uninstall
279
+ npm uninstall -g infracensus-collector
280
+ ```
281
+
282
+ The identity is left on disk on purpose (the key in it was shown once); the
283
+ uninstall names the directory and the purge command. Then remove the collector
284
+ in the portal under **Admin → Collectors** — the uninstaller says this too.
285
+ Until you do, its key remains valid for anyone who kept a copy.
286
+
287
+ ---
288
+
289
+ ## If something is wrong
290
+
291
+ **It stops with no error in the log — check EDR / antivirus.** The daemon logs
292
+ uncaught exceptions and keeps running, so a code fault always leaves an `ERROR`
293
+ line; silence means the process was killed from outside. To a behavioural
294
+ engine, a process that sweeps thousands of addresses, opens SSH sessions with
295
+ stored credentials and spawns PowerShell for AD/WMI looks like lateral
296
+ movement. Add a tightly-scoped exclusion and check the OS event log around the
297
+ time it vanished.
298
+
299
+ **Scans sit at "Waiting for a collector."** Either no collector is authorized
300
+ for that site (portal, Admin → Collectors) or the collector is not polling
301
+ (`doctor` on its host, or the UI's "last poll" timestamp).
302
+
303
+ **The UI shows the login form and rejects a good password.** The control plane
304
+ must be reachable *from the collector host* for login to verify — a 502 on the
305
+ login call means the uplink, not the password. Also check the account is active
306
+ and belongs to the same tenant as the collector.
307
+
308
+ **It starts and immediately exits.** Run it in the foreground:
309
+ `netdoc-collector run` (checkout: `npx tsx src/cli.ts run`). The usual causes
310
+ are a missing or malformed config, an unreachable control plane URL, or a
311
+ revoked key.
312
+
313
+ ---
314
+
315
+ ## Files on this host
316
+
317
+ | File | Path (installed / checkout) |
318
+ |---|---|
319
+ | Config — holds the key | Linux `/etc/netdoc-collector/collector.env` · Windows `C:\ProgramData\InfraCensus Collector\collector.config.json` · checkout `collector.config.json` (override: `COLLECTOR_CONFIG`) |
320
+ | Daemon log | Linux service `/var/lib/netdoc-collector/collector-daemon.log` · otherwise `collector-daemon.log` beside the package |
321
+ | Use-log ledger | Linux service `/var/lib/netdoc-collector/collector-use.log` · otherwise `collector-use.log` beside the package (override: `COLLECTOR_USE_LOG`) |
322
+ | PID file | `.collector-daemon.pid` beside the package (best-effort — under the sandboxed systemd unit, liveness is systemd's answer, not the file's) |
323
+ | Bundled entrypoint | `dist/cli.js` — what npm's `bin` shim, the systemd unit and the Windows service all run |
@@ -0,0 +1,91 @@
1
+ import {
2
+ AxapiClient,
3
+ buildLoadBalancerInventory
4
+ } from "./chunk-7EGPKFND.js";
5
+ import {
6
+ ROUTES
7
+ } from "./chunk-ZIUWFLKI.js";
8
+
9
+ // src/a10-index.ts
10
+ var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
11
+ function makeLogger(minLevel = "info") {
12
+ const min = LEVEL_RANK[minLevel];
13
+ return (level, msg, meta) => {
14
+ if (LEVEL_RANK[level] < min) return;
15
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level.toUpperCase().padEnd(5)} ${msg}`;
16
+ const out = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
17
+ if (meta !== void 0) out(line, meta);
18
+ else out(line);
19
+ };
20
+ }
21
+ function reqEnv(name) {
22
+ const v = process.env[name];
23
+ if (!v || !v.trim()) throw new Error(`missing required env var ${name}`);
24
+ return v.trim();
25
+ }
26
+ function printSummary(inv, log) {
27
+ log("info", "=== DRY RUN \u2014 load-balancer inventory NOT pushed ===");
28
+ log("info", `appliance : ${inv.appliance.hostname ?? inv.appliance.id}${inv.appliance.version ? ` (v${inv.appliance.version})` : ""}`);
29
+ log("info", `virtualServers : ${inv.virtualServers.length}`);
30
+ log("info", `serviceGroups : ${inv.serviceGroups.length}`);
31
+ log("info", `healthMonitors : ${inv.healthMonitors.length}`);
32
+ log("info", `servers : ${inv.servers.length}`);
33
+ for (const vs of inv.virtualServers.slice(0, 5)) {
34
+ const ports = vs.ports.map((p) => `${p.port}/${p.protocol}`).join(", ");
35
+ log("info", ` vip \u2022 ${vs.name} ${vs.ipAddress ?? ""} [${ports}]`);
36
+ }
37
+ }
38
+ async function main() {
39
+ const dryRun = process.argv.includes("--dry-run");
40
+ const debug = process.argv.includes("--debug") || process.env.LOG_LEVEL === "debug";
41
+ const log = makeLogger(debug ? "debug" : "info");
42
+ const a10Insecure = /^(1|true|yes)$/i.test(process.env.A10_INSECURE ?? "");
43
+ if (a10Insecure) {
44
+ log("warn", "A10_INSECURE=true \u2014 cert verification off + legacy TLS/ciphers allowed. Trusted appliance only.");
45
+ }
46
+ const controller = new AbortController();
47
+ const onSigint = () => {
48
+ log("warn", "SIGINT received \u2014 shutting down\u2026");
49
+ controller.abort();
50
+ };
51
+ process.on("SIGINT", onSigint);
52
+ const a10Url = reqEnv("A10_URL");
53
+ const client = new AxapiClient(
54
+ { baseUrl: a10Url, username: reqEnv("A10_USER"), password: reqEnv("A10_PASS"), signal: controller.signal, insecure: a10Insecure },
55
+ log
56
+ );
57
+ const tenancy = {
58
+ collectorId: reqEnv("COLLECTOR_ID"),
59
+ tenantId: reqEnv("TENANT_ID"),
60
+ orgId: reqEnv("ORG_ID"),
61
+ siteId: reqEnv("SITE_ID")
62
+ };
63
+ try {
64
+ log("info", `A10 collector starting (dryRun=${dryRun})`);
65
+ log("info", `appliance: ${a10Url}`);
66
+ await client.connect();
67
+ const inventory = await buildLoadBalancerInventory(client, tenancy, log);
68
+ if (dryRun) {
69
+ printSummary(inventory, log);
70
+ return;
71
+ }
72
+ const cpUrl = reqEnv("CONTROL_PLANE_URL").replace(/\/+$/, "");
73
+ log("info", `pushing load-balancer inventory to ${cpUrl}`);
74
+ const res = await fetch(`${cpUrl}${ROUTES.ingestLb}`, {
75
+ method: "POST",
76
+ headers: { "content-type": "application/json", authorization: `Bearer ${reqEnv("COLLECTOR_API_KEY")}` },
77
+ body: JSON.stringify(inventory),
78
+ signal: controller.signal
79
+ });
80
+ if (!res.ok) throw new Error(`push failed: HTTP ${res.status} ${await res.text()}`);
81
+ log("info", "=== PUSH OK ===");
82
+ } finally {
83
+ await client.close();
84
+ process.off("SIGINT", onSigint);
85
+ }
86
+ }
87
+ main().catch((err) => {
88
+ console.error(`${(/* @__PURE__ */ new Date()).toISOString()} ERROR ${err.message}`);
89
+ process.exitCode = 1;
90
+ });
91
+ //# sourceMappingURL=a10-index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/a10-index.ts"],
4
+ "sourcesContent": ["/**\r\n * A10 load-balancer collector CLI entry point.\r\n *\r\n * tsx src/a10-index.ts inventory the A10 via aXAPI and push to /ingest/loadbalancer\r\n * tsx src/a10-index.ts --dry-run inventory and print a summary WITHOUT pushing\r\n *\r\n * A SEPARATE one-shot command from the network `scan` (an appliance isn't a CIDR\r\n * target). Connects to ACOS over aXAPI v3, builds a canonical LoadBalancerInventory\r\n * (appliance + VIPs + service groups + monitors + servers) stamped with the tenancy\r\n * envelope, and either prints it (dry-run) or POSTs it outbound with the collector\r\n * Bearer key. Credentials come from env and are NEVER logged.\r\n */\r\nimport { AxapiClient } from \"./a10/axapi.js\";\r\nimport { buildLoadBalancerInventory } from \"./a10/scan.js\";\r\nimport { ROUTES, type LbIngestBody } from \"@netdoc/collector-contract\";\r\nimport type { LoadBalancerInventory } from \"@netdoc/canonical-model\";\r\nimport type { Logger } from \"./core/runner.js\";\r\n\r\nconst LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 } as const;\r\n\r\nfunction makeLogger(minLevel: keyof typeof LEVEL_RANK = \"info\"): Logger {\r\n const min = LEVEL_RANK[minLevel];\r\n return (level, msg, meta) => {\r\n if (LEVEL_RANK[level] < min) return;\r\n const line = `${new Date().toISOString()} ${level.toUpperCase().padEnd(5)} ${msg}`;\r\n const out = level === \"error\" ? console.error : level === \"warn\" ? console.warn : console.log;\r\n if (meta !== undefined) out(line, meta);\r\n else out(line);\r\n };\r\n}\r\n\r\nfunction reqEnv(name: string): string {\r\n const v = process.env[name];\r\n if (!v || !v.trim()) throw new Error(`missing required env var ${name}`);\r\n return v.trim();\r\n}\r\n\r\nfunction printSummary(inv: LoadBalancerInventory, log: Logger): void {\r\n log(\"info\", \"=== DRY RUN \u2014 load-balancer inventory NOT pushed ===\");\r\n log(\"info\", `appliance : ${inv.appliance.hostname ?? inv.appliance.id}${inv.appliance.version ? ` (v${inv.appliance.version})` : \"\"}`);\r\n log(\"info\", `virtualServers : ${inv.virtualServers.length}`);\r\n log(\"info\", `serviceGroups : ${inv.serviceGroups.length}`);\r\n log(\"info\", `healthMonitors : ${inv.healthMonitors.length}`);\r\n log(\"info\", `servers : ${inv.servers.length}`);\r\n for (const vs of inv.virtualServers.slice(0, 5)) {\r\n const ports = vs.ports.map((p) => `${p.port}/${p.protocol}`).join(\", \");\r\n log(\"info\", ` vip \u2022 ${vs.name} ${vs.ipAddress ?? \"\"} [${ports}]`);\r\n }\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n const dryRun = process.argv.includes(\"--dry-run\");\r\n const debug = process.argv.includes(\"--debug\") || process.env.LOG_LEVEL === \"debug\";\r\n const log = makeLogger(debug ? \"debug\" : \"info\");\r\n\r\n // ACOS ships a self-signed cert; older boxes also only speak legacy TLS/ciphers.\r\n // A10_INSECURE=1 relaxes both, via a per-client TLS agent (see AxapiClient).\r\n const a10Insecure = /^(1|true|yes)$/i.test(process.env.A10_INSECURE ?? \"\");\r\n if (a10Insecure) {\r\n log(\"warn\", \"A10_INSECURE=true \u2014 cert verification off + legacy TLS/ciphers allowed. Trusted appliance only.\");\r\n }\r\n\r\n const controller = new AbortController();\r\n const onSigint = (): void => {\r\n log(\"warn\", \"SIGINT received \u2014 shutting down\u2026\");\r\n controller.abort();\r\n };\r\n process.on(\"SIGINT\", onSigint);\r\n\r\n const a10Url = reqEnv(\"A10_URL\");\r\n const client = new AxapiClient(\r\n { baseUrl: a10Url, username: reqEnv(\"A10_USER\"), password: reqEnv(\"A10_PASS\"), signal: controller.signal, insecure: a10Insecure },\r\n log,\r\n );\r\n const tenancy = {\r\n collectorId: reqEnv(\"COLLECTOR_ID\"),\r\n tenantId: reqEnv(\"TENANT_ID\"),\r\n orgId: reqEnv(\"ORG_ID\"),\r\n siteId: reqEnv(\"SITE_ID\"),\r\n };\r\n\r\n try {\r\n log(\"info\", `A10 collector starting (dryRun=${dryRun})`);\r\n log(\"info\", `appliance: ${a10Url}`); // host only; credentials NEVER logged\r\n await client.connect();\r\n\r\n const inventory = await buildLoadBalancerInventory(client, tenancy, log);\r\n\r\n if (dryRun) {\r\n printSummary(inventory, log);\r\n return;\r\n }\r\n\r\n const cpUrl = reqEnv(\"CONTROL_PLANE_URL\").replace(/\\/+$/, \"\");\r\n log(\"info\", `pushing load-balancer inventory to ${cpUrl}`);\r\n const res = await fetch(`${cpUrl}${ROUTES.ingestLb}`, {\r\n method: \"POST\",\r\n headers: { \"content-type\": \"application/json\", authorization: `Bearer ${reqEnv(\"COLLECTOR_API_KEY\")}` },\r\n body: JSON.stringify(inventory satisfies LbIngestBody),\r\n signal: controller.signal,\r\n });\r\n if (!res.ok) throw new Error(`push failed: HTTP ${res.status} ${await res.text()}`);\r\n log(\"info\", \"=== PUSH OK ===\");\r\n } finally {\r\n await client.close();\r\n process.off(\"SIGINT\", onSigint);\r\n }\r\n}\r\n\r\nmain().catch((err) => {\r\n console.error(`${new Date().toISOString()} ERROR ${(err as Error).message}`);\r\n process.exitCode = 1;\r\n});\r\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,114 @@
1
+ import {
2
+ ArmClient,
3
+ buildAzureInventory,
4
+ listAccessibleSubscriptions
5
+ } from "./chunk-DBJNP764.js";
6
+ import {
7
+ pushAzureInventory
8
+ } from "./chunk-ZCZOUDBC.js";
9
+ import {
10
+ loadAzureConfig
11
+ } from "./chunk-VEMGGUZ6.js";
12
+ import "./chunk-ZIUWFLKI.js";
13
+
14
+ // src/azure-index.ts
15
+ var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
16
+ function makeLogger(minLevel = "info") {
17
+ const min = LEVEL_RANK[minLevel];
18
+ return (level, msg, meta) => {
19
+ if (LEVEL_RANK[level] < min) return;
20
+ const line = `${(/* @__PURE__ */ new Date()).toISOString()} ${level.toUpperCase().padEnd(5)} ${msg}`;
21
+ const out = level === "error" ? console.error : level === "warn" ? console.warn : console.log;
22
+ if (meta !== void 0) out(line, meta);
23
+ else out(line);
24
+ };
25
+ }
26
+ function errMsg(err) {
27
+ return err instanceof Error ? err.message : String(err);
28
+ }
29
+ function printSummary(inv, log) {
30
+ log("info", "=== DRY RUN \u2014 AzureInventory NOT pushed ===");
31
+ log("info", `subscription : ${inv.subscription.name} (${inv.subscriptionId})`);
32
+ log("info", `tenant : ${inv.tenantId}`);
33
+ log("info", `directory tenant : ${inv.tenantDirId}`);
34
+ log("info", `capturedAt : ${inv.capturedAt}`);
35
+ log("info", `state : ${inv.subscription.state}`);
36
+ log(
37
+ "info",
38
+ `management group : ${inv.subscription.managementGroupName ?? inv.subscription.managementGroupId ?? "not observed"}`
39
+ );
40
+ log("info", `resources : ${inv.resources.length}`);
41
+ const internetFacingNsgs = inv.network.filter((n) => n.kind === "nsg" && n.hasRiskyInbound).length;
42
+ log("info", `network entities : ${inv.network.length} (${internetFacingNsgs} NSG(s) with any-source inbound)`);
43
+ log("info", `rbac assignments : ${inv.rbac.length} (${inv.rbac.filter((r) => r.isPrivileged).length} privileged)`);
44
+ if (inv.defender) {
45
+ log(
46
+ "info",
47
+ `defender : secureScore=${inv.defender.secureScore ?? "?"} ${Object.keys(inv.defender.planCoverage).length} plan(s), ${inv.defender.recommendations.length} recommendation(s)`
48
+ );
49
+ } else {
50
+ log("info", "defender : not observed (missing Microsoft.Security role/RP registration?)");
51
+ }
52
+ const nonCompliant = inv.policy.filter((p) => p.complianceState === "NonCompliant").length;
53
+ log("info", `policy : ${inv.policy.length} assignment(s) (${nonCompliant} non-compliant)`);
54
+ }
55
+ async function main() {
56
+ const argv = process.argv.slice(2);
57
+ const dryRun = argv.includes("--dry-run");
58
+ const debug = argv.includes("--debug") || process.env.LOG_LEVEL === "debug";
59
+ const log = makeLogger(debug ? "debug" : "info");
60
+ const config = loadAzureConfig();
61
+ const controller = new AbortController();
62
+ const onSigint = () => {
63
+ log("warn", "SIGINT received \u2014 shutting down\u2026");
64
+ controller.abort();
65
+ };
66
+ process.on("SIGINT", onSigint);
67
+ log("info", `Azure Cloud Posture (CSPM) collector starting (dryRun=${dryRun}) \u2014 ARM tenant ${config.azureTenantId}`);
68
+ try {
69
+ const client = new ArmClient(
70
+ {
71
+ tenantId: config.azureTenantId,
72
+ clientId: config.azureClientId,
73
+ clientSecret: config.azureClientSecret,
74
+ signal: controller.signal
75
+ },
76
+ log
77
+ );
78
+ const subs = await listAccessibleSubscriptions(client, config.azureSubscriptions);
79
+ log(
80
+ "info",
81
+ `found ${subs.length} accessible subscription(s): ${subs.map((s) => `${s.displayName} (${s.subscriptionId})`).join(", ")}`
82
+ );
83
+ for (const sub of subs) {
84
+ const stamp = {
85
+ collectorId: config.collectorId,
86
+ tenantId: config.tenantId,
87
+ siteId: config.siteId,
88
+ tenantDirId: config.azureTenantId
89
+ };
90
+ const inventory = await buildAzureInventory(client, sub, stamp, log);
91
+ if (dryRun) {
92
+ printSummary(inventory, log);
93
+ continue;
94
+ }
95
+ log("info", `pushing Azure inventory for subscription ${sub.subscriptionId} to ${config.controlPlaneUrl}`);
96
+ const result = await pushAzureInventory(
97
+ inventory,
98
+ { controlPlaneUrl: config.controlPlaneUrl, apiKey: config.apiKey },
99
+ log,
100
+ controller.signal
101
+ );
102
+ log("info", `=== PUSH OK (subscription ${sub.subscriptionId}) ===`);
103
+ if (result.snapshotId) log("info", `snapshotId : ${result.snapshotId}`);
104
+ if (result.version !== void 0) log("info", `version : ${result.version}`);
105
+ }
106
+ } finally {
107
+ process.off("SIGINT", onSigint);
108
+ }
109
+ }
110
+ main().catch((err) => {
111
+ console.error(`${(/* @__PURE__ */ new Date()).toISOString()} ERROR ${errMsg(err)}`);
112
+ process.exitCode = 1;
113
+ });
114
+ //# sourceMappingURL=azure-index.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/azure-index.ts"],
4
+ "sourcesContent": ["/**\r\n * Azure Cloud Posture (CSPM) collector CLI entry point.\r\n *\r\n * pnpm azure-scan inventory every subscription the configured ARM\r\n * service principal can see (or the AZURE_SUBSCRIPTIONS\r\n * allow-list, if set) and push each to /ingest/azure\r\n * pnpm azure-scan:dry inventory and print a summary WITHOUT pushing\r\n * tsx src/azure-index.ts --dry-run (equivalent to azure-scan:dry)\r\n *\r\n * ONE service principal (ARM app registration, OAuth2 client-credentials) that\r\n * may see MANY subscriptions \u2014 unlike identity-index.ts's \"one Graph app\r\n * registration = one cloud org\", this loops {@link listAccessibleSubscriptions}\r\n * and builds + pushes ONE AzureInventory PER subscription, each versioned\r\n * independently on the control plane by subscriptionId. A gather that 403s on a\r\n * missing role/unregistered resource provider degrades that ONE signal\r\n * gracefully within a single subscription's build (see azure/plugin.ts); only a\r\n * failure to acquire a token or list subscriptions at all fails the whole run.\r\n *\r\n * Ctrl-C (SIGINT) triggers a cooperative AbortSignal so in-flight requests stop.\r\n */\r\nimport { loadAzureConfig } from \"./config.js\";\r\nimport { pushAzureInventory } from \"./core/push.js\";\r\nimport { ArmClient } from \"./azure/arm.js\";\r\nimport { buildAzureInventory, listAccessibleSubscriptions, type AzureStamp } from \"./azure/plugin.js\";\r\nimport type { Logger } from \"./core/runner.js\";\r\nimport type { AzureInventory } from \"@netdoc/collector-contract\";\r\n\r\nconst LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 } as const;\r\n\r\nfunction makeLogger(minLevel: keyof typeof LEVEL_RANK = \"info\"): Logger {\r\n const min = LEVEL_RANK[minLevel];\r\n return (level, msg, meta) => {\r\n if (LEVEL_RANK[level] < min) return;\r\n const line = `${new Date().toISOString()} ${level.toUpperCase().padEnd(5)} ${msg}`;\r\n const out = level === \"error\" ? console.error : level === \"warn\" ? console.warn : console.log;\r\n if (meta !== undefined) out(line, meta);\r\n else out(line);\r\n };\r\n}\r\n\r\nfunction errMsg(err: unknown): string {\r\n return err instanceof Error ? err.message : String(err);\r\n}\r\n\r\nfunction printSummary(inv: AzureInventory, log: Logger): void {\r\n log(\"info\", \"=== DRY RUN \u2014 AzureInventory NOT pushed ===\");\r\n log(\"info\", `subscription : ${inv.subscription.name} (${inv.subscriptionId})`);\r\n log(\"info\", `tenant : ${inv.tenantId}`);\r\n log(\"info\", `directory tenant : ${inv.tenantDirId}`);\r\n log(\"info\", `capturedAt : ${inv.capturedAt}`);\r\n log(\"info\", `state : ${inv.subscription.state}`);\r\n log(\r\n \"info\",\r\n `management group : ${inv.subscription.managementGroupName ?? inv.subscription.managementGroupId ?? \"not observed\"}`,\r\n );\r\n log(\"info\", `resources : ${inv.resources.length}`);\r\n // `hasRiskyInbound` is true when SOME inbound Allow rule names an any-source\r\n // prefix; it says nothing about which port that rule opens, so an NSG fronting\r\n // a public web server (443 from the internet, exactly as intended) sets it.\r\n // Reporting that count as \"risky\" told the operator about an exposure this\r\n // payload cannot substantiate \u2014 the per-port verdict belongs to the control\r\n // plane's audit engine, which reads the rules shipped alongside the flag.\r\n const internetFacingNsgs = inv.network.filter((n) => n.kind === \"nsg\" && n.hasRiskyInbound).length;\r\n log(\"info\", `network entities : ${inv.network.length} (${internetFacingNsgs} NSG(s) with any-source inbound)`);\r\n log(\"info\", `rbac assignments : ${inv.rbac.length} (${inv.rbac.filter((r) => r.isPrivileged).length} privileged)`);\r\n if (inv.defender) {\r\n log(\r\n \"info\",\r\n `defender : secureScore=${inv.defender.secureScore ?? \"?\"} ` +\r\n `${Object.keys(inv.defender.planCoverage).length} plan(s), ${inv.defender.recommendations.length} recommendation(s)`,\r\n );\r\n } else {\r\n log(\"info\", \"defender : not observed (missing Microsoft.Security role/RP registration?)\");\r\n }\r\n const nonCompliant = inv.policy.filter((p) => p.complianceState === \"NonCompliant\").length;\r\n log(\"info\", `policy : ${inv.policy.length} assignment(s) (${nonCompliant} non-compliant)`);\r\n}\r\n\r\nasync function main(): Promise<void> {\r\n const argv = process.argv.slice(2);\r\n const dryRun = argv.includes(\"--dry-run\");\r\n const debug = argv.includes(\"--debug\") || process.env.LOG_LEVEL === \"debug\";\r\n const log = makeLogger(debug ? \"debug\" : \"info\");\r\n\r\n const config = loadAzureConfig();\r\n\r\n const controller = new AbortController();\r\n const onSigint = (): void => {\r\n log(\"warn\", \"SIGINT received \u2014 shutting down\u2026\");\r\n controller.abort();\r\n };\r\n process.on(\"SIGINT\", onSigint);\r\n\r\n log(\"info\", `Azure Cloud Posture (CSPM) collector starting (dryRun=${dryRun}) \u2014 ARM tenant ${config.azureTenantId}`);\r\n\r\n try {\r\n const client = new ArmClient(\r\n {\r\n tenantId: config.azureTenantId,\r\n clientId: config.azureClientId,\r\n clientSecret: config.azureClientSecret,\r\n signal: controller.signal,\r\n },\r\n log,\r\n );\r\n\r\n const subs = await listAccessibleSubscriptions(client, config.azureSubscriptions);\r\n log(\r\n \"info\",\r\n `found ${subs.length} accessible subscription(s): ${subs.map((s) => `${s.displayName} (${s.subscriptionId})`).join(\", \")}`,\r\n );\r\n\r\n for (const sub of subs) {\r\n const stamp: AzureStamp = {\r\n collectorId: config.collectorId,\r\n tenantId: config.tenantId,\r\n siteId: config.siteId,\r\n tenantDirId: config.azureTenantId,\r\n };\r\n const inventory = await buildAzureInventory(client, sub, stamp, log);\r\n\r\n if (dryRun) {\r\n printSummary(inventory, log);\r\n continue;\r\n }\r\n\r\n log(\"info\", `pushing Azure inventory for subscription ${sub.subscriptionId} to ${config.controlPlaneUrl}`);\r\n const result = await pushAzureInventory(\r\n inventory,\r\n { controlPlaneUrl: config.controlPlaneUrl, apiKey: config.apiKey },\r\n log,\r\n controller.signal,\r\n );\r\n log(\"info\", `=== PUSH OK (subscription ${sub.subscriptionId}) ===`);\r\n if (result.snapshotId) log(\"info\", `snapshotId : ${result.snapshotId}`);\r\n if (result.version !== undefined) log(\"info\", `version : ${result.version}`);\r\n }\r\n } finally {\r\n process.off(\"SIGINT\", onSigint);\r\n }\r\n}\r\n\r\nmain().catch((err) => {\r\n console.error(`${new Date().toISOString()} ERROR ${errMsg(err)}`);\r\n process.exitCode = 1;\r\n});\r\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }