@sonde/packs 0.1.0 → 0.1.1
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/.turbo/turbo-build.log +6 -0
- package/.turbo/turbo-test.log +814 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/CHANGELOG.md +10 -0
- package/dist/index.d.ts +16 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/integrations/citrix.d.ts +13 -0
- package/dist/integrations/citrix.d.ts.map +1 -0
- package/dist/integrations/citrix.js +420 -0
- package/dist/integrations/citrix.js.map +1 -0
- package/dist/integrations/citrix.test.d.ts +2 -0
- package/dist/integrations/citrix.test.d.ts.map +1 -0
- package/dist/integrations/citrix.test.js +464 -0
- package/dist/integrations/citrix.test.js.map +1 -0
- package/dist/integrations/graph.d.ts +9 -0
- package/dist/integrations/graph.d.ts.map +1 -0
- package/dist/integrations/graph.js +290 -0
- package/dist/integrations/graph.js.map +1 -0
- package/dist/integrations/graph.test.d.ts +2 -0
- package/dist/integrations/graph.test.d.ts.map +1 -0
- package/dist/integrations/graph.test.js +356 -0
- package/dist/integrations/graph.test.js.map +1 -0
- package/dist/integrations/httpbin.d.ts +3 -0
- package/dist/integrations/httpbin.d.ts.map +1 -0
- package/dist/integrations/httpbin.js +70 -0
- package/dist/integrations/httpbin.js.map +1 -0
- package/dist/integrations/nutanix.d.ts +18 -0
- package/dist/integrations/nutanix.d.ts.map +1 -0
- package/dist/integrations/nutanix.js +1121 -0
- package/dist/integrations/nutanix.js.map +1 -0
- package/dist/integrations/nutanix.test.d.ts +2 -0
- package/dist/integrations/nutanix.test.d.ts.map +1 -0
- package/dist/integrations/nutanix.test.js +978 -0
- package/dist/integrations/nutanix.test.js.map +1 -0
- package/dist/integrations/proxmox.d.ts +12 -0
- package/dist/integrations/proxmox.d.ts.map +1 -0
- package/dist/integrations/proxmox.js +733 -0
- package/dist/integrations/proxmox.js.map +1 -0
- package/dist/integrations/proxmox.test.d.ts +2 -0
- package/dist/integrations/proxmox.test.d.ts.map +1 -0
- package/dist/integrations/proxmox.test.js +697 -0
- package/dist/integrations/proxmox.test.js.map +1 -0
- package/dist/integrations/servicenow.d.ts +3 -0
- package/dist/integrations/servicenow.d.ts.map +1 -0
- package/dist/integrations/servicenow.js +257 -0
- package/dist/integrations/servicenow.js.map +1 -0
- package/dist/integrations/servicenow.test.d.ts +2 -0
- package/dist/integrations/servicenow.test.d.ts.map +1 -0
- package/dist/integrations/servicenow.test.js +217 -0
- package/dist/integrations/servicenow.test.js.map +1 -0
- package/dist/integrations/splunk.d.ts +9 -0
- package/dist/integrations/splunk.d.ts.map +1 -0
- package/dist/integrations/splunk.js +242 -0
- package/dist/integrations/splunk.js.map +1 -0
- package/dist/integrations/splunk.test.d.ts +2 -0
- package/dist/integrations/splunk.test.d.ts.map +1 -0
- package/dist/integrations/splunk.test.js +323 -0
- package/dist/integrations/splunk.test.js.map +1 -0
- package/dist/mysql/index.d.ts +3 -0
- package/dist/mysql/index.d.ts.map +1 -0
- package/dist/mysql/index.js +13 -0
- package/dist/mysql/index.js.map +1 -0
- package/dist/mysql/manifest.d.ts +3 -0
- package/dist/mysql/manifest.d.ts.map +1 -0
- package/dist/mysql/manifest.js +69 -0
- package/dist/mysql/manifest.js.map +1 -0
- package/dist/mysql/probes/databases-list.d.ts +13 -0
- package/dist/mysql/probes/databases-list.d.ts.map +1 -0
- package/dist/mysql/probes/databases-list.js +31 -0
- package/dist/mysql/probes/databases-list.js.map +1 -0
- package/dist/mysql/probes/databases-list.test.d.ts +2 -0
- package/dist/mysql/probes/databases-list.test.d.ts.map +1 -0
- package/dist/mysql/probes/databases-list.test.js +54 -0
- package/dist/mysql/probes/databases-list.test.js.map +1 -0
- package/dist/mysql/probes/processlist.d.ts +18 -0
- package/dist/mysql/probes/processlist.d.ts.map +1 -0
- package/dist/mysql/probes/processlist.js +36 -0
- package/dist/mysql/probes/processlist.js.map +1 -0
- package/dist/mysql/probes/processlist.test.d.ts +2 -0
- package/dist/mysql/probes/processlist.test.d.ts.map +1 -0
- package/dist/mysql/probes/processlist.test.js +41 -0
- package/dist/mysql/probes/processlist.test.js.map +1 -0
- package/dist/mysql/probes/status.d.ts +14 -0
- package/dist/mysql/probes/status.d.ts.map +1 -0
- package/dist/mysql/probes/status.js +40 -0
- package/dist/mysql/probes/status.js.map +1 -0
- package/dist/mysql/probes/status.test.d.ts +2 -0
- package/dist/mysql/probes/status.test.d.ts.map +1 -0
- package/dist/mysql/probes/status.test.js +43 -0
- package/dist/mysql/probes/status.test.js.map +1 -0
- package/dist/nginx/index.d.ts +3 -0
- package/dist/nginx/index.d.ts.map +1 -0
- package/dist/nginx/index.js +13 -0
- package/dist/nginx/index.js.map +1 -0
- package/dist/nginx/manifest.d.ts +3 -0
- package/dist/nginx/manifest.d.ts.map +1 -0
- package/dist/nginx/manifest.js +68 -0
- package/dist/nginx/manifest.js.map +1 -0
- package/dist/nginx/probes/access-log-tail.d.ts +9 -0
- package/dist/nginx/probes/access-log-tail.d.ts.map +1 -0
- package/dist/nginx/probes/access-log-tail.js +14 -0
- package/dist/nginx/probes/access-log-tail.js.map +1 -0
- package/dist/nginx/probes/access-log-tail.test.d.ts +2 -0
- package/dist/nginx/probes/access-log-tail.test.d.ts.map +1 -0
- package/dist/nginx/probes/access-log-tail.test.js +40 -0
- package/dist/nginx/probes/access-log-tail.test.js.map +1 -0
- package/dist/nginx/probes/config-test.d.ts +8 -0
- package/dist/nginx/probes/config-test.d.ts.map +1 -0
- package/dist/nginx/probes/config-test.js +18 -0
- package/dist/nginx/probes/config-test.js.map +1 -0
- package/dist/nginx/probes/config-test.test.d.ts +2 -0
- package/dist/nginx/probes/config-test.test.d.ts.map +1 -0
- package/dist/nginx/probes/config-test.test.js +35 -0
- package/dist/nginx/probes/config-test.test.js.map +1 -0
- package/dist/nginx/probes/error-log-tail.d.ts +9 -0
- package/dist/nginx/probes/error-log-tail.d.ts.map +1 -0
- package/dist/nginx/probes/error-log-tail.js +14 -0
- package/dist/nginx/probes/error-log-tail.js.map +1 -0
- package/dist/nginx/probes/error-log-tail.test.d.ts +2 -0
- package/dist/nginx/probes/error-log-tail.test.d.ts.map +1 -0
- package/dist/nginx/probes/error-log-tail.test.js +34 -0
- package/dist/nginx/probes/error-log-tail.test.js.map +1 -0
- package/dist/postgres/index.d.ts +3 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +13 -0
- package/dist/postgres/index.js.map +1 -0
- package/dist/postgres/manifest.d.ts +3 -0
- package/dist/postgres/manifest.d.ts.map +1 -0
- package/dist/postgres/manifest.js +90 -0
- package/dist/postgres/manifest.js.map +1 -0
- package/dist/postgres/probes/connections-active.d.ts +17 -0
- package/dist/postgres/probes/connections-active.d.ts.map +1 -0
- package/dist/postgres/probes/connections-active.js +37 -0
- package/dist/postgres/probes/connections-active.js.map +1 -0
- package/dist/postgres/probes/connections-active.test.d.ts +2 -0
- package/dist/postgres/probes/connections-active.test.d.ts.map +1 -0
- package/dist/postgres/probes/connections-active.test.js +36 -0
- package/dist/postgres/probes/connections-active.test.js.map +1 -0
- package/dist/postgres/probes/databases-list.d.ts +14 -0
- package/dist/postgres/probes/databases-list.d.ts.map +1 -0
- package/dist/postgres/probes/databases-list.js +34 -0
- package/dist/postgres/probes/databases-list.js.map +1 -0
- package/dist/postgres/probes/databases-list.test.d.ts +2 -0
- package/dist/postgres/probes/databases-list.test.d.ts.map +1 -0
- package/dist/postgres/probes/databases-list.test.js +49 -0
- package/dist/postgres/probes/databases-list.test.js.map +1 -0
- package/dist/postgres/probes/query-slow.d.ts +17 -0
- package/dist/postgres/probes/query-slow.d.ts.map +1 -0
- package/dist/postgres/probes/query-slow.js +37 -0
- package/dist/postgres/probes/query-slow.js.map +1 -0
- package/dist/postgres/probes/query-slow.test.d.ts +2 -0
- package/dist/postgres/probes/query-slow.test.d.ts.map +1 -0
- package/dist/postgres/probes/query-slow.test.js +30 -0
- package/dist/postgres/probes/query-slow.test.js.map +1 -0
- package/dist/proxmox/index.d.ts +3 -0
- package/dist/proxmox/index.d.ts.map +1 -0
- package/dist/proxmox/index.js +23 -0
- package/dist/proxmox/index.js.map +1 -0
- package/dist/proxmox/manifest.d.ts +3 -0
- package/dist/proxmox/manifest.d.ts.map +1 -0
- package/dist/proxmox/manifest.js +75 -0
- package/dist/proxmox/manifest.js.map +1 -0
- package/dist/proxmox/probes/ceph-status.d.ts +36 -0
- package/dist/proxmox/probes/ceph-status.d.ts.map +1 -0
- package/dist/proxmox/probes/ceph-status.js +71 -0
- package/dist/proxmox/probes/ceph-status.js.map +1 -0
- package/dist/proxmox/probes/ceph-status.test.d.ts +2 -0
- package/dist/proxmox/probes/ceph-status.test.d.ts.map +1 -0
- package/dist/proxmox/probes/ceph-status.test.js +115 -0
- package/dist/proxmox/probes/ceph-status.test.js.map +1 -0
- package/dist/proxmox/probes/cluster-config.d.ts +31 -0
- package/dist/proxmox/probes/cluster-config.d.ts.map +1 -0
- package/dist/proxmox/probes/cluster-config.js +72 -0
- package/dist/proxmox/probes/cluster-config.js.map +1 -0
- package/dist/proxmox/probes/cluster-config.test.d.ts +2 -0
- package/dist/proxmox/probes/cluster-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/cluster-config.test.js +107 -0
- package/dist/proxmox/probes/cluster-config.test.js.map +1 -0
- package/dist/proxmox/probes/ha-status.d.ts +18 -0
- package/dist/proxmox/probes/ha-status.d.ts.map +1 -0
- package/dist/proxmox/probes/ha-status.js +38 -0
- package/dist/proxmox/probes/ha-status.js.map +1 -0
- package/dist/proxmox/probes/ha-status.test.d.ts +2 -0
- package/dist/proxmox/probes/ha-status.test.d.ts.map +1 -0
- package/dist/proxmox/probes/ha-status.test.js +66 -0
- package/dist/proxmox/probes/ha-status.test.js.map +1 -0
- package/dist/proxmox/probes/lvm.d.ts +35 -0
- package/dist/proxmox/probes/lvm.d.ts.map +1 -0
- package/dist/proxmox/probes/lvm.js +75 -0
- package/dist/proxmox/probes/lvm.js.map +1 -0
- package/dist/proxmox/probes/lvm.test.d.ts +2 -0
- package/dist/proxmox/probes/lvm.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lvm.test.js +128 -0
- package/dist/proxmox/probes/lvm.test.js.map +1 -0
- package/dist/proxmox/probes/lxc-config.d.ts +29 -0
- package/dist/proxmox/probes/lxc-config.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-config.js +67 -0
- package/dist/proxmox/probes/lxc-config.js.map +1 -0
- package/dist/proxmox/probes/lxc-config.test.d.ts +2 -0
- package/dist/proxmox/probes/lxc-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-config.test.js +77 -0
- package/dist/proxmox/probes/lxc-config.test.js.map +1 -0
- package/dist/proxmox/probes/lxc-list.d.ts +20 -0
- package/dist/proxmox/probes/lxc-list.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-list.js +49 -0
- package/dist/proxmox/probes/lxc-list.js.map +1 -0
- package/dist/proxmox/probes/lxc-list.test.d.ts +2 -0
- package/dist/proxmox/probes/lxc-list.test.d.ts.map +1 -0
- package/dist/proxmox/probes/lxc-list.test.js +51 -0
- package/dist/proxmox/probes/lxc-list.test.js.map +1 -0
- package/dist/proxmox/probes/vm-config.d.ts +21 -0
- package/dist/proxmox/probes/vm-config.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-config.js +58 -0
- package/dist/proxmox/probes/vm-config.js.map +1 -0
- package/dist/proxmox/probes/vm-config.test.d.ts +2 -0
- package/dist/proxmox/probes/vm-config.test.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-config.test.js +80 -0
- package/dist/proxmox/probes/vm-config.test.js.map +1 -0
- package/dist/proxmox/probes/vm-locks.d.ts +16 -0
- package/dist/proxmox/probes/vm-locks.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-locks.js +35 -0
- package/dist/proxmox/probes/vm-locks.js.map +1 -0
- package/dist/proxmox/probes/vm-locks.test.d.ts +2 -0
- package/dist/proxmox/probes/vm-locks.test.d.ts.map +1 -0
- package/dist/proxmox/probes/vm-locks.test.js +54 -0
- package/dist/proxmox/probes/vm-locks.test.js.map +1 -0
- package/dist/redis/index.d.ts +3 -0
- package/dist/redis/index.d.ts.map +1 -0
- package/dist/redis/index.js +13 -0
- package/dist/redis/index.js.map +1 -0
- package/dist/redis/manifest.d.ts +3 -0
- package/dist/redis/manifest.d.ts.map +1 -0
- package/dist/redis/manifest.js +51 -0
- package/dist/redis/manifest.js.map +1 -0
- package/dist/redis/probes/info.d.ts +15 -0
- package/dist/redis/probes/info.d.ts.map +1 -0
- package/dist/redis/probes/info.js +32 -0
- package/dist/redis/probes/info.js.map +1 -0
- package/dist/redis/probes/info.test.d.ts +2 -0
- package/dist/redis/probes/info.test.d.ts.map +1 -0
- package/dist/redis/probes/info.test.js +64 -0
- package/dist/redis/probes/info.test.js.map +1 -0
- package/dist/redis/probes/keys-count.d.ts +13 -0
- package/dist/redis/probes/keys-count.d.ts.map +1 -0
- package/dist/redis/probes/keys-count.js +24 -0
- package/dist/redis/probes/keys-count.js.map +1 -0
- package/dist/redis/probes/keys-count.test.d.ts +2 -0
- package/dist/redis/probes/keys-count.test.d.ts.map +1 -0
- package/dist/redis/probes/keys-count.test.js +37 -0
- package/dist/redis/probes/keys-count.test.js.map +1 -0
- package/dist/redis/probes/memory-usage.d.ts +16 -0
- package/dist/redis/probes/memory-usage.d.ts.map +1 -0
- package/dist/redis/probes/memory-usage.js +31 -0
- package/dist/redis/probes/memory-usage.js.map +1 -0
- package/dist/redis/probes/memory-usage.test.d.ts +2 -0
- package/dist/redis/probes/memory-usage.test.d.ts.map +1 -0
- package/dist/redis/probes/memory-usage.test.js +48 -0
- package/dist/redis/probes/memory-usage.test.js.map +1 -0
- package/dist/runbooks/nutanix.d.ts +3 -0
- package/dist/runbooks/nutanix.d.ts.map +1 -0
- package/dist/runbooks/nutanix.js +619 -0
- package/dist/runbooks/nutanix.js.map +1 -0
- package/dist/runbooks/nutanix.test.d.ts +2 -0
- package/dist/runbooks/nutanix.test.d.ts.map +1 -0
- package/dist/runbooks/nutanix.test.js +971 -0
- package/dist/runbooks/nutanix.test.js.map +1 -0
- package/dist/runbooks/proxmox.d.ts +3 -0
- package/dist/runbooks/proxmox.d.ts.map +1 -0
- package/dist/runbooks/proxmox.js +451 -0
- package/dist/runbooks/proxmox.js.map +1 -0
- package/dist/runbooks/proxmox.test.d.ts +2 -0
- package/dist/runbooks/proxmox.test.d.ts.map +1 -0
- package/dist/runbooks/proxmox.test.js +700 -0
- package/dist/runbooks/proxmox.test.js.map +1 -0
- package/dist/signatures.d.ts +2 -0
- package/dist/signatures.d.ts.map +1 -0
- package/dist/signatures.js +2 -0
- package/dist/signatures.js.map +1 -0
- package/dist/types.d.ts +53 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts +6 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +10 -1
- package/dist/validation.js.map +1 -1
- package/package.json +1 -1
- package/src/index.ts +60 -6
- package/src/integrations/citrix.test.ts +592 -0
- package/src/integrations/citrix.ts +557 -0
- package/src/integrations/graph.test.ts +478 -0
- package/src/integrations/graph.ts +413 -0
- package/src/integrations/httpbin.ts +72 -0
- package/src/integrations/nutanix.test.ts +1508 -0
- package/src/integrations/nutanix.ts +1460 -0
- package/src/integrations/proxmox.test.ts +1020 -0
- package/src/integrations/proxmox.ts +989 -0
- package/src/integrations/servicenow.test.ts +314 -0
- package/src/integrations/servicenow.ts +285 -0
- package/src/integrations/splunk.test.ts +440 -0
- package/src/integrations/splunk.ts +356 -0
- package/src/mysql/index.ts +14 -0
- package/src/mysql/manifest.ts +70 -0
- package/src/mysql/probes/databases-list.test.ts +62 -0
- package/src/mysql/probes/databases-list.ts +45 -0
- package/src/mysql/probes/processlist.test.ts +47 -0
- package/src/mysql/probes/processlist.ts +55 -0
- package/src/mysql/probes/status.test.ts +50 -0
- package/src/mysql/probes/status.ts +56 -0
- package/src/nginx/index.ts +14 -0
- package/src/nginx/manifest.ts +69 -0
- package/src/nginx/probes/access-log-tail.test.ts +51 -0
- package/src/nginx/probes/access-log-tail.ts +23 -0
- package/src/nginx/probes/config-test.test.ts +47 -0
- package/src/nginx/probes/config-test.ts +24 -0
- package/src/nginx/probes/error-log-tail.test.ts +44 -0
- package/src/nginx/probes/error-log-tail.ts +23 -0
- package/src/postgres/index.ts +14 -0
- package/src/postgres/manifest.ts +91 -0
- package/src/postgres/probes/connections-active.test.ts +42 -0
- package/src/postgres/probes/connections-active.ts +55 -0
- package/src/postgres/probes/databases-list.test.ts +57 -0
- package/src/postgres/probes/databases-list.ts +49 -0
- package/src/postgres/probes/query-slow.test.ts +37 -0
- package/src/postgres/probes/query-slow.ts +55 -0
- package/src/proxmox/index.ts +24 -0
- package/src/proxmox/manifest.ts +76 -0
- package/src/proxmox/probes/ceph-status.test.ts +126 -0
- package/src/proxmox/probes/ceph-status.ts +116 -0
- package/src/proxmox/probes/cluster-config.test.ts +118 -0
- package/src/proxmox/probes/cluster-config.ts +97 -0
- package/src/proxmox/probes/ha-status.test.ts +76 -0
- package/src/proxmox/probes/ha-status.ts +56 -0
- package/src/proxmox/probes/lvm.test.ts +140 -0
- package/src/proxmox/probes/lvm.ts +121 -0
- package/src/proxmox/probes/lxc-config.test.ts +89 -0
- package/src/proxmox/probes/lxc-config.ts +90 -0
- package/src/proxmox/probes/lxc-list.test.ts +60 -0
- package/src/proxmox/probes/lxc-list.ts +67 -0
- package/src/proxmox/probes/vm-config.test.ts +93 -0
- package/src/proxmox/probes/vm-config.ts +77 -0
- package/src/proxmox/probes/vm-locks.test.ts +63 -0
- package/src/proxmox/probes/vm-locks.ts +49 -0
- package/src/redis/index.ts +14 -0
- package/src/redis/manifest.ts +52 -0
- package/src/redis/probes/info.test.ts +73 -0
- package/src/redis/probes/info.ts +46 -0
- package/src/redis/probes/keys-count.test.ts +44 -0
- package/src/redis/probes/keys-count.ts +38 -0
- package/src/redis/probes/memory-usage.test.ts +54 -0
- package/src/redis/probes/memory-usage.ts +46 -0
- package/src/runbooks/nutanix.test.ts +1138 -0
- package/src/runbooks/nutanix.ts +941 -0
- package/src/runbooks/proxmox.test.ts +838 -0
- package/src/runbooks/proxmox.ts +626 -0
- package/src/signatures.ts +1 -0
- package/src/types.ts +62 -0
- package/src/validation.ts +21 -1
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
let tokenCache = null;
|
|
2
|
+
/** Acquire or reuse a Graph OAuth2 token via client_credentials grant */
|
|
3
|
+
export async function ensureGraphToken(credentials, fetchFn) {
|
|
4
|
+
if (tokenCache && Date.now() < tokenCache.expiresAt - 30_000) {
|
|
5
|
+
return tokenCache.accessToken;
|
|
6
|
+
}
|
|
7
|
+
const tenantId = credentials.credentials.tenantId ?? '';
|
|
8
|
+
const clientId = credentials.credentials.clientId ?? '';
|
|
9
|
+
const clientSecret = credentials.credentials.clientSecret ?? '';
|
|
10
|
+
const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`;
|
|
11
|
+
const body = new URLSearchParams({
|
|
12
|
+
grant_type: 'client_credentials',
|
|
13
|
+
scope: 'https://graph.microsoft.com/.default',
|
|
14
|
+
client_id: clientId,
|
|
15
|
+
client_secret: clientSecret,
|
|
16
|
+
});
|
|
17
|
+
const res = await fetchFn(tokenUrl, {
|
|
18
|
+
method: 'POST',
|
|
19
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
20
|
+
body: body.toString(),
|
|
21
|
+
});
|
|
22
|
+
if (!res.ok) {
|
|
23
|
+
throw new Error(`Graph token request failed: ${res.status} ${res.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
const data = (await res.json());
|
|
26
|
+
tokenCache = {
|
|
27
|
+
accessToken: data.access_token,
|
|
28
|
+
expiresAt: Date.now() + data.expires_in * 1000,
|
|
29
|
+
};
|
|
30
|
+
return tokenCache.accessToken;
|
|
31
|
+
}
|
|
32
|
+
/** Clear the cached token (used for testing) */
|
|
33
|
+
export function clearTokenCache() {
|
|
34
|
+
tokenCache = null;
|
|
35
|
+
}
|
|
36
|
+
/** Fetch a Graph API endpoint with pagination */
|
|
37
|
+
export async function graphFetch(path, config, credentials, fetchFn, queryParams, maxPages = 5) {
|
|
38
|
+
const token = await ensureGraphToken(credentials, fetchFn);
|
|
39
|
+
const baseUrl = `${config.endpoint.replace(/\/$/, '')}${path}`;
|
|
40
|
+
const url = new URL(baseUrl);
|
|
41
|
+
if (queryParams) {
|
|
42
|
+
for (const [key, value] of Object.entries(queryParams)) {
|
|
43
|
+
url.searchParams.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const headers = {
|
|
47
|
+
Authorization: `Bearer ${token}`,
|
|
48
|
+
Accept: 'application/json',
|
|
49
|
+
...config.headers,
|
|
50
|
+
};
|
|
51
|
+
const results = [];
|
|
52
|
+
let nextUrl = url.toString();
|
|
53
|
+
let page = 0;
|
|
54
|
+
while (nextUrl && page < maxPages) {
|
|
55
|
+
const res = await fetchFn(nextUrl, { headers });
|
|
56
|
+
if (!res.ok)
|
|
57
|
+
throw new Error(`Graph API returned ${res.status}: ${res.statusText}`);
|
|
58
|
+
const data = (await res.json());
|
|
59
|
+
results.push(...(data.value ?? []));
|
|
60
|
+
nextUrl = data['@odata.nextLink'];
|
|
61
|
+
page++;
|
|
62
|
+
}
|
|
63
|
+
return results;
|
|
64
|
+
}
|
|
65
|
+
/** Fetch a Graph API endpoint with 403 handling for Intune endpoints */
|
|
66
|
+
async function graphFetchIntune(path, config, credentials, fetchFn, queryParams, maxPages = 5) {
|
|
67
|
+
try {
|
|
68
|
+
return await graphFetch(path, config, credentials, fetchFn, queryParams, maxPages);
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
if (error instanceof Error && error.message.includes('403')) {
|
|
72
|
+
throw new Error('Intune license or permissions required. Ensure the app registration has DeviceManagementManagedDevices.Read.All permission and an Intune license is active.');
|
|
73
|
+
}
|
|
74
|
+
throw error;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// --- Probe handlers ---
|
|
78
|
+
const userLookup = async (params, config, credentials, fetchFn) => {
|
|
79
|
+
const q = params?.q ?? '';
|
|
80
|
+
if (!q)
|
|
81
|
+
throw new Error('q parameter is required (name, email, or UPN)');
|
|
82
|
+
const filter = `startsWith(displayName,'${q}') or mail eq '${q}' or userPrincipalName eq '${q}'`;
|
|
83
|
+
const select = 'id,displayName,mail,userPrincipalName,jobTitle,department,officeLocation,accountEnabled';
|
|
84
|
+
const items = await graphFetch('/users', config, credentials, fetchFn, { $filter: filter, $select: select }, 1);
|
|
85
|
+
return { users: items, count: items.length };
|
|
86
|
+
};
|
|
87
|
+
const userGroups = async (params, config, credentials, fetchFn) => {
|
|
88
|
+
const userId = params?.id ?? '';
|
|
89
|
+
if (!userId)
|
|
90
|
+
throw new Error('id parameter is required (user object ID or UPN)');
|
|
91
|
+
const items = (await graphFetch(`/users/${userId}/memberOf`, config, credentials, fetchFn, {
|
|
92
|
+
$select: 'id,displayName',
|
|
93
|
+
}));
|
|
94
|
+
const groups = items.filter((item) => item['@odata.type'] === '#microsoft.graph.group');
|
|
95
|
+
return {
|
|
96
|
+
groups: groups.map((g) => ({ id: g.id, displayName: g.displayName })),
|
|
97
|
+
count: groups.length,
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
const signinRecent = async (params, config, credentials, fetchFn) => {
|
|
101
|
+
const user = params?.user ?? '';
|
|
102
|
+
if (!user)
|
|
103
|
+
throw new Error('user parameter is required (UPN)');
|
|
104
|
+
const hours = params?.hours || 24;
|
|
105
|
+
const cutoff = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
106
|
+
const filter = `userPrincipalName eq '${user}' and createdDateTime ge ${cutoff}`;
|
|
107
|
+
const select = 'createdDateTime,appDisplayName,ipAddress,status,location,deviceDetail,riskLevelDuringSignIn';
|
|
108
|
+
const items = await graphFetch('/auditLogs/signIns', config, credentials, fetchFn, {
|
|
109
|
+
$filter: filter,
|
|
110
|
+
$select: select,
|
|
111
|
+
});
|
|
112
|
+
return { signIns: items, count: items.length, periodHours: hours };
|
|
113
|
+
};
|
|
114
|
+
const usersRisky = async (params, config, credentials, fetchFn) => {
|
|
115
|
+
const level = params?.level || 'high';
|
|
116
|
+
const filter = `riskLevel eq '${level}'`;
|
|
117
|
+
const select = 'id,userDisplayName,userPrincipalName,riskLevel,riskState,riskDetail,riskLastUpdatedDateTime';
|
|
118
|
+
const items = await graphFetch('/identityProtection/riskyUsers', config, credentials, fetchFn, {
|
|
119
|
+
$filter: filter,
|
|
120
|
+
$select: select,
|
|
121
|
+
});
|
|
122
|
+
return { riskyUsers: items, count: items.length, riskLevel: level };
|
|
123
|
+
};
|
|
124
|
+
const intuneDevicesCompliance = async (params, config, credentials, fetchFn) => {
|
|
125
|
+
const user = params?.user;
|
|
126
|
+
const select = 'id,deviceName,operatingSystem,osVersion,complianceState,lastSyncDateTime,userPrincipalName,model,manufacturer';
|
|
127
|
+
const queryParams = { $select: select };
|
|
128
|
+
if (user) {
|
|
129
|
+
queryParams.$filter = `userPrincipalName eq '${user}'`;
|
|
130
|
+
}
|
|
131
|
+
const items = await graphFetchIntune('/deviceManagement/managedDevices', config, credentials, fetchFn, queryParams);
|
|
132
|
+
return { devices: items, count: items.length };
|
|
133
|
+
};
|
|
134
|
+
const intuneDevicesNoncompliant = async (_params, config, credentials, fetchFn) => {
|
|
135
|
+
const select = 'id,deviceName,operatingSystem,osVersion,complianceState,lastSyncDateTime,userPrincipalName,model,manufacturer';
|
|
136
|
+
const items = await graphFetchIntune('/deviceManagement/managedDevices', config, credentials, fetchFn, { $filter: "complianceState eq 'noncompliant'", $select: select });
|
|
137
|
+
return { devices: items, count: items.length };
|
|
138
|
+
};
|
|
139
|
+
const intuneAppsStatus = async (_params, config, credentials, fetchFn) => {
|
|
140
|
+
const apps = (await graphFetchIntune('/deviceAppManagement/mobileApps', config, credentials, fetchFn, { $select: 'id,displayName,publisher' }));
|
|
141
|
+
const token = await ensureGraphToken(credentials, fetchFn);
|
|
142
|
+
const headers = {
|
|
143
|
+
Authorization: `Bearer ${token}`,
|
|
144
|
+
Accept: 'application/json',
|
|
145
|
+
...config.headers,
|
|
146
|
+
};
|
|
147
|
+
const appsWithStatus = await Promise.all(apps.map(async (app) => {
|
|
148
|
+
try {
|
|
149
|
+
const summaryUrl = `${config.endpoint.replace(/\/$/, '')}/deviceAppManagement/mobileApps/${app.id}/installSummary`;
|
|
150
|
+
const res = await fetchFn(summaryUrl, { headers });
|
|
151
|
+
if (res.ok) {
|
|
152
|
+
const summary = (await res.json());
|
|
153
|
+
return { ...app, installSummary: summary };
|
|
154
|
+
}
|
|
155
|
+
return { ...app, installSummary: null };
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return { ...app, installSummary: null };
|
|
159
|
+
}
|
|
160
|
+
}));
|
|
161
|
+
return { apps: appsWithStatus, count: appsWithStatus.length };
|
|
162
|
+
};
|
|
163
|
+
// --- Pack definition ---
|
|
164
|
+
export const graphPack = {
|
|
165
|
+
manifest: {
|
|
166
|
+
name: 'graph',
|
|
167
|
+
type: 'integration',
|
|
168
|
+
version: '0.1.0',
|
|
169
|
+
description: 'Microsoft Graph — Entra ID users, sign-in logs, risky users, Intune device compliance',
|
|
170
|
+
requires: { groups: [], files: [], commands: [] },
|
|
171
|
+
probes: [
|
|
172
|
+
{
|
|
173
|
+
name: 'user.lookup',
|
|
174
|
+
description: 'Look up Entra ID users by name, email, or UPN',
|
|
175
|
+
capability: 'observe',
|
|
176
|
+
params: {
|
|
177
|
+
q: {
|
|
178
|
+
type: 'string',
|
|
179
|
+
description: 'Search query (display name prefix, email, or UPN)',
|
|
180
|
+
required: true,
|
|
181
|
+
},
|
|
182
|
+
},
|
|
183
|
+
timeout: 15000,
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
name: 'user.groups',
|
|
187
|
+
description: 'List group memberships for a user',
|
|
188
|
+
capability: 'observe',
|
|
189
|
+
params: {
|
|
190
|
+
id: {
|
|
191
|
+
type: 'string',
|
|
192
|
+
description: 'User object ID or UPN',
|
|
193
|
+
required: true,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
timeout: 15000,
|
|
197
|
+
},
|
|
198
|
+
{
|
|
199
|
+
name: 'signin.recent',
|
|
200
|
+
description: 'Recent sign-in logs for a user',
|
|
201
|
+
capability: 'observe',
|
|
202
|
+
params: {
|
|
203
|
+
user: {
|
|
204
|
+
type: 'string',
|
|
205
|
+
description: 'User principal name (UPN)',
|
|
206
|
+
required: true,
|
|
207
|
+
},
|
|
208
|
+
hours: {
|
|
209
|
+
type: 'number',
|
|
210
|
+
description: 'Lookback period in hours (default: 24)',
|
|
211
|
+
required: false,
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
timeout: 30000,
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
name: 'users.risky',
|
|
218
|
+
description: 'Users flagged by Identity Protection at a given risk level',
|
|
219
|
+
capability: 'observe',
|
|
220
|
+
params: {
|
|
221
|
+
level: {
|
|
222
|
+
type: 'string',
|
|
223
|
+
description: "Risk level filter: 'low', 'medium', or 'high' (default: 'high')",
|
|
224
|
+
required: false,
|
|
225
|
+
},
|
|
226
|
+
},
|
|
227
|
+
timeout: 15000,
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
name: 'intune.devices.compliance',
|
|
231
|
+
description: 'Intune managed device compliance status',
|
|
232
|
+
capability: 'observe',
|
|
233
|
+
params: {
|
|
234
|
+
user: {
|
|
235
|
+
type: 'string',
|
|
236
|
+
description: 'Optional UPN filter — if omitted, returns all managed devices',
|
|
237
|
+
required: false,
|
|
238
|
+
},
|
|
239
|
+
},
|
|
240
|
+
timeout: 30000,
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
name: 'intune.devices.noncompliant',
|
|
244
|
+
description: 'Intune devices with noncompliant status',
|
|
245
|
+
capability: 'observe',
|
|
246
|
+
params: {},
|
|
247
|
+
timeout: 30000,
|
|
248
|
+
},
|
|
249
|
+
{
|
|
250
|
+
name: 'intune.apps.status',
|
|
251
|
+
description: 'Intune mobile app inventory with install summaries',
|
|
252
|
+
capability: 'observe',
|
|
253
|
+
params: {},
|
|
254
|
+
timeout: 30000,
|
|
255
|
+
},
|
|
256
|
+
],
|
|
257
|
+
runbook: {
|
|
258
|
+
category: 'identity',
|
|
259
|
+
probes: ['user.lookup', 'users.risky', 'intune.devices.noncompliant'],
|
|
260
|
+
parallel: true,
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
handlers: {
|
|
264
|
+
'user.lookup': userLookup,
|
|
265
|
+
'user.groups': userGroups,
|
|
266
|
+
'signin.recent': signinRecent,
|
|
267
|
+
'users.risky': usersRisky,
|
|
268
|
+
'intune.devices.compliance': intuneDevicesCompliance,
|
|
269
|
+
'intune.devices.noncompliant': intuneDevicesNoncompliant,
|
|
270
|
+
'intune.apps.status': intuneAppsStatus,
|
|
271
|
+
},
|
|
272
|
+
testConnection: async (config, credentials, fetchFn) => {
|
|
273
|
+
try {
|
|
274
|
+
const token = await ensureGraphToken(credentials, fetchFn);
|
|
275
|
+
const url = `${config.endpoint.replace(/\/$/, '')}/organization?$select=id&$top=1`;
|
|
276
|
+
const res = await fetchFn(url, {
|
|
277
|
+
headers: {
|
|
278
|
+
Authorization: `Bearer ${token}`,
|
|
279
|
+
Accept: 'application/json',
|
|
280
|
+
...config.headers,
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
return res.ok;
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
},
|
|
289
|
+
};
|
|
290
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.js","sourceRoot":"","sources":["../../src/integrations/graph.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"graph.test.d.ts","sourceRoot":"","sources":["../../src/integrations/graph.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { clearTokenCache, ensureGraphToken, graphFetch, graphPack } from './graph.js';
|
|
3
|
+
const graphConfig = {
|
|
4
|
+
endpoint: 'https://graph.microsoft.com/v1.0',
|
|
5
|
+
};
|
|
6
|
+
const graphCreds = {
|
|
7
|
+
packName: 'graph',
|
|
8
|
+
authMethod: 'oauth2',
|
|
9
|
+
credentials: {
|
|
10
|
+
tenantId: 'tenant-123',
|
|
11
|
+
clientId: 'client-id',
|
|
12
|
+
clientSecret: 'client-secret',
|
|
13
|
+
},
|
|
14
|
+
};
|
|
15
|
+
const handler = (name) => {
|
|
16
|
+
const h = graphPack.handlers[name];
|
|
17
|
+
if (!h)
|
|
18
|
+
throw new Error(`Handler ${name} not found`);
|
|
19
|
+
return h;
|
|
20
|
+
};
|
|
21
|
+
function callArgs(fn, index) {
|
|
22
|
+
const args = fn.mock.calls[index];
|
|
23
|
+
if (!args)
|
|
24
|
+
throw new Error(`No call at index ${index}`);
|
|
25
|
+
return args;
|
|
26
|
+
}
|
|
27
|
+
/** Mock fetch that returns a token response on first call, then Graph API responses */
|
|
28
|
+
function mockGraphFetch(graphValue = [], nextLink) {
|
|
29
|
+
let callCount = 0;
|
|
30
|
+
return vi.fn().mockImplementation(() => {
|
|
31
|
+
callCount++;
|
|
32
|
+
if (callCount === 1) {
|
|
33
|
+
return Promise.resolve(new Response(JSON.stringify({ access_token: 'graph-token-123', expires_in: 3600 }), {
|
|
34
|
+
status: 200,
|
|
35
|
+
headers: { 'Content-Type': 'application/json' },
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
const body = { value: graphValue };
|
|
39
|
+
if (nextLink)
|
|
40
|
+
body['@odata.nextLink'] = nextLink;
|
|
41
|
+
return Promise.resolve(new Response(JSON.stringify(body), {
|
|
42
|
+
status: 200,
|
|
43
|
+
headers: { 'Content-Type': 'application/json' },
|
|
44
|
+
}));
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
/** Mock fetch returning paginated Graph responses (token first, then pages) */
|
|
48
|
+
function mockGraphPaginated(pages) {
|
|
49
|
+
let callCount = 0;
|
|
50
|
+
return vi.fn().mockImplementation(() => {
|
|
51
|
+
callCount++;
|
|
52
|
+
if (callCount === 1) {
|
|
53
|
+
return Promise.resolve(new Response(JSON.stringify({ access_token: 'graph-token-123', expires_in: 3600 }), {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
const pageIdx = callCount - 2;
|
|
59
|
+
const value = pages[pageIdx] ?? [];
|
|
60
|
+
const nextLink = pageIdx < pages.length - 1
|
|
61
|
+
? `https://graph.microsoft.com/v1.0/next?$skip=${(pageIdx + 1) * 100}`
|
|
62
|
+
: undefined;
|
|
63
|
+
const body = { value };
|
|
64
|
+
if (nextLink)
|
|
65
|
+
body['@odata.nextLink'] = nextLink;
|
|
66
|
+
return Promise.resolve(new Response(JSON.stringify(body), {
|
|
67
|
+
status: 200,
|
|
68
|
+
headers: { 'Content-Type': 'application/json' },
|
|
69
|
+
}));
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
function mockFetchError(status) {
|
|
73
|
+
return vi.fn().mockResolvedValue(new Response('Error', { status, statusText: 'Error' }));
|
|
74
|
+
}
|
|
75
|
+
/** Mock fetch: token first, then specific status for API call */
|
|
76
|
+
function mockGraphWithApiStatus(apiStatus) {
|
|
77
|
+
let callCount = 0;
|
|
78
|
+
return vi.fn().mockImplementation(() => {
|
|
79
|
+
callCount++;
|
|
80
|
+
if (callCount === 1) {
|
|
81
|
+
return Promise.resolve(new Response(JSON.stringify({ access_token: 'graph-token-123', expires_in: 3600 }), {
|
|
82
|
+
status: 200,
|
|
83
|
+
headers: { 'Content-Type': 'application/json' },
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
return Promise.resolve(new Response('Error', { status: apiStatus, statusText: 'Forbidden' }));
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
clearTokenCache();
|
|
91
|
+
});
|
|
92
|
+
describe('graph pack', () => {
|
|
93
|
+
describe('token acquisition', () => {
|
|
94
|
+
it('acquires token via client_credentials grant', async () => {
|
|
95
|
+
const fetchFn = vi.fn().mockResolvedValue(new Response(JSON.stringify({ access_token: 'tok-abc', expires_in: 3600 }), {
|
|
96
|
+
status: 200,
|
|
97
|
+
headers: { 'Content-Type': 'application/json' },
|
|
98
|
+
}));
|
|
99
|
+
const token = await ensureGraphToken(graphCreds, fetchFn);
|
|
100
|
+
expect(token).toBe('tok-abc');
|
|
101
|
+
const [url, init] = callArgs(fetchFn, 0);
|
|
102
|
+
expect(url).toContain('login.microsoftonline.com/tenant-123/oauth2/v2.0/token');
|
|
103
|
+
expect(init.method).toBe('POST');
|
|
104
|
+
const body = init.body;
|
|
105
|
+
expect(body).toContain('grant_type=client_credentials');
|
|
106
|
+
expect(body).toContain('scope=https%3A%2F%2Fgraph.microsoft.com%2F.default');
|
|
107
|
+
expect(body).toContain('client_id=client-id');
|
|
108
|
+
});
|
|
109
|
+
it('caches token and reuses on subsequent calls', async () => {
|
|
110
|
+
const fetchFn = vi.fn().mockResolvedValue(new Response(JSON.stringify({ access_token: 'tok-cached', expires_in: 3600 }), {
|
|
111
|
+
status: 200,
|
|
112
|
+
headers: { 'Content-Type': 'application/json' },
|
|
113
|
+
}));
|
|
114
|
+
const t1 = await ensureGraphToken(graphCreds, fetchFn);
|
|
115
|
+
const t2 = await ensureGraphToken(graphCreds, fetchFn);
|
|
116
|
+
expect(t1).toBe('tok-cached');
|
|
117
|
+
expect(t2).toBe('tok-cached');
|
|
118
|
+
expect(fetchFn).toHaveBeenCalledTimes(1);
|
|
119
|
+
});
|
|
120
|
+
it('throws on token request failure', async () => {
|
|
121
|
+
const fetchFn = mockFetchError(401);
|
|
122
|
+
await expect(ensureGraphToken(graphCreds, fetchFn)).rejects.toThrow('Graph token request failed: 401');
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
describe('pagination', () => {
|
|
126
|
+
it('fetches a single page when no nextLink', async () => {
|
|
127
|
+
const fetchFn = mockGraphFetch([{ id: '1' }, { id: '2' }]);
|
|
128
|
+
const results = await graphFetch('/users', graphConfig, graphCreds, fetchFn);
|
|
129
|
+
expect(results).toEqual([{ id: '1' }, { id: '2' }]);
|
|
130
|
+
});
|
|
131
|
+
it('follows nextLink across multiple pages', async () => {
|
|
132
|
+
const fetchFn = mockGraphPaginated([[{ id: '1' }], [{ id: '2' }], [{ id: '3' }]]);
|
|
133
|
+
const results = await graphFetch('/users', graphConfig, graphCreds, fetchFn);
|
|
134
|
+
expect(results).toEqual([{ id: '1' }, { id: '2' }, { id: '3' }]);
|
|
135
|
+
});
|
|
136
|
+
it('caps at maxPages', async () => {
|
|
137
|
+
const fetchFn = mockGraphPaginated([[{ id: '1' }], [{ id: '2' }], [{ id: '3' }]]);
|
|
138
|
+
const results = await graphFetch('/users', graphConfig, graphCreds, fetchFn, undefined, 2);
|
|
139
|
+
expect(results).toEqual([{ id: '1' }, { id: '2' }]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('testConnection', () => {
|
|
143
|
+
it('returns true on 200', async () => {
|
|
144
|
+
const fetchFn = mockGraphFetch([{ id: 'org-1' }]);
|
|
145
|
+
const result = await graphPack.testConnection(graphConfig, graphCreds, fetchFn);
|
|
146
|
+
expect(result).toBe(true);
|
|
147
|
+
const [url] = callArgs(fetchFn, 1);
|
|
148
|
+
expect(url).toContain('/organization');
|
|
149
|
+
expect(url).toContain('$select=id');
|
|
150
|
+
});
|
|
151
|
+
it('returns false on non-200', async () => {
|
|
152
|
+
const fetchFn = mockGraphWithApiStatus(403);
|
|
153
|
+
const result = await graphPack.testConnection(graphConfig, graphCreds, fetchFn);
|
|
154
|
+
expect(result).toBe(false);
|
|
155
|
+
});
|
|
156
|
+
it('returns false on network error', async () => {
|
|
157
|
+
const fetchFn = vi.fn().mockRejectedValue(new Error('ECONNREFUSED'));
|
|
158
|
+
const result = await graphPack.testConnection(graphConfig, graphCreds, fetchFn);
|
|
159
|
+
expect(result).toBe(false);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
describe('user.lookup', () => {
|
|
163
|
+
it('searches users by query with correct filter', async () => {
|
|
164
|
+
const users = [
|
|
165
|
+
{ id: 'u1', displayName: 'Alice', mail: 'alice@corp.com', accountEnabled: true },
|
|
166
|
+
];
|
|
167
|
+
const fetchFn = mockGraphFetch(users);
|
|
168
|
+
const result = (await handler('user.lookup')({ q: 'alice@corp.com' }, graphConfig, graphCreds, fetchFn));
|
|
169
|
+
expect(result.count).toBe(1);
|
|
170
|
+
expect(result.users).toEqual(users);
|
|
171
|
+
const [url] = callArgs(fetchFn, 1);
|
|
172
|
+
expect(url).toContain('/users');
|
|
173
|
+
expect(url).toContain('alice%40corp.com');
|
|
174
|
+
});
|
|
175
|
+
it('throws when q is missing', async () => {
|
|
176
|
+
const fetchFn = mockGraphFetch([]);
|
|
177
|
+
await expect(handler('user.lookup')({}, graphConfig, graphCreds, fetchFn)).rejects.toThrow('q parameter is required');
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
describe('user.groups', () => {
|
|
181
|
+
it('returns group memberships filtered to groups', async () => {
|
|
182
|
+
const memberOf = [
|
|
183
|
+
{
|
|
184
|
+
'@odata.type': '#microsoft.graph.group',
|
|
185
|
+
id: 'g1',
|
|
186
|
+
displayName: 'SG-Sonde-Users',
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
'@odata.type': '#microsoft.graph.directoryRole',
|
|
190
|
+
id: 'r1',
|
|
191
|
+
displayName: 'Global Admin',
|
|
192
|
+
},
|
|
193
|
+
];
|
|
194
|
+
const fetchFn = mockGraphFetch(memberOf);
|
|
195
|
+
const result = (await handler('user.groups')({ id: 'u1' }, graphConfig, graphCreds, fetchFn));
|
|
196
|
+
expect(result.count).toBe(1);
|
|
197
|
+
expect(result.groups[0]?.displayName).toBe('SG-Sonde-Users');
|
|
198
|
+
const [url] = callArgs(fetchFn, 1);
|
|
199
|
+
expect(url).toContain('/users/u1/memberOf');
|
|
200
|
+
});
|
|
201
|
+
it('throws when id is missing', async () => {
|
|
202
|
+
const fetchFn = mockGraphFetch([]);
|
|
203
|
+
await expect(handler('user.groups')({}, graphConfig, graphCreds, fetchFn)).rejects.toThrow('id parameter is required');
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
describe('signin.recent', () => {
|
|
207
|
+
it('fetches sign-in logs for a user', async () => {
|
|
208
|
+
const signIns = [
|
|
209
|
+
{
|
|
210
|
+
createdDateTime: '2026-01-01T12:00:00Z',
|
|
211
|
+
appDisplayName: 'Outlook',
|
|
212
|
+
ipAddress: '10.0.0.1',
|
|
213
|
+
},
|
|
214
|
+
];
|
|
215
|
+
const fetchFn = mockGraphFetch(signIns);
|
|
216
|
+
const result = (await handler('signin.recent')({ user: 'alice@corp.com', hours: 12 }, graphConfig, graphCreds, fetchFn));
|
|
217
|
+
expect(result.count).toBe(1);
|
|
218
|
+
expect(result.periodHours).toBe(12);
|
|
219
|
+
const [url] = callArgs(fetchFn, 1);
|
|
220
|
+
expect(url).toContain('/auditLogs/signIns');
|
|
221
|
+
expect(url).toContain('alice%40corp.com');
|
|
222
|
+
});
|
|
223
|
+
it('throws when user is missing', async () => {
|
|
224
|
+
const fetchFn = mockGraphFetch([]);
|
|
225
|
+
await expect(handler('signin.recent')({}, graphConfig, graphCreds, fetchFn)).rejects.toThrow('user parameter is required');
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
describe('users.risky', () => {
|
|
229
|
+
it('fetches risky users at specified level', async () => {
|
|
230
|
+
const risky = [{ id: 'u1', userDisplayName: 'Bob', riskLevel: 'high', riskState: 'atRisk' }];
|
|
231
|
+
const fetchFn = mockGraphFetch(risky);
|
|
232
|
+
const result = (await handler('users.risky')({ level: 'medium' }, graphConfig, graphCreds, fetchFn));
|
|
233
|
+
expect(result.count).toBe(1);
|
|
234
|
+
expect(result.riskLevel).toBe('medium');
|
|
235
|
+
const [url] = callArgs(fetchFn, 1);
|
|
236
|
+
expect(url).toContain('/identityProtection/riskyUsers');
|
|
237
|
+
expect(url).toContain('medium');
|
|
238
|
+
});
|
|
239
|
+
it('defaults to high risk level', async () => {
|
|
240
|
+
const fetchFn = mockGraphFetch([]);
|
|
241
|
+
const result = (await handler('users.risky')({}, graphConfig, graphCreds, fetchFn));
|
|
242
|
+
expect(result.riskLevel).toBe('high');
|
|
243
|
+
const [url] = callArgs(fetchFn, 1);
|
|
244
|
+
expect(url).toContain('high');
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
describe('intune.devices.compliance', () => {
|
|
248
|
+
it('fetches managed devices with optional user filter', async () => {
|
|
249
|
+
const devices = [
|
|
250
|
+
{
|
|
251
|
+
id: 'd1',
|
|
252
|
+
deviceName: 'LAPTOP-01',
|
|
253
|
+
complianceState: 'compliant',
|
|
254
|
+
userPrincipalName: 'alice@corp.com',
|
|
255
|
+
},
|
|
256
|
+
];
|
|
257
|
+
const fetchFn = mockGraphFetch(devices);
|
|
258
|
+
const result = (await handler('intune.devices.compliance')({ user: 'alice@corp.com' }, graphConfig, graphCreds, fetchFn));
|
|
259
|
+
expect(result.count).toBe(1);
|
|
260
|
+
const [url] = callArgs(fetchFn, 1);
|
|
261
|
+
expect(url).toContain('/deviceManagement/managedDevices');
|
|
262
|
+
expect(url).toContain('alice%40corp.com');
|
|
263
|
+
});
|
|
264
|
+
it('fetches all devices when no user filter', async () => {
|
|
265
|
+
const fetchFn = mockGraphFetch([]);
|
|
266
|
+
await handler('intune.devices.compliance')({}, graphConfig, graphCreds, fetchFn);
|
|
267
|
+
const [url] = callArgs(fetchFn, 1);
|
|
268
|
+
expect(url).toContain('/deviceManagement/managedDevices');
|
|
269
|
+
expect(url).not.toContain('%24filter');
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
describe('intune.devices.noncompliant', () => {
|
|
273
|
+
it('fetches only noncompliant devices', async () => {
|
|
274
|
+
const devices = [{ id: 'd2', deviceName: 'LAPTOP-02', complianceState: 'noncompliant' }];
|
|
275
|
+
const fetchFn = mockGraphFetch(devices);
|
|
276
|
+
const result = (await handler('intune.devices.noncompliant')({}, graphConfig, graphCreds, fetchFn));
|
|
277
|
+
expect(result.count).toBe(1);
|
|
278
|
+
const [url] = callArgs(fetchFn, 1);
|
|
279
|
+
expect(url).toContain('noncompliant');
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
describe('Intune 403 handling', () => {
|
|
283
|
+
it('throws friendly error on 403 for Intune endpoints', async () => {
|
|
284
|
+
const fetchFn = mockGraphWithApiStatus(403);
|
|
285
|
+
await expect(handler('intune.devices.compliance')({}, graphConfig, graphCreds, fetchFn)).rejects.toThrow('Intune license or permissions required');
|
|
286
|
+
});
|
|
287
|
+
});
|
|
288
|
+
describe('intune.apps.status', () => {
|
|
289
|
+
it('fetches apps with install summaries', async () => {
|
|
290
|
+
// Call 1: token, Call 2: mobileApps list, Call 3: token (cached), Call 4: installSummary
|
|
291
|
+
let callCount = 0;
|
|
292
|
+
const fetchFn = vi.fn().mockImplementation((url) => {
|
|
293
|
+
callCount++;
|
|
294
|
+
if (callCount === 1) {
|
|
295
|
+
return Promise.resolve(new Response(JSON.stringify({ access_token: 'graph-token-123', expires_in: 3600 }), {
|
|
296
|
+
status: 200,
|
|
297
|
+
headers: { 'Content-Type': 'application/json' },
|
|
298
|
+
}));
|
|
299
|
+
}
|
|
300
|
+
if (callCount === 2) {
|
|
301
|
+
return Promise.resolve(new Response(JSON.stringify({ value: [{ id: 'app1', displayName: 'Teams', publisher: 'MS' }] }), { status: 200, headers: { 'Content-Type': 'application/json' } }));
|
|
302
|
+
}
|
|
303
|
+
// installSummary call
|
|
304
|
+
if (typeof url === 'string' && url.includes('installSummary')) {
|
|
305
|
+
return Promise.resolve(new Response(JSON.stringify({ installedDeviceCount: 10, failedDeviceCount: 2 }), {
|
|
306
|
+
status: 200,
|
|
307
|
+
headers: { 'Content-Type': 'application/json' },
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
return Promise.resolve(new Response('{}', { status: 200 }));
|
|
311
|
+
});
|
|
312
|
+
const result = (await handler('intune.apps.status')({}, graphConfig, graphCreds, fetchFn));
|
|
313
|
+
expect(result.count).toBe(1);
|
|
314
|
+
expect(result.apps[0]?.displayName).toBe('Teams');
|
|
315
|
+
expect(result.apps[0]?.installSummary).toEqual({
|
|
316
|
+
installedDeviceCount: 10,
|
|
317
|
+
failedDeviceCount: 2,
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
describe('manifest', () => {
|
|
322
|
+
it('has correct name and probe count', () => {
|
|
323
|
+
expect(graphPack.manifest.name).toBe('graph');
|
|
324
|
+
expect(graphPack.manifest.probes).toHaveLength(7);
|
|
325
|
+
});
|
|
326
|
+
it('all handlers match manifest probes', () => {
|
|
327
|
+
const probeNames = graphPack.manifest.probes.map((p) => p.name);
|
|
328
|
+
const handlerNames = Object.keys(graphPack.handlers);
|
|
329
|
+
expect(handlerNames.sort()).toEqual(probeNames.sort());
|
|
330
|
+
});
|
|
331
|
+
it('has correct timeouts (15s for user probes, 30s for sign-in/Intune)', () => {
|
|
332
|
+
const probeMap = new Map(graphPack.manifest.probes.map((p) => [p.name, p.timeout]));
|
|
333
|
+
expect(probeMap.get('user.lookup')).toBe(15000);
|
|
334
|
+
expect(probeMap.get('user.groups')).toBe(15000);
|
|
335
|
+
expect(probeMap.get('users.risky')).toBe(15000);
|
|
336
|
+
expect(probeMap.get('signin.recent')).toBe(30000);
|
|
337
|
+
expect(probeMap.get('intune.devices.compliance')).toBe(30000);
|
|
338
|
+
expect(probeMap.get('intune.devices.noncompliant')).toBe(30000);
|
|
339
|
+
expect(probeMap.get('intune.apps.status')).toBe(30000);
|
|
340
|
+
});
|
|
341
|
+
it('has identity runbook', () => {
|
|
342
|
+
expect(graphPack.manifest.runbook).toEqual({
|
|
343
|
+
category: 'identity',
|
|
344
|
+
probes: ['user.lookup', 'users.risky', 'intune.devices.noncompliant'],
|
|
345
|
+
parallel: true,
|
|
346
|
+
});
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
describe('error handling', () => {
|
|
350
|
+
it('throws on non-200 API response', async () => {
|
|
351
|
+
const fetchFn = mockGraphWithApiStatus(500);
|
|
352
|
+
await expect(handler('user.lookup')({ q: 'test' }, graphConfig, graphCreds, fetchFn)).rejects.toThrow('Graph API returned 500');
|
|
353
|
+
});
|
|
354
|
+
});
|
|
355
|
+
});
|
|
356
|
+
//# sourceMappingURL=graph.test.js.map
|