@oneuptime/common 12.0.22 → 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 +2 -0
- package/Models/DatabaseModels/NetworkDevice.ts +44 -0
- package/Models/DatabaseModels/NetworkDeviceAutoImportRule.ts +564 -0
- package/Models/DatabaseModels/NetworkDeviceDiscoveryScan.ts +38 -0
- package/Server/Infrastructure/Postgres/SchemaMigrations/1789100000000-AddNetworkDeviceAutoImportRule.ts +60 -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 +6 -0
- package/Server/Services/Index.ts +2 -0
- package/Server/Services/NetworkDeviceAutoImportRuleEngineService.ts +917 -0
- package/Server/Services/NetworkDeviceAutoImportRuleService.ts +181 -0
- package/Server/Utils/Monitor/NetworkInventoryUtil.ts +39 -1
- package/Tests/Server/Services/AutoImportScanCredentialSelect.test.ts +205 -0
- package/Tests/Server/Services/NetworkDeviceAutoImportRuleEngineService.test.ts +933 -0
- package/Tests/Server/Utils/Monitor/NetworkInventoryUtil.test.ts +97 -1
- 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/NetworkAutomation/RuleRunResult.ts +84 -0
- package/Types/Permission.ts +47 -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 +2 -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/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/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 +6 -0
- package/build/dist/Server/Infrastructure/Postgres/SchemaMigrations/Index.js.map +1 -1
- package/build/dist/Server/Services/Index.js +2 -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/Utils/Monitor/NetworkInventoryUtil.js +26 -0
- package/build/dist/Server/Utils/Monitor/NetworkInventoryUtil.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,181 @@
|
|
|
1
|
+
import DatabaseService from "./DatabaseService";
|
|
2
|
+
import Model from "../../Models/DatabaseModels/NetworkDeviceAutoImportRule";
|
|
3
|
+
import { OnCreate, OnUpdate } from "../Types/Database/Hooks";
|
|
4
|
+
import CreateBy from "../Types/Database/CreateBy";
|
|
5
|
+
import UpdateBy from "../Types/Database/UpdateBy";
|
|
6
|
+
import CaptureSpan from "../Utils/Telemetry/CaptureSpan";
|
|
7
|
+
import LIMIT_MAX from "../../Types/Database/LimitMax";
|
|
8
|
+
import BadDataException from "../../Types/Exception/BadDataException";
|
|
9
|
+
import RulePatternMatchUtil from "../../Utils/Rules/RulePatternMatchUtil";
|
|
10
|
+
import ScanTargetUtil from "../../Utils/NetworkDiscovery/ScanTargetUtil";
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Write-time validation for auto-import rules, following the
|
|
14
|
+
* NetworkSiteAssignmentRuleService contract: a condition that can only ever
|
|
15
|
+
* match nothing is rejected where the user can see it, not logged about by
|
|
16
|
+
* the engine long after they left the form.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/*
|
|
20
|
+
* What a sysObjectID condition may contain: an optional leading dot, then
|
|
21
|
+
* digits, dots and '*' wildcards. Hoisted so the literal is not the object
|
|
22
|
+
* of a member expression, which `wrap-regex` and Prettier cannot agree on —
|
|
23
|
+
* same reason as CidrMatchUtil.
|
|
24
|
+
*/
|
|
25
|
+
const OID_PATTERN_SHAPE: RegExp = /^\.?[\d.*]+$/;
|
|
26
|
+
export class Service extends DatabaseService<Model> {
|
|
27
|
+
public constructor() {
|
|
28
|
+
super(Model);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@CaptureSpan()
|
|
32
|
+
protected override async onBeforeCreate(
|
|
33
|
+
createBy: CreateBy<Model>,
|
|
34
|
+
): Promise<OnCreate<Model>> {
|
|
35
|
+
this.validateCriteria({
|
|
36
|
+
ipMatchTarget: createBy.data.ipMatchTarget,
|
|
37
|
+
sysNamePattern: createBy.data.sysNamePattern,
|
|
38
|
+
sysDescrPattern: createBy.data.sysDescrPattern,
|
|
39
|
+
sysObjectIdPattern: createBy.data.sysObjectIdPattern,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return { createBy, carryForward: null };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@CaptureSpan()
|
|
46
|
+
protected override async onBeforeUpdate(
|
|
47
|
+
updateBy: UpdateBy<Model>,
|
|
48
|
+
): Promise<OnUpdate<Model>> {
|
|
49
|
+
const dataKeys: Array<string> = Object.keys(updateBy.data || {});
|
|
50
|
+
|
|
51
|
+
const isCriteriaChange: boolean =
|
|
52
|
+
dataKeys.includes("ipMatchTarget") ||
|
|
53
|
+
dataKeys.includes("sysNamePattern") ||
|
|
54
|
+
dataKeys.includes("sysDescrPattern") ||
|
|
55
|
+
dataKeys.includes("sysObjectIdPattern");
|
|
56
|
+
|
|
57
|
+
if (!isCriteriaChange) {
|
|
58
|
+
return { updateBy, carryForward: null };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* The update may clear one criterion while another only exists on the
|
|
63
|
+
* stored row, so validate the RESULTING state of every matched row.
|
|
64
|
+
*/
|
|
65
|
+
const existingRules: Array<Model> = await this.findBy({
|
|
66
|
+
query: updateBy.query,
|
|
67
|
+
select: {
|
|
68
|
+
_id: true,
|
|
69
|
+
ipMatchTarget: true,
|
|
70
|
+
sysNamePattern: true,
|
|
71
|
+
sysDescrPattern: true,
|
|
72
|
+
sysObjectIdPattern: true,
|
|
73
|
+
},
|
|
74
|
+
limit: LIMIT_MAX,
|
|
75
|
+
skip: 0,
|
|
76
|
+
props: {
|
|
77
|
+
isRoot: true,
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const data: Record<string, unknown> = updateBy.data as unknown as Record<
|
|
82
|
+
string,
|
|
83
|
+
unknown
|
|
84
|
+
>;
|
|
85
|
+
|
|
86
|
+
for (const existingRule of existingRules) {
|
|
87
|
+
this.validateCriteria({
|
|
88
|
+
ipMatchTarget: dataKeys.includes("ipMatchTarget")
|
|
89
|
+
? (data["ipMatchTarget"] as string | null)
|
|
90
|
+
: existingRule.ipMatchTarget,
|
|
91
|
+
sysNamePattern: dataKeys.includes("sysNamePattern")
|
|
92
|
+
? (data["sysNamePattern"] as string | null)
|
|
93
|
+
: existingRule.sysNamePattern,
|
|
94
|
+
sysDescrPattern: dataKeys.includes("sysDescrPattern")
|
|
95
|
+
? (data["sysDescrPattern"] as string | null)
|
|
96
|
+
: existingRule.sysDescrPattern,
|
|
97
|
+
sysObjectIdPattern: dataKeys.includes("sysObjectIdPattern")
|
|
98
|
+
? (data["sysObjectIdPattern"] as string | null)
|
|
99
|
+
: existingRule.sysObjectIdPattern,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { updateBy, carryForward: null };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private validateCriteria(data: {
|
|
107
|
+
ipMatchTarget?: string | null | undefined;
|
|
108
|
+
sysNamePattern?: string | null | undefined;
|
|
109
|
+
sysDescrPattern?: string | null | undefined;
|
|
110
|
+
sysObjectIdPattern?: string | null | undefined;
|
|
111
|
+
}): void {
|
|
112
|
+
const ipMatchTarget: string = (data.ipMatchTarget || "").trim();
|
|
113
|
+
const sysNamePattern: string = (data.sysNamePattern || "").trim();
|
|
114
|
+
const sysDescrPattern: string = (data.sysDescrPattern || "").trim();
|
|
115
|
+
const sysObjectIdPattern: string = (data.sysObjectIdPattern || "").trim();
|
|
116
|
+
|
|
117
|
+
/*
|
|
118
|
+
* A rule with no conditions matches nothing (see AutoImportRuleMatcher) —
|
|
119
|
+
* and would read as "match everything" to whoever finds it later.
|
|
120
|
+
*/
|
|
121
|
+
if (
|
|
122
|
+
!ipMatchTarget &&
|
|
123
|
+
!sysNamePattern &&
|
|
124
|
+
!sysDescrPattern &&
|
|
125
|
+
!sysObjectIdPattern
|
|
126
|
+
) {
|
|
127
|
+
throw new BadDataException(
|
|
128
|
+
"At least one of Host IP Is In, System Name Pattern, System Description Pattern, or System Object ID Pattern is required.",
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/*
|
|
133
|
+
* Well-formedness only, deliberately NOT the scan-size ceiling: matching
|
|
134
|
+
* an address against 10.0.0.0/8 is a containment check, not a sweep, so
|
|
135
|
+
* a condition covering a huge block is legitimate here even though a
|
|
136
|
+
* scan target that size is not.
|
|
137
|
+
*/
|
|
138
|
+
if (ipMatchTarget && !ScanTargetUtil.isValid(ipMatchTarget)) {
|
|
139
|
+
throw new BadDataException(
|
|
140
|
+
`"${ipMatchTarget}" is not a valid Host IP condition. ${ScanTargetUtil.getSyntaxHint()}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
this.validatePattern("System Name Pattern", sysNamePattern);
|
|
145
|
+
this.validatePattern("System Description Pattern", sysDescrPattern);
|
|
146
|
+
this.validateOidPattern(sysObjectIdPattern);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private validatePattern(title: string, pattern: string): void {
|
|
150
|
+
if (!pattern || RulePatternMatchUtil.isSupportedPattern(pattern)) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
throw new BadDataException(
|
|
155
|
+
`${title} is neither a valid regular expression nor a '*' wildcard pattern, so it would never match: ${pattern}`,
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
* The sysObjectID condition is NOT free-text: an OID is a dotted numeric
|
|
161
|
+
* arc, matched as a literal-dot '*' glob or an arc prefix (see
|
|
162
|
+
* AutoImportRuleMatcher.matchesOidPattern — regex-first matching would make
|
|
163
|
+
* "1.3.6.1.4.1.9.*" match enterprise 94 too). So the only characters that
|
|
164
|
+
* can ever match anything are digits, dots and '*'; anything else is a
|
|
165
|
+
* pattern that silently never fires, which is exactly what this service
|
|
166
|
+
* exists to reject at the write.
|
|
167
|
+
*/
|
|
168
|
+
private validateOidPattern(pattern: string): void {
|
|
169
|
+
if (!pattern) {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (!OID_PATTERN_SHAPE.test(pattern)) {
|
|
174
|
+
throw new BadDataException(
|
|
175
|
+
`System Object ID Pattern must be an OID prefix (1.3.6.1.4.1.9) or a '*' wildcard OID pattern (1.3.6.1.4.1.9.*) — digits, dots and '*' only: ${pattern}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
export default new Service();
|
|
@@ -15,7 +15,9 @@ import EndpointAttachmentUtil, {
|
|
|
15
15
|
} from "../../../Utils/Monitor/EndpointAttachmentUtil";
|
|
16
16
|
import SnmpSystemInfo from "../../../Types/Monitor/SnmpMonitor/SnmpSystemInfo";
|
|
17
17
|
import SnmpEntityInfo from "../../../Types/Monitor/SnmpMonitor/SnmpEntityInfo";
|
|
18
|
-
import SnmpVendorTemplateUtil
|
|
18
|
+
import SnmpVendorTemplateUtil, {
|
|
19
|
+
SnmpVendorTemplate,
|
|
20
|
+
} from "../../../Types/Monitor/SnmpMonitor/SnmpVendorTemplate";
|
|
19
21
|
import ObjectID from "../../../Types/ObjectID";
|
|
20
22
|
import OneUptimeDate from "../../../Types/Date";
|
|
21
23
|
import logger from "../Logger";
|
|
@@ -55,6 +57,9 @@ export default class NetworkInventoryUtil {
|
|
|
55
57
|
select: {
|
|
56
58
|
_id: true,
|
|
57
59
|
siteId: true,
|
|
60
|
+
// For the vendor-template auto-apply below.
|
|
61
|
+
autoApplyVendorHealthTemplate: true,
|
|
62
|
+
snmpOids: true,
|
|
58
63
|
},
|
|
59
64
|
props: {
|
|
60
65
|
isRoot: true,
|
|
@@ -150,6 +155,39 @@ export default class NetworkInventoryUtil {
|
|
|
150
155
|
if (vendor) {
|
|
151
156
|
deviceUpdate["vendor"] = vendor.substring(0, 100);
|
|
152
157
|
}
|
|
158
|
+
|
|
159
|
+
/*
|
|
160
|
+
* Vendor health template auto-apply — the automatic counterpart of the
|
|
161
|
+
* dashboard's vendor-template banner, for devices that opted in
|
|
162
|
+
* (auto-imported devices do; hand-made ones keep the manual flow).
|
|
163
|
+
*
|
|
164
|
+
* Deliberately narrow: only when the device has NO health OIDs at all.
|
|
165
|
+
* An existing list — template-seeded and pruned, or hand-built — is
|
|
166
|
+
* the operator's, and a poll must never edit it. That also makes this
|
|
167
|
+
* one-shot in practice: the first poll that fingerprints the vendor
|
|
168
|
+
* seeds the list, and every later poll sees a non-empty list and
|
|
169
|
+
* leaves it alone (including after the operator empties it on purpose
|
|
170
|
+
* AND turns the toggle off; with the toggle still on, an emptied list
|
|
171
|
+
* re-seeds next poll, which is what "auto-apply" says on the tin).
|
|
172
|
+
*/
|
|
173
|
+
if (
|
|
174
|
+
ownedDevice.autoApplyVendorHealthTemplate &&
|
|
175
|
+
(ownedDevice.snmpOids || []).length === 0 &&
|
|
176
|
+
systemInfo?.sysObjectId
|
|
177
|
+
) {
|
|
178
|
+
const vendorTemplate: SnmpVendorTemplate | undefined =
|
|
179
|
+
SnmpVendorTemplateUtil.matchBySysObjectId(systemInfo.sysObjectId);
|
|
180
|
+
|
|
181
|
+
if (vendorTemplate) {
|
|
182
|
+
deviceUpdate["snmpOids"] = SnmpVendorTemplateUtil.mergeOids(
|
|
183
|
+
[],
|
|
184
|
+
vendorTemplate.id,
|
|
185
|
+
);
|
|
186
|
+
logger.debug(
|
|
187
|
+
`Auto-applied the "${vendorTemplate.label}" vendor health template to network device ${deviceId.toString()} (sysObjectID ${systemInfo.sysObjectId}).`,
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
153
191
|
if (entityInfo?.model) {
|
|
154
192
|
deviceUpdate["deviceModel"] = entityInfo.model.substring(0, 100);
|
|
155
193
|
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* The auto-import rule engine reads a completed scan's SNMP credentials
|
|
3
|
+
* through AUTO_IMPORT_SCAN_CREDENTIAL_SELECT and copies them onto every
|
|
4
|
+
* device it creates via DiscoveredDeviceBuilder. The probe claim endpoint
|
|
5
|
+
* (App/FeatureSet/Telemetry/API/ProbeIngest/DiscoveryScan.ts) selects the
|
|
6
|
+
* same credential columns for the same reason: an unselected column arrives
|
|
7
|
+
* undefined and is silently NOT copied/used — no error anywhere, just a
|
|
8
|
+
* device (or a sweep) that can never authenticate.
|
|
9
|
+
*
|
|
10
|
+
* That failure mode is exactly the kind that drifts in: someone adds a
|
|
11
|
+
* credential column to the scan model, wires it into ONE of the three
|
|
12
|
+
* consumers, and the other two quietly drop it. So this suite pins the list
|
|
13
|
+
* in BOTH directions against the claim endpoint's select and against the
|
|
14
|
+
* builder — read from source with fs, the
|
|
15
|
+
* DiscoveryScanClaimHookFreeSafety.test.ts technique, because the select
|
|
16
|
+
* literal in a route handler is not importable. If an assertion here fails,
|
|
17
|
+
* its variable name says which side is missing which key; add the key to
|
|
18
|
+
* that side rather than relaxing the pin.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/*
|
|
22
|
+
* The engine's import closure reaches DatabaseService (and through it
|
|
23
|
+
* PasswordHash) via the three collaborating services. None of them is called
|
|
24
|
+
* here — this suite only wants the exported select constant — so the service
|
|
25
|
+
* MODULES are stubbed out before the engine is imported, keeping the suite
|
|
26
|
+
* Postgres-free and immune to the local-only PasswordHash ts-jest compile
|
|
27
|
+
* failure.
|
|
28
|
+
*/
|
|
29
|
+
jest.mock("../../../Server/Services/NetworkDeviceService", () => {
|
|
30
|
+
return {
|
|
31
|
+
__esModule: true,
|
|
32
|
+
default: {},
|
|
33
|
+
};
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
jest.mock("../../../Server/Services/NetworkDeviceDiscoveryScanService", () => {
|
|
37
|
+
return {
|
|
38
|
+
__esModule: true,
|
|
39
|
+
default: {},
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
jest.mock("../../../Server/Services/NetworkDeviceAutoImportRuleService", () => {
|
|
44
|
+
return {
|
|
45
|
+
__esModule: true,
|
|
46
|
+
default: {},
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// The engine's sweep lock would otherwise pull the Redis client in.
|
|
51
|
+
jest.mock("../../../Server/Infrastructure/Semaphore", () => {
|
|
52
|
+
return {
|
|
53
|
+
__esModule: true,
|
|
54
|
+
default: {},
|
|
55
|
+
};
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
jest.mock("../../../Server/Utils/Logger", () => {
|
|
59
|
+
return {
|
|
60
|
+
__esModule: true,
|
|
61
|
+
default: {
|
|
62
|
+
debug: jest.fn(),
|
|
63
|
+
info: jest.fn(),
|
|
64
|
+
warn: jest.fn(),
|
|
65
|
+
error: jest.fn(),
|
|
66
|
+
trace: jest.fn(),
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
import { AUTO_IMPORT_SCAN_CREDENTIAL_SELECT } from "../../../Server/Services/NetworkDeviceAutoImportRuleEngineService";
|
|
72
|
+
import { describe, expect, test } from "@jest/globals";
|
|
73
|
+
import fs from "fs";
|
|
74
|
+
import path from "path";
|
|
75
|
+
|
|
76
|
+
const REPO_ROOT: string = path.join(__dirname, "..", "..", "..", "..");
|
|
77
|
+
|
|
78
|
+
const CLAIM_ENDPOINT_SOURCE: string = fs.readFileSync(
|
|
79
|
+
path.join(
|
|
80
|
+
REPO_ROOT,
|
|
81
|
+
"App",
|
|
82
|
+
"FeatureSet",
|
|
83
|
+
"Telemetry",
|
|
84
|
+
"API",
|
|
85
|
+
"ProbeIngest",
|
|
86
|
+
"DiscoveryScan.ts",
|
|
87
|
+
),
|
|
88
|
+
"utf8",
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
const BUILDER_SOURCE: string = fs.readFileSync(
|
|
92
|
+
path.join(
|
|
93
|
+
REPO_ROOT,
|
|
94
|
+
"Common",
|
|
95
|
+
"Utils",
|
|
96
|
+
"NetworkDiscovery",
|
|
97
|
+
"DiscoveredDeviceBuilder.ts",
|
|
98
|
+
),
|
|
99
|
+
"utf8",
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
/*
|
|
103
|
+
* Every `snmp*: true` in the claim endpoint is a select entry — the file's
|
|
104
|
+
* only other snmp mention is the `snmpReachable !== false` responder count,
|
|
105
|
+
* which this shape can never match.
|
|
106
|
+
*/
|
|
107
|
+
function extractSelectedSnmpColumns(source: string): Array<string> {
|
|
108
|
+
return Array.from(source.matchAll(/\b(snmp\w+)\s*:\s*true\b/g)).map(
|
|
109
|
+
(match: RegExpMatchArray) => {
|
|
110
|
+
return match[1]!;
|
|
111
|
+
},
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
describe("AUTO_IMPORT_SCAN_CREDENTIAL_SELECT", () => {
|
|
116
|
+
test("carries exactly the credential columns an import copies", () => {
|
|
117
|
+
expect(Object.keys(AUTO_IMPORT_SCAN_CREDENTIAL_SELECT).sort()).toEqual(
|
|
118
|
+
[
|
|
119
|
+
"probeId",
|
|
120
|
+
"snmpVersion",
|
|
121
|
+
"snmpCommunityString",
|
|
122
|
+
"snmpPort",
|
|
123
|
+
"snmpV3SecurityLevel",
|
|
124
|
+
"snmpV3Username",
|
|
125
|
+
"snmpV3AuthProtocol",
|
|
126
|
+
"snmpV3AuthKey",
|
|
127
|
+
"snmpV3PrivProtocol",
|
|
128
|
+
"snmpV3PrivKey",
|
|
129
|
+
].sort(),
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
// Guard against the extraction regex rotting into a vacuous pass.
|
|
134
|
+
test("the claim endpoint's select is extractable from its source", () => {
|
|
135
|
+
const selected: Array<string> = extractSelectedSnmpColumns(
|
|
136
|
+
CLAIM_ENDPOINT_SOURCE,
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
expect(selected).toContain("snmpVersion");
|
|
140
|
+
expect(selected).toContain("snmpV3PrivKey");
|
|
141
|
+
// The responder-count read is not a select entry and must not match.
|
|
142
|
+
expect(selected).not.toContain("snmpReachable");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/*
|
|
146
|
+
* Direction one: a credential column the probe sweeps WITH but the engine
|
|
147
|
+
* does not select would import devices that poll with less than the scan
|
|
148
|
+
* used — a v3 scan importing v2c-shaped devices, say. Any name listed in
|
|
149
|
+
* this failure is selected by the claim endpoint and missing from
|
|
150
|
+
* AUTO_IMPORT_SCAN_CREDENTIAL_SELECT.
|
|
151
|
+
*/
|
|
152
|
+
test("every snmp column the probe claim endpoint selects is in the engine's select", () => {
|
|
153
|
+
const snmpColumnsSelectedByClaimEndpointButMissingFromEngineSelect: Array<string> =
|
|
154
|
+
extractSelectedSnmpColumns(CLAIM_ENDPOINT_SOURCE).filter(
|
|
155
|
+
(column: string) => {
|
|
156
|
+
return !(column in AUTO_IMPORT_SCAN_CREDENTIAL_SELECT);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(
|
|
161
|
+
snmpColumnsSelectedByClaimEndpointButMissingFromEngineSelect,
|
|
162
|
+
).toEqual([]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
/*
|
|
166
|
+
* Direction two: a credential column the engine copies onto devices but
|
|
167
|
+
* the probe never receives would mean sweeps and imports disagree about
|
|
168
|
+
* what the credentials ARE. Any name listed in this failure is a key of
|
|
169
|
+
* AUTO_IMPORT_SCAN_CREDENTIAL_SELECT missing from the claim endpoint's
|
|
170
|
+
* select.
|
|
171
|
+
*/
|
|
172
|
+
test("every snmp key of the engine's select is selected by the probe claim endpoint", () => {
|
|
173
|
+
const claimSelectedColumns: Array<string> = extractSelectedSnmpColumns(
|
|
174
|
+
CLAIM_ENDPOINT_SOURCE,
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
const engineSnmpKeysMissingFromClaimEndpointSelect: Array<string> =
|
|
178
|
+
Object.keys(AUTO_IMPORT_SCAN_CREDENTIAL_SELECT)
|
|
179
|
+
.filter((key: string) => {
|
|
180
|
+
return key.startsWith("snmp");
|
|
181
|
+
})
|
|
182
|
+
.filter((key: string) => {
|
|
183
|
+
return !claimSelectedColumns.includes(key);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
expect(engineSnmpKeysMissingFromClaimEndpointSelect).toEqual([]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
/*
|
|
190
|
+
* And the last hop: selecting a credential the builder never copies is the
|
|
191
|
+
* same silent drop one module later. Every key of the select must appear
|
|
192
|
+
* in DiscoveredDeviceBuilder.ts — the copy is a same-named field access,
|
|
193
|
+
* so a plain occurrence check is a faithful pin. Any name listed in this
|
|
194
|
+
* failure is selected by the engine and never mentioned by the builder.
|
|
195
|
+
*/
|
|
196
|
+
test("every key of the engine's select is copied by the device builder", () => {
|
|
197
|
+
const engineSelectKeysNeverMentionedByBuilder: Array<string> = Object.keys(
|
|
198
|
+
AUTO_IMPORT_SCAN_CREDENTIAL_SELECT,
|
|
199
|
+
).filter((key: string) => {
|
|
200
|
+
return !BUILDER_SOURCE.includes(key);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(engineSelectKeysNeverMentionedByBuilder).toEqual([]);
|
|
204
|
+
});
|
|
205
|
+
});
|