@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.
- package/Models/DatabaseModels/Index.ts +4 -0
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +564 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +38 -0
- package/Models/DatabaseModels/UserTwoFactorBackupCode.ts +262 -0
- package/Server/API/UserTwoFactorBackupCodeAPI.ts +258 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.ts +63 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.ts +26 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/Index.ts +8 -0
- package/Server/Services/Index.ts +4 -0
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
- package/Server/Services/UserService.ts +42 -0
- package/Server/Services/UserTwoFactorBackupCodeService.ts +355 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
- package/Server/Utils/TwoFactorBackupCode.ts +266 -0
- package/Tests/Server/API/UserAuthenticationAPI.test.ts +16 -3
- package/Tests/Server/API/UserTwoFactorAuthAdminAPI.test.ts +18 -2
- package/Tests/Server/API/UserTwoFactorBackupCodeAPI.test.ts +1390 -0
- package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
- package/Tests/Server/Services/UserAuthenticationService.test.ts +23 -1
- package/Tests/Server/Services/UserTwoFactorAuthAdmin.test.ts +21 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeAdminSurface.test.ts +919 -0
- package/Tests/Server/Services/UserTwoFactorBackupCodeService.test.ts +862 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
- package/Tests/Server/Utils/TwoFactorBackupCode.test.ts +475 -0
- package/Tests/Utils/NetworkDiscovery/AutoImportRuleMatcher.test.ts +516 -0
- package/Tests/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.test.ts +272 -0
- package/Tests/Utils/NetworkDiscovery/ScanTargetUtil.test.ts +164 -0
- package/Types/Email/EmailTemplateType.ts +2 -0
- package/Types/NetworkAutomation/RuleRunResult.ts +84 -0
- package/Types/Permission.ts +47 -0
- package/Types/UserAuthenticationStatus.ts +16 -0
- package/Utils/NetworkDiscovery/AutoImportRuleMatcher.ts +257 -0
- package/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.ts +183 -0
- package/Utils/NetworkDiscovery/DiscoveredHostUtil.ts +95 -0
- package/Utils/NetworkDiscovery/DiscoveryImportEligibility.ts +66 -0
- package/Utils/NetworkDiscovery/ScanTargetUtil.ts +82 -0
- package/build/dist/Models/DatabaseModels/Index.js +4 -0
- package/build/dist/Models/DatabaseModels/Index.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDevice.js +45 -0
- package/build/dist/Models/DatabaseModels/NetworkDevice.js.map +1 -1
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js +589 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js +28 -0
- package/build/dist/Models/DatabaseModels/NetworkDeviceDiscoveryScan.js.map +1 -1
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js +277 -0
- package/build/dist/Models/DatabaseModels/UserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js +201 -0
- package/build/dist/Server/API/UserTwoFactorBackupCodeAPI.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js +35 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js +46 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddUserTwoFactorBackupCode.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789200000000-AddSysObjectIdPatternToAutoImportRule.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js +19 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/1789300000000-AddAutoApplyVendorHealthTemplate.js.map +1 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js +8 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/Index.js +4 -0
- package/build/dist/Server/Services/Index.js.map +1 -1
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js +711 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleEngineService.js.map +1 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js +155 -0
- package/build/dist/Server/Services/NetworkDeviceAutoImportRuleService.js.map +1 -0
- package/build/dist/Server/Services/UserService.js +39 -0
- package/build/dist/Server/Services/UserService.js.map +1 -1
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js +327 -0
- package/build/dist/Server/Services/UserTwoFactorBackupCodeService.js.map +1 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js +26 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.js.map +1 -1
- package/build/dist/Server/Utils/TwoFactorBackupCode.js +269 -0
- package/build/dist/Server/Utils/TwoFactorBackupCode.js.map +1 -0
- package/build/dist/Types/Email/EmailTemplateType.js +2 -0
- package/build/dist/Types/Email/EmailTemplateType.js.map +1 -1
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js +24 -0
- package/build/dist/Types/NetworkAutomation/RuleRunResult.js.map +1 -1
- package/build/dist/Types/Permission.js +41 -0
- package/build/dist/Types/Permission.js.map +1 -1
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js +149 -0
- package/build/dist/Utils/NetworkDiscovery/AutoImportRuleMatcher.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js +121 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredDeviceBuilder.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js +75 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveredHostUtil.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js +59 -0
- package/build/dist/Utils/NetworkDiscovery/DiscoveryImportEligibility.js.map +1 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js +60 -0
- package/build/dist/Utils/NetworkDiscovery/ScanTargetUtil.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { DiscoveredNetworkDevice } from "../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
|
|
2
|
+
import NetworkDeviceMonitoringMethod from "../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
|
|
3
|
+
import CidrMatchUtil from "../NetworkSite/CidrMatchUtil";
|
|
4
|
+
import RulePatternMatchUtil from "../Rules/RulePatternMatchUtil";
|
|
5
|
+
import { monitoringMethodForDiscoveredHost } from "./DiscoveryImportEligibility";
|
|
6
|
+
import ScanTargetUtil from "./ScanTargetUtil";
|
|
7
|
+
|
|
8
|
+
/*
|
|
9
|
+
* Matching for network device auto-import rules: which discovered hosts a
|
|
10
|
+
* rule claims, and whether the rule set as a whole imports a host.
|
|
11
|
+
*
|
|
12
|
+
* Pure and dependency-free (no models beyond the host interface, no logger,
|
|
13
|
+
* no DB) so rule decisions are unit-testable — the same arrangement as
|
|
14
|
+
* CidrMatchUtil for site assignment rules and RulePatternMatchUtil for
|
|
15
|
+
* label/owner rules, both of which this reuses rather than re-implements.
|
|
16
|
+
*
|
|
17
|
+
* Semantics, mirroring the neighbouring rule engines:
|
|
18
|
+
*
|
|
19
|
+
* - Populated conditions on one rule are ANDed; an empty condition is "not
|
|
20
|
+
* configured" and skipped. A rule with NO conditions matches NOTHING
|
|
21
|
+
* (the site-rule precedent: an all-wildcard rule is a typo, not a
|
|
22
|
+
* match-everything).
|
|
23
|
+
* - OR is expressed as multiple rules: a host imports when ANY import rule
|
|
24
|
+
* matches it and NO exclusion rule matches it.
|
|
25
|
+
* - Exclusion rules veto. "Never import X" needs to be writable without
|
|
26
|
+
* regex negative-lookahead gymnastics, and a veto is how an operator
|
|
27
|
+
* carves printers and phones out of a broad subnet rule.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* The rule columns matching reads. NetworkDeviceAutoImportRule rows satisfy
|
|
32
|
+
* this structurally, and tests can build literals.
|
|
33
|
+
*/
|
|
34
|
+
export interface AutoImportRuleCandidate {
|
|
35
|
+
ipMatchTarget?: string | null | undefined;
|
|
36
|
+
sysNamePattern?: string | null | undefined;
|
|
37
|
+
sysDescrPattern?: string | null | undefined;
|
|
38
|
+
/*
|
|
39
|
+
* Matched against the host's SNMP sysObjectID — the vendor's registered
|
|
40
|
+
* enterprise OID (1.3.6.1.4.1.<enterprise>...), the canonical vendor
|
|
41
|
+
* fingerprint. NOT the free-text pattern syntax of the two fields above:
|
|
42
|
+
* an OID is a dotted numeric arc, so this is a whole-string '*' glob
|
|
43
|
+
* (dots literal) or, with no '*', an arc-prefix test — see
|
|
44
|
+
* matchesOidPattern. Only hosts whose scan carried the field can match: a
|
|
45
|
+
* ping-only host, or one reported by a probe from before the field
|
|
46
|
+
* existed, never satisfies a configured sysObjectIdPattern.
|
|
47
|
+
*/
|
|
48
|
+
sysObjectIdPattern?: string | null | undefined;
|
|
49
|
+
includePingOnlyHosts?: boolean | null | undefined;
|
|
50
|
+
isExclusion?: boolean | null | undefined;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** How the rule set as a whole read one discovered host. */
|
|
54
|
+
export interface AutoImportHostEvaluation {
|
|
55
|
+
// True when an import rule matched and no exclusion rule vetoed.
|
|
56
|
+
shouldImport: boolean;
|
|
57
|
+
// Import rules whose conditions (and ping-only gate) the host satisfied.
|
|
58
|
+
matchedRules: Array<AutoImportRuleCandidate>;
|
|
59
|
+
// The exclusion rule that vetoed, when one did.
|
|
60
|
+
excludedByRule?: AutoImportRuleCandidate | undefined;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/*
|
|
64
|
+
* Longest subject string a pattern is evaluated against. SNMP DisplayStrings
|
|
65
|
+
* top out at 255 octets, so anything longer only appears in a hostile or
|
|
66
|
+
* corrupt probe payload — and the jsonb these hosts come from is stored
|
|
67
|
+
* verbatim, unbounded. Patterns run through JavaScript's backtracking regex
|
|
68
|
+
* engine, whose worst case grows with subject length, so the subject is
|
|
69
|
+
* clamped before matching: a rule author writing a pathological pattern can
|
|
70
|
+
* only ever run it against a bounded string. (The same author already holds
|
|
71
|
+
* Edit permission on rules — this bounds the blast radius, it does not make
|
|
72
|
+
* hostile patterns free.)
|
|
73
|
+
*/
|
|
74
|
+
const MAX_PATTERN_SUBJECT_LENGTH: number = 500;
|
|
75
|
+
|
|
76
|
+
function clampSubject(value: string | undefined): string | undefined {
|
|
77
|
+
if (typeof value !== "string") {
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return value.length > MAX_PATTERN_SUBJECT_LENGTH
|
|
82
|
+
? value.substring(0, MAX_PATTERN_SUBJECT_LENGTH)
|
|
83
|
+
: value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/*
|
|
87
|
+
* Strips the leading dot some agents prefix OIDs with (".1.3.6.1.4.1.9...")
|
|
88
|
+
* so a pattern and a value in the two spellings still line up.
|
|
89
|
+
*/
|
|
90
|
+
function normalizeOid(value: string): string {
|
|
91
|
+
return value.startsWith(".") ? value.substring(1) : value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
* OID-aware matching for the sysObjectID condition — deliberately NOT
|
|
96
|
+
* RulePatternMatchUtil, whose regex-FIRST semantics are systematically wrong
|
|
97
|
+
* for a dotted numeric arc: as a regex, "1.3.6.1.4.1.9.*" has every '.'
|
|
98
|
+
* matching any character and is unanchored, so it also matches enterprise
|
|
99
|
+
* 94, 990, 9148... — the exact vendors a "Cisco only" rule exists to keep
|
|
100
|
+
* out, in a pipeline that creates devices unattended. Here:
|
|
101
|
+
*
|
|
102
|
+
* - A pattern containing '*' is a whole-string glob with LITERAL dots
|
|
103
|
+
* (CidrMatchUtil's ReDoS-safe matcher): "1.3.6.1.4.1.9.*" matches the
|
|
104
|
+
* Cisco subtree and nothing else.
|
|
105
|
+
* - A pattern with no '*' is an arc-prefix test on sub-identifier
|
|
106
|
+
* boundaries: "1.3.6.1.4.1.9" matches itself and everything under it,
|
|
107
|
+
* and can never match "1.3.6.1.4.1.94" — the same comparison
|
|
108
|
+
* SnmpVendorTemplateUtil.getEnterpriseNumber makes.
|
|
109
|
+
*/
|
|
110
|
+
export function matchesOidPattern(
|
|
111
|
+
value: string | undefined,
|
|
112
|
+
pattern: string,
|
|
113
|
+
): boolean {
|
|
114
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const oid: string = normalizeOid(value.trim());
|
|
119
|
+
const oidPattern: string = normalizeOid(pattern);
|
|
120
|
+
|
|
121
|
+
if (oidPattern.includes("*")) {
|
|
122
|
+
return CidrMatchUtil.hostnameMatchesWildcard(oid, oidPattern);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return oid === oidPattern || oid.startsWith(oidPattern + ".");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export class AutoImportRuleMatcher {
|
|
129
|
+
// True when the host answered ping but not SNMP.
|
|
130
|
+
private static isPingOnlyHost(host: DiscoveredNetworkDevice): boolean {
|
|
131
|
+
return (
|
|
132
|
+
monitoringMethodForDiscoveredHost(host) ===
|
|
133
|
+
NetworkDeviceMonitoringMethod.Monitor
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/*
|
|
138
|
+
* True when this rule's populated conditions all match the host.
|
|
139
|
+
*
|
|
140
|
+
* The ping-only gate applies to IMPORT rules only: a host that never
|
|
141
|
+
* answered SNMP imports only through a rule that explicitly opted into
|
|
142
|
+
* ping-only hosts — an SNMP credential typo makes a whole subnet report as
|
|
143
|
+
* ping-only, and a rule silently importing hundreds of half-identified
|
|
144
|
+
* hosts on a typo is the failure mode this default exists to prevent. An
|
|
145
|
+
* EXCLUSION rule has no such gate: a veto is a veto whatever the host
|
|
146
|
+
* answered with.
|
|
147
|
+
*/
|
|
148
|
+
public static ruleMatchesHost(
|
|
149
|
+
rule: AutoImportRuleCandidate,
|
|
150
|
+
host: DiscoveredNetworkDevice,
|
|
151
|
+
): boolean {
|
|
152
|
+
const ipMatchTarget: string = (rule.ipMatchTarget || "").trim();
|
|
153
|
+
const sysNamePattern: string = (rule.sysNamePattern || "").trim();
|
|
154
|
+
const sysDescrPattern: string = (rule.sysDescrPattern || "").trim();
|
|
155
|
+
const sysObjectIdPattern: string = (rule.sysObjectIdPattern || "").trim();
|
|
156
|
+
|
|
157
|
+
// A rule with no conditions matches nothing — the site-rule precedent.
|
|
158
|
+
if (
|
|
159
|
+
!ipMatchTarget &&
|
|
160
|
+
!sysNamePattern &&
|
|
161
|
+
!sysDescrPattern &&
|
|
162
|
+
!sysObjectIdPattern
|
|
163
|
+
) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (
|
|
168
|
+
!rule.isExclusion &&
|
|
169
|
+
!rule.includePingOnlyHosts &&
|
|
170
|
+
AutoImportRuleMatcher.isPingOnlyHost(host)
|
|
171
|
+
) {
|
|
172
|
+
return false;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (ipMatchTarget) {
|
|
176
|
+
if (
|
|
177
|
+
!host.ipAddress ||
|
|
178
|
+
!ScanTargetUtil.contains(ipMatchTarget, host.ipAddress)
|
|
179
|
+
) {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/*
|
|
185
|
+
* Pattern semantics are RulePatternMatchUtil's, identical to label/owner
|
|
186
|
+
* rules: case-insensitive unanchored regex first, '*' glob fallback —
|
|
187
|
+
* so "contains WB1678" is written the same way here as everywhere else.
|
|
188
|
+
* A host missing the field never matches a configured pattern.
|
|
189
|
+
*/
|
|
190
|
+
if (
|
|
191
|
+
sysNamePattern &&
|
|
192
|
+
!RulePatternMatchUtil.matches(clampSubject(host.sysName), sysNamePattern)
|
|
193
|
+
) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (
|
|
198
|
+
sysDescrPattern &&
|
|
199
|
+
!RulePatternMatchUtil.matches(
|
|
200
|
+
clampSubject(host.sysDescr),
|
|
201
|
+
sysDescrPattern,
|
|
202
|
+
)
|
|
203
|
+
) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (
|
|
208
|
+
sysObjectIdPattern &&
|
|
209
|
+
!matchesOidPattern(clampSubject(host.sysObjectId), sysObjectIdPattern)
|
|
210
|
+
) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* What the whole rule set says about one host: import, and through which
|
|
219
|
+
* rules — or vetoed, and by which exclusion.
|
|
220
|
+
*
|
|
221
|
+
* Exclusions are evaluated first so the evaluation can short-circuit into
|
|
222
|
+
* an explainable answer: "excluded by rule R" beats "matched rules A and B
|
|
223
|
+
* and was then excluded" for the operator reading a dry run.
|
|
224
|
+
*/
|
|
225
|
+
public static evaluateHost(
|
|
226
|
+
rules: Array<AutoImportRuleCandidate>,
|
|
227
|
+
host: DiscoveredNetworkDevice,
|
|
228
|
+
): AutoImportHostEvaluation {
|
|
229
|
+
for (const rule of rules) {
|
|
230
|
+
if (
|
|
231
|
+
rule.isExclusion &&
|
|
232
|
+
AutoImportRuleMatcher.ruleMatchesHost(rule, host)
|
|
233
|
+
) {
|
|
234
|
+
return {
|
|
235
|
+
shouldImport: false,
|
|
236
|
+
matchedRules: [],
|
|
237
|
+
excludedByRule: rule,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const matchedRules: Array<AutoImportRuleCandidate> = rules.filter(
|
|
243
|
+
(rule: AutoImportRuleCandidate): boolean => {
|
|
244
|
+
return (
|
|
245
|
+
!rule.isExclusion && AutoImportRuleMatcher.ruleMatchesHost(rule, host)
|
|
246
|
+
);
|
|
247
|
+
},
|
|
248
|
+
);
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
shouldImport: matchedRules.length > 0,
|
|
252
|
+
matchedRules: matchedRules,
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default AutoImportRuleMatcher;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import NetworkDevice from "../../Models/DatabaseModels/NetworkDevice";
|
|
2
|
+
import { DiscoveredNetworkDevice } from "../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
|
|
3
|
+
import NetworkDeviceMonitoringMethod from "../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
|
|
4
|
+
import ObjectID from "../../Types/ObjectID";
|
|
5
|
+
import { monitoringMethodForDiscoveredHost } from "./DiscoveryImportEligibility";
|
|
6
|
+
|
|
7
|
+
/*
|
|
8
|
+
* One discovered host -> one NetworkDevice, the same way everywhere.
|
|
9
|
+
*
|
|
10
|
+
* This mapping used to live inline in the Dashboard's Discovery page import
|
|
11
|
+
* loop. The server-side auto-import rule engine needs the identical recipe —
|
|
12
|
+
* a host imported by a rule and the same host imported by hand must be the
|
|
13
|
+
* same device — so the recipe lives here and both callers use it. Anything
|
|
14
|
+
* added to one path by editing this file is automatically added to the other.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/*
|
|
18
|
+
* Longest device name the builder will emit.
|
|
19
|
+
*
|
|
20
|
+
* The name column is varchar(100) and the create path THROWS on overflow (no
|
|
21
|
+
* truncation), but the real ceiling is the slug: it is slugify(name) plus a
|
|
22
|
+
* dash and ten random digits into its own varchar(100), so a name over ~88
|
|
23
|
+
* characters fails the create with a slug-length error even though the name
|
|
24
|
+
* itself fits. SNMP sysName is a DisplayString of up to 255 octets, so
|
|
25
|
+
* over-long names are routine on real gear, and the collision fallback below
|
|
26
|
+
* appends up to 18 more characters (" (255.255.255.255)"). 80 leaves headroom
|
|
27
|
+
* for both.
|
|
28
|
+
*/
|
|
29
|
+
export const MAX_DEVICE_NAME_LENGTH: number = 80;
|
|
30
|
+
|
|
31
|
+
// NetworkDevice.description is stored to 500 characters; sysDescr can be 255+.
|
|
32
|
+
export const MAX_DEVICE_DESCRIPTION_LENGTH: number = 500;
|
|
33
|
+
|
|
34
|
+
function truncate(value: string, maxLength: number): string {
|
|
35
|
+
return value.length > maxLength ? value.substring(0, maxLength) : value;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The name a discovered host imports under: its sysName, or its address. */
|
|
39
|
+
export function buildDeviceName(host: DiscoveredNetworkDevice): string {
|
|
40
|
+
return truncate(
|
|
41
|
+
(host.sysName || "").trim() || host.ipAddress,
|
|
42
|
+
MAX_DEVICE_NAME_LENGTH,
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* The fallback name when `buildDeviceName`'s answer is already taken.
|
|
48
|
+
*
|
|
49
|
+
* Device names are unique per project, and two devices legitimately sharing a
|
|
50
|
+
* sysName (a factory default, a cloned config) is common on real estates. The
|
|
51
|
+
* address is what tells them apart, so it goes into the name — with the
|
|
52
|
+
* sysName cut down first so the composed string still fits under the same
|
|
53
|
+
* ceiling.
|
|
54
|
+
*/
|
|
55
|
+
export function buildFallbackDeviceName(host: DiscoveredNetworkDevice): string {
|
|
56
|
+
const suffix: string = ` (${host.ipAddress})`;
|
|
57
|
+
const baseName: string = buildDeviceName(host);
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
truncate(baseName, Math.max(1, MAX_DEVICE_NAME_LENGTH - suffix.length)) +
|
|
61
|
+
suffix
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/*
|
|
66
|
+
* The scan columns the builder copies onto an SNMP device. The
|
|
67
|
+
* NetworkDeviceDiscoveryScan model satisfies this structurally, so both the
|
|
68
|
+
* dashboard (holding a scan model) and the rule engine (holding a scan row it
|
|
69
|
+
* selected itself) can pass their scan straight in.
|
|
70
|
+
*/
|
|
71
|
+
export interface DiscoveredDeviceScanSource {
|
|
72
|
+
probeId?: ObjectID | undefined;
|
|
73
|
+
snmpVersion?: string | undefined;
|
|
74
|
+
snmpCommunityString?: string | undefined;
|
|
75
|
+
snmpPort?: number | undefined;
|
|
76
|
+
snmpV3SecurityLevel?: string | undefined;
|
|
77
|
+
snmpV3Username?: string | undefined;
|
|
78
|
+
snmpV3AuthProtocol?: string | undefined;
|
|
79
|
+
snmpV3AuthKey?: string | undefined;
|
|
80
|
+
snmpV3PrivProtocol?: string | undefined;
|
|
81
|
+
snmpV3PrivKey?: string | undefined;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* The NetworkDevice a discovered host imports as.
|
|
86
|
+
*
|
|
87
|
+
* An SNMP-reachable host becomes a polled device carrying the scan's probe
|
|
88
|
+
* and credentials. A ping-only host becomes a monitor-backed device with no
|
|
89
|
+
* probe, no credentials and polling off: it is recorded so it can belong to a
|
|
90
|
+
* site and appear on the topology map, and binding a monitor to it stays a
|
|
91
|
+
* separate, deliberate step.
|
|
92
|
+
*
|
|
93
|
+
* The caller supplies the name (normally `buildDeviceName(host)`) so the
|
|
94
|
+
* name-collision retry can rebuild the same device under
|
|
95
|
+
* `buildFallbackDeviceName(host)` without re-deciding anything else.
|
|
96
|
+
*/
|
|
97
|
+
export function buildNetworkDeviceFromDiscoveredHost(data: {
|
|
98
|
+
projectId: ObjectID;
|
|
99
|
+
host: DiscoveredNetworkDevice;
|
|
100
|
+
scan: DiscoveredDeviceScanSource;
|
|
101
|
+
name?: string | undefined;
|
|
102
|
+
/*
|
|
103
|
+
* Turn on the device's vendor-health-template auto-apply. The rule
|
|
104
|
+
* engine sets this — a zero-touch import should end with health metrics,
|
|
105
|
+
* not an empty OID list waiting for a click. The manual Review dialog
|
|
106
|
+
* leaves it unset: an operator importing by hand gets the vendor banner
|
|
107
|
+
* and decides, which is the existing contract for hand-made devices.
|
|
108
|
+
*/
|
|
109
|
+
autoApplyVendorHealthTemplate?: boolean | undefined;
|
|
110
|
+
}): NetworkDevice {
|
|
111
|
+
const host: DiscoveredNetworkDevice = data.host;
|
|
112
|
+
|
|
113
|
+
const device: NetworkDevice = new NetworkDevice();
|
|
114
|
+
device.projectId = data.projectId;
|
|
115
|
+
device.name = data.name || buildDeviceName(host);
|
|
116
|
+
// The address is the device's hostname AND the registered-host dedup key.
|
|
117
|
+
device.hostname = host.ipAddress;
|
|
118
|
+
|
|
119
|
+
if (host.sysDescr) {
|
|
120
|
+
device.description = truncate(host.sysDescr, MAX_DEVICE_DESCRIPTION_LENGTH);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const monitoringMethod: NetworkDeviceMonitoringMethod =
|
|
124
|
+
monitoringMethodForDiscoveredHost(host);
|
|
125
|
+
device.monitoringMethod = monitoringMethod;
|
|
126
|
+
|
|
127
|
+
if (monitoringMethod === NetworkDeviceMonitoringMethod.Monitor) {
|
|
128
|
+
/*
|
|
129
|
+
* A ping-only host is never SNMP-polled, so the vendor-template
|
|
130
|
+
* auto-apply (which keys off a polled sysObjectID) stays off too.
|
|
131
|
+
*/
|
|
132
|
+
device.isPollingEnabled = false;
|
|
133
|
+
return device;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (data.autoApplyVendorHealthTemplate) {
|
|
137
|
+
device.autoApplyVendorHealthTemplate = true;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (data.scan.probeId) {
|
|
141
|
+
// Re-wrapped: the scan may hold a serialized id rather than an ObjectID.
|
|
142
|
+
device.probeId = new ObjectID(data.scan.probeId.toString());
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (data.scan.snmpVersion) {
|
|
146
|
+
device.snmpVersion = data.scan.snmpVersion;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (data.scan.snmpCommunityString) {
|
|
150
|
+
device.snmpCommunityString = data.scan.snmpCommunityString;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (data.scan.snmpPort) {
|
|
154
|
+
device.snmpPort = data.scan.snmpPort;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Carry the v3 credentials so a v3 scan imports as a v3 device.
|
|
158
|
+
if (data.scan.snmpV3SecurityLevel) {
|
|
159
|
+
device.snmpV3SecurityLevel = data.scan.snmpV3SecurityLevel;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (data.scan.snmpV3Username) {
|
|
163
|
+
device.snmpV3Username = data.scan.snmpV3Username;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (data.scan.snmpV3AuthProtocol) {
|
|
167
|
+
device.snmpV3AuthProtocol = data.scan.snmpV3AuthProtocol;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (data.scan.snmpV3AuthKey) {
|
|
171
|
+
device.snmpV3AuthKey = data.scan.snmpV3AuthKey;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (data.scan.snmpV3PrivProtocol) {
|
|
175
|
+
device.snmpV3PrivProtocol = data.scan.snmpV3PrivProtocol;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (data.scan.snmpV3PrivKey) {
|
|
179
|
+
device.snmpV3PrivKey = data.scan.snmpV3PrivKey;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return device;
|
|
183
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { DiscoveredNetworkDevice } from "../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Normalisation for a scan's stored results, shared by every reader of the
|
|
5
|
+
* `discoveredDevices` jsonb column: the dashboard's Review dialog and the
|
|
6
|
+
* server-side auto-import rule engine. It moved here from the Dashboard's
|
|
7
|
+
* DiscoveredHostFilter when the engine appeared, for the reason spelled out
|
|
8
|
+
* on normalizeDiscoveredHosts below — a server that matched raw jsonb would
|
|
9
|
+
* re-hit every landmine the dialog already fixed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* What a nullish or non-object entry in the jsonb reads as: nothing. Written
|
|
14
|
+
* as a type guard so the normalisation below narrows properly.
|
|
15
|
+
*/
|
|
16
|
+
function isDiscoveredHostObject(
|
|
17
|
+
host: unknown,
|
|
18
|
+
): host is DiscoveredNetworkDevice {
|
|
19
|
+
return Boolean(host) && typeof host === "object";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The scan's rows, cleaned up so every rule downstream sees the same thing.
|
|
24
|
+
*
|
|
25
|
+
* `discoveredDevices` is jsonb written verbatim from the probe's payload, and
|
|
26
|
+
* the only guard on it checks that the VALUE is an array — never that its
|
|
27
|
+
* elements are shaped like hosts. Everything downstream keys off `ipAddress`,
|
|
28
|
+
* so three payload shapes that a probe should never send, but that nothing
|
|
29
|
+
* stops it sending, each broke something:
|
|
30
|
+
*
|
|
31
|
+
* - A `null` element. Every predicate dereferences the host, so one null row
|
|
32
|
+
* threw a TypeError the moment the operator clicked a filter button —
|
|
33
|
+
* inside the modal body, during render.
|
|
34
|
+
* - A non-string address. `10` is written into the selection record as the
|
|
35
|
+
* key "10" (object keys are strings) but matched out of the imported-set
|
|
36
|
+
* with `Set.has(10)`, which does not coerce — so the host imported and
|
|
37
|
+
* then could never be retired, and pressing Import again duplicated it.
|
|
38
|
+
* - The same address on two rows with different frozen `isAlreadyRegistered`
|
|
39
|
+
* values. One checkbox governs both rows, but only one of them refused to
|
|
40
|
+
* import, so whether a device already in the inventory got created a
|
|
41
|
+
* second time depended on which order the probe happened to list them in.
|
|
42
|
+
*
|
|
43
|
+
* All three are fixed here rather than at each call site, so the row the
|
|
44
|
+
* operator sees, the badge above it, the list Import walks, and the hosts an
|
|
45
|
+
* auto-import rule evaluates can never be working from different readings of
|
|
46
|
+
* the same payload.
|
|
47
|
+
*/
|
|
48
|
+
export function normalizeDiscoveredHosts(
|
|
49
|
+
hosts: Array<DiscoveredNetworkDevice>,
|
|
50
|
+
): Array<DiscoveredNetworkDevice> {
|
|
51
|
+
const cleaned: Array<DiscoveredNetworkDevice> = [];
|
|
52
|
+
|
|
53
|
+
for (const host of hosts) {
|
|
54
|
+
if (!isDiscoveredHostObject(host)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/*
|
|
59
|
+
* Trimmed as well as stringified: " " is not an address, but it is
|
|
60
|
+
* truthy, so it used to pass selectability and import as a Network Device
|
|
61
|
+
* whose hostname was a single space.
|
|
62
|
+
*/
|
|
63
|
+
cleaned.push({
|
|
64
|
+
...host,
|
|
65
|
+
ipAddress:
|
|
66
|
+
host.ipAddress === undefined || host.ipAddress === null
|
|
67
|
+
? ""
|
|
68
|
+
: String(host.ipAddress).trim(),
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// An address the scan reports as registered is registered on every row.
|
|
73
|
+
const registeredIpAddresses: Set<string> = new Set<string>();
|
|
74
|
+
|
|
75
|
+
for (const host of cleaned) {
|
|
76
|
+
if (host.ipAddress && host.isAlreadyRegistered) {
|
|
77
|
+
registeredIpAddresses.add(host.ipAddress);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (registeredIpAddresses.size === 0) {
|
|
82
|
+
return cleaned;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return cleaned.map((host: DiscoveredNetworkDevice) => {
|
|
86
|
+
if (
|
|
87
|
+
host.isAlreadyRegistered ||
|
|
88
|
+
!registeredIpAddresses.has(host.ipAddress)
|
|
89
|
+
) {
|
|
90
|
+
return host;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { ...host, isAlreadyRegistered: true };
|
|
94
|
+
});
|
|
95
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { DiscoveredNetworkDevice } from "../../Models/DatabaseModels/NetworkDeviceDiscoveryScan";
|
|
2
|
+
import NetworkDeviceMonitoringMethod from "../../Types/NetworkDevice/NetworkDeviceMonitoringMethod";
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
* Pure, react-free import rule for discovery scan results.
|
|
6
|
+
*
|
|
7
|
+
* Every host the sweep found alive is importable; what differs is HOW. A host
|
|
8
|
+
* that answered SNMP becomes a polled device with the scan's credentials. A
|
|
9
|
+
* host that answered only ping becomes a monitor-backed device: no probe, no
|
|
10
|
+
* credentials, health supplied later by a monitor the operator binds to it.
|
|
11
|
+
*
|
|
12
|
+
* Ping-only hosts used to be refused outright, on the reasoning that they
|
|
13
|
+
* would turn up as ARP/FDB endpoints once their switch was monitored. That is
|
|
14
|
+
* true only where a monitored switch sees them, and an endpoint is not a
|
|
15
|
+
* device — it cannot belong to a site, carry labels, own an owner rule, or be
|
|
16
|
+
* one end of a link. Issue #3023 is what that gap looks like from the outside:
|
|
17
|
+
* "devices I monitor manually don't appear in the topology at all".
|
|
18
|
+
*
|
|
19
|
+
* Lives in Common (it started life next to the Discovery page component)
|
|
20
|
+
* because the dashboard's Review dialog and the server-side auto-import rule
|
|
21
|
+
* engine both decide "how does this host import" through it, and the two must
|
|
22
|
+
* never disagree: the group a host is shown under in the dialog and the
|
|
23
|
+
* monitoring method a rule imports it with are the same decision.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Which monitoring method a discovered host should be imported under.
|
|
28
|
+
*
|
|
29
|
+
* Only an EXPLICIT snmpReachable === false means ping-only: scans stored
|
|
30
|
+
* before the field existed carry undefined, and every host on those scans
|
|
31
|
+
* answered SNMP (ping-only sweeps did not exist yet), so legacy rows keep
|
|
32
|
+
* importing as SNMP devices.
|
|
33
|
+
*/
|
|
34
|
+
export function monitoringMethodForDiscoveredHost(
|
|
35
|
+
host: DiscoveredNetworkDevice | null | undefined,
|
|
36
|
+
): NetworkDeviceMonitoringMethod {
|
|
37
|
+
/*
|
|
38
|
+
* Nullish hosts read as SNMP rather than throwing. `discoveredDevices` is
|
|
39
|
+
* jsonb written verbatim from the probe's payload and the only guard on it
|
|
40
|
+
* (DiscoveryScanOutcome.getDiscoveredHosts) checks that the VALUE is an
|
|
41
|
+
* array — never that its elements are objects. A single null element used
|
|
42
|
+
* to take the whole page down from inside a table cell or a modal body,
|
|
43
|
+
* where a thrown TypeError has nowhere useful to go. Every other kind of
|
|
44
|
+
* junk element (a number, a string, {}) already read as SNMP; null is now
|
|
45
|
+
* simply consistent with them.
|
|
46
|
+
*/
|
|
47
|
+
return host?.snmpReachable === false
|
|
48
|
+
? NetworkDeviceMonitoringMethod.Monitor
|
|
49
|
+
: NetworkDeviceMonitoringMethod.Snmp;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True when the discovered host can be imported as a Network Device. */
|
|
53
|
+
export function isImportableDiscoveredHost(
|
|
54
|
+
_host: DiscoveredNetworkDevice,
|
|
55
|
+
): boolean {
|
|
56
|
+
/*
|
|
57
|
+
* Every alive host now is. The predicate survives because the Discovery
|
|
58
|
+
* page filters and counts through it, and because it is the seam where a
|
|
59
|
+
* future "don't import X" rule for MANUAL review would belong. (The
|
|
60
|
+
* automatic path already has one: an auto-import exclusion rule vetoes
|
|
61
|
+
* hosts for the rule engine — deliberately without reaching into this
|
|
62
|
+
* dialog, where a human is looking at the list and unticking a host is
|
|
63
|
+
* the veto.)
|
|
64
|
+
*/
|
|
65
|
+
return true;
|
|
66
|
+
}
|