infracensus-collector 1.6.21 → 1.6.23

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 (101) hide show
  1. package/LICENSE +23 -23
  2. package/dist/a10-index.js +4 -4
  3. package/dist/azure-index.js +8 -7
  4. package/dist/azure-index.js.map +1 -1
  5. package/dist/{chunk-SSKRKLNM.js → chunk-25TNYVF5.js} +131 -38
  6. package/dist/chunk-25TNYVF5.js.map +7 -0
  7. package/dist/{chunk-NH4RDH3P.js → chunk-2CKQ2AXQ.js} +24 -5
  8. package/dist/chunk-2CKQ2AXQ.js.map +7 -0
  9. package/dist/{chunk-SSELCAMT.js → chunk-3WK4UW4I.js} +3 -3
  10. package/dist/chunk-3WK4UW4I.js.map +7 -0
  11. package/dist/{chunk-WYJH3AW5.js → chunk-42UQ3SP6.js} +48 -4
  12. package/dist/chunk-42UQ3SP6.js.map +7 -0
  13. package/dist/{chunk-QW4IV7RU.js → chunk-6YAG2LJC.js} +3 -3
  14. package/dist/chunk-6YAG2LJC.js.map +7 -0
  15. package/dist/{chunk-4RUBV5WB.js → chunk-7ESBA5C5.js} +1 -1
  16. package/dist/chunk-7ESBA5C5.js.map +7 -0
  17. package/dist/{chunk-2XRLDUAC.js → chunk-ALPMCXOV.js} +5487 -6073
  18. package/dist/chunk-ALPMCXOV.js.map +7 -0
  19. package/dist/{chunk-MP6JAAFY.js → chunk-CMBYYYAZ.js} +4 -4
  20. package/dist/{chunk-HGADZRD3.js → chunk-H2WWXFBM.js} +10 -9
  21. package/dist/chunk-H2WWXFBM.js.map +7 -0
  22. package/dist/{chunk-FJ22GRKW.js → chunk-I5A5QI3R.js} +45 -7
  23. package/dist/chunk-I5A5QI3R.js.map +7 -0
  24. package/dist/{chunk-FQNQEK35.js → chunk-MAEF22DU.js} +3 -3
  25. package/dist/{chunk-FNCTUV2L.js → chunk-Q6MEI374.js} +2 -2
  26. package/dist/{chunk-FNCTUV2L.js.map → chunk-Q6MEI374.js.map} +1 -1
  27. package/dist/{chunk-4UOTVQ2D.js → chunk-QQ67FRXQ.js} +218 -29
  28. package/dist/chunk-QQ67FRXQ.js.map +7 -0
  29. package/dist/{chunk-FXUH6FPP.js → chunk-QZS2SMFT.js} +68 -9
  30. package/dist/chunk-QZS2SMFT.js.map +7 -0
  31. package/dist/{chunk-YWOXCYLX.js → chunk-S6QIVL5K.js} +4 -4
  32. package/dist/{chunk-DIS3S4SY.js → chunk-TGEOODHQ.js} +32 -2
  33. package/dist/{chunk-DIS3S4SY.js.map → chunk-TGEOODHQ.js.map} +3 -3
  34. package/dist/{chunk-DI6WLXKR.js → chunk-VSG4FRGL.js} +2 -33
  35. package/dist/{chunk-DI6WLXKR.js.map → chunk-VSG4FRGL.js.map} +3 -3
  36. package/dist/chunk-X46NKIFT.js +96 -0
  37. package/dist/chunk-X46NKIFT.js.map +7 -0
  38. package/dist/{chunk-HDNYHF57.js → chunk-XCEVKY55.js} +41 -9
  39. package/dist/chunk-XCEVKY55.js.map +7 -0
  40. package/dist/{chunk-32BYFAKO.js → chunk-XLB7GCCC.js} +38 -17
  41. package/dist/chunk-XLB7GCCC.js.map +7 -0
  42. package/dist/{chunk-WPRMFIPU.js → chunk-XNSIIJRX.js} +16 -3
  43. package/dist/chunk-XNSIIJRX.js.map +7 -0
  44. package/dist/{chunk-CW3YCPEC.js → chunk-XSHYWQAH.js} +1 -1
  45. package/dist/chunk-XSHYWQAH.js.map +7 -0
  46. package/dist/cli.js +14 -13
  47. package/dist/cli.js.map +1 -1
  48. package/dist/{config-5S3SX2ZB.js → config-W26KGYIS.js} +4 -4
  49. package/dist/configimport-index.js +5 -5
  50. package/dist/configimport-index.js.map +1 -1
  51. package/dist/hyperv-index.js +15 -7
  52. package/dist/hyperv-index.js.map +2 -2
  53. package/dist/identity-index.js +8 -7
  54. package/dist/identity-index.js.map +1 -1
  55. package/dist/index.js +938 -118
  56. package/dist/index.js.map +4 -4
  57. package/dist/{linux-7OIQNB4O.js → linux-Y4ZUMDY3.js} +2 -2
  58. package/dist/linux-Y4ZUMDY3.js.map +7 -0
  59. package/dist/pkgRoot-RFOX7XXU.js +7 -0
  60. package/dist/sensitive-data-index.js +88 -11
  61. package/dist/sensitive-data-index.js.map +3 -3
  62. package/dist/{service-AFZPX4K7.js → service-RDLVUKU2.js} +2 -2
  63. package/dist/sql-index.js +15 -7
  64. package/dist/sql-index.js.map +2 -2
  65. package/dist/{status-7YZQI2LY.js → status-Y34C7AR5.js} +8 -8
  66. package/dist/{tools-BTYQGJEB.js → tools-KT77LANU.js} +2 -2
  67. package/dist/tools-KT77LANU.js.map +7 -0
  68. package/dist/vmware-index.js +16 -8
  69. package/dist/vmware-index.js.map +2 -2
  70. package/dist/voice-index.js +8 -8
  71. package/dist/{windows-2P76ECNN.js → windows-B4L24PIR.js} +2 -2
  72. package/package.json +97 -97
  73. package/scripts/bundle.mjs +124 -124
  74. package/scripts/lib/enrolment.d.mts +62 -62
  75. package/scripts/lib/prompt.mjs +98 -98
  76. package/dist/chunk-2XRLDUAC.js.map +0 -7
  77. package/dist/chunk-32BYFAKO.js.map +0 -7
  78. package/dist/chunk-4RUBV5WB.js.map +0 -7
  79. package/dist/chunk-4UOTVQ2D.js.map +0 -7
  80. package/dist/chunk-CW3YCPEC.js.map +0 -7
  81. package/dist/chunk-FJ22GRKW.js.map +0 -7
  82. package/dist/chunk-FXUH6FPP.js.map +0 -7
  83. package/dist/chunk-HDNYHF57.js.map +0 -7
  84. package/dist/chunk-HGADZRD3.js.map +0 -7
  85. package/dist/chunk-NH4RDH3P.js.map +0 -7
  86. package/dist/chunk-QW4IV7RU.js.map +0 -7
  87. package/dist/chunk-SSELCAMT.js.map +0 -7
  88. package/dist/chunk-SSKRKLNM.js.map +0 -7
  89. package/dist/chunk-WPRMFIPU.js.map +0 -7
  90. package/dist/chunk-WYJH3AW5.js.map +0 -7
  91. package/dist/linux-7OIQNB4O.js.map +0 -7
  92. package/dist/pkgRoot-TT3C3BUM.js +0 -7
  93. package/dist/tools-BTYQGJEB.js.map +0 -7
  94. /package/dist/{chunk-MP6JAAFY.js.map → chunk-CMBYYYAZ.js.map} +0 -0
  95. /package/dist/{chunk-FQNQEK35.js.map → chunk-MAEF22DU.js.map} +0 -0
  96. /package/dist/{chunk-YWOXCYLX.js.map → chunk-S6QIVL5K.js.map} +0 -0
  97. /package/dist/{config-5S3SX2ZB.js.map → config-W26KGYIS.js.map} +0 -0
  98. /package/dist/{pkgRoot-TT3C3BUM.js.map → pkgRoot-RFOX7XXU.js.map} +0 -0
  99. /package/dist/{service-AFZPX4K7.js.map → service-RDLVUKU2.js.map} +0 -0
  100. /package/dist/{status-7YZQI2LY.js.map → status-Y34C7AR5.js.map} +0 -0
  101. /package/dist/{windows-2P76ECNN.js.map → windows-B4L24PIR.js.map} +0 -0
package/LICENSE CHANGED
@@ -1,23 +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
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/dist/a10-index.js CHANGED
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  AxapiClient,
3
3
  buildLoadBalancerInventory
4
- } from "./chunk-NH4RDH3P.js";
4
+ } from "./chunk-2CKQ2AXQ.js";
5
5
  import {
6
6
  requireEntitlementToScan
7
- } from "./chunk-4UOTVQ2D.js";
7
+ } from "./chunk-QQ67FRXQ.js";
8
8
  import {
9
9
  ROUTES
10
- } from "./chunk-2XRLDUAC.js";
11
- import "./chunk-CW3YCPEC.js";
10
+ } from "./chunk-ALPMCXOV.js";
11
+ import "./chunk-XSHYWQAH.js";
12
12
 
13
13
  // src/a10-index.ts
14
14
  var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
@@ -2,19 +2,20 @@ import {
2
2
  ArmClient,
3
3
  buildAzureInventory,
4
4
  listAccessibleSubscriptions
5
- } from "./chunk-32BYFAKO.js";
5
+ } from "./chunk-XLB7GCCC.js";
6
+ import "./chunk-X46NKIFT.js";
6
7
  import {
7
8
  pushAzureInventory
8
- } from "./chunk-YWOXCYLX.js";
9
- import "./chunk-FNCTUV2L.js";
9
+ } from "./chunk-S6QIVL5K.js";
10
+ import "./chunk-Q6MEI374.js";
10
11
  import {
11
12
  loadAzureConfig
12
- } from "./chunk-FQNQEK35.js";
13
+ } from "./chunk-MAEF22DU.js";
13
14
  import {
14
15
  requireEntitlementToScan
15
- } from "./chunk-4UOTVQ2D.js";
16
- import "./chunk-2XRLDUAC.js";
17
- import "./chunk-CW3YCPEC.js";
16
+ } from "./chunk-QQ67FRXQ.js";
17
+ import "./chunk-ALPMCXOV.js";
18
+ import "./chunk-XSHYWQAH.js";
18
19
 
19
20
  // src/azure-index.ts
20
21
  var LEVEL_RANK = { debug: 10, info: 20, warn: 30, error: 40 };
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/azure-index.ts"],
4
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\nimport { requireEntitlementToScan } from \"./entitlement/index.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 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 // \u2500\u2500 PAID-LICENCE GATE \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\r\n //\r\n // BEFORE the client is built and before a credential leaves this process.\r\n // `core/planRefusal.ts` named this exact path as the one its own refusal\r\n // handling could not save: \"a refusal is learned at PUSH \u2014 which for a\r\n // one-shot `netdoc-collector scan azure` is after the collector has already\r\n // logged into the customer's Azure subscriptions and read their whole inventory\". Asking\r\n // first is what closes it.\r\n //\r\n // `--dry-run` IS GATED TOO, deliberately. A dry run still authenticates to\r\n // the customer's appliance and reads their estate; it merely declines to\r\n // upload it. The connection is the thing an unentitled tenant must not make.\r\n await requireEntitlementToScan(config, log, \"the one-shot Azure Cloud Posture scan\");\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 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": ";;;;;;;;;;;;;;;;;;;AA4BA,IAAM,aAAa,EAAE,OAAO,IAAI,MAAM,IAAI,MAAM,IAAI,OAAO,GAAG;AAE9D,SAAS,WAAW,WAAoC,QAAgB;AACtE,QAAM,MAAM,WAAW,QAAQ;AAC/B,SAAO,CAAC,OAAO,KAAK,SAAS;AAC3B,QAAI,WAAW,KAAK,IAAI,IAAK;AAC7B,UAAM,OAAO,IAAG,oBAAI,KAAK,GAAE,YAAY,CAAC,IAAI,MAAM,YAAY,EAAE,OAAO,CAAC,CAAC,IAAI,GAAG;AAChF,UAAM,MAAM,UAAU,UAAU,QAAQ,QAAQ,UAAU,SAAS,QAAQ,OAAO,QAAQ;AAC1F,QAAI,SAAS,OAAW,KAAI,MAAM,IAAI;AAAA,QACjC,KAAI,IAAI;AAAA,EACf;AACF;AAEA,SAAS,OAAO,KAAsB;AACpC,SAAO,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AACxD;AAEA,SAAS,aAAa,KAAqB,KAAmB;AAC5D,MAAI,QAAQ,kDAA6C;AACzD,MAAI,QAAQ,sBAAsB,IAAI,aAAa,IAAI,KAAK,IAAI,cAAc,GAAG;AACjF,MAAI,QAAQ,sBAAsB,IAAI,QAAQ,EAAE;AAChD,MAAI,QAAQ,sBAAsB,IAAI,WAAW,EAAE;AACnD,MAAI,QAAQ,sBAAsB,IAAI,UAAU,EAAE;AAClD,MAAI,QAAQ,sBAAsB,IAAI,aAAa,KAAK,EAAE;AAC1D;AAAA,IACE;AAAA,IACA,sBAAsB,IAAI,aAAa,uBAAuB,IAAI,aAAa,qBAAqB,cAAc;AAAA,EACpH;AACA,MAAI,QAAQ,sBAAsB,IAAI,UAAU,MAAM,EAAE;AAOxD,QAAM,qBAAqB,IAAI,QAAQ,OAAO,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,eAAe,EAAE;AAC5F,MAAI,QAAQ,sBAAsB,IAAI,QAAQ,MAAM,KAAK,kBAAkB,kCAAkC;AAC7G,MAAI,QAAQ,sBAAsB,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,OAAO,CAAC,MAAM,EAAE,YAAY,EAAE,MAAM,cAAc;AACjH,MAAI,IAAI,UAAU;AAChB;AAAA,MACE;AAAA,MACA,kCAAkC,IAAI,SAAS,eAAe,GAAG,IAC5D,OAAO,KAAK,IAAI,SAAS,YAAY,EAAE,MAAM,aAAa,IAAI,SAAS,gBAAgB,MAAM;AAAA,IACpG;AAAA,EACF,OAAO;AACL,QAAI,QAAQ,oFAAoF;AAAA,EAClG;AACA,QAAM,eAAe,IAAI,OAAO,OAAO,CAAC,MAAM,EAAE,oBAAoB,cAAc,EAAE;AACpF,MAAI,QAAQ,sBAAsB,IAAI,OAAO,MAAM,mBAAmB,YAAY,iBAAiB;AACrG;AAEA,eAAe,OAAsB;AACnC,QAAM,OAAO,QAAQ,KAAK,MAAM,CAAC;AACjC,QAAM,SAAS,KAAK,SAAS,WAAW;AACxC,QAAM,QAAQ,KAAK,SAAS,SAAS,KAAK,QAAQ,IAAI,cAAc;AACpE,QAAM,MAAM,WAAW,QAAQ,UAAU,MAAM;AAE/C,QAAM,SAAS,gBAAgB;AAc/B,QAAM,yBAAyB,QAAQ,KAAK,uCAAuC;AAGnF,QAAM,aAAa,IAAI,gBAAgB;AACvC,QAAM,WAAW,MAAY;AAC3B,QAAI,QAAQ,4CAAkC;AAC9C,eAAW,MAAM;AAAA,EACnB;AACA,UAAQ,GAAG,UAAU,QAAQ;AAE7B,MAAI,QAAQ,yDAAyD,MAAM,uBAAkB,OAAO,aAAa,EAAE;AAEnH,MAAI;AACF,UAAM,SAAS,IAAI;AAAA,MACjB;AAAA,QACE,UAAU,OAAO;AAAA,QACjB,UAAU,OAAO;AAAA,QACjB,cAAc,OAAO;AAAA,QACrB,QAAQ,WAAW;AAAA,MACrB;AAAA,MACA;AAAA,IACF;AAEA,UAAM,OAAO,MAAM,4BAA4B,QAAQ,OAAO,kBAAkB;AAChF;AAAA,MACE;AAAA,MACA,SAAS,KAAK,MAAM,gCAAgC,KAAK,IAAI,CAAC,MAAM,GAAG,EAAE,WAAW,KAAK,EAAE,cAAc,GAAG,EAAE,KAAK,IAAI,CAAC;AAAA,IAC1H;AAEA,eAAW,OAAO,MAAM;AACtB,YAAM,QAAoB;AAAA,QACxB,aAAa,OAAO;AAAA,QACpB,UAAU,OAAO;AAAA,QACjB,QAAQ,OAAO;AAAA,QACf,aAAa,OAAO;AAAA,MACtB;AACA,YAAM,YAAY,MAAM,oBAAoB,QAAQ,KAAK,OAAO,GAAG;AAEnE,UAAI,QAAQ;AACV,qBAAa,WAAW,GAAG;AAC3B;AAAA,MACF;AAEA,UAAI,QAAQ,4CAA4C,IAAI,cAAc,OAAO,OAAO,eAAe,EAAE;AACzG,YAAM,SAAS,MAAM;AAAA,QACnB;AAAA,QACA,EAAE,iBAAiB,OAAO,iBAAiB,QAAQ,OAAO,OAAO;AAAA,QACjE;AAAA,QACA,WAAW;AAAA,MACb;AACA,UAAI,QAAQ,6BAA6B,IAAI,cAAc,OAAO;AAClE,UAAI,OAAO,WAAY,KAAI,QAAQ,iBAAiB,OAAO,UAAU,EAAE;AACvE,UAAI,OAAO,YAAY,OAAW,KAAI,QAAQ,iBAAiB,OAAO,OAAO,EAAE;AAAA,IACjF;AAAA,EACF,UAAE;AACA,YAAQ,IAAI,UAAU,QAAQ;AAAA,EAChC;AACF;AAEA,KAAK,EAAE,MAAM,CAAC,QAAQ;AACpB,UAAQ,MAAM,IAAG,oBAAI,KAAK,GAAE,YAAY,CAAC,UAAU,OAAO,GAAG,CAAC,EAAE;AAChE,UAAQ,WAAW;AACrB,CAAC;",
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  insecureDispatcher
3
- } from "./chunk-4RUBV5WB.js";
3
+ } from "./chunk-7ESBA5C5.js";
4
4
  import {
5
5
  DialPlanEntry,
6
6
  UnityVoicemail,
@@ -13,10 +13,23 @@ import {
13
13
  VoiceRegion,
14
14
  VoiceSystem,
15
15
  WebexCalling
16
- } from "./chunk-2XRLDUAC.js";
16
+ } from "./chunk-ALPMCXOV.js";
17
17
 
18
18
  // src/voice/axl.ts
19
19
  import { XMLParser } from "fast-xml-parser";
20
+ var AXL_REQUEST_TIMEOUT_MS = 12e4;
21
+ var RISPORT_REQUEST_TIMEOUT_MS = 6e4;
22
+ function requestSignal(signal, timeoutMs) {
23
+ const timeout = AbortSignal.timeout(timeoutMs);
24
+ return signal ? AbortSignal.any([signal, timeout]) : timeout;
25
+ }
26
+ function macFromCucmDeviceName(name) {
27
+ if (!name) return void 0;
28
+ const m = /^(?:SEP|ATA|AN|VGC)([0-9A-F]{12})$/i.exec(name.trim());
29
+ if (!m) return void 0;
30
+ const hex = m[1].toUpperCase();
31
+ return hex.match(/.{2}/g).join(":");
32
+ }
20
33
  var noopLog = () => {
21
34
  };
22
35
  function xml(v) {
@@ -33,6 +46,11 @@ function buildListRequest(spec) {
33
46
  function errMsg(err) {
34
47
  return err instanceof Error ? err.message : String(err);
35
48
  }
49
+ function isSchemaRejection(err) {
50
+ return /cvc-complex-type|cvc-|unmarshall?ing error|invalid content was found|not expected|returnedTags/i.test(
51
+ errMsg(err)
52
+ );
53
+ }
36
54
  function bodySnippet(text) {
37
55
  const flat = text.replace(/\s+/g, " ").trim();
38
56
  return flat.length > 120 ? `${flat.slice(0, 120)}\u2026` : flat;
@@ -124,25 +142,52 @@ var AxlClient = class {
124
142
  const ret = asRecord(pick(firstResponse(body), "return"));
125
143
  return asStr(ret.componentVersion) ?? asStr(pick(ret.componentVersion, "version"));
126
144
  }
127
- /** listPhone — every registered/provisioned phone + soft-client device. */
145
+ /**
146
+ * listPhone — every registered/provisioned phone + soft-client device.
147
+ *
148
+ * ── TWO TAG SETS (2026-09-15) ───────────────────────────────────────────────
149
+ *
150
+ * `ownerUserName` and `loadInformation` (the configured firmware load) are
151
+ * both returned by `listPhone` and both have had a home in the contract
152
+ * (`VoiceEndpoint.ownerUser` / `.firmware`) since it was written — they were
153
+ * simply never asked for, so every phone the portal shows has no owner and no
154
+ * firmware. They are asked for now. But a returnedTag the cluster's schema
155
+ * does not know fails the WHOLE request with a schema fault — and listPhone is
156
+ * the one call that gives the estate its phones — so a schema rejection is
157
+ * retried once with the original six tags, and the retreat is logged, rather
158
+ * than letting two optional columns cost the endpoint list.
159
+ */
128
160
  async listPhones() {
129
- const body = await this.call(
130
- buildListRequest({
131
- op: "listPhone",
132
- tags: ["name", "description", "product", "protocol", "devicePoolName", "callingSearchSpaceName"]
133
- })
134
- );
161
+ const baseTags = ["name", "description", "product", "protocol", "devicePoolName", "callingSearchSpaceName"];
162
+ const extraTags = ["ownerUserName", "loadInformation"];
163
+ let body;
164
+ try {
165
+ body = await this.call(buildListRequest({ op: "listPhone", tags: [...baseTags, ...extraTags] }));
166
+ } catch (err) {
167
+ if (!isSchemaRejection(err)) throw err;
168
+ this.log(
169
+ "warn",
170
+ `listPhone: this cluster's AXL schema rejected the ${extraTags.join("/")} tags (${errMsg(err)}); retrying without them \u2014 phones in this snapshot carry no owner or firmware`
171
+ );
172
+ body = await this.call(buildListRequest({ op: "listPhone", tags: baseTags }));
173
+ }
135
174
  const ret = asRecord(pick(firstResponse(body), "return"));
136
175
  return toArray(ret.phone).map((raw) => {
137
176
  const p = asRecord(raw);
177
+ const name = asStr(p.name);
178
+ const firmware = asStr(p.loadInformation);
179
+ const macAddress = macFromCucmDeviceName(name);
138
180
  return {
139
181
  uuid: asStr(p["@_uuid"]),
140
- name: asStr(p.name),
182
+ name,
141
183
  description: asStr(p.description),
142
184
  model: asStr(p.product),
143
185
  protocol: asStr(p.protocol),
144
186
  devicePoolName: asStr(p.devicePoolName),
145
- callingSearchSpaceName: asStr(p.callingSearchSpaceName)
187
+ callingSearchSpaceName: asStr(p.callingSearchSpaceName),
188
+ ...asStr(p.ownerUserName) ? { ownerUserName: asStr(p.ownerUserName) } : {},
189
+ ...firmware ? { firmware } : {},
190
+ ...macAddress ? { macAddress } : {}
146
191
  };
147
192
  });
148
193
  }
@@ -943,7 +988,9 @@ var AxlClient = class {
943
988
  Authorization: this.authHeader
944
989
  },
945
990
  body: envelope,
946
- ...this.signal ? { signal: this.signal } : {}
991
+ // Bounded: a hung RIS service is the best-effort field staying "unknown",
992
+ // not the whole build waiting on it. See RISPORT_REQUEST_TIMEOUT_MS.
993
+ signal: requestSignal(this.signal, RISPORT_REQUEST_TIMEOUT_MS)
947
994
  });
948
995
  const text = await res.text();
949
996
  if (!res.ok) throw new Error(`RisPort70 request failed: HTTP ${res.status}`);
@@ -960,8 +1007,14 @@ var AxlClient = class {
960
1007
  if (!name) continue;
961
1008
  const statusRaw = (asStr(dev.Status) ?? "").toLowerCase();
962
1009
  const status = statusRaw.includes("unregistered") ? "unregistered" : statusRaw.includes("registered") ? "registered" : "unknown";
963
- const ipAddress = asStr(pick(asRecord(dev.IPAddress), "IP"));
964
- out.set(name, { status, ...ipAddress ? { ipAddress } : {} });
1010
+ const ipItems = toArray(asRecord(dev.IPAddress).item);
1011
+ const ipAddress = asStr(pick(ipItems[0], "IP")) ?? asStr(pick(asRecord(dev.IPAddress), "IP"));
1012
+ const firmware = asStr(dev.ActiveLoadID);
1013
+ out.set(name, {
1014
+ status,
1015
+ ...ipAddress ? { ipAddress } : {},
1016
+ ...firmware ? { firmware } : {}
1017
+ });
965
1018
  }
966
1019
  }
967
1020
  } catch (err) {
@@ -974,17 +1027,28 @@ var AxlClient = class {
974
1027
  async call(innerXml) {
975
1028
  if (this.signal?.aborted) throw new Error("AXL request aborted");
976
1029
  const envelope = `<?xml version="1.0" encoding="UTF-8"?><soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:ns="http://www.cisco.com/AXL/API/${xml(this.version)}"><soapenv:Body>${innerXml}</soapenv:Body></soapenv:Envelope>`;
977
- const res = await fetch(this.base, {
978
- ...this.tlsInit,
979
- method: "POST",
980
- headers: {
981
- "Content-Type": "text/xml; charset=utf-8",
982
- SOAPAction: `CUCM:DB ver=${this.version}`,
983
- Authorization: this.authHeader
984
- },
985
- body: envelope,
986
- ...this.signal ? { signal: this.signal } : {}
987
- });
1030
+ let res;
1031
+ try {
1032
+ res = await fetch(this.base, {
1033
+ ...this.tlsInit,
1034
+ method: "POST",
1035
+ headers: {
1036
+ "Content-Type": "text/xml; charset=utf-8",
1037
+ SOAPAction: `CUCM:DB ver=${this.version}`,
1038
+ Authorization: this.authHeader
1039
+ },
1040
+ body: envelope,
1041
+ // Job cancellation OR the per-request ceiling — see AXL_REQUEST_TIMEOUT_MS.
1042
+ signal: requestSignal(this.signal, AXL_REQUEST_TIMEOUT_MS)
1043
+ });
1044
+ } catch (err) {
1045
+ if (err instanceof Error && err.name === "TimeoutError") {
1046
+ throw new Error(
1047
+ `AXL at ${this.base} did not answer within ${Math.round(AXL_REQUEST_TIMEOUT_MS / 1e3)}s \u2014 the connection opened but no reply came back, so the node is reachable and something behind port 8443 is hung or the request is being dropped in flight (a stateful firewall, a CUCM mid-upgrade, a stopped Cisco AXL Web Service)`
1048
+ );
1049
+ }
1050
+ throw err;
1051
+ }
988
1052
  const text = await res.text();
989
1053
  if (res.status === 401) {
990
1054
  throw new Error(
@@ -1028,6 +1092,7 @@ var AxlClient = class {
1028
1092
  // src/voice/cupi.ts
1029
1093
  var noopLog2 = () => {
1030
1094
  };
1095
+ var CUPI_REQUEST_TIMEOUT_MS = 3e4;
1031
1096
  function errMsg2(err) {
1032
1097
  return err instanceof Error ? err.message : String(err);
1033
1098
  }
@@ -1093,12 +1158,23 @@ var CupiClient = class {
1093
1158
  // --- transport -------------------------------------------------------------
1094
1159
  async get(path) {
1095
1160
  if (this.signal?.aborted) throw new Error("CUPI request aborted");
1096
- const res = await fetch(`${this.base}${path}`, {
1097
- ...this.tlsInit,
1098
- method: "GET",
1099
- headers: { Accept: "application/json", Authorization: this.authHeader },
1100
- ...this.signal ? { signal: this.signal } : {}
1101
- });
1161
+ const timeout = AbortSignal.timeout(CUPI_REQUEST_TIMEOUT_MS);
1162
+ let res;
1163
+ try {
1164
+ res = await fetch(`${this.base}${path}`, {
1165
+ ...this.tlsInit,
1166
+ method: "GET",
1167
+ headers: { Accept: "application/json", Authorization: this.authHeader },
1168
+ signal: this.signal ? AbortSignal.any([this.signal, timeout]) : timeout
1169
+ });
1170
+ } catch (err) {
1171
+ if (err instanceof Error && err.name === "TimeoutError") {
1172
+ throw new Error(
1173
+ `CUPI GET ${path} at ${this.base} did not answer within ${Math.round(CUPI_REQUEST_TIMEOUT_MS / 1e3)}s \u2014 the connection opened but no reply came back (Unity Connection hung, or the request dropped in flight)`
1174
+ );
1175
+ }
1176
+ throw err;
1177
+ }
1102
1178
  const text = await res.text();
1103
1179
  if (!res.ok) {
1104
1180
  throw new Error(`CUPI GET ${path} failed: HTTP ${res.status}: ${text.slice(0, 300)}`);
@@ -1116,6 +1192,7 @@ var CupiClient = class {
1116
1192
  var noopLog3 = () => {
1117
1193
  };
1118
1194
  var BASE = "https://webexapis.com/v1/";
1195
+ var WEBEX_REQUEST_TIMEOUT_MS = 3e4;
1119
1196
  function errMsg3(err) {
1120
1197
  return err instanceof Error ? err.message : String(err);
1121
1198
  }
@@ -1224,11 +1301,22 @@ var WebexClient = class {
1224
1301
  }
1225
1302
  async getPage(url) {
1226
1303
  if (this.signal?.aborted) throw new Error("Webex request aborted");
1227
- const res = await fetch(url, {
1228
- method: "GET",
1229
- headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
1230
- ...this.signal ? { signal: this.signal } : {}
1231
- });
1304
+ const timeout = AbortSignal.timeout(WEBEX_REQUEST_TIMEOUT_MS);
1305
+ let res;
1306
+ try {
1307
+ res = await fetch(url, {
1308
+ method: "GET",
1309
+ headers: { Accept: "application/json", Authorization: `Bearer ${this.token}` },
1310
+ signal: this.signal ? AbortSignal.any([this.signal, timeout]) : timeout
1311
+ });
1312
+ } catch (err) {
1313
+ if (err instanceof Error && err.name === "TimeoutError") {
1314
+ throw new Error(
1315
+ `Webex GET ${url} did not answer within ${Math.round(WEBEX_REQUEST_TIMEOUT_MS / 1e3)}s \u2014 the connection opened but no reply came back (a Webex outage, or an egress proxy holding the request)`
1316
+ );
1317
+ }
1318
+ throw err;
1319
+ }
1232
1320
  const text = await res.text();
1233
1321
  if (!res.ok) {
1234
1322
  throw new Error(`Webex GET ${url} failed: HTTP ${res.status}: ${text.slice(0, 300)}`);
@@ -1678,9 +1766,14 @@ async function buildCucmInventory(client, host, stamp, log = noopLog4) {
1678
1766
  callingSearchSpace: p.callingSearchSpaceName ?? null,
1679
1767
  registrationStatus: reg?.status ?? "unknown",
1680
1768
  ownerUser: p.ownerUserName ?? null,
1769
+ // Derived from the device name by listPhones (2026-09-15) — the join key
1770
+ // the plane's voice_endpoints MAC index exists for; it was always null.
1681
1771
  macAddress: p.macAddress ?? null,
1682
1772
  ipAddress: reg?.ipAddress ?? p.ipAddress ?? null,
1683
- firmware: p.firmware ?? null
1773
+ // The load the phone is RUNNING (RisPort ActiveLoadID) over the load it is
1774
+ // CONFIGURED for (listPhone loadInformation, blank when the device pool's
1775
+ // default applies) — the first is what an upgrade audit needs.
1776
+ firmware: reg?.firmware ?? p.firmware ?? null
1684
1777
  });
1685
1778
  });
1686
1779
  const dialPlan = [
@@ -1951,4 +2044,4 @@ export {
1951
2044
  buildUnityInventory,
1952
2045
  buildWebexInventory
1953
2046
  };
1954
- //# sourceMappingURL=chunk-SSKRKLNM.js.map
2047
+ //# sourceMappingURL=chunk-25TNYVF5.js.map