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.
- package/LICENSE +23 -0
- package/README.md +323 -0
- package/dist/a10-index.js +91 -0
- package/dist/a10-index.js.map +7 -0
- package/dist/azure-index.js +114 -0
- package/dist/azure-index.js.map +7 -0
- package/dist/chunk-2VVO5SGB.js +443 -0
- package/dist/chunk-2VVO5SGB.js.map +7 -0
- package/dist/chunk-3JF5HO4Y.js +372 -0
- package/dist/chunk-3JF5HO4Y.js.map +7 -0
- package/dist/chunk-3MIZN7SP.js +5160 -0
- package/dist/chunk-3MIZN7SP.js.map +7 -0
- package/dist/chunk-42NDGVLF.js +78 -0
- package/dist/chunk-42NDGVLF.js.map +7 -0
- package/dist/chunk-7EGPKFND.js +349 -0
- package/dist/chunk-7EGPKFND.js.map +7 -0
- package/dist/chunk-7ESBA5C5.js +31 -0
- package/dist/chunk-7ESBA5C5.js.map +7 -0
- package/dist/chunk-7HFPVDT3.js +1237 -0
- package/dist/chunk-7HFPVDT3.js.map +7 -0
- package/dist/chunk-DBJNP764.js +774 -0
- package/dist/chunk-DBJNP764.js.map +7 -0
- package/dist/chunk-IQU5HMXO.js +1837 -0
- package/dist/chunk-IQU5HMXO.js.map +7 -0
- package/dist/chunk-LNYJAPA2.js +28 -0
- package/dist/chunk-LNYJAPA2.js.map +7 -0
- package/dist/chunk-U2UV5MMR.js +18 -0
- package/dist/chunk-U2UV5MMR.js.map +7 -0
- package/dist/chunk-VEMGGUZ6.js +528 -0
- package/dist/chunk-VEMGGUZ6.js.map +7 -0
- package/dist/chunk-VXDH6AVJ.js +1047 -0
- package/dist/chunk-VXDH6AVJ.js.map +7 -0
- package/dist/chunk-VZFXWTU2.js +267 -0
- package/dist/chunk-VZFXWTU2.js.map +7 -0
- package/dist/chunk-XSHYWQAH.js +20 -0
- package/dist/chunk-XSHYWQAH.js.map +7 -0
- package/dist/chunk-ZCZOUDBC.js +225 -0
- package/dist/chunk-ZCZOUDBC.js.map +7 -0
- package/dist/chunk-ZIUWFLKI.js +11945 -0
- package/dist/chunk-ZIUWFLKI.js.map +7 -0
- package/dist/cli.js +193 -0
- package/dist/cli.js.map +7 -0
- package/dist/config-AA7OSIVF.js +23 -0
- package/dist/config-AA7OSIVF.js.map +7 -0
- package/dist/configimport-index.js +286 -0
- package/dist/configimport-index.js.map +7 -0
- package/dist/hyperv-index.js +105 -0
- package/dist/hyperv-index.js.map +7 -0
- package/dist/identity-index.js +105 -0
- package/dist/identity-index.js.map +7 -0
- package/dist/index.js +11444 -0
- package/dist/index.js.map +7 -0
- package/dist/linux-QMB5WPDK.js +247 -0
- package/dist/linux-QMB5WPDK.js.map +7 -0
- package/dist/pkgRoot-RFOX7XXU.js +7 -0
- package/dist/pkgRoot-RFOX7XXU.js.map +7 -0
- package/dist/sensitive-data-index.js +383 -0
- package/dist/sensitive-data-index.js.map +7 -0
- package/dist/service-U6Y7MN75.js +11 -0
- package/dist/service-U6Y7MN75.js.map +7 -0
- package/dist/sql-index.js +106 -0
- package/dist/sql-index.js.map +7 -0
- package/dist/status-SBN5WZZX.js +26 -0
- package/dist/status-SBN5WZZX.js.map +7 -0
- package/dist/tools-KT77LANU.js +93 -0
- package/dist/tools-KT77LANU.js.map +7 -0
- package/dist/vmware-index.js +118 -0
- package/dist/vmware-index.js.map +7 -0
- package/dist/voice-index.js +181 -0
- package/dist/voice-index.js.map +7 -0
- package/dist/windows-ZUCEMMDI.js +323 -0
- package/dist/windows-ZUCEMMDI.js.map +7 -0
- package/package.json +95 -0
- package/scripts/bundle.mjs +113 -0
- package/scripts/collector-ctl.mjs +143 -0
- package/scripts/collector.env.example +48 -0
- package/scripts/doctor.mjs +199 -0
- package/scripts/enrol.mjs +215 -0
- package/scripts/generate-ssh-key-fixtures.mjs +84 -0
- package/scripts/install-record.mjs +65 -0
- package/scripts/install.sh +203 -0
- package/scripts/lib/daemon-log.mjs +132 -0
- package/scripts/lib/deregister.mjs +81 -0
- package/scripts/lib/enrolment.mjs +266 -0
- package/scripts/lib/install-manifest.mjs +160 -0
- package/scripts/lib/plane-probe.mjs +100 -0
- package/scripts/lib/secure-file.mjs +71 -0
- package/scripts/lib/service-state.mjs +100 -0
- package/scripts/uninstall-local.mjs +133 -0
- package/scripts/uninstall.sh +84 -0
- 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
|
+
}
|