@prosopo/user-access-policy 3.14.14 → 3.14.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build$colon$cjs.log +3 -6
- package/.turbo/turbo-build$colon$tsc.log +14 -17
- package/.turbo/turbo-build.log +4 -10
- package/CHANGELOG.md +37 -0
- package/LICENSE +201 -0
- package/dist/cjs/ruleInput/policyInput.cjs +1 -1
- package/dist/ruleInput/policyInput.d.ts +2 -2
- package/dist/ruleInput/policyInput.d.ts.map +1 -1
- package/dist/ruleInput/policyInput.js +2 -2
- package/dist/ruleInput/policyInput.js.map +1 -1
- package/package.json +83 -81
- package/src/.export.ts +0 -65
- package/src/api/.export.ts +0 -25
- package/src/api/accessRulesApiClient.ts +0 -13
- package/src/api/delete/.export.ts +0 -18
- package/src/api/delete/deleteAllRules.ts +0 -47
- package/src/api/delete/deleteRuleGroups.ts +0 -96
- package/src/api/delete/deleteRules.ts +0 -87
- package/src/api/read/.export.ts +0 -25
- package/src/api/read/fetchRules.ts +0 -96
- package/src/api/read/findRuleIds.ts +0 -97
- package/src/api/read/getMissingIds.ts +0 -81
- package/src/api/ruleApiRoutes.ts +0 -146
- package/src/api/rulesApiClient.ts +0 -154
- package/src/api/write/.export.ts +0 -15
- package/src/api/write/insertRules.ts +0 -185
- package/src/api/write/rehashRules.ts +0 -85
- package/src/classifyBrowser.ts +0 -77
- package/src/classifyOs.ts +0 -74
- package/src/headerMatch.ts +0 -180
- package/src/matchedRule.ts +0 -99
- package/src/mongoose/.export.ts +0 -15
- package/src/mongoose/mongooseRuleSchema.ts +0 -75
- package/src/redis/.export.ts +0 -17
- package/src/redis/reader/redisAggregate.ts +0 -157
- package/src/redis/reader/redisRulesQuery.ts +0 -268
- package/src/redis/reader/redisRulesReader.ts +0 -738
- package/src/redis/reader/redisRulesSplitQuery.ts +0 -234
- package/src/redis/redisClient.ts +0 -125
- package/src/redis/redisRuleIndex.ts +0 -121
- package/src/redis/redisRulesStorage.ts +0 -68
- package/src/redis/redisRulesWriter.ts +0 -172
- package/src/rule.ts +0 -122
- package/src/ruleInput/.export.ts +0 -19
- package/src/ruleInput/policyInput.ts +0 -120
- package/src/ruleInput/ruleInput.ts +0 -123
- package/src/ruleInput/userScopeInput.ts +0 -142
- package/src/ruleRecord.ts +0 -77
- package/src/rulesStorage.ts +0 -90
- package/src/tests/blockingRuleShapes.unit.test.ts +0 -244
- package/src/tests/classifyBrowser.unit.test.ts +0 -138
- package/src/tests/classifyOs.unit.test.ts +0 -97
- package/src/tests/headerMatch.unit.test.ts +0 -350
- package/src/tests/insertRulesEndpoint.unit.test.ts +0 -89
- package/src/tests/matchedRule.unit.test.ts +0 -184
- package/src/tests/policyInput.unit.test.ts +0 -218
- package/src/tests/redis/deferredPoolClause.benchmark.ts +0 -249
- package/src/tests/redis/reader/redisAggregate.unit.test.ts +0 -121
- package/src/tests/redis/reader/redisRulesQuery.unit.test.ts +0 -349
- package/src/tests/redis/reader/redisRulesSplitQuery.unit.test.ts +0 -297
- package/src/tests/redis/redisRulesReaderLoad.benchmark.integration.test.ts +0 -520
- package/src/tests/redis/redisRulesReaderRank.benchmark.integration.test.ts +0 -350
- package/src/tests/redis/redisRulesStorage.integration.test.ts +0 -1811
- package/src/tests/testLogger.ts +0 -38
- package/src/tests/transformRule.unit.test.ts +0 -345
- package/src/transformRule.ts +0 -133
- package/tsconfig.cjs.json +0 -41
- package/tsconfig.json +0 -47
- package/tsconfig.tsbuildinfo +0 -1
- package/tsconfig.types.json +0 -9
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import type { RedisClientType } from "redis";
|
|
16
|
-
import { describe, expect, it, vi } from "vitest";
|
|
17
|
-
import { searchRedisKeysBounded } from "#policy/redis/reader/redisAggregate.js";
|
|
18
|
-
|
|
19
|
-
// Match the search-side reply shape produced by
|
|
20
|
-
// `@redis/search`'s `SEARCH_NOCONTENT.transformReply` — a flat
|
|
21
|
-
// `[total, key1, key2, ...]` collapsed into `{ total, documents }`.
|
|
22
|
-
// This shape is the whole point of switching the split-query path to
|
|
23
|
-
// NOCONTENT: unlike the aggregate + LOAD @__key path it can't hit the
|
|
24
|
-
// null-tuple crash in `transformTuplesReply` that fires against
|
|
25
|
-
// field:ja4Hash probes in prod.
|
|
26
|
-
type SearchNoContentReply = { total: number; documents: string[] };
|
|
27
|
-
|
|
28
|
-
type FakeSearchClient = {
|
|
29
|
-
ft: {
|
|
30
|
-
searchNoContent: (
|
|
31
|
-
index: string,
|
|
32
|
-
query: string,
|
|
33
|
-
options: { DIALECT?: number; LIMIT?: { from: number; size: number } },
|
|
34
|
-
) => Promise<SearchNoContentReply>;
|
|
35
|
-
};
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
const makeClient = (reply: SearchNoContentReply): FakeSearchClient => ({
|
|
39
|
-
ft: {
|
|
40
|
-
searchNoContent: vi.fn().mockResolvedValue(reply),
|
|
41
|
-
},
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
describe("searchRedisKeysBounded", () => {
|
|
45
|
-
it("returns the flat documents list from FT.SEARCH NOCONTENT", async () => {
|
|
46
|
-
const client = makeClient({
|
|
47
|
-
total: 3,
|
|
48
|
-
documents: ["access_rule:a", "access_rule:b", "access_rule:c"],
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
// The caller uses a bounded max — 500 in production.
|
|
52
|
-
const keys = await searchRedisKeysBounded(
|
|
53
|
-
client as unknown as RedisClientType,
|
|
54
|
-
"@type:{block} @ja4Hash:{t13d_probe}",
|
|
55
|
-
500,
|
|
56
|
-
);
|
|
57
|
-
|
|
58
|
-
expect(keys).toEqual(["access_rule:a", "access_rule:b", "access_rule:c"]);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("passes the caller's cap through as the FT.SEARCH LIMIT size", async () => {
|
|
62
|
-
const searchNoContent = vi
|
|
63
|
-
.fn()
|
|
64
|
-
.mockResolvedValue({ total: 0, documents: [] });
|
|
65
|
-
const client = {
|
|
66
|
-
ft: { searchNoContent },
|
|
67
|
-
} as unknown as RedisClientType;
|
|
68
|
-
|
|
69
|
-
await searchRedisKeysBounded(client, "@ja4Hash:{x}", 500);
|
|
70
|
-
|
|
71
|
-
expect(searchNoContent).toHaveBeenCalledTimes(1);
|
|
72
|
-
const call = searchNoContent.mock.calls[0];
|
|
73
|
-
expect(call).toBeDefined();
|
|
74
|
-
const options = call?.[2] as {
|
|
75
|
-
DIALECT?: number;
|
|
76
|
-
LIMIT?: { from: number; size: number };
|
|
77
|
-
};
|
|
78
|
-
expect(options.LIMIT).toEqual({ from: 0, size: 500 });
|
|
79
|
-
// DIALECT 2 is required by the ismissing() clauses the split
|
|
80
|
-
// query builder emits.
|
|
81
|
-
expect(options.DIALECT).toBe(2);
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it("returns an empty list when the search matches nothing", async () => {
|
|
85
|
-
const client = makeClient({ total: 0, documents: [] });
|
|
86
|
-
|
|
87
|
-
const keys = await searchRedisKeysBounded(
|
|
88
|
-
client as unknown as RedisClientType,
|
|
89
|
-
"@ja4Hash:{never_indexed}",
|
|
90
|
-
500,
|
|
91
|
-
);
|
|
92
|
-
|
|
93
|
-
expect(keys).toEqual([]);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
// Regression: the aggregate + LOAD path this helper replaces throws
|
|
97
|
-
// TypeError: Cannot read properties of null (reading 'length')
|
|
98
|
-
// when the RediSearch coordinator returns a `null` result row (seen
|
|
99
|
-
// continuously on field:ja4Hash probes in prod, thousands per hour).
|
|
100
|
-
// The NOCONTENT reply carries no nested tuples, so a well-formed
|
|
101
|
-
// reply — even with an unexpected trailing null in the raw wire array
|
|
102
|
-
// — cannot reach the tuples decoder at all. Assert we return the
|
|
103
|
-
// documents list as-is without introspecting individual entries.
|
|
104
|
-
it("does not iterate individual document tuples (null-safe reply shape)", async () => {
|
|
105
|
-
// Simulate a bare-string documents array — the shape produced by
|
|
106
|
-
// SEARCH_NOCONTENT. Every entry is a string key; there are no
|
|
107
|
-
// per-doc tuples for a `.length` lookup to trip over.
|
|
108
|
-
const client = makeClient({
|
|
109
|
-
total: 2,
|
|
110
|
-
documents: ["access_rule:one", "access_rule:two"],
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
await expect(
|
|
114
|
-
searchRedisKeysBounded(
|
|
115
|
-
client as unknown as RedisClientType,
|
|
116
|
-
"@ja4Hash:{t13d2013h2_a09f3c656075_7f0f34a4126d}",
|
|
117
|
-
500,
|
|
118
|
-
),
|
|
119
|
-
).resolves.toEqual(["access_rule:one", "access_rule:two"]);
|
|
120
|
-
});
|
|
121
|
-
});
|
|
@@ -1,349 +0,0 @@
|
|
|
1
|
-
// Copyright 2021-2026 Prosopo (UK) Ltd.
|
|
2
|
-
//
|
|
3
|
-
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
-
// you may not use this file except in compliance with the License.
|
|
5
|
-
// You may obtain a copy of the License at
|
|
6
|
-
//
|
|
7
|
-
// http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
-
//
|
|
9
|
-
// Unless required by applicable law or agreed to in writing, software
|
|
10
|
-
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
-
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
-
// See the License for the specific language governing permissions and
|
|
13
|
-
// limitations under the License.
|
|
14
|
-
|
|
15
|
-
import { describe, expect, it } from "vitest";
|
|
16
|
-
import { getRulesRedisQuery } from "#policy/redis/reader/redisRulesQuery.js";
|
|
17
|
-
import {
|
|
18
|
-
type AccessRulesFilter,
|
|
19
|
-
FilterScopeMatch,
|
|
20
|
-
} from "#policy/rulesStorage.js";
|
|
21
|
-
|
|
22
|
-
describe("getRulesRedisQuery", () => {
|
|
23
|
-
it("puts ismissing(x) for field x passed in as `undefined` when user scope match is exact", () => {
|
|
24
|
-
const filter = {
|
|
25
|
-
userScope: {
|
|
26
|
-
numericIp: BigInt(100),
|
|
27
|
-
ja4Hash: "ja4Hash",
|
|
28
|
-
userAgentHash: undefined,
|
|
29
|
-
},
|
|
30
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
31
|
-
} as AccessRulesFilter;
|
|
32
|
-
|
|
33
|
-
const query = getRulesRedisQuery(filter, false);
|
|
34
|
-
|
|
35
|
-
expect(query).toBe(
|
|
36
|
-
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) )",
|
|
37
|
-
);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("puts ismissing(x) for field x passed in as `undefined` when user scope match is exact and for missing fields when matchingFieldsOnly is set", () => {
|
|
41
|
-
const filter = {
|
|
42
|
-
userScope: {
|
|
43
|
-
numericIp: BigInt(100),
|
|
44
|
-
ja4Hash: "ja4Hash",
|
|
45
|
-
userAgentHash: undefined,
|
|
46
|
-
},
|
|
47
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
48
|
-
} as AccessRulesFilter;
|
|
49
|
-
|
|
50
|
-
const query = getRulesRedisQuery(filter, true);
|
|
51
|
-
|
|
52
|
-
expect(query).toBe(
|
|
53
|
-
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@userId) ismissing(@headersHash) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) ismissing(@browser) ismissing(@headerMatch) ismissing(@headerName) ismissing(@headerValue) ismissing(@headerOperator) ismissing(@webBotAuthAgent) )",
|
|
54
|
-
);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it("puts ismissing(x) for multiple fields passed in as `undefined` when user scope match is exact", () => {
|
|
58
|
-
const filter = {
|
|
59
|
-
userScope: {
|
|
60
|
-
numericIp: BigInt(100),
|
|
61
|
-
ja4Hash: "ja4Hash",
|
|
62
|
-
userAgentHash: undefined,
|
|
63
|
-
headersHash: undefined,
|
|
64
|
-
userId: undefined,
|
|
65
|
-
},
|
|
66
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
67
|
-
} as AccessRulesFilter;
|
|
68
|
-
|
|
69
|
-
const query = getRulesRedisQuery(filter, false);
|
|
70
|
-
|
|
71
|
-
expect(query).toBe(
|
|
72
|
-
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) )",
|
|
73
|
-
);
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it("does not put ismissing(x) for multiple fields passed in as `undefined` when user scope match is greedy", () => {
|
|
77
|
-
const filter = {
|
|
78
|
-
userScope: {
|
|
79
|
-
numericIp: BigInt(100),
|
|
80
|
-
ja4Hash: "ja4Hash",
|
|
81
|
-
userAgentHash: undefined,
|
|
82
|
-
headersHash: undefined,
|
|
83
|
-
userId: undefined,
|
|
84
|
-
},
|
|
85
|
-
userScopeMatch: FilterScopeMatch.Greedy,
|
|
86
|
-
} as AccessRulesFilter;
|
|
87
|
-
|
|
88
|
-
const query = getRulesRedisQuery(filter, false);
|
|
89
|
-
|
|
90
|
-
expect(query).toBe(
|
|
91
|
-
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) | @ja4Hash:{ja4Hash} )",
|
|
92
|
-
);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it("puts ismissing(x) for multiple fields passed in as `undefined` when user scope match is exact 2", () => {
|
|
96
|
-
const filter = {
|
|
97
|
-
userScope: {
|
|
98
|
-
numericIp: undefined,
|
|
99
|
-
ja4Hash: "ja4Hash",
|
|
100
|
-
userAgentHash: undefined,
|
|
101
|
-
headersHash: undefined,
|
|
102
|
-
userId: undefined,
|
|
103
|
-
},
|
|
104
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
105
|
-
} as AccessRulesFilter;
|
|
106
|
-
|
|
107
|
-
const query = getRulesRedisQuery(filter, false);
|
|
108
|
-
|
|
109
|
-
expect(query).toBe(
|
|
110
|
-
"( ismissing(@numericIp) ismissing(@numericIpMaskMin) ismissing(@numericIpMaskMax) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) )",
|
|
111
|
-
);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it("does not put ismissing(numericIpMaskMin) and does not put ismissing(numericIpMaskMax) when numericIp is passed in", () => {
|
|
115
|
-
const filter = {
|
|
116
|
-
userScope: {
|
|
117
|
-
numericIp: BigInt(100),
|
|
118
|
-
ja4Hash: "ja4Hash",
|
|
119
|
-
userAgentHash: undefined,
|
|
120
|
-
headersHash: undefined,
|
|
121
|
-
userId: undefined,
|
|
122
|
-
},
|
|
123
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
124
|
-
} as AccessRulesFilter;
|
|
125
|
-
|
|
126
|
-
const query = getRulesRedisQuery(filter, true);
|
|
127
|
-
|
|
128
|
-
expect(query).toBe(
|
|
129
|
-
"( ( @numericIp:[100 100] | ( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[100 +inf] ) ) @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) ismissing(@browser) ismissing(@headerMatch) ismissing(@headerName) ismissing(@headerValue) ismissing(@headerOperator) ismissing(@webBotAuthAgent) )",
|
|
130
|
-
);
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it("does not put ismissing(numericIp) when numericIpMaskMin and numericIpMaskMax are passed in", () => {
|
|
134
|
-
const filter = {
|
|
135
|
-
userScope: {
|
|
136
|
-
numericIpMaskMin: BigInt(100),
|
|
137
|
-
numericIpMaskMax: BigInt(200),
|
|
138
|
-
ja4Hash: "ja4Hash",
|
|
139
|
-
userAgentHash: undefined,
|
|
140
|
-
headersHash: undefined,
|
|
141
|
-
userId: undefined,
|
|
142
|
-
},
|
|
143
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
144
|
-
} as AccessRulesFilter;
|
|
145
|
-
|
|
146
|
-
const query = getRulesRedisQuery(filter, true);
|
|
147
|
-
|
|
148
|
-
expect(query).toBe(
|
|
149
|
-
"( @numericIpMaskMin:[-inf 100] @numericIpMaskMax:[200 +inf] @ja4Hash:{ja4Hash} ismissing(@userAgentHash) ismissing(@headersHash) ismissing(@userId) ismissing(@headHash) ismissing(@coords) ismissing(@countryCode) ismissing(@asn) ismissing(@os) ismissing(@browser) ismissing(@headerMatch) ismissing(@headerName) ismissing(@headerValue) ismissing(@headerOperator) ismissing(@webBotAuthAgent) )",
|
|
150
|
-
);
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it("emits NUMERIC range syntax for asn, not TAG syntax", () => {
|
|
154
|
-
const filter = {
|
|
155
|
-
userScope: {
|
|
156
|
-
asn: 205016,
|
|
157
|
-
},
|
|
158
|
-
userScopeMatch: FilterScopeMatch.Greedy,
|
|
159
|
-
} as AccessRulesFilter;
|
|
160
|
-
|
|
161
|
-
const query = getRulesRedisQuery(filter, false);
|
|
162
|
-
|
|
163
|
-
// asn is indexed as NUMERIC; TAG syntax (@asn:{205016}) would silently
|
|
164
|
-
// fail to match. Must use range syntax.
|
|
165
|
-
expect(query).toContain("@asn:[205016 205016]");
|
|
166
|
-
expect(query).not.toContain("@asn:{");
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
it("does not duplicate ismissing for numericIpMaskMin and numericIpMaskMax when matchingFieldsOnly is true and all IP fields are undefined", () => {
|
|
170
|
-
const filter = {
|
|
171
|
-
userScope: {
|
|
172
|
-
userId: "user123",
|
|
173
|
-
},
|
|
174
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
175
|
-
} as AccessRulesFilter;
|
|
176
|
-
|
|
177
|
-
const query = getRulesRedisQuery(filter, true);
|
|
178
|
-
|
|
179
|
-
// numericIp handler emits all three ismissing clauses when all IP fields are undefined.
|
|
180
|
-
// The individual numericIpMaskMin and numericIpMaskMax handlers should not duplicate them.
|
|
181
|
-
const numericIpMaskMinMatches = query.match(
|
|
182
|
-
/ismissing\(@numericIpMaskMin\)/g,
|
|
183
|
-
);
|
|
184
|
-
const numericIpMaskMaxMatches = query.match(
|
|
185
|
-
/ismissing\(@numericIpMaskMax\)/g,
|
|
186
|
-
);
|
|
187
|
-
|
|
188
|
-
expect(numericIpMaskMinMatches).toHaveLength(1);
|
|
189
|
-
expect(numericIpMaskMaxMatches).toHaveLength(1);
|
|
190
|
-
expect(query).toContain("ismissing(@numericIp)");
|
|
191
|
-
expect(query).toContain("@userId:{user123}");
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it("emits ismissing for numericIpMaskMin when numericIpMaskMax is defined but numericIpMaskMin is not", () => {
|
|
195
|
-
const filter = {
|
|
196
|
-
userScope: {
|
|
197
|
-
numericIpMaskMax: BigInt(200),
|
|
198
|
-
},
|
|
199
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
200
|
-
} as AccessRulesFilter;
|
|
201
|
-
|
|
202
|
-
const query = getRulesRedisQuery(filter, true);
|
|
203
|
-
|
|
204
|
-
// numericIp handler returns "" because numericIpMaskMax is defined.
|
|
205
|
-
// numericIpMaskMin handler should still emit ismissing since the numericIp handler didn't cover it.
|
|
206
|
-
expect(query).toContain("ismissing(@numericIpMaskMin)");
|
|
207
|
-
expect(query).toContain("@numericIpMaskMax:[200 +inf]");
|
|
208
|
-
expect(query).not.toContain("ismissing(@numericIpMaskMax)");
|
|
209
|
-
});
|
|
210
|
-
|
|
211
|
-
it("emits ismissing for numericIpMaskMax when numericIpMaskMin is defined but numericIpMaskMax is not", () => {
|
|
212
|
-
const filter = {
|
|
213
|
-
userScope: {
|
|
214
|
-
numericIpMaskMin: BigInt(100),
|
|
215
|
-
},
|
|
216
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
217
|
-
} as AccessRulesFilter;
|
|
218
|
-
|
|
219
|
-
const query = getRulesRedisQuery(filter, true);
|
|
220
|
-
|
|
221
|
-
expect(query).toContain("@numericIpMaskMin:[-inf 100]");
|
|
222
|
-
expect(query).toContain("ismissing(@numericIpMaskMax)");
|
|
223
|
-
expect(query).not.toContain("ismissing(@numericIpMaskMin)");
|
|
224
|
-
});
|
|
225
|
-
|
|
226
|
-
it("includes headHash in query when provided", () => {
|
|
227
|
-
const filter = {
|
|
228
|
-
userScope: {
|
|
229
|
-
headHash: "abc123def456",
|
|
230
|
-
},
|
|
231
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
232
|
-
} as AccessRulesFilter;
|
|
233
|
-
|
|
234
|
-
const query = getRulesRedisQuery(filter, false);
|
|
235
|
-
|
|
236
|
-
expect(query).toBe("( @headHash:{abc123def456} )");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("includes coords in query when provided with escaped special characters", () => {
|
|
240
|
-
const filter = {
|
|
241
|
-
userScope: {
|
|
242
|
-
coords: "[[[100,200]]]",
|
|
243
|
-
},
|
|
244
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
245
|
-
} as AccessRulesFilter;
|
|
246
|
-
|
|
247
|
-
const query = getRulesRedisQuery(filter, false);
|
|
248
|
-
|
|
249
|
-
// Special characters like [, ], and , should be escaped
|
|
250
|
-
expect(query).toContain("@coords:{\\[\\[\\[100\\,200\\]\\]\\]}");
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it("includes both headHash and coords when both provided", () => {
|
|
254
|
-
const filter = {
|
|
255
|
-
userScope: {
|
|
256
|
-
headHash: "abc123def456",
|
|
257
|
-
coords: "[[[100,200]]]",
|
|
258
|
-
},
|
|
259
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
260
|
-
} as AccessRulesFilter;
|
|
261
|
-
|
|
262
|
-
const query = getRulesRedisQuery(filter, false);
|
|
263
|
-
|
|
264
|
-
expect(query).toContain("@headHash:{abc123def456}");
|
|
265
|
-
expect(query).toContain("@coords:{\\[\\[\\[100\\,200\\]\\]\\]}");
|
|
266
|
-
});
|
|
267
|
-
|
|
268
|
-
it("puts ismissing(headHash) and ismissing(coords) when not provided and matchingFieldsOnly is true", () => {
|
|
269
|
-
const filter = {
|
|
270
|
-
userScope: {
|
|
271
|
-
ja4Hash: "ja4Hash",
|
|
272
|
-
},
|
|
273
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
274
|
-
} as AccessRulesFilter;
|
|
275
|
-
|
|
276
|
-
const query = getRulesRedisQuery(filter, true);
|
|
277
|
-
|
|
278
|
-
expect(query).toContain("ismissing(@headHash)");
|
|
279
|
-
expect(query).toContain("ismissing(@coords)");
|
|
280
|
-
});
|
|
281
|
-
|
|
282
|
-
it("combines headHash with other fields correctly in exact match", () => {
|
|
283
|
-
const filter = {
|
|
284
|
-
userScope: {
|
|
285
|
-
numericIp: BigInt(100),
|
|
286
|
-
ja4Hash: "ja4Hash",
|
|
287
|
-
headHash: "abc123def456",
|
|
288
|
-
userAgentHash: undefined,
|
|
289
|
-
},
|
|
290
|
-
userScopeMatch: FilterScopeMatch.Exact,
|
|
291
|
-
} as AccessRulesFilter;
|
|
292
|
-
|
|
293
|
-
const query = getRulesRedisQuery(filter, false);
|
|
294
|
-
|
|
295
|
-
expect(query).toContain("@numericIp:[100 100]");
|
|
296
|
-
expect(query).toContain("@ja4Hash:{ja4Hash}");
|
|
297
|
-
expect(query).toContain("@headHash:{abc123def456}");
|
|
298
|
-
expect(query).toContain("ismissing(@userAgentHash)");
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
it("prepends @type:{block} when blockOnly is set on the filter", () => {
|
|
302
|
-
const filter = {
|
|
303
|
-
userScope: {
|
|
304
|
-
ja4Hash: "ja4Hash",
|
|
305
|
-
},
|
|
306
|
-
userScopeMatch: FilterScopeMatch.Greedy,
|
|
307
|
-
blockOnly: true,
|
|
308
|
-
} as AccessRulesFilter;
|
|
309
|
-
|
|
310
|
-
const query = getRulesRedisQuery(filter, false);
|
|
311
|
-
|
|
312
|
-
expect(query).toContain("@type:{block}");
|
|
313
|
-
// blockOnly precedes the userScope clause so callers get the
|
|
314
|
-
// pre-filter benefit on a single Redis pass.
|
|
315
|
-
const typeIdx = query.indexOf("@type:{block}");
|
|
316
|
-
const userScopeIdx = query.indexOf("@ja4Hash");
|
|
317
|
-
expect(typeIdx).toBeGreaterThanOrEqual(0);
|
|
318
|
-
expect(typeIdx).toBeLessThan(userScopeIdx);
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("omits @type:{block} when blockOnly is not set", () => {
|
|
322
|
-
const filter = {
|
|
323
|
-
userScope: {
|
|
324
|
-
ja4Hash: "ja4Hash",
|
|
325
|
-
},
|
|
326
|
-
userScopeMatch: FilterScopeMatch.Greedy,
|
|
327
|
-
} as AccessRulesFilter;
|
|
328
|
-
|
|
329
|
-
const query = getRulesRedisQuery(filter, false);
|
|
330
|
-
|
|
331
|
-
expect(query).not.toContain("@type:");
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
it("composes blockOnly with clientId policy scope", () => {
|
|
335
|
-
const filter = {
|
|
336
|
-
policyScope: { clientId: "site-A" },
|
|
337
|
-
policyScopeMatch: FilterScopeMatch.Greedy,
|
|
338
|
-
userScope: { ja4Hash: "ja4Hash" },
|
|
339
|
-
userScopeMatch: FilterScopeMatch.Greedy,
|
|
340
|
-
blockOnly: true,
|
|
341
|
-
} as AccessRulesFilter;
|
|
342
|
-
|
|
343
|
-
const query = getRulesRedisQuery(filter, false);
|
|
344
|
-
|
|
345
|
-
expect(query).toContain("@type:{block}");
|
|
346
|
-
expect(query).toContain("@clientId:{site-A}");
|
|
347
|
-
expect(query).toContain("@ja4Hash:{ja4Hash}");
|
|
348
|
-
});
|
|
349
|
-
});
|