@sagentix-inc/evidence-collector 0.2.1 → 0.3.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.
@@ -40,8 +40,10 @@ const DISK_ENCRYPTION_TYPES = new Set(["EncryptionAtRestWithPlatformKey", "Encry
40
40
  // Synapse dedicated SQL pools), SQL servers on machines, App Service, Storage (storage accounts), Key Vault, Containers
41
41
  // (Kubernetes clusters and registries), open-source relational databases (defender-for-databases-introduction:
42
42
  // PostgreSQL and MySQL Flexible Server), Cosmos DB and APIs (API Management). Each type name was confirmed on the
43
- // service's REST reference on 2026-10-26, except Microsoft.ContainerRegistry/registries and
44
- // Microsoft.DBforMySQL/flexibleServers [Unverified: the REST page did not load]. Arm (Defender for Resource Manager)
43
+ // service's REST reference on 2026-10-26; Microsoft.ContainerRegistry/registries and Microsoft.DBforMySQL/flexibleServers,
44
+ // whose reference pages did not load, on 2026-10-27 in Microsoft's REST specifications (github.com/Azure/azure-rest-api-specs
45
+ // at 2455f14a8f: ContainerRegistry Registry stable 2025-11-01 and DBforMySQL FlexibleServers stable 2024-12-30 both list
46
+ // /subscriptions/{subscriptionId}/providers/<that type>). Arm (Defender for Resource Manager)
45
47
  // watches the subscription's own management operations, so every subscription needs it. Any other plan (CloudPosture,
46
48
  // whose foundational tier is free, and plans added after this list) is reported and not judged.
47
49
  export const PLAN_TYPES = {
@@ -0,0 +1,286 @@
1
+ // Microsoft Configuration Manager (ConfigMgr, formerly MECM / SCCM), read through the administration service, the
2
+ // SMS Provider's REST API, with GET only. On-premises: the collector runs on a domain-joined Windows host that reaches
3
+ // the SMS Provider over HTTPS (port 443), as a domain service account that is a Configuration Manager administrative
4
+ // user holding the built-in "Read-only Analyst" role. No secret is stored: the request carries that account's own
5
+ // Windows credentials.
6
+ //
7
+ // Pages, all read 2026-10-28 (learn.microsoft.com/intune/configmgr/..., "last updated" in brackets):
8
+ // develop/adminservice/overview [2023-11-07]: an OData v4 REST API; the WMI route
9
+ // https://<SMSProviderFQDN>/AdminService/wmi/<ClassName> "supports both GET and POST commands to over 700
10
+ // classes"; class names are case-sensitive; "your user account needs to be an administrative user in
11
+ // Configuration Manager. If you access the administration service via a cloud management gateway, you need to
12
+ // have an account in Microsoft Entra ID."
13
+ // develop/adminservice/usage [2022-10-04]: Invoke-RestMethod -Method 'Get' -Uri "https://SMSProviderFQDN/AdminService/
14
+ // wmi/SMS_Site" -UseDefaultCredentials; $filter on the WMI route (SMS_Collection?$filter=startswith(Name,'All')
15
+ // eq true); Power BI connects with "Windows Authentication".
16
+ // develop/adminservice/set-up [2022-10-04]: the site's self-signed certificate is bound by default ("open HTTPS
17
+ // port 443"), or a PKI certificate; the CMG endpoint replaces the FQDN for internet access.
18
+ // develop/adminservice/faq [2026-08-28]: "the administration service currently doesn't publish an OpenAPI (Swagger)
19
+ // document"; it "still uses Configuration Manager built-in role-based authorization".
20
+ // core/understand/fundamentals-of-role-based-administration [2022-10-04]: Read-only Analyst "Grants permissions to
21
+ // view all Configuration Manager objects."
22
+ // core/servers/deploy/configure/configure-role-based-administration [2025-07-17]: Administration > Security >
23
+ // Administrative Users > Add User or Group; "you can only specify domain users or domain security groups".
24
+ // Classes (develop/reference/...):
25
+ // SMS_CombinedDeviceResources [2022-10-04]: ResourceID, SerialNumber, IsClient, IsObsolete, Unknown,
26
+ // LastActiveTime ("Comes from Client Health. Represents the last reported time the client was active"),
27
+ // LastPolicyRequest, EPEnabled, EPAntivirusEnabled, EPAntivirusSignatureLastUpdateDateTime. Name, UserName and
28
+ // UserDomainName come back too and are never read into a result.
29
+ // SMS_SoftwareUpdate / SMS_SoftwareUpdateBase [2022-10-10]: CI_ID, IsDeployed ("true if the software update has
30
+ // been deployed"), IsExpired, DatePosted ("when the software update was published"), SeverityName.
31
+ // SMS_UpdateComplianceStatus [2022-10-10]: MachineID, CI_ID, ArticleID, Status 0 "Detection state unknown",
32
+ // 1 "Update is not required", 2 "Update is required", 3 "Update is installed".
33
+ // SMS_G_System_CI_ComplianceState [2022-10-04]: ResourceID, CI_ID, LocalizedDisplayName, ComplianceState 0
34
+ // "Compliance State Unknown", 1 "Compliant", 2 "Non-Compliant", 4 "Error".
35
+ // Invoke-WebRequest (powershell/module/microsoft.powershell.utility/invoke-webrequest, 5.1) [ms.date 2026-01-20]:
36
+ // -UseDefaultCredentials "uses the credentials of the current user to send the web request"; -UseBasicParsing
37
+ // is "required when Internet Explorer is not installed"; -MaximumRedirection 0 "prevents all redirection".
38
+ //
39
+ // Why PowerShell: the administration service authenticates with Windows authentication on premises, which Node's
40
+ // fetch cannot send. Each GET runs Windows PowerShell 5.1 Invoke-WebRequest -Method Get -UseDefaultCredentials (the
41
+ // documented call, with the status code kept), started with -EncodedCommand so no quoting reaches a shell; the URL
42
+ // is passed base-64 inside the script and must stay under https://<CONFIGMGR_PROVIDER>/AdminService/. Through a
43
+ // cloud management gateway the service wants a Microsoft Entra token for a user account; no page documents an
44
+ // app-only (client credentials) token for it, so that path is not built.
45
+ //
46
+ // Left out, with the reason: disk encryption (BitLocker), firewall, OS version and screen lock. No documented server
47
+ // class carries them: BitLocker management reports (protect/deploy-use/bitlocker/view-reports [2021-11-23]) read
48
+ // hardware inventory classes the reference does not document, SMS_R_System and SMS_CombinedDeviceResources give
49
+ // only a free-form OS string, and firewall and screen lock are not inventoried by default. A configuration baseline
50
+ // that tests them shows in baselines_compliant. Paging: @odata.nextLink is followed when the service gives one; no
51
+ // page documents the administration service's paging or rate limits.
52
+ import { execFile } from "node:child_process";
53
+ import { Emitter, getAll, isObj, realSleep, str, withRetry, listRead, stampRead, DAY_MS } from "./readonly_http.js";
54
+ import { deviceResource, label, posInt } from "./device_common.js";
55
+ import { CMD, encode } from "./windows.js";
56
+ export const CONFIGMGR_ENV = { provider: "CONFIGMGR_PROVIDER" };
57
+ export const CONFIGMGR_CHECKS = ["client_installed", "last_seen", "antimalware_enabled", "antimalware_signatures_current", "updates_installed", "baselines_compliant"];
58
+ export const DEFAULTS = { seenDays: 14, signatureDays: 7, patchDays: 30, maxDevices: 10000 };
59
+ export const PATHS = {
60
+ devices: "/SMS_CombinedDeviceResources",
61
+ updates: "/SMS_SoftwareUpdate?$filter=IsDeployed eq true",
62
+ updateStatus: "/SMS_UpdateComplianceStatus?$filter=Status eq 2 or Status eq 0",
63
+ baselines: "/SMS_G_System_CI_ComplianceState",
64
+ };
65
+ const FQDN = /^(?=.{1,253}$)([a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)(\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i;
66
+ /** The PowerShell script for one GET: prints {"status":n,"body":"..."} as one ASCII line. */
67
+ export function getScript(url) {
68
+ return [
69
+ "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue'",
70
+ `$u = [Text.Encoding]::UTF8.GetString([Convert]::FromBase64String('${Buffer.from(url, "utf8").toString("base64")}'))`,
71
+ "function out($o) { [regex]::Replace((ConvertTo-Json -InputObject $o -Compress -Depth 4), '[^\\x00-\\x7F]', { param($m) '\\u{0:x4}' -f [int][char]$m.Value }) }",
72
+ "try { $r = Invoke-WebRequest -Uri $u -Method Get -UseDefaultCredentials -UseBasicParsing -MaximumRedirection 0 -Headers @{ Accept = 'application/json' }",
73
+ " out @{ status = [int]$r.StatusCode; body = [string]$r.Content } }",
74
+ "catch { $resp = $_.Exception.Response",
75
+ " if ($resp) { $t = ''; try { $t = (New-Object IO.StreamReader($resp.GetResponseStream())).ReadToEnd() } catch { }; out @{ status = [int]$resp.StatusCode; body = $t } }",
76
+ " else { out @{ status = 0; body = [string]$_.Exception.Message } } }",
77
+ ].join("\n");
78
+ }
79
+ /** Like windows.ts realExec, with room for a large device list (256 MiB, five minutes). */
80
+ export const bigExec = (cmd, args, timeoutMs = 300_000) => new Promise((resolve) => {
81
+ execFile(cmd, [...args], { timeout: timeoutMs, maxBuffer: 256 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
82
+ const e = err;
83
+ resolve({ code: e ? (typeof e.code === "number" ? e.code : -1) : 0, stdout: String(stdout), stderr: String(stderr), missing: e?.code === "ENOENT" });
84
+ });
85
+ });
86
+ /** A GET-only administration service client that sends the running account's Windows credentials, or what is missing. */
87
+ export function configmgrFromEnv(env, exec = bigExec, platform = process.platform) {
88
+ const host = (env[CONFIGMGR_ENV.provider] ?? "").trim().replace(/^https:\/\//i, "").replace(/\/+$/, "");
89
+ if (!host)
90
+ return `${CONFIGMGR_ENV.provider} is not set (the SMS Provider's FQDN, e.g. cm01.corp.example.com)`;
91
+ if (!FQDN.test(host))
92
+ return `${CONFIGMGR_ENV.provider} must be a host name, e.g. cm01.corp.example.com`;
93
+ if (platform !== "win32")
94
+ return "the administration service takes Windows authentication: run the collector on a domain-joined Windows host, as the Read-only Analyst service account";
95
+ const base = `https://${host}/AdminService/wmi`;
96
+ return {
97
+ async get(url) {
98
+ const full = /^https?:\/\//i.test(url) ? url : `${base}${url}`;
99
+ const t = new URL(full);
100
+ // The account's credentials go with every request, so only this provider's administration service is asked.
101
+ if (t.protocol !== "https:" || t.origin !== new URL(`https://${host}`).origin)
102
+ return { status: 0, headers: {}, body: { message: `refused a request to ${t.origin}; this client reads https://${host} only` } };
103
+ if (!/^\/adminservice\//i.test(t.pathname))
104
+ return { status: 0, headers: {}, body: { message: `refused ${t.pathname}: outside /AdminService/` } };
105
+ const r = await exec(CMD.powershell, ["-NoLogo", "-NoProfile", "-NonInteractive", "-EncodedCommand", encode(getScript(t.href))]);
106
+ if (r.missing)
107
+ return { status: 0, headers: {}, body: { message: "powershell.exe not found" } };
108
+ let o;
109
+ try {
110
+ o = JSON.parse(r.stdout.trim().split(/\r?\n/).pop() ?? "");
111
+ }
112
+ catch {
113
+ return { status: 0, headers: {}, body: { message: `PowerShell gave no answer (exit ${r.code})` } };
114
+ }
115
+ if (!isObj(o))
116
+ return { status: 0, headers: {}, body: { message: "PowerShell gave no answer" } };
117
+ const status = Number(o.status) || 0;
118
+ let body = o.body;
119
+ try {
120
+ body = typeof o.body === "string" && o.body ? JSON.parse(o.body) : o.body;
121
+ }
122
+ catch { /* keep text */ }
123
+ if (status === 0)
124
+ body = { message: String(o.body ?? "request failed").replace(/\s+/g, " ").slice(0, 300) };
125
+ return { status, headers: {}, body };
126
+ },
127
+ };
128
+ }
129
+ const values = (b) => (isObj(b) && Array.isArray(b.value) ? b.value : undefined);
130
+ const nextLink = (r) => (isObj(r.body) ? str(r.body["@odata.nextLink"]) : undefined);
131
+ const list = (api, path, maxPages = 500) => getAll(api, path, values, nextLink, maxPages);
132
+ /** A date as the administration service may write it: ISO 8601, or a WMI DMTF datetime (yyyymmddHHMMSS.mmmmmm+UUU). */
133
+ export function cmDate(v) {
134
+ if (typeof v !== "string" || !v)
135
+ return undefined;
136
+ const m = v.match(/^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.\d{6}([+-])(\d{3})$/);
137
+ if (m) {
138
+ const t = Date.UTC(+m[1], +m[2] - 1, +m[3], +m[4], +m[5], +m[6]) - (m[7] === "+" ? 1 : -1) * Number(m[8]) * 60_000;
139
+ return Number.isFinite(t) ? t : undefined;
140
+ }
141
+ const t = Date.parse(v);
142
+ return Number.isFinite(t) ? t : undefined;
143
+ }
144
+ const age = (v, now) => { const t = cmDate(v); return t === undefined ? undefined : Math.floor((now.getTime() - t) / DAY_MS); };
145
+ const num = (v) => (typeof v === "number" ? v : typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v)) ? Number(v) : undefined);
146
+ export async function configmgrChecks(api, provider, o, now) {
147
+ const out = new Emitter("configmgr", `configmgr ${provider}`);
148
+ const devR = await list(api, PATHS.devices);
149
+ if ("error" in devR) {
150
+ for (const c of CONFIGMGR_CHECKS)
151
+ out.skip(c, devR.error, "SMS_CombinedDeviceResources");
152
+ return out.results;
153
+ }
154
+ const all = devR.items.filter(isObj).filter((d) => d.Unknown !== true && d.IsObsolete !== true);
155
+ const devices = all.slice(0, o.maxDevices);
156
+ const truncated = devR.truncated || all.length > o.maxDevices;
157
+ if (!devices.length) {
158
+ out.skipAll(CONFIGMGR_CHECKS, "Configuration Manager lists no device (unknown and obsolete records not counted); nothing to judge");
159
+ return out.results;
160
+ }
161
+ const cap = truncated ? `; only the first ${o.maxDevices} devices read (--configmgr-max-devices)` : "";
162
+ // Software updates: the deployed ones, and each device's required or unknown rows for them.
163
+ const updR = await list(api, PATHS.updates);
164
+ const staR = "error" in updR ? undefined : await list(api, PATHS.updateStatus);
165
+ const deployed = new Map();
166
+ if (!("error" in updR))
167
+ for (const u of updR.items.filter(isObj)) {
168
+ const id = num(u.CI_ID);
169
+ if (id !== undefined)
170
+ deployed.set(id, u);
171
+ }
172
+ const rowsBy = new Map();
173
+ if (staR && !("error" in staR))
174
+ for (const s of staR.items.filter(isObj)) {
175
+ const m = num(s.MachineID), ci = num(s.CI_ID);
176
+ if (m === undefined || ci === undefined || !deployed.has(ci))
177
+ continue;
178
+ rowsBy.set(m, [...(rowsBy.get(m) ?? []), s]);
179
+ }
180
+ const updErr = "error" in updR ? updR.error : staR && "error" in staR ? staR.error : undefined;
181
+ const ciR = await list(api, PATHS.baselines);
182
+ const ciBy = new Map();
183
+ if (!("error" in ciR))
184
+ for (const s of ciR.items.filter(isObj)) {
185
+ const m = num(s.ResourceID);
186
+ if (m !== undefined)
187
+ ciBy.set(m, [...(ciBy.get(m) ?? []), s]);
188
+ }
189
+ const from = out.results.length;
190
+ for (const d of devices) {
191
+ const rid = num(d.ResourceID);
192
+ const res = deviceResource("configmgr device", d.ResourceID, d.SerialNumber);
193
+ const client = d.IsClient;
194
+ if (typeof client === "boolean")
195
+ out.add("client_installed", client ? "pass" : "fail", `IsClient is ${client}${client ? "" : ": discovered, but no Configuration Manager client reports from it"}${cap}`, res);
196
+ else
197
+ out.add("client_installed", "skipped", "IsClient not reported", res);
198
+ if (client !== true) {
199
+ for (const c of CONFIGMGR_CHECKS.filter((x) => x !== "client_installed"))
200
+ out.add(c, "skipped", "no Configuration Manager client on this device, so nothing it reports", res);
201
+ continue;
202
+ }
203
+ const seenField = d.LastActiveTime ? "LastActiveTime" : "LastPolicyRequest";
204
+ const a = age(d[seenField], now);
205
+ if (a === undefined)
206
+ out.add("last_seen", "skipped", "neither LastActiveTime nor LastPolicyRequest is reported", res);
207
+ else
208
+ out.add("last_seen", a <= o.seenDays ? "pass" : "fail", `${seenField} ${a} day(s) ago; at most ${o.seenDays} allowed (--configmgr-seen-days)`, res);
209
+ if (typeof d.EPEnabled !== "boolean") {
210
+ for (const c of ["antimalware_enabled", "antimalware_signatures_current"])
211
+ out.add(c, "skipped", "Endpoint Protection status not reported (EPEnabled empty): Configuration Manager does not manage this device's antimalware", res);
212
+ }
213
+ else {
214
+ const on = d.EPEnabled === true && d.EPAntivirusEnabled === true;
215
+ out.add("antimalware_enabled", on ? "pass" : "fail", `EPEnabled is ${d.EPEnabled}, EPAntivirusEnabled is ${String(d.EPAntivirusEnabled)}`, res);
216
+ const s = age(d.EPAntivirusSignatureLastUpdateDateTime, now);
217
+ if (s === undefined)
218
+ out.add("antimalware_signatures_current", "skipped", "EPAntivirusSignatureLastUpdateDateTime not reported", res);
219
+ else
220
+ out.add("antimalware_signatures_current", s <= o.signatureDays ? "pass" : "fail", `antivirus signatures updated ${s} day(s) ago; at most ${o.signatureDays} allowed (--configmgr-signature-days)`, res);
221
+ }
222
+ if (updErr)
223
+ out.skip("updates_installed", updErr, "software update compliance");
224
+ else {
225
+ const rows = rid === undefined ? [] : rowsBy.get(rid) ?? [];
226
+ const late = rows.filter((r) => num(r.Status) === 2).filter((r) => {
227
+ const u = deployed.get(num(r.CI_ID));
228
+ const posted = age(u.DatePosted, now);
229
+ return u.IsExpired !== true && posted !== undefined && posted > o.patchDays;
230
+ });
231
+ const unknown = rows.filter((r) => num(r.Status) === 0);
232
+ const show = (xs) => xs.slice(0, 5).map((r) => { const u = deployed.get(num(r.CI_ID)); return `${label(r.ArticleID ? `KB${r.ArticleID}` : r.CI_ID, 20)} ${label(u.SeverityName, 20)}`; }).join(", ") + (xs.length > 5 ? `, and ${xs.length - 5} more` : "");
233
+ if (late.length)
234
+ out.add("updates_installed", "fail", `${late.length} deployed update(s) published more than ${o.patchDays} days ago are still required (--configmgr-patch-days): ${show(late)}`, res);
235
+ else if (unknown.length)
236
+ out.add("updates_installed", "skipped", `no late required update, but ${unknown.length} deployed update(s) have detection state unknown on this device`, res);
237
+ else
238
+ out.add("updates_installed", "pass", `no deployed update published more than ${o.patchDays} days ago is required (${deployed.size} deployed update(s) known to the site)`, res);
239
+ }
240
+ if ("error" in ciR)
241
+ out.skip("baselines_compliant", ciR.error, "SMS_G_System_CI_ComplianceState", res);
242
+ else {
243
+ const rows = rid === undefined ? [] : ciBy.get(rid) ?? [];
244
+ const bad = rows.filter((r) => [2, 4].includes(num(r.ComplianceState) ?? -1)), good = rows.filter((r) => num(r.ComplianceState) === 1);
245
+ const names = (xs) => xs.slice(0, 5).map((r) => `${label(r.LocalizedDisplayName, 60)} ${num(r.ComplianceState) === 4 ? "Error" : "Non-Compliant"}`).join(", ") + (xs.length > 5 ? `, and ${xs.length - 5} more` : "");
246
+ if (bad.length)
247
+ out.add("baselines_compliant", "fail", `${bad.length} of ${rows.length} configuration item(s) not compliant: ${names(bad)}`, res);
248
+ else if (good.length)
249
+ out.add("baselines_compliant", "pass", `${good.length} of ${rows.length} configuration item(s) Compliant, none Non-Compliant or Error`, res);
250
+ else
251
+ out.add("baselines_compliant", "skipped", rows.length ? `${rows.length} configuration item(s), none with a known compliance state yet` : "no configuration baseline reports a state for this device", res);
252
+ }
253
+ }
254
+ stampRead(out.results, from, listRead({ items: devices, truncated, pages: devR.pages }));
255
+ return out.results;
256
+ }
257
+ export const configmgr = {
258
+ name: "configmgr",
259
+ options: {
260
+ "configmgr-seen-days": { type: "string", help: `days since a client was last active before last_seen fails (default ${DEFAULTS.seenDays})` },
261
+ "configmgr-signature-days": { type: "string", help: `days since the Endpoint Protection antivirus signatures updated before antimalware_signatures_current fails (default ${DEFAULTS.signatureDays})` },
262
+ "configmgr-patch-days": { type: "string", help: `days a deployed update may stay required after it was published (default ${DEFAULTS.patchDays})` },
263
+ "configmgr-max-devices": { type: "string", help: `devices judged, at most (default ${DEFAULTS.maxDevices})` },
264
+ },
265
+ emits: CONFIGMGR_CHECKS.map((c) => `configmgr.${c}`),
266
+ describe: () => "Configuration Manager site through the administration service on CONFIGMGR_PROVIDER (Windows authentication, Read-only Analyst)",
267
+ async run(ctx) {
268
+ const provider = (ctx.env[CONFIGMGR_ENV.provider] ?? "").trim().replace(/^https:\/\//i, "").replace(/\/+$/, "") || "(unknown)";
269
+ const given = ctx.clients.configmgr;
270
+ const built = given !== undefined ? given : configmgrFromEnv(ctx.env);
271
+ if (!built || typeof built === "string") {
272
+ const out = new Emitter("configmgr", `configmgr ${provider}`);
273
+ out.skipAll(CONFIGMGR_CHECKS, typeof built === "string" ? built : "no administration service client");
274
+ return out.results;
275
+ }
276
+ const api = withRetry(built, ctx.clients.sleep ?? realSleep);
277
+ const o = {
278
+ seenDays: posInt(ctx.opts["configmgr-seen-days"], DEFAULTS.seenDays),
279
+ signatureDays: posInt(ctx.opts["configmgr-signature-days"], DEFAULTS.signatureDays),
280
+ patchDays: posInt(ctx.opts["configmgr-patch-days"], DEFAULTS.patchDays),
281
+ maxDevices: posInt(ctx.opts["configmgr-max-devices"], DEFAULTS.maxDevices),
282
+ };
283
+ // No secret is held: the Windows credentials never pass through the collector.
284
+ return configmgrChecks(api, provider, o, ctx.now);
285
+ },
286
+ };
@@ -1,8 +1,18 @@
1
+ import { configmgr } from "./configmgr.js";
1
2
  import { defender_endpoint } from "./defender_endpoint.js";
3
+ import { endpoint_central } from "./endpoint_central.js";
2
4
  import { fleet } from "./fleet.js";
5
+ import { googleEndpoints } from "./google_endpoints.js";
3
6
  import { intune } from "./intune.js";
7
+ import { ivanti_mdm } from "./ivanti_mdm.js";
4
8
  import { jamf } from "./jamf.js";
5
9
  import { kandji } from "./kandji.js";
6
10
  import { macos } from "./macos.js";
11
+ import { ninjaone } from "./ninjaone.js";
7
12
  import { windows } from "./windows.js";
8
- export const DEVICE_SOURCES = [fleet, intune, jamf, kandji, defender_endpoint, macos, windows];
13
+ import { workspaceOne } from "./workspace_one.js";
14
+ export const DEVICE_SOURCES = [
15
+ fleet, intune, jamf, kandji, defender_endpoint,
16
+ workspaceOne, configmgr, googleEndpoints, ivanti_mdm, ninjaone, endpoint_central,
17
+ macos, windows,
18
+ ];