@oneuptime/common 12.0.21 → 12.0.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 (94) hide show
  1. package/Models/DatabaseModels/Index.ts +4 -0
  2. package/Models/DatabaseModels/NetworkDevice.ts +44 -0
  3. package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +564 -0
  4. package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +38 -0
  5. package/Models/DatabaseModels/UserTwoFactorBackupCode.ts +262 -0
  6. package/Server/API/UserTwoFactorBackupCodeAPI.ts +258 -0
  7. package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
  8. package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts +63 -0
  9. package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
  10. package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
  11. package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
  12. package/Server/Services/Index.ts +4 -0
  13. package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
  14. package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
  15. package/Server/Services/UserService.ts +42 -0
  16. package/Server/Services/UserTwoFactorBackupCodeService.ts +355 -0
  17. package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
  18. package/Server/Utils/TwoFactorBackupCode.ts +266 -0
  19. package/Tests/Server/API/UserAuthenticationAPI.test.ts +16 -3
  20. package/Tests/Server/API/UserTwoFactorAuthAdminAPI.test.ts +18 -2
  21. package/Tests/Server/API/UserTwoFactorBackupCodeAPI.test.ts +1390 -0
  22. package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
  23. package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
  24. package/Tests/Server/Services/UserAuthenticationService.test.ts +23 -1
  25. package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +21 -0
  26. package/Tests/Server/Services/UserTwoFactorBackupCodeAdminSurface.test.ts +919 -0
  27. package/Tests/Server/Services/UserTwoFactorBackupCodeService.test.ts +862 -0
  28. package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
  29. package/Tests/Server/Utils/TwoFactorBackupCode.test.ts +475 -0
  30. package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
  31. package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
  32. package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
  33. package/Types/Email/EmailTemplateType.ts +2 -0
  34. package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
  35. package/Types/Permission.ts +47 -0
  36. package/Types/UserAuthenticationStatus.ts +16 -0
  37. package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
  38. package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
  39. package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
  40. package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
  41. package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
  42. package/build/dist/Models/DatabaseModels/Index.js +4 -0
  43. package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
  44. package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
  45. package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
  46. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +589 -0
  47. package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
  48. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +28 -0
  49. package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
  50. package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js +277 -0
  51. package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js.map +1 -0
  52. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +201 -0
  53. package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -0
  54. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
  55. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
  56. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js +46 -0
  57. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js.map +1 -0
  58. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
  59. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
  60. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
  61. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
  62. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
  63. package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
  64. package/build/dist/Server/Services/Index.js +4 -0
  65. package/build/dist/Server/Services/Index.js.map +1 -1
  66. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
  67. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
  68. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
  69. package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
  70. package/build/dist/Server/Services/UserService.js +39 -0
  71. package/build/dist/Server/Services/UserService.js.map +1 -1
  72. package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js +327 -0
  73. package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js.map +1 -0
  74. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
  75. package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
  76. package/build/dist/Server/Utils/TwoFactorBackupCode.js +269 -0
  77. package/build/dist/Server/Utils/TwoFactorBackupCode.js.map +1 -0
  78. package/build/dist/Types/Email/EmailTemplateType.js +2 -0
  79. package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
  80. package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
  81. package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
  82. package/build/dist/Types/Permission.js +41 -0
  83. package/build/dist/Types/Permission.js.map +1 -1
  84. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
  85. package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
  86. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
  87. package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
  88. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
  89. package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
  90. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
  91. package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
  92. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
  93. package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
  94. package/package.json +1 -1
@@ -0,0 +1,516 @@
1
+ import AutoImportRuleMatcher, {
2
+ AutoImportHostEvaluation,
3
+ AutoImportRuleCandidate,
4
+ } from "../../../Utils/NetworkDiscovery/AutoImportRuleMatcher";
5
+ import { DiscoveredNetworkDevice } from "../../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
6
+ import { describe, expect, it } from "@jest/globals";
7
+
8
+ /*
9
+ * Contract under test — which discovered hosts an auto-import rule claims,
10
+ * and what the rule set as a whole says about one host (issue #3378).
11
+ *
12
+ * The semantics worth pinning are the ones an operator relies on without
13
+ * reading the code: populated conditions AND within a rule, an empty rule
14
+ * matches NOTHING (not everything), OR is expressed as multiple rules, an
15
+ * exclusion rule vetoes unconditionally, and a ping-only host needs an
16
+ * explicit opt-in before any import rule may claim it.
17
+ */
18
+
19
+ function host(
20
+ overrides: Partial<DiscoveredNetworkDevice> = {},
21
+ ): DiscoveredNetworkDevice {
22
+ return {
23
+ ipAddress: "10.0.0.5",
24
+ sysName: "switch-WB1678-01",
25
+ sysDescr: "Cisco IOS Software, C2960X",
26
+ ...overrides,
27
+ };
28
+ }
29
+
30
+ describe("AutoImportRuleMatcher.ruleMatchesHost", () => {
31
+ /*
32
+ * The site-rule precedent: an all-wildcard rule is a typo, not a
33
+ * match-everything. A rule that claimed every host on an empty form would
34
+ * import whole subnets the moment it was saved half-finished.
35
+ */
36
+ it("matches nothing when the rule has no conditions", () => {
37
+ expect(AutoImportRuleMatcher.ruleMatchesHost({}, host())).toBe(false);
38
+ expect(
39
+ AutoImportRuleMatcher.ruleMatchesHost(
40
+ { ipMatchTarget: "", sysNamePattern: "", sysDescrPattern: "" },
41
+ host(),
42
+ ),
43
+ ).toBe(false);
44
+ // Whitespace-only conditions are "not configured", not conditions.
45
+ expect(
46
+ AutoImportRuleMatcher.ruleMatchesHost(
47
+ { ipMatchTarget: " ", sysNamePattern: " " },
48
+ host(),
49
+ ),
50
+ ).toBe(false);
51
+ // Nor does the ping-only opt-in count as a condition by itself.
52
+ expect(
53
+ AutoImportRuleMatcher.ruleMatchesHost(
54
+ { includePingOnlyHosts: true },
55
+ host(),
56
+ ),
57
+ ).toBe(false);
58
+ });
59
+
60
+ it("matches on a single populated condition", () => {
61
+ expect(
62
+ AutoImportRuleMatcher.ruleMatchesHost(
63
+ { ipMatchTarget: "10.0.0.0/24" },
64
+ host(),
65
+ ),
66
+ ).toBe(true);
67
+ expect(
68
+ AutoImportRuleMatcher.ruleMatchesHost(
69
+ { ipMatchTarget: "10.0.1.0/24" },
70
+ host(),
71
+ ),
72
+ ).toBe(false);
73
+ });
74
+
75
+ it("ANDs populated conditions: ip and sysName must both hold", () => {
76
+ const rule: AutoImportRuleCandidate = {
77
+ ipMatchTarget: "10.0.0.0/24",
78
+ sysNamePattern: "WB1678",
79
+ };
80
+
81
+ expect(AutoImportRuleMatcher.ruleMatchesHost(rule, host())).toBe(true);
82
+
83
+ // Right subnet, wrong name.
84
+ expect(
85
+ AutoImportRuleMatcher.ruleMatchesHost(
86
+ rule,
87
+ host({ sysName: "switch-XX9999-01" }),
88
+ ),
89
+ ).toBe(false);
90
+
91
+ // Right name, wrong subnet.
92
+ expect(
93
+ AutoImportRuleMatcher.ruleMatchesHost(
94
+ rule,
95
+ host({ ipAddress: "192.168.1.5" }),
96
+ ),
97
+ ).toBe(false);
98
+ });
99
+
100
+ /*
101
+ * Pattern semantics are RulePatternMatchUtil's, shared with the label and
102
+ * owner rules: a plain string is an unanchored case-insensitive regex
103
+ * ("contains"), and a '*' glob works too — the syntax the neighbouring
104
+ * site rules taught operators (OneUptime/oneuptime#2940).
105
+ */
106
+ it("matches sysName as an unanchored substring regex", () => {
107
+ expect(
108
+ AutoImportRuleMatcher.ruleMatchesHost(
109
+ { sysNamePattern: "WB1678" },
110
+ host({ sysName: "switch-WB1678-01" }),
111
+ ),
112
+ ).toBe(true);
113
+ expect(
114
+ AutoImportRuleMatcher.ruleMatchesHost(
115
+ { sysNamePattern: "WB1678" },
116
+ host({ sysName: "switch-XX9999-01" }),
117
+ ),
118
+ ).toBe(false);
119
+ });
120
+
121
+ it("matches sysName as a '*' glob as well", () => {
122
+ expect(
123
+ AutoImportRuleMatcher.ruleMatchesHost(
124
+ { sysNamePattern: "*WB1678*" },
125
+ host({ sysName: "switch-WB1678-01" }),
126
+ ),
127
+ ).toBe(true);
128
+ expect(
129
+ AutoImportRuleMatcher.ruleMatchesHost(
130
+ { sysNamePattern: "*WB1678*" },
131
+ host({ sysName: "switch-XX9999-01" }),
132
+ ),
133
+ ).toBe(false);
134
+ });
135
+
136
+ it("matches sysDescr with the same regex-first-glob-fallback semantics", () => {
137
+ expect(
138
+ AutoImportRuleMatcher.ruleMatchesHost(
139
+ { sysDescrPattern: "C2960X" },
140
+ host(),
141
+ ),
142
+ ).toBe(true);
143
+ expect(
144
+ AutoImportRuleMatcher.ruleMatchesHost(
145
+ { sysDescrPattern: "*IOS*" },
146
+ host(),
147
+ ),
148
+ ).toBe(true);
149
+ expect(
150
+ AutoImportRuleMatcher.ruleMatchesHost(
151
+ { sysDescrPattern: "JunOS" },
152
+ host(),
153
+ ),
154
+ ).toBe(false);
155
+ });
156
+
157
+ /*
158
+ * A ping-only host has no SNMP identity, so a host with no sysName at all
159
+ * is routine — and a configured name pattern must read it as "does not
160
+ * match", never as "nothing to check".
161
+ */
162
+ it("never matches a configured sysNamePattern against a host with no sysName", () => {
163
+ expect(
164
+ AutoImportRuleMatcher.ruleMatchesHost(
165
+ { sysNamePattern: "WB1678" },
166
+ host({ sysName: undefined }),
167
+ ),
168
+ ).toBe(false);
169
+ expect(
170
+ AutoImportRuleMatcher.ruleMatchesHost(
171
+ { sysNamePattern: "*WB1678*" },
172
+ host({ sysName: undefined }),
173
+ ),
174
+ ).toBe(false);
175
+ });
176
+
177
+ describe("the sysObjectID condition", () => {
178
+ /*
179
+ * The vendor fingerprint: sysObjectID is the vendor's registered
180
+ * enterprise OID, so "any Cisco device" is a glob over the 1.3.6.1.4.1.9
181
+ * arc. Deliberately NOT the free-text semantics of the other two
182
+ * pattern conditions — OID matching is a literal-dot whole-string glob,
183
+ * or an arc-prefix test when the pattern has no '*'.
184
+ */
185
+ it("matches a vendor by enterprise-arc glob and counts as a condition on its own", () => {
186
+ const ciscoRule: AutoImportRuleCandidate = {
187
+ sysObjectIdPattern: "1.3.6.1.4.1.9.*",
188
+ };
189
+
190
+ expect(
191
+ AutoImportRuleMatcher.ruleMatchesHost(
192
+ ciscoRule,
193
+ host({ sysObjectId: "1.3.6.1.4.1.9.1.1745" }),
194
+ ),
195
+ ).toBe(true);
196
+
197
+ // A Juniper box sits under a different arc and stays out.
198
+ expect(
199
+ AutoImportRuleMatcher.ruleMatchesHost(
200
+ ciscoRule,
201
+ host({ sysObjectId: "1.3.6.1.4.1.2636.1.1.1.2.137" }),
202
+ ),
203
+ ).toBe(false);
204
+ });
205
+
206
+ /*
207
+ * The regression that forced OID-aware matching: under the free-text
208
+ * matcher's regex-FIRST semantics, "1.3.6.1.4.1.9.*" compiled as an
209
+ * unanchored regex where every '.' matches anything, so "any Cisco
210
+ * device" also imported (or, as an exclusion, silently vetoed) every
211
+ * enterprise whose number merely STARTS with 9 — Nokia (94), 990,
212
+ * 9148... In an OID, dots are dots and 9 is not 94.
213
+ */
214
+ it("never bleeds into sibling enterprise arcs that share a digit prefix", () => {
215
+ const ciscoGlob: AutoImportRuleCandidate = {
216
+ sysObjectIdPattern: "1.3.6.1.4.1.9.*",
217
+ };
218
+
219
+ for (const foreignOid of [
220
+ "1.3.6.1.4.1.94.1.21", // Nokia
221
+ "1.3.6.1.4.1.990.2.1",
222
+ "1.3.6.1.4.1.9999.1.2",
223
+ ]) {
224
+ expect(
225
+ AutoImportRuleMatcher.ruleMatchesHost(
226
+ ciscoGlob,
227
+ host({ sysObjectId: foreignOid }),
228
+ ),
229
+ ).toBe(false);
230
+ }
231
+
232
+ // And the same holds when the rule is an exclusion veto.
233
+ const excludeCisco: AutoImportRuleCandidate = {
234
+ sysObjectIdPattern: "1.3.6.1.4.1.9.*",
235
+ isExclusion: true,
236
+ };
237
+ const nokiaHost: DiscoveredNetworkDevice = host({
238
+ sysObjectId: "1.3.6.1.4.1.94.1.21",
239
+ });
240
+ const importAll: AutoImportRuleCandidate = {
241
+ ipMatchTarget: "10.0.0.0/8",
242
+ };
243
+
244
+ const evaluation: AutoImportHostEvaluation =
245
+ AutoImportRuleMatcher.evaluateHost(
246
+ [excludeCisco, importAll],
247
+ nokiaHost,
248
+ );
249
+
250
+ expect(evaluation.excludedByRule).toBeUndefined();
251
+ expect(evaluation.shouldImport).toBe(true);
252
+ });
253
+
254
+ it("treats a starless pattern as an arc prefix on sub-identifier boundaries", () => {
255
+ const ciscoPrefix: AutoImportRuleCandidate = {
256
+ sysObjectIdPattern: "1.3.6.1.4.1.9",
257
+ };
258
+
259
+ // The arc itself and everything under it.
260
+ expect(
261
+ AutoImportRuleMatcher.ruleMatchesHost(
262
+ ciscoPrefix,
263
+ host({ sysObjectId: "1.3.6.1.4.1.9" }),
264
+ ),
265
+ ).toBe(true);
266
+ expect(
267
+ AutoImportRuleMatcher.ruleMatchesHost(
268
+ ciscoPrefix,
269
+ host({ sysObjectId: "1.3.6.1.4.1.9.1.1745" }),
270
+ ),
271
+ ).toBe(true);
272
+
273
+ // Never the sibling arc that shares the digit prefix.
274
+ expect(
275
+ AutoImportRuleMatcher.ruleMatchesHost(
276
+ ciscoPrefix,
277
+ host({ sysObjectId: "1.3.6.1.4.1.94.1.21" }),
278
+ ),
279
+ ).toBe(false);
280
+ });
281
+
282
+ it("tolerates the leading dot some agents prefix OIDs with, on either side", () => {
283
+ expect(
284
+ AutoImportRuleMatcher.ruleMatchesHost(
285
+ { sysObjectIdPattern: "1.3.6.1.4.1.9.*" },
286
+ host({ sysObjectId: ".1.3.6.1.4.1.9.1.1745" }),
287
+ ),
288
+ ).toBe(true);
289
+
290
+ expect(
291
+ AutoImportRuleMatcher.ruleMatchesHost(
292
+ { sysObjectIdPattern: ".1.3.6.1.4.1.9" },
293
+ host({ sysObjectId: "1.3.6.1.4.1.9.1.1745" }),
294
+ ),
295
+ ).toBe(true);
296
+ });
297
+
298
+ it("a glob cannot match mid-OID — the pattern must cover the whole arc", () => {
299
+ expect(
300
+ AutoImportRuleMatcher.ruleMatchesHost(
301
+ { sysObjectIdPattern: "2.1.1.*" },
302
+ host({ sysObjectId: "1.3.6.1.4.1.14988.2.1.1.5" }),
303
+ ),
304
+ ).toBe(false);
305
+ });
306
+
307
+ it("ANDs with the other conditions like any criterion", () => {
308
+ const rule: AutoImportRuleCandidate = {
309
+ ipMatchTarget: "10.0.0.0/24",
310
+ sysObjectIdPattern: "1.3.6.1.4.1.9.*",
311
+ };
312
+
313
+ expect(
314
+ AutoImportRuleMatcher.ruleMatchesHost(
315
+ rule,
316
+ host({ sysObjectId: "1.3.6.1.4.1.9.1.1745" }),
317
+ ),
318
+ ).toBe(true);
319
+
320
+ expect(
321
+ AutoImportRuleMatcher.ruleMatchesHost(
322
+ rule,
323
+ host({
324
+ ipAddress: "192.168.7.1",
325
+ sysObjectId: "1.3.6.1.4.1.9.1.1745",
326
+ }),
327
+ ),
328
+ ).toBe(false);
329
+ });
330
+
331
+ /*
332
+ * Ping-only hosts and rows stored by pre-sysObjectId probes carry no
333
+ * sysObjectId at all. A configured vendor condition must not match a
334
+ * host whose vendor is unknown — the same missing-value rule the
335
+ * sysName/sysDescr patterns follow.
336
+ */
337
+ it("never matches a host whose scan did not carry sysObjectId", () => {
338
+ expect(
339
+ AutoImportRuleMatcher.ruleMatchesHost(
340
+ { sysObjectIdPattern: "1.3.6.1.4.1.9.*" },
341
+ host({ sysObjectId: undefined }),
342
+ ),
343
+ ).toBe(false);
344
+ });
345
+ });
346
+
347
+ describe("pattern subject clamping", () => {
348
+ /*
349
+ * The jsonb these hosts come from is the probe's payload stored
350
+ * verbatim and unbounded, while patterns run through JavaScript's
351
+ * backtracking regex engine — so subjects are clamped to 500 characters
352
+ * before matching. Real SNMP DisplayStrings top out at 255 octets;
353
+ * only hostile or corrupt payloads are longer, and the clamp bounds
354
+ * what any pattern can be made to chew on.
355
+ */
356
+ it("does not see pattern evidence past the 500-character clamp", () => {
357
+ const sysDescrWithLateEvidence: string =
358
+ "x".repeat(520) + "NEEDLE" + "x".repeat(74);
359
+ expect(sysDescrWithLateEvidence).toHaveLength(600);
360
+
361
+ expect(
362
+ AutoImportRuleMatcher.ruleMatchesHost(
363
+ { sysDescrPattern: "NEEDLE" },
364
+ host({ sysDescr: sysDescrWithLateEvidence }),
365
+ ),
366
+ ).toBe(false);
367
+ });
368
+
369
+ it("still sees evidence that sits before the clamp", () => {
370
+ const sysDescrWithEarlyEvidence: string =
371
+ "x".repeat(100) + "NEEDLE" + "x".repeat(494);
372
+ expect(sysDescrWithEarlyEvidence).toHaveLength(600);
373
+
374
+ expect(
375
+ AutoImportRuleMatcher.ruleMatchesHost(
376
+ { sysDescrPattern: "NEEDLE" },
377
+ host({ sysDescr: sysDescrWithEarlyEvidence }),
378
+ ),
379
+ ).toBe(true);
380
+ });
381
+ });
382
+
383
+ describe("the ping-only gate", () => {
384
+ /*
385
+ * An SNMP credential typo makes a whole subnet report as ping-only; a
386
+ * rule silently importing hundreds of half-identified hosts on that typo
387
+ * is the failure mode the default-closed gate exists to prevent.
388
+ */
389
+ it("does not let an import rule claim a ping-only host by default", () => {
390
+ expect(
391
+ AutoImportRuleMatcher.ruleMatchesHost(
392
+ { ipMatchTarget: "10.0.0.0/24" },
393
+ host({ snmpReachable: false }),
394
+ ),
395
+ ).toBe(false);
396
+ });
397
+
398
+ it("lets an import rule claim a ping-only host after the explicit opt-in", () => {
399
+ expect(
400
+ AutoImportRuleMatcher.ruleMatchesHost(
401
+ { ipMatchTarget: "10.0.0.0/24", includePingOnlyHosts: true },
402
+ host({ snmpReachable: false }),
403
+ ),
404
+ ).toBe(true);
405
+ });
406
+
407
+ /*
408
+ * Scans stored before snmpReachable existed carry undefined, and every
409
+ * host on them answered SNMP by construction — so legacy rows must keep
410
+ * importing without the opt-in.
411
+ */
412
+ it("reads an undefined snmpReachable (legacy scan) as SNMP-reachable", () => {
413
+ expect(
414
+ AutoImportRuleMatcher.ruleMatchesHost(
415
+ { ipMatchTarget: "10.0.0.0/24" },
416
+ host({ snmpReachable: undefined }),
417
+ ),
418
+ ).toBe(true);
419
+ });
420
+
421
+ // A veto is a veto whatever the host answered with.
422
+ it("lets an exclusion rule match a ping-only host regardless of the opt-in", () => {
423
+ expect(
424
+ AutoImportRuleMatcher.ruleMatchesHost(
425
+ { ipMatchTarget: "10.0.0.0/24", isExclusion: true },
426
+ host({ snmpReachable: false }),
427
+ ),
428
+ ).toBe(true);
429
+ });
430
+ });
431
+ });
432
+
433
+ describe("AutoImportRuleMatcher.evaluateHost", () => {
434
+ const matchingRuleA: AutoImportRuleCandidate = {
435
+ ipMatchTarget: "10.0.0.0/24",
436
+ };
437
+ const matchingRuleB: AutoImportRuleCandidate = {
438
+ sysNamePattern: "WB1678",
439
+ };
440
+ const nonMatchingRule: AutoImportRuleCandidate = {
441
+ ipMatchTarget: "192.168.0.0/16",
442
+ };
443
+
444
+ // OR across rules: any import rule matching is enough.
445
+ it("imports when any import rule matches, listing exactly the ones that did", () => {
446
+ const evaluation: AutoImportHostEvaluation =
447
+ AutoImportRuleMatcher.evaluateHost(
448
+ [nonMatchingRule, matchingRuleA, matchingRuleB],
449
+ host(),
450
+ );
451
+
452
+ expect(evaluation.shouldImport).toBe(true);
453
+ expect(evaluation.matchedRules).toEqual([matchingRuleA, matchingRuleB]);
454
+ expect(evaluation.excludedByRule).toBeUndefined();
455
+ });
456
+
457
+ /*
458
+ * The veto, and its explainability contract: the answer is "excluded by
459
+ * rule R" with NO matched rules listed, so a dry run reads as one decision
460
+ * rather than "matched A and B and then didn't".
461
+ */
462
+ it("lets an exclusion rule veto even when import rules match", () => {
463
+ const exclusionRule: AutoImportRuleCandidate = {
464
+ ipMatchTarget: "10.0.0.5",
465
+ isExclusion: true,
466
+ };
467
+
468
+ const evaluation: AutoImportHostEvaluation =
469
+ AutoImportRuleMatcher.evaluateHost(
470
+ [matchingRuleA, exclusionRule, matchingRuleB],
471
+ host(),
472
+ );
473
+
474
+ expect(evaluation.shouldImport).toBe(false);
475
+ expect(evaluation.matchedRules).toEqual([]);
476
+ expect(evaluation.excludedByRule).toBe(exclusionRule);
477
+ });
478
+
479
+ it("does not veto through an exclusion rule the host does not match", () => {
480
+ const irrelevantExclusion: AutoImportRuleCandidate = {
481
+ ipMatchTarget: "172.16.0.0/12",
482
+ isExclusion: true,
483
+ };
484
+
485
+ const evaluation: AutoImportHostEvaluation =
486
+ AutoImportRuleMatcher.evaluateHost(
487
+ [matchingRuleA, irrelevantExclusion],
488
+ host(),
489
+ );
490
+
491
+ expect(evaluation.shouldImport).toBe(true);
492
+ expect(evaluation.matchedRules).toEqual([matchingRuleA]);
493
+ expect(evaluation.excludedByRule).toBeUndefined();
494
+ });
495
+
496
+ it("does not import when no rules exist", () => {
497
+ const evaluation: AutoImportHostEvaluation =
498
+ AutoImportRuleMatcher.evaluateHost([], host());
499
+
500
+ expect(evaluation.shouldImport).toBe(false);
501
+ expect(evaluation.matchedRules).toEqual([]);
502
+ expect(evaluation.excludedByRule).toBeUndefined();
503
+ });
504
+
505
+ it("does not import when only exclusion rules exist", () => {
506
+ const evaluation: AutoImportHostEvaluation =
507
+ AutoImportRuleMatcher.evaluateHost(
508
+ [{ ipMatchTarget: "10.0.0.0/24", isExclusion: true }],
509
+ host(),
510
+ );
511
+
512
+ expect(evaluation.shouldImport).toBe(false);
513
+ expect(evaluation.matchedRules).toEqual([]);
514
+ expect(evaluation.excludedByRule).toBeDefined();
515
+ });
516
+ });