@wowok/agent-mcp 2.4.1 → 2.4.3
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/dist/index.d.ts +2 -2
- package/dist/index.js +11 -1624
- package/dist/knowledge/guard-ledger.js +14 -0
- package/dist/knowledge/guard-lint.d.ts +1 -0
- package/dist/knowledge/guard-lint.js +83 -3
- package/dist/knowledge/guard-migration.d.ts +48 -0
- package/dist/knowledge/guard-migration.js +228 -0
- package/dist/knowledge/guard-risk.d.ts +2 -0
- package/dist/knowledge/guard-risk.js +827 -57
- package/dist/knowledge/guard-templates.d.ts +1 -0
- package/dist/knowledge/guard-templates.js +278 -2
- package/dist/project/graph.d.ts +1 -0
- package/dist/project/graph.js +27 -1
- package/dist/project/index.d.ts +1 -0
- package/dist/project/index.js +39 -2
- package/dist/project/namespace.d.ts +11 -1
- package/dist/project/namespace.js +27 -2
- package/dist/project/query.d.ts +2 -0
- package/dist/project/query.js +56 -15
- package/dist/rules.d.ts +12 -0
- package/dist/rules.js +9 -0
- package/dist/schema/call/allocation.d.ts +10 -10
- package/dist/schema/call/base.js +3 -3
- package/dist/schema/call/machine.d.ts +38 -38
- package/dist/schema/call/semantic.js +1 -1
- package/dist/schema/call/service.d.ts +7 -7
- package/dist/schema/operations.d.ts +48 -48
- package/dist/schema/query/index.d.ts +146 -146
- package/dist/schema/query/index.js +15 -9
- package/dist/schema/utils/guard-query-utils.d.ts +7 -0
- package/dist/schema/utils/guard-query-utils.js +14 -1
- package/dist/schemas/bridge_operation.output.json +1 -1
- package/dist/schemas/index.json +1 -1
- package/dist/schemas/messenger_operation.output.json +1 -1
- package/dist/schemas/onchain_events.output.json +1 -1
- package/dist/schemas/onchain_operations.output.json +2 -2
- package/dist/schemas/onchain_operations.schema.json +11 -20
- package/dist/schemas/onchain_operations_allocation.schema.json +7 -10
- package/dist/schemas/onchain_operations_machine.schema.json +2 -5
- package/dist/schemas/onchain_operations_service.schema.json +2 -5
- package/dist/schemas/onchain_table_data.output.json +6 -12
- package/dist/tools/handlers/bridge.d.ts +1 -0
- package/dist/tools/handlers/bridge.js +1 -0
- package/dist/tools/handlers/config.d.ts +2 -0
- package/dist/tools/handlers/config.js +71 -0
- package/dist/tools/handlers/file-export.d.ts +3 -0
- package/dist/tools/handlers/file-export.js +90 -0
- package/dist/tools/handlers/local.d.ts +30 -0
- package/dist/tools/handlers/local.js +27 -0
- package/dist/tools/handlers/messenger.d.ts +16 -0
- package/dist/tools/handlers/messenger.js +187 -0
- package/dist/tools/handlers/onchain.d.ts +2 -0
- package/dist/tools/handlers/onchain.js +246 -0
- package/dist/tools/handlers/project.d.ts +2 -0
- package/dist/tools/handlers/project.js +53 -0
- package/dist/tools/handlers/query.d.ts +5 -0
- package/dist/tools/handlers/query.js +256 -0
- package/dist/tools/handlers/schema-query.d.ts +2 -0
- package/dist/tools/handlers/schema-query.js +92 -0
- package/dist/tools/handlers/trust.d.ts +2 -0
- package/dist/tools/handlers/trust.js +194 -0
- package/dist/tools/handlers/wip.d.ts +2 -0
- package/dist/tools/handlers/wip.js +44 -0
- package/dist/tools/index.d.ts +13 -0
- package/dist/tools/index.js +479 -0
- package/dist/tools/rules-hook.d.ts +2 -0
- package/dist/tools/rules-hook.js +22 -0
- package/dist/tools/shared.d.ts +29 -0
- package/dist/tools/shared.js +130 -0
- package/dist/tools/types.d.ts +35 -0
- package/dist/tools/types.js +1 -0
- package/dist/tools/wrap.d.ts +6 -0
- package/dist/tools/wrap.js +55 -0
- package/package.json +19 -5
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { GUARDQUERY } from "@wowok/wowok";
|
|
1
2
|
function rootStr(ctx) {
|
|
2
3
|
return JSON.stringify(ctx.root).toLowerCase();
|
|
3
4
|
}
|
|
@@ -31,6 +32,415 @@ function extractWitnessCodes(root) {
|
|
|
31
32
|
}
|
|
32
33
|
return codes;
|
|
33
34
|
}
|
|
35
|
+
const WITNESS_CHAIN = {
|
|
36
|
+
100: ["Order", "Progress"],
|
|
37
|
+
101: ["Order", "Machine"],
|
|
38
|
+
102: ["Order", "Service"],
|
|
39
|
+
103: ["Progress", "Machine"],
|
|
40
|
+
104: ["Arb", "Order"],
|
|
41
|
+
105: ["Arb", "Arbitration"],
|
|
42
|
+
106: ["Arb", "Order", "Progress"],
|
|
43
|
+
107: ["Arb", "Order", "Service"],
|
|
44
|
+
108: ["Arb", "Order", "Machine"],
|
|
45
|
+
};
|
|
46
|
+
function extractWitnessIdentifierPairs(root) {
|
|
47
|
+
const pairs = [];
|
|
48
|
+
const visited = new WeakSet();
|
|
49
|
+
function walk(node) {
|
|
50
|
+
if (node === null || typeof node !== "object")
|
|
51
|
+
return;
|
|
52
|
+
if (visited.has(node))
|
|
53
|
+
return;
|
|
54
|
+
visited.add(node);
|
|
55
|
+
if (typeof node.convert_witness === "number" &&
|
|
56
|
+
typeof node.identifier === "number") {
|
|
57
|
+
pairs.push({ code: node.convert_witness, identifier: node.identifier });
|
|
58
|
+
}
|
|
59
|
+
for (const key of Object.keys(node)) {
|
|
60
|
+
const child = node[key];
|
|
61
|
+
if (Array.isArray(child)) {
|
|
62
|
+
for (const item of child)
|
|
63
|
+
walk(item);
|
|
64
|
+
}
|
|
65
|
+
else if (child !== null && typeof child === "object") {
|
|
66
|
+
walk(child);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
walk(root);
|
|
71
|
+
return pairs;
|
|
72
|
+
}
|
|
73
|
+
function getChildNodes(node) {
|
|
74
|
+
if (!node || typeof node !== "object")
|
|
75
|
+
return [];
|
|
76
|
+
const children = [];
|
|
77
|
+
if (Array.isArray(node.nodes))
|
|
78
|
+
children.push(...node.nodes);
|
|
79
|
+
if (node.left)
|
|
80
|
+
children.push(node.left);
|
|
81
|
+
if (node.right)
|
|
82
|
+
children.push(node.right);
|
|
83
|
+
if (node.vec)
|
|
84
|
+
children.push(node.vec);
|
|
85
|
+
if (node.target)
|
|
86
|
+
children.push(node.target);
|
|
87
|
+
if (node.object && typeof node.object === "object")
|
|
88
|
+
children.push(node.object);
|
|
89
|
+
if (Array.isArray(node.parameters))
|
|
90
|
+
children.push(...node.parameters);
|
|
91
|
+
return children.filter((c) => c && typeof c === "object");
|
|
92
|
+
}
|
|
93
|
+
function findSignerLogicEqualNodes(root) {
|
|
94
|
+
const results = [];
|
|
95
|
+
function walk(node, parentType) {
|
|
96
|
+
if (!node || typeof node !== "object")
|
|
97
|
+
return;
|
|
98
|
+
if (node.type === "logic_equal") {
|
|
99
|
+
const operands = Array.isArray(node.nodes)
|
|
100
|
+
? node.nodes
|
|
101
|
+
: node.left && node.right
|
|
102
|
+
? [node.left, node.right]
|
|
103
|
+
: [];
|
|
104
|
+
if (operands.length === 2) {
|
|
105
|
+
const signerIdx = operands.findIndex((op) => op?.type === "context" && op?.context === "Signer");
|
|
106
|
+
if (signerIdx >= 0) {
|
|
107
|
+
results.push({
|
|
108
|
+
node,
|
|
109
|
+
parentType,
|
|
110
|
+
otherOperand: operands[1 - signerIdx],
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
for (const child of getChildNodes(node)) {
|
|
116
|
+
walk(child, node.type);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
walk(root, null);
|
|
120
|
+
return results;
|
|
121
|
+
}
|
|
122
|
+
function detectLevel1StrictBinding(root, table) {
|
|
123
|
+
const signerChecks = findSignerLogicEqualNodes(root);
|
|
124
|
+
for (const check of signerChecks) {
|
|
125
|
+
const other = check.otherOperand;
|
|
126
|
+
if (other?.type === "identifier") {
|
|
127
|
+
const entry = table.find((e) => e.identifier === other.identifier);
|
|
128
|
+
if (entry &&
|
|
129
|
+
!entry.b_submission &&
|
|
130
|
+
entry.value_type === "Address" &&
|
|
131
|
+
typeof entry.value === "string" &&
|
|
132
|
+
entry.value.length > 0) {
|
|
133
|
+
return {
|
|
134
|
+
detected: true,
|
|
135
|
+
entry: {
|
|
136
|
+
identifier: entry.identifier,
|
|
137
|
+
value: entry.value,
|
|
138
|
+
name: entry.name,
|
|
139
|
+
},
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return { detected: false };
|
|
145
|
+
}
|
|
146
|
+
function isSignerCheckInLogicOr(root) {
|
|
147
|
+
const signerChecks = findSignerLogicEqualNodes(root);
|
|
148
|
+
return signerChecks.some((check) => check.parentType === "logic_or");
|
|
149
|
+
}
|
|
150
|
+
const QUERY_INVARIANT_MAP = (() => {
|
|
151
|
+
const map = new Map();
|
|
152
|
+
for (const q of GUARDQUERY) {
|
|
153
|
+
if (q.invariant !== undefined) {
|
|
154
|
+
map.set(q.id, q.invariant);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return map;
|
|
158
|
+
})();
|
|
159
|
+
function extractQueryIds(root) {
|
|
160
|
+
const ids = [];
|
|
161
|
+
const str = JSON.stringify(root);
|
|
162
|
+
const numMatches = str.matchAll(/"query"\s*:\s*(\d+)/g);
|
|
163
|
+
for (const m of numMatches) {
|
|
164
|
+
const id = parseInt(m[1], 10);
|
|
165
|
+
if (!ids.includes(id))
|
|
166
|
+
ids.push(id);
|
|
167
|
+
}
|
|
168
|
+
const nameMatches = str.matchAll(/"query"\s*:\s*"([^"]+)"/g);
|
|
169
|
+
for (const m of nameMatches) {
|
|
170
|
+
const name = m[1];
|
|
171
|
+
const entry = GUARDQUERY.find((q) => q.name === name);
|
|
172
|
+
if (entry && !ids.includes(entry.id))
|
|
173
|
+
ids.push(entry.id);
|
|
174
|
+
}
|
|
175
|
+
return ids;
|
|
176
|
+
}
|
|
177
|
+
function getQueryInvariant(queryId) {
|
|
178
|
+
return QUERY_INVARIANT_MAP.get(queryId);
|
|
179
|
+
}
|
|
180
|
+
function rootHasNonZeroQuery(root) {
|
|
181
|
+
const ids = extractQueryIds(root);
|
|
182
|
+
return ids.some((id) => QUERY_INVARIANT_MAP.has(id));
|
|
183
|
+
}
|
|
184
|
+
function rootAllQueriesNonZero(root) {
|
|
185
|
+
const ids = extractQueryIds(root);
|
|
186
|
+
if (ids.length === 0)
|
|
187
|
+
return false;
|
|
188
|
+
return ids.every((id) => QUERY_INVARIANT_MAP.has(id));
|
|
189
|
+
}
|
|
190
|
+
const REENTRANCY_SPECS = [
|
|
191
|
+
{
|
|
192
|
+
sceneId: "reward_guard",
|
|
193
|
+
level: "critical",
|
|
194
|
+
existsPrimitives: [1613],
|
|
195
|
+
countPrimitives: [1612],
|
|
196
|
+
addressPrimitives: [1626],
|
|
197
|
+
rationale: "Guard pass distributes reward funds to the Claimant AND creates a reward_record, but the Guard conditions (identity, time, node state) do NOT naturally invalidate after a claim. " +
|
|
198
|
+
"Without an explicit anti-reentrancy primitive, the same Claimant can repeatedly pass the Guard and DRAIN THE ENTIRE POOL. " +
|
|
199
|
+
"This is the most severe reentrancy scene — direct fund loss.",
|
|
200
|
+
mitigation: "Add ONE of the following to the root (wrapped in logic_and with existing conditions):\n" +
|
|
201
|
+
" - logic_not(query('reward.record has', object=reward, parameters=[claimant_address])) — RECOMMENDED (per-user Bool check)\n" +
|
|
202
|
+
" - logic_equal([query('reward.record count', object=reward), identifier(N)]) where table[N].value=0 — total-claim count is 0\n" +
|
|
203
|
+
" - logic_not(query('reward.record.find_first', object=reward, where={...})) — no matching record found\n" +
|
|
204
|
+
"Reference template: MyShop_Advanced Guard 7 condition 4 (logic_not(query_reward_record_exists(where.storeFromId=order_id)))",
|
|
205
|
+
},
|
|
206
|
+
{
|
|
207
|
+
sceneId: "arbitration_voting_guard",
|
|
208
|
+
level: "critical",
|
|
209
|
+
existsPrimitives: [1404, 1406],
|
|
210
|
+
countPrimitives: [1405],
|
|
211
|
+
addressPrimitives: [],
|
|
212
|
+
rationale: "Guard pass counts the vote with the extracted weight. Without anti-reentrancy, the same voter can vote MULTIPLE times, " +
|
|
213
|
+
"multiplying their weight and skewing the arbitration result — which directly determines fund ownership via compensation. " +
|
|
214
|
+
"The voter's eligibility (identity, reputation) does NOT naturally invalidate after voting. " +
|
|
215
|
+
"Note: arb.voted_count (1403) is total voter count (NOT per-voter) and does NOT serve as an anti-reentrancy primitive for an individual voter.",
|
|
216
|
+
mitigation: "Add ONE of the following to the root (wrapped in logic_and):\n" +
|
|
217
|
+
" - logic_not(query('arb.voted has', object=arb, parameters=[voter_address])) — RECOMMENDED (per-voter Bool)\n" +
|
|
218
|
+
" - logic_not(query('arb.voted.agree has', object=arb, parameters=[voter_address, proposition_index])) — per-voter-per-proposition\n" +
|
|
219
|
+
" - logic_equal([query('arb.voted.agree count', object=arb, parameters=[voter_address]), identifier(N)]) where table[N].value=0",
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
sceneId: "arbitration_usage_guard",
|
|
223
|
+
level: "high",
|
|
224
|
+
existsPrimitives: [1565],
|
|
225
|
+
countPrimitives: [1564],
|
|
226
|
+
addressPrimitives: [],
|
|
227
|
+
rationale: "Guard pass creates a new Arb case. Without anti-reentrancy, a customer can create multiple Arb cases for the SAME Order " +
|
|
228
|
+
"(up to MAX_DISPUTE_COUNT=10 per order.move L96), harassing the counterparty and inflating arbitration system load. " +
|
|
229
|
+
"Not direct fund loss, but severe system abuse. order::dispute (order.move L93-99) deduplicates by Arb address but does NOT prevent multiple distinct Arbs.",
|
|
230
|
+
mitigation: "Add ONE of the following to the root (wrapped in logic_and):\n" +
|
|
231
|
+
" - logic_not(query('order.dispute has', object=order, parameters=[existing_arb_address])) — per-Arb check\n" +
|
|
232
|
+
" - logic_equal([query('order.dispute count', object=order), identifier(N)]) where table[N].value=0 — STRICT: block any prior dispute\n" +
|
|
233
|
+
" - logic_as_u256_less_or_equal([query('order.dispute count', object=order), identifier(N)]) where table[N].value=K — LENIENT: allow up to K disputes",
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
sceneId: "service_order_allocators_guard",
|
|
237
|
+
level: "info",
|
|
238
|
+
existsPrimitives: [1568],
|
|
239
|
+
countPrimitives: [],
|
|
240
|
+
addressPrimitives: [1569],
|
|
241
|
+
rationale: "Protocol-level protection: order::on_allocation (order.move L116-119) asserts ONE Allocation per Order (E_ALLOCATION_ALREADY_SPECIFIED). " +
|
|
242
|
+
"After alloc() executes, typical Rate-mode allocators (sum=10000) and Surplus-mode allocators drain balance to 0, making reentrancy unprofitable. " +
|
|
243
|
+
"EDGE CASE: Amount-mode-only allocators (no Surplus, total < full balance) leave remainder; a second alloc() could extract more — mitigated by preferring Rate/Surplus modes. " +
|
|
244
|
+
"External deposit via receive() is possible but irrational (attacker funds the Allocation they attack).",
|
|
245
|
+
mitigation: "Default: no action needed (protocol-level guard). IF using Amount-mode-only allocators without Surplus: " +
|
|
246
|
+
"consider adding logic_not(query('order.allocation some', object=order)) OR switch to Rate/Surplus mode to drain balance completely.",
|
|
247
|
+
},
|
|
248
|
+
{
|
|
249
|
+
sceneId: "service_buy_guard",
|
|
250
|
+
level: "info",
|
|
251
|
+
existsPrimitives: [],
|
|
252
|
+
countPrimitives: [],
|
|
253
|
+
addressPrimitives: [],
|
|
254
|
+
rationale: "Each purchase creates a fresh Order; no existing Order state is modified. Default: no reentrancy risk. " +
|
|
255
|
+
"IF business intent is 'limit one purchase per user' (limited-edition goods, whitelist single-buy), an anti-reentrancy check is REQUIRED " +
|
|
256
|
+
"but no direct GUARDQUERY primitive exists for per-user purchase history.",
|
|
257
|
+
mitigation: "IF limit-purchase intent: record purchases in a Repository and add logic_not(query('repository.data has', ...)) to root; " +
|
|
258
|
+
"OR revoke EntityRegistrar membership after first buy; OR use an off-chain allowlist with on-chain Merkle proof.",
|
|
259
|
+
},
|
|
260
|
+
{
|
|
261
|
+
sceneId: "repository_write_guard",
|
|
262
|
+
level: "info",
|
|
263
|
+
existsPrimitives: [1166],
|
|
264
|
+
countPrimitives: [],
|
|
265
|
+
addressPrimitives: [],
|
|
266
|
+
rationale: "Default writes are idempotent (same ID overwrites, different ID appends); no fund flow. " +
|
|
267
|
+
"IF business intent is 'one write per ID' (voting, immutable credential, single-attestation), an anti-reentrancy check is needed.",
|
|
268
|
+
mitigation: "IF one-write-per-ID intent: add logic_not(query('repository.data has', object=repository, parameters=[policy_name, user_or_object_id])) to root.",
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
sceneId: "gen_passport_guard",
|
|
272
|
+
level: "info",
|
|
273
|
+
existsPrimitives: [],
|
|
274
|
+
countPrimitives: [],
|
|
275
|
+
addressPrimitives: [],
|
|
276
|
+
rationale: "Each gen_passport call generates an independent Passport (one Passport per operation). " +
|
|
277
|
+
"Multiple Passports for the same user are usually valid (each serves a different subsequent operation). " +
|
|
278
|
+
"IF business intent is 'one active Passport per user', an anti-reentrancy check is needed but no direct GUARDQUERY primitive exists.",
|
|
279
|
+
mitigation: "IF one-passport-per-user intent: use off-chain tracking or a Repository to record issued Passport IDs.",
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
const QUERY_TYPE_ALIASES = new Map([
|
|
283
|
+
["query_reward_record_exists", 1613],
|
|
284
|
+
["query_reward_record_count", 1612],
|
|
285
|
+
["query_reward_record_find", 1626],
|
|
286
|
+
["query_progress_history_find", 1274],
|
|
287
|
+
["query_progress_history_session_find", 1275],
|
|
288
|
+
["query_progress_history_session_forward_find", 1276],
|
|
289
|
+
["query_progress_history_session_count", 1273],
|
|
290
|
+
["query_progress_history_session_forward_count", 1262],
|
|
291
|
+
]);
|
|
292
|
+
function detectReentrancyProtection(root, table, spec) {
|
|
293
|
+
const allPrimitives = new Set([
|
|
294
|
+
...spec.existsPrimitives,
|
|
295
|
+
...spec.countPrimitives,
|
|
296
|
+
...spec.addressPrimitives,
|
|
297
|
+
]);
|
|
298
|
+
if (allPrimitives.size === 0) {
|
|
299
|
+
return {
|
|
300
|
+
state: "absent",
|
|
301
|
+
evidence: "No anti-reentrancy primitive available in GUARDQUERY for this scene",
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
const idToName = new Map();
|
|
305
|
+
const nameToId = new Map();
|
|
306
|
+
for (const id of allPrimitives) {
|
|
307
|
+
const entry = GUARDQUERY.find((q) => q.id === id);
|
|
308
|
+
if (entry) {
|
|
309
|
+
idToName.set(id, entry.name);
|
|
310
|
+
nameToId.set(entry.name, id);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
const primitiveNames = Array.from(nameToId.keys());
|
|
314
|
+
function resolvePrimitiveId(node) {
|
|
315
|
+
if (!node || typeof node !== "object")
|
|
316
|
+
return -1;
|
|
317
|
+
if (typeof node.query === "number" && allPrimitives.has(node.query)) {
|
|
318
|
+
return node.query;
|
|
319
|
+
}
|
|
320
|
+
if (typeof node.query === "string" && nameToId.has(node.query)) {
|
|
321
|
+
return nameToId.get(node.query);
|
|
322
|
+
}
|
|
323
|
+
if (typeof node.type === "string" && QUERY_TYPE_ALIASES.has(node.type)) {
|
|
324
|
+
const aliasId = QUERY_TYPE_ALIASES.get(node.type);
|
|
325
|
+
if (allPrimitives.has(aliasId)) {
|
|
326
|
+
return aliasId;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (typeof node.type === "string" && nameToId.has(node.type)) {
|
|
330
|
+
return nameToId.get(node.type);
|
|
331
|
+
}
|
|
332
|
+
return -1;
|
|
333
|
+
}
|
|
334
|
+
function isZeroValue(node) {
|
|
335
|
+
if (!node || typeof node !== "object")
|
|
336
|
+
return false;
|
|
337
|
+
if (typeof node.type === "string" &&
|
|
338
|
+
["number", "u8", "u16", "u32", "u64", "u128", "u256"].includes(node.type)) {
|
|
339
|
+
return node.value === 0 || node.value === "0" || node.value === 0n;
|
|
340
|
+
}
|
|
341
|
+
if (node.type === "identifier" && typeof node.identifier === "number") {
|
|
342
|
+
const entry = table.find((e) => e.identifier === node.identifier);
|
|
343
|
+
if (entry && !entry.b_submission) {
|
|
344
|
+
return entry.value === 0 || entry.value === "0" || entry.value === 0n;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
function getSiblings(parent, child) {
|
|
350
|
+
if (!parent || typeof parent !== "object")
|
|
351
|
+
return [];
|
|
352
|
+
const siblings = [];
|
|
353
|
+
if (Array.isArray(parent.nodes)) {
|
|
354
|
+
for (const n of parent.nodes) {
|
|
355
|
+
if (n !== child)
|
|
356
|
+
siblings.push(n);
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
if (parent.left && parent.left !== child)
|
|
360
|
+
siblings.push(parent.left);
|
|
361
|
+
if (parent.right && parent.right !== child)
|
|
362
|
+
siblings.push(parent.right);
|
|
363
|
+
return siblings.filter((s) => s && typeof s === "object");
|
|
364
|
+
}
|
|
365
|
+
let foundPrimitive = false;
|
|
366
|
+
let foundProtected = false;
|
|
367
|
+
let pseudoEvidence = "";
|
|
368
|
+
let foundPrimitiveName = "";
|
|
369
|
+
function walk(node, parent) {
|
|
370
|
+
if (!node || typeof node !== "object")
|
|
371
|
+
return;
|
|
372
|
+
const primId = resolvePrimitiveId(node);
|
|
373
|
+
if (primId >= 0) {
|
|
374
|
+
foundPrimitive = true;
|
|
375
|
+
const primName = idToName.get(primId) ?? `query_${primId}`;
|
|
376
|
+
foundPrimitiveName = primName;
|
|
377
|
+
const parentType = parent?.type ?? null;
|
|
378
|
+
let properlyWrapped = false;
|
|
379
|
+
const isExistsStyle = spec.existsPrimitives.includes(primId);
|
|
380
|
+
const isAddressStyle = spec.addressPrimitives.includes(primId);
|
|
381
|
+
const isCountStyle = spec.countPrimitives.includes(primId);
|
|
382
|
+
if ((isExistsStyle || isAddressStyle) && parentType === "logic_not") {
|
|
383
|
+
properlyWrapped = true;
|
|
384
|
+
}
|
|
385
|
+
if (isCountStyle && parentType === "logic_equal") {
|
|
386
|
+
const siblings = getSiblings(parent, node);
|
|
387
|
+
if (siblings.some((sib) => isZeroValue(sib))) {
|
|
388
|
+
properlyWrapped = true;
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
if (properlyWrapped) {
|
|
392
|
+
foundProtected = true;
|
|
393
|
+
}
|
|
394
|
+
else if (!pseudoEvidence) {
|
|
395
|
+
const expected = isCountStyle
|
|
396
|
+
? "logic_equal with a zero operand"
|
|
397
|
+
: "logic_not";
|
|
398
|
+
pseudoEvidence = `Primitive "${primName}" found but its immediate parent is ${parentType ?? "root"} (expected ${expected}). The primitive is present but does NOT actually prevent reentrancy — this is a false sense of security.`;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const children = [];
|
|
402
|
+
if (Array.isArray(node.nodes))
|
|
403
|
+
children.push(...node.nodes);
|
|
404
|
+
if (node.left)
|
|
405
|
+
children.push(node.left);
|
|
406
|
+
if (node.right)
|
|
407
|
+
children.push(node.right);
|
|
408
|
+
if (node.node)
|
|
409
|
+
children.push(node.node);
|
|
410
|
+
if (node.vec)
|
|
411
|
+
children.push(node.vec);
|
|
412
|
+
if (node.target)
|
|
413
|
+
children.push(node.target);
|
|
414
|
+
if (node.object && typeof node.object === "object")
|
|
415
|
+
children.push(node.object);
|
|
416
|
+
if (Array.isArray(node.parameters))
|
|
417
|
+
children.push(...node.parameters);
|
|
418
|
+
for (const child of children) {
|
|
419
|
+
if (child && typeof child === "object") {
|
|
420
|
+
walk(child, node);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
walk(root, null);
|
|
425
|
+
if (foundProtected) {
|
|
426
|
+
return {
|
|
427
|
+
state: "protected",
|
|
428
|
+
evidence: `Anti-reentrancy primitive "${foundPrimitiveName}" is properly wrapped`,
|
|
429
|
+
foundPrimitiveName,
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (foundPrimitive) {
|
|
433
|
+
return {
|
|
434
|
+
state: "pseudo",
|
|
435
|
+
evidence: pseudoEvidence,
|
|
436
|
+
foundPrimitiveName,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
return {
|
|
440
|
+
state: "absent",
|
|
441
|
+
evidence: `No anti-reentrancy primitive found in root. Expected one of: ${primitiveNames.join(", ")}`,
|
|
442
|
+
};
|
|
443
|
+
}
|
|
34
444
|
const RISK_RULES = [
|
|
35
445
|
{
|
|
36
446
|
id: "R-C1-01",
|
|
@@ -156,57 +566,74 @@ const RISK_RULES = [
|
|
|
156
566
|
{
|
|
157
567
|
id: "R-C2-02",
|
|
158
568
|
data_source_class: "type2_witness_derived",
|
|
159
|
-
trigger: "Guard uses witness but source object's object_type may not match the witness source type",
|
|
569
|
+
trigger: "Guard uses witness but source object's object_type may not match the witness source type, or multi-hop chain has missing intermediate objects",
|
|
160
570
|
check: (ctx) => {
|
|
161
|
-
const
|
|
162
|
-
|
|
571
|
+
const pairs = extractWitnessIdentifierPairs(ctx.root);
|
|
572
|
+
const validPairs = pairs.filter((p) => p.code >= 100 && p.code <= 108);
|
|
573
|
+
if (validPairs.length === 0)
|
|
163
574
|
return null;
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
const rootStr = JSON.stringify(ctx.root);
|
|
170
|
-
const mismatches = [];
|
|
171
|
-
for (const code of codes) {
|
|
172
|
-
const expectedSource = witnessSourceTypes[code];
|
|
173
|
-
if (!expectedSource)
|
|
575
|
+
const hardMismatches = [];
|
|
576
|
+
const missingAnnotations = [];
|
|
577
|
+
for (const { code, identifier } of validPairs) {
|
|
578
|
+
const chain = WITNESS_CHAIN[code];
|
|
579
|
+
if (!chain)
|
|
174
580
|
continue;
|
|
175
|
-
const
|
|
176
|
-
const
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
581
|
+
const expectedSource = chain[0];
|
|
582
|
+
const entry = ctx.table.find((e) => e.identifier === identifier);
|
|
583
|
+
if (!entry)
|
|
584
|
+
continue;
|
|
585
|
+
const chainStr = `${code} (${chain.join("→")})`;
|
|
586
|
+
if (entry.object_type) {
|
|
587
|
+
if (entry.object_type !== expectedSource) {
|
|
588
|
+
hardMismatches.push(`witness ${chainStr} references #${identifier} ` +
|
|
589
|
+
`but object_type=${entry.object_type} (expected ${expectedSource})`);
|
|
590
|
+
}
|
|
181
591
|
}
|
|
182
|
-
|
|
183
|
-
|
|
592
|
+
else {
|
|
593
|
+
missingAnnotations.push(`witness ${chainStr} references #${identifier} ` +
|
|
594
|
+
`but object_type is not set (expected ${expectedSource})`);
|
|
184
595
|
}
|
|
185
|
-
|
|
186
|
-
const
|
|
187
|
-
|
|
188
|
-
|
|
596
|
+
if (chain.length === 3) {
|
|
597
|
+
const intermediateType = chain[1];
|
|
598
|
+
const hasIntermediate = ctx.table.some((e) => e.object_type === intermediateType);
|
|
599
|
+
if (!hasIntermediate) {
|
|
600
|
+
missingAnnotations.push(`witness ${chainStr} multi-hop chain expects intermediate ` +
|
|
601
|
+
`${intermediateType} in table, but no entry with object_type=${intermediateType} found`);
|
|
189
602
|
}
|
|
190
603
|
}
|
|
191
604
|
}
|
|
192
|
-
|
|
605
|
+
const totalIssues = hardMismatches.length + missingAnnotations.length;
|
|
606
|
+
if (totalIssues === 0)
|
|
193
607
|
return null;
|
|
608
|
+
const level = hardMismatches.length > 0 ? "medium" : "low";
|
|
609
|
+
const evidenceParts = [
|
|
610
|
+
...hardMismatches,
|
|
611
|
+
...missingAnnotations,
|
|
612
|
+
];
|
|
194
613
|
return {
|
|
195
614
|
id: "R-C2-02",
|
|
196
615
|
data_source_class: "type2_witness_derived",
|
|
197
|
-
level
|
|
616
|
+
level,
|
|
198
617
|
category: "data_source_trust",
|
|
199
|
-
title:
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
"
|
|
618
|
+
title: hardMismatches.length > 0
|
|
619
|
+
? "witness source object_type explicitly contradicts the witness derivation chain"
|
|
620
|
+
: "witness source object_type annotation missing or multi-hop chain incomplete",
|
|
621
|
+
description: "Guard uses witness derivation, but the source object's object_type in table is either " +
|
|
622
|
+
"inconsistent with the witness expected source type, or the annotation is missing " +
|
|
623
|
+
"(preventing validation), or the multi-hop chain lacks an intermediate object.\n" +
|
|
624
|
+
evidenceParts.map((e) => ` - ${e}`).join("\n") +
|
|
625
|
+
"\nNote: object_type is an auxiliary annotation field on table entries; it is not strictly " +
|
|
626
|
+
"validated at on-chain creation. However, wrong or missing annotations cause semantic " +
|
|
627
|
+
"misunderstanding and maintenance difficulty, and may indicate a deeper structural mismatch.",
|
|
204
628
|
affected_stakeholders: ["provider"],
|
|
205
|
-
scenario:
|
|
206
|
-
|
|
207
|
-
"
|
|
208
|
-
|
|
209
|
-
|
|
629
|
+
scenario: hardMismatches.length > 0
|
|
630
|
+
? "Type annotation mismatch: witness source type annotation is wrong, causing semantic confusion"
|
|
631
|
+
: "Type annotation missing: witness source type cannot be validated, raising maintenance risk",
|
|
632
|
+
mitigation: "1) Set or fix the table entry's object_type field to match the witness source type;\n" +
|
|
633
|
+
"2) witness 100-102 source type is Order; 103 is Progress; 104-108 is Arb;\n" +
|
|
634
|
+
"3) For multi-hop witnesses (106-108), ensure the intermediate Order object is also in the table;\n" +
|
|
635
|
+
"4) Use wowok_buildin_info(info='guard instructions') to query the full witness definition",
|
|
636
|
+
evidence: evidenceParts.join("; "),
|
|
210
637
|
};
|
|
211
638
|
},
|
|
212
639
|
},
|
|
@@ -290,6 +717,12 @@ const RISK_RULES = [
|
|
|
290
717
|
return null;
|
|
291
718
|
if (ctx.scene?.binding_field === "voting_guard")
|
|
292
719
|
return null;
|
|
720
|
+
if (ctx.scene?.id === "service_order_allocators_guard" &&
|
|
721
|
+
ctx.sharing_recipients &&
|
|
722
|
+
ctx.sharing_recipients.length > 0 &&
|
|
723
|
+
!ctx.sharing_recipients.includes("Signer")) {
|
|
724
|
+
return null;
|
|
725
|
+
}
|
|
293
726
|
if (!hasSignerCheck(ctx)) {
|
|
294
727
|
if (ctx.scene?.id === "machine_forward_guard") {
|
|
295
728
|
return {
|
|
@@ -436,6 +869,170 @@ const RISK_RULES = [
|
|
|
436
869
|
};
|
|
437
870
|
},
|
|
438
871
|
},
|
|
872
|
+
{
|
|
873
|
+
id: "R-C3-05",
|
|
874
|
+
data_source_class: "type3_submitted_object",
|
|
875
|
+
trigger: "Guard accepts a runtime-submitted Order/Progress/Arb object (Type3) but does NOT verify the submitted object belongs to the current project (Machine or Service). " +
|
|
876
|
+
"The WoWok runtime does NOT enforce any binding between the submitted order_id and the actual Progress being forwarded — verify_guard receives only the caller-provided submission bytes, never the Progress object under advancement (passport.move:27,251-253; progress.move:458-483). " +
|
|
877
|
+
"A caller can therefore submit an unrelated order_id from a DIFFERENT project while forwarding a different Progress, causing witness=100 (Order->Progress) to evaluate against foreign data.",
|
|
878
|
+
check: (ctx) => {
|
|
879
|
+
const submittedAddrs = ctx.table.filter((e) => e.b_submission && e.value_type === "Address");
|
|
880
|
+
if (submittedAddrs.length === 0)
|
|
881
|
+
return null;
|
|
882
|
+
const witnessCodes = extractWitnessCodes(ctx.root);
|
|
883
|
+
const hasOrderWitness = witnessCodes.some((c) => [100, 101, 102].includes(c));
|
|
884
|
+
const hasProgressWitness = witnessCodes.some((c) => [103].includes(c));
|
|
885
|
+
const hasArbWitness = witnessCodes.some((c) => [104, 105, 106, 107, 108].includes(c));
|
|
886
|
+
const submittedOrders = submittedAddrs.filter((e) => e.object_type === "Order" || (hasOrderWitness && !e.object_type));
|
|
887
|
+
const submittedProgress = submittedAddrs.filter((e) => e.object_type === "Progress" || (hasProgressWitness && !e.object_type));
|
|
888
|
+
const submittedArbs = submittedAddrs.filter((e) => e.object_type === "Arb" || (hasArbWitness && !e.object_type));
|
|
889
|
+
const hasProjectRelevantSubmission = submittedOrders.length > 0 ||
|
|
890
|
+
submittedProgress.length > 0 ||
|
|
891
|
+
submittedArbs.length > 0;
|
|
892
|
+
if (!hasProjectRelevantSubmission)
|
|
893
|
+
return null;
|
|
894
|
+
const affectedSceneIds = [
|
|
895
|
+
"machine_forward_guard",
|
|
896
|
+
"service_order_allocators_guard",
|
|
897
|
+
"arbitration_usage_guard",
|
|
898
|
+
"arbitration_voting_guard",
|
|
899
|
+
"reward_guard",
|
|
900
|
+
"progress_submission_guard",
|
|
901
|
+
];
|
|
902
|
+
const sceneId = ctx.scene?.id;
|
|
903
|
+
const isAffectedScene = !sceneId || affectedSceneIds.includes(sceneId);
|
|
904
|
+
if (!isAffectedScene)
|
|
905
|
+
return null;
|
|
906
|
+
const PROJECT_BINDING_QUERY_IDS = new Set([1563, 1560, 1250, 1402, 1401]);
|
|
907
|
+
const rootQueryIds = extractQueryIds(ctx.root);
|
|
908
|
+
const hasProjectBinding = rootQueryIds.some((id) => PROJECT_BINDING_QUERY_IDS.has(id));
|
|
909
|
+
if (hasProjectBinding)
|
|
910
|
+
return null;
|
|
911
|
+
const criticalScenes = new Set([
|
|
912
|
+
"machine_forward_guard",
|
|
913
|
+
"service_order_allocators_guard",
|
|
914
|
+
]);
|
|
915
|
+
const level = sceneId && criticalScenes.has(sceneId)
|
|
916
|
+
? "critical"
|
|
917
|
+
: "high";
|
|
918
|
+
const unboundEntries = [];
|
|
919
|
+
for (const e of submittedOrders) {
|
|
920
|
+
unboundEntries.push(`#${e.identifier}${e.object_type ? "(Order)" : "(inferred Order via witness 100-102)"}`);
|
|
921
|
+
}
|
|
922
|
+
for (const e of submittedProgress) {
|
|
923
|
+
unboundEntries.push(`#${e.identifier}${e.object_type ? "(Progress)" : "(inferred Progress via witness 103)"}`);
|
|
924
|
+
}
|
|
925
|
+
for (const e of submittedArbs) {
|
|
926
|
+
unboundEntries.push(`#${e.identifier}${e.object_type ? "(Arb)" : "(inferred Arb via witness 104-108)"}`);
|
|
927
|
+
}
|
|
928
|
+
const sceneLabel = sceneId ?? "unknown scene (conservative flag)";
|
|
929
|
+
return {
|
|
930
|
+
id: "R-C3-05",
|
|
931
|
+
data_source_class: "type3_submitted_object",
|
|
932
|
+
level,
|
|
933
|
+
category: "submission_forgery",
|
|
934
|
+
title: "Submitted Order/Progress/Arb is NOT verified to belong to the current project (cross-project bypass — CRITICAL trust-boundary gap)",
|
|
935
|
+
description: `Guard accepts runtime-submitted Order/Progress/Arb object(s) [${unboundEntries.join(", ")}] but the root does NOT verify the submitted object belongs to the current project (no query of order.service/order.machine/progress.machine/arb.service compared to a project constant).\n\n` +
|
|
936
|
+
"TRUST-BOUNDARY GAP (verified via Move source):\n" +
|
|
937
|
+
" - The Passport submission is built by the CALLER, not auto-injected by the runtime (passport.move:120,149,157)\n" +
|
|
938
|
+
" - verify_guard receives ONLY the caller-provided submission bytes — it never sees the Progress/Order actually being forwarded (passport.move:27,251-253)\n" +
|
|
939
|
+
" - next_with_passport performs NO linkage check between the passport submission and the Progress under advancement (progress.move:458-483)\n" +
|
|
940
|
+
" - Even order_next_with_passport's E_ORDER_NOT_MATCH only checks the separate `order:&Order` parameter, NOT the order_id embedded in the passport submission (progress.move:452)\n\n" +
|
|
941
|
+
`EXPLOIT (scene=${sceneLabel}):\n` +
|
|
942
|
+
" 1. Attacker holds Order Y from project B (where progress.current_time is old enough to pass the time-lock)\n" +
|
|
943
|
+
" 2. Attacker calls forward on Order X's Progress (project A, freshly entered the node)\n" +
|
|
944
|
+
" 3. Attacker submits Order Y's ID in the passport (lying about which order is being forwarded)\n" +
|
|
945
|
+
" 4. Guard's witness=100 reads Order Y's Progress -> time-lock passes (Order Y is old)\n" +
|
|
946
|
+
" 5. forward actually executes on Order X's Progress -> TIME-LOCK BYPASSED\n\n" +
|
|
947
|
+
"This is distinct from R-C3-01 (Signer binding — WHO can submit) and R-C3-03 (type matching — WHAT type is submitted). " +
|
|
948
|
+
"R-C3-05 addresses WHETHER the submitted object belongs to the current project — a third orthogonal axis of submission trust. " +
|
|
949
|
+
"Even with R-C3-01 satisfied (authorized Signer) and R-C3-03 satisfied (correct type), the Signer can still submit an unrelated project's object.",
|
|
950
|
+
affected_stakeholders: ["customer", "provider", "arbitrator"],
|
|
951
|
+
scenario: "Cross-project bypass: authorized signer submits an unrelated project's order/progress/arb to bypass time-lock, status, or allocation conditions",
|
|
952
|
+
mitigation: "Add a project-binding check to the root (wrap existing root in logic_and):\n" +
|
|
953
|
+
" - For submitted Order: add logic_equal[query(1563: order.service), identifier[N](project_service_address)] — RECOMMENDED (service is the canonical project anchor)\n" +
|
|
954
|
+
" - For submitted Order: add logic_equal[query(1560: order.machine), identifier[N](project_machine_address)] — alternative (machine anchor)\n" +
|
|
955
|
+
" - For submitted Progress: add logic_equal[query(1250: progress.machine), identifier[N](project_machine_address)]\n" +
|
|
956
|
+
" - For submitted Arb: add logic_equal[query(1402: arb.service), identifier[N](project_service_address)]\n" +
|
|
957
|
+
" - Use witness=102 (TypeOrderService) on the submitted Order to derive the Service, then compare to the project service constant\n\n" +
|
|
958
|
+
"Intentional cross-project exception: if the Guard is DESIGNED to accept cross-project submissions (rare), document this explicitly in the description and add a binding_hint annotation 'cross_project_intentional:true' to suppress this rule.",
|
|
959
|
+
evidence: `Unbound submitted project-relevant entries: ${unboundEntries.join(", ")}. ` +
|
|
960
|
+
`Scene: ${sceneLabel}. ` +
|
|
961
|
+
`Missing project-binding queries: none of [1563(order.service), 1560(order.machine), 1250(progress.machine), 1402(arb.service)] present in root.`,
|
|
962
|
+
};
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
{
|
|
966
|
+
id: "R-C3-06",
|
|
967
|
+
data_source_class: "type3_submitted_object",
|
|
968
|
+
trigger: "Guard is bound to Service order_allocators (service_order_allocators_guard scene). " +
|
|
969
|
+
"In this scene the Guard decides IF allocation happens, while sharing.who decides WHERE funds go. " +
|
|
970
|
+
"If sharing.who=Signer, the transaction caller receives the funds — so a Guard that does NOT bind " +
|
|
971
|
+
"context(Signer) lets ANYONE who passes the Guard steal 100% of the order funds. " +
|
|
972
|
+
"This is a Guard+sharing coupling risk: neither piece alone reveals the theft — only their combination.",
|
|
973
|
+
check: (ctx) => {
|
|
974
|
+
if (ctx.scene?.id !== "service_order_allocators_guard")
|
|
975
|
+
return null;
|
|
976
|
+
if (hasSignerCheck(ctx))
|
|
977
|
+
return null;
|
|
978
|
+
const recipients = ctx.sharing_recipients;
|
|
979
|
+
const sharingProvided = recipients !== undefined && recipients.length > 0;
|
|
980
|
+
const hasSignerSharing = sharingProvided && recipients.includes("Signer");
|
|
981
|
+
if (sharingProvided && !hasSignerSharing)
|
|
982
|
+
return null;
|
|
983
|
+
const level = hasSignerSharing ? "critical" : "medium";
|
|
984
|
+
const recipientsLabel = sharingProvided
|
|
985
|
+
? `[${recipients.join(", ")}]`
|
|
986
|
+
: "(not provided — conservative flag)";
|
|
987
|
+
const titleSuffix = hasSignerSharing
|
|
988
|
+
? "sharing.who=Signer confirmed — anyone can steal 100% of funds"
|
|
989
|
+
: "if sharing.who=Signer, anyone can steal 100% of funds";
|
|
990
|
+
return {
|
|
991
|
+
id: "R-C3-06",
|
|
992
|
+
data_source_class: "type3_submitted_object",
|
|
993
|
+
level,
|
|
994
|
+
category: "binding_risk",
|
|
995
|
+
title: `allocators Guard without Signer binding — ${titleSuffix}`,
|
|
996
|
+
description: `Guard is bound to Service order_allocators but does NOT verify context(Signer). ` +
|
|
997
|
+
`In the allocators scene, sharing.who determines the fund recipient:\n` +
|
|
998
|
+
` - sharing.who=Signer → funds flow to the CALLER (transaction signer)\n` +
|
|
999
|
+
` - sharing.who=Entity → funds flow to a FIXED address (Treasury/personal)\n` +
|
|
1000
|
+
` - sharing.who=GuardIdentifier → funds flow to a Guard-table-derived address\n\n` +
|
|
1001
|
+
`When sharing.who=Signer and the Guard does not bind Signer, ANY caller who passes the Guard ` +
|
|
1002
|
+
`receives the order funds — a direct theft. Even an authorized caller is unnecessary: the attacker ` +
|
|
1003
|
+
`only needs to pass the Guard's condition (e.g. submit any completed order_id), then the funds go to ` +
|
|
1004
|
+
`their own address.\n\n` +
|
|
1005
|
+
`This is distinct from R-C3-01 (generic Signer binding) and R-C3-05 (cross-project bypass). ` +
|
|
1006
|
+
`R-C3-06 addresses the allocators-specific Guard+sharing coupling: the SAFE design uses ` +
|
|
1007
|
+
`sharing.who=Entity so the recipient is fixed, making Guard Signer binding unnecessary.\n\n` +
|
|
1008
|
+
`sharing_recipients hint: ${recipientsLabel}`,
|
|
1009
|
+
affected_stakeholders: ["provider", "customer"],
|
|
1010
|
+
scenario: "Fund theft: attacker passes the allocators Guard (e.g. submits a completed order_id) and " +
|
|
1011
|
+
"sharing.who=Signer routes 100% of the order funds to the attacker's address",
|
|
1012
|
+
mitigation: "TWO mitigation options:\n" +
|
|
1013
|
+
" (A) Traditional: add context(Signer) binding to the Guard root " +
|
|
1014
|
+
"(logic_equal[context(Signer), identifier[N](authorized_address)]) so only the authorized " +
|
|
1015
|
+
"recipient can trigger allocation.\n" +
|
|
1016
|
+
" (B) PREFERRED: use sharing.who=Entity (Treasury object or fixed personal address) in the " +
|
|
1017
|
+
"Allocator's sharing config. Funds then always flow to the designated recipient regardless of " +
|
|
1018
|
+
"who triggers allocation — the Guard does NOT need to bind Signer. This is more robust because " +
|
|
1019
|
+
"it does not depend on Guard correctness.\n\n" +
|
|
1020
|
+
"Design checklist when using sharing.who=Entity:\n" +
|
|
1021
|
+
" - If Entity points to a Treasury object, verify the Treasury's permission is consistent with " +
|
|
1022
|
+
"the Service's permission (different permissions = different permission organizations = minor risk).\n" +
|
|
1023
|
+
" - allocators require unique guard addresses: create separate Guard objects (identical root/table, " +
|
|
1024
|
+
"different descriptions) for each allocator.\n" +
|
|
1025
|
+
" - First-match-wins: only the first Allocator whose Guard passes executes; multiple allocators " +
|
|
1026
|
+
"represent alternative strategies, not a simultaneous split.\n" +
|
|
1027
|
+
" - Still add order.service project binding (query 1563) to prevent cross-project bypass (R-C3-05) " +
|
|
1028
|
+
"triggering premature allocation.",
|
|
1029
|
+
evidence: `Scene: service_order_allocators_guard. ` +
|
|
1030
|
+
`Guard binds Signer: no. ` +
|
|
1031
|
+
`sharing_recipients: ${recipientsLabel}. ` +
|
|
1032
|
+
`Level rationale: ${hasSignerSharing ? "Signer sharing confirmed → critical" : "sharing hint absent → conservative medium"}.`,
|
|
1033
|
+
};
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
439
1036
|
{
|
|
440
1037
|
id: "R-C4-01",
|
|
441
1038
|
data_source_class: "type4_system_context",
|
|
@@ -488,7 +1085,7 @@ const RISK_RULES = [
|
|
|
488
1085
|
category: "system_context_risk",
|
|
489
1086
|
title: "Clock time-lock should have tolerance to avoid boundary invalidation from validator block-time variance",
|
|
490
1087
|
description: "Guard uses context(Clock) for time-lock comparison. Clock comes from the timestamp_ms field (U64 millisecond timestamp) of system shared object 0x6.\n" +
|
|
491
|
-
"The
|
|
1088
|
+
"The Wow on-chain timestamp is produced by validator consensus and has a slight deviation from real time (usually < 1 second), " +
|
|
492
1089
|
"and block-time variance may cause timestamp jumps. Using precise boundaries (>= or ==) may cause:\n" +
|
|
493
1090
|
" - The time-lock expiration transaction is packaged exactly at the moment but fails due to slight timestamp deviation\n" +
|
|
494
1091
|
" - Or conversely, slight deviation causes the time-lock to pass early\n" +
|
|
@@ -530,10 +1127,63 @@ const RISK_RULES = [
|
|
|
530
1127
|
};
|
|
531
1128
|
},
|
|
532
1129
|
},
|
|
1130
|
+
{
|
|
1131
|
+
id: "R-C4-04",
|
|
1132
|
+
data_source_class: "type4_system_context",
|
|
1133
|
+
trigger: "Guard uses logic_equal[context(Signer), identifier[N](fixed_address)] — Level 1 strict single-identity binding. " +
|
|
1134
|
+
"Only ONE address can pass the Guard, and because Guards are immutable, changing the authorized address requires " +
|
|
1135
|
+
"rebuilding the Guard and migrating all references. This creates a convenience trade-off: maximum security but " +
|
|
1136
|
+
"maximum inflexibility (key loss or personnel change permanently blocks the operation).",
|
|
1137
|
+
check: (ctx) => {
|
|
1138
|
+
if (!hasSignerCheck(ctx))
|
|
1139
|
+
return null;
|
|
1140
|
+
if (isSignerCheckInLogicOr(ctx.root))
|
|
1141
|
+
return null;
|
|
1142
|
+
const detection = detectLevel1StrictBinding(ctx.root, ctx.table);
|
|
1143
|
+
if (!detection.detected || !detection.entry)
|
|
1144
|
+
return null;
|
|
1145
|
+
const entry = detection.entry;
|
|
1146
|
+
return {
|
|
1147
|
+
id: "R-C4-04",
|
|
1148
|
+
data_source_class: "type4_system_context",
|
|
1149
|
+
level: "info",
|
|
1150
|
+
category: "system_context_risk",
|
|
1151
|
+
title: "Level 1 strict single-identity Signer binding — convenience trade-off (consider Level 2/3 alternatives)",
|
|
1152
|
+
description: `Guard uses logic_equal[context(Signer), identifier[${entry.identifier}](fixed address)] — only the address "${entry.value}" can pass.\n` +
|
|
1153
|
+
"This is Level 1 strict single-identity binding: maximally secure but maximally inconvenient.\n" +
|
|
1154
|
+
"Guards are immutable on-chain. If the authorized address becomes unavailable (key loss, personnel change, " +
|
|
1155
|
+
"role rotation), the Guard permanently blocks the operation — the only recovery is to build a new Guard " +
|
|
1156
|
+
"and migrate ALL references (Machine forward guards, Service buy_guard, allocators, etc.).\n\n" +
|
|
1157
|
+
"Three verifier constraint levels trade off security vs convenience:\n" +
|
|
1158
|
+
" - Level 1 (current): strict single-identity — one address, no flexibility (AVOID unless justified)\n" +
|
|
1159
|
+
" - Level 2: identity-set — logic_or of multiple valid identities (e.g. order.owner OR order.agent; " +
|
|
1160
|
+
"permission.owner OR permission.admin). Recommended for role-based access.\n" +
|
|
1161
|
+
" - Level 3: scene-combined — verify whether Signer binding is even needed. Many scenes (allocators with " +
|
|
1162
|
+
"sharing.who=Entity, machine forward with permissionIndex) already ensure safety without Signer binding.\n\n" +
|
|
1163
|
+
"Use Level 1 only when: (a) the role is permanently tied to one address, AND (b) the designer explicitly " +
|
|
1164
|
+
"accepts the immutability lock-in risk. Otherwise prefer Level 2 or Level 3.",
|
|
1165
|
+
affected_stakeholders: ["provider"],
|
|
1166
|
+
scenario: "Immutability lock-in: authorized address becomes unavailable, Guard permanently blocks the operation, " +
|
|
1167
|
+
"requiring full Guard rebuild and reference migration",
|
|
1168
|
+
mitigation: "Consider these alternatives before settling on Level 1 strict binding:\n" +
|
|
1169
|
+
" (A) Level 2 identity-set (RECOMMENDED for role-based access): use logic_or to allow multiple valid identities.\n" +
|
|
1170
|
+
" - Order holder: logic_or[logic_equal[query(1562: order.owner), context(Signer)], query(1567: order.agent has, parameters: [context(Signer)])]\n" +
|
|
1171
|
+
" - Service provider: logic_or[logic_equal[query(1002: permission.owner), context(Signer)], query(1004: permission.admin has, parameters: [context(Signer)])]\n" +
|
|
1172
|
+
" - Dynamic permission (survives rotation): submit permission address, verify query(1488: service.permission) == submitted, then check 1002/1004\n" +
|
|
1173
|
+
" (B) Level 3 scene-combined: evaluate whether Signer binding is needed at all.\n" +
|
|
1174
|
+
" - If using allocators with sharing.who=Entity (Treasury): funds flow to a fixed recipient regardless of caller — no Signer binding needed (R-C3-06 safe)\n" +
|
|
1175
|
+
" - If using machine forward: permissionIndex already verifies operator identity — Signer binding may be redundant\n" +
|
|
1176
|
+
" (C) If Level 1 is truly required: document the justification in the Guard description and ensure a key-recovery / address-rotation plan exists.",
|
|
1177
|
+
evidence: `Level 1 strict binding detected: logic_equal[context(Signer), identifier[${entry.identifier}]] ` +
|
|
1178
|
+
`where identifier[${entry.identifier}] is a fixed Address constant (b_submission=false, value="${entry.value}"${entry.name ? `, name="${entry.name}"` : ""}). ` +
|
|
1179
|
+
`Not wrapped in logic_or (not Level 2 identity-set).`,
|
|
1180
|
+
};
|
|
1181
|
+
},
|
|
1182
|
+
},
|
|
533
1183
|
{
|
|
534
1184
|
id: "R-X1-01",
|
|
535
1185
|
data_source_class: "cross_type",
|
|
536
|
-
trigger: "Time-lock query
|
|
1186
|
+
trigger: "Time-lock uses logic_as_u256_greater_or_equal with a baseline query that has NO non-zero invariant (i.e. the query MAY legitimately return 0 when it succeeds), causing Clock >= 0 + timeout to always be true",
|
|
537
1187
|
check: (ctx) => {
|
|
538
1188
|
const str = rootStr(ctx);
|
|
539
1189
|
const hasGreaterOrEqual = str.includes("logic_as_u256_greater_or_equal");
|
|
@@ -542,46 +1192,79 @@ const RISK_RULES = [
|
|
|
542
1192
|
const hasQuery = str.includes('"query"');
|
|
543
1193
|
if (!(hasGreaterOrEqual && hasClock && hasCalcAdd && hasQuery))
|
|
544
1194
|
return null;
|
|
1195
|
+
if (rootAllQueriesNonZero(ctx.root))
|
|
1196
|
+
return null;
|
|
1197
|
+
const queryIds = extractQueryIds(ctx.root);
|
|
1198
|
+
const riskyQueries = queryIds.filter((id) => !QUERY_INVARIANT_MAP.has(id));
|
|
1199
|
+
const evidence = riskyQueries.length > 0
|
|
1200
|
+
? `Queries without non-zero invariant: ${riskyQueries
|
|
1201
|
+
.map((id) => {
|
|
1202
|
+
const q = GUARDQUERY.find((x) => x.id === id);
|
|
1203
|
+
return q ? `${q.id} (${q.name})` : `${id}`;
|
|
1204
|
+
})
|
|
1205
|
+
.join(", ")}`
|
|
1206
|
+
: "Baseline query may return 0";
|
|
545
1207
|
return {
|
|
546
1208
|
id: "R-X1-01",
|
|
547
1209
|
data_source_class: "cross_type",
|
|
548
1210
|
level: "medium",
|
|
549
1211
|
category: "logic_gap",
|
|
550
|
-
title: "Time-lock query may return 0 causing >= comparison to always be true",
|
|
551
|
-
description: "Guard uses logic_as_u256_greater_or_equal for time-lock comparison, and the compared baseline value comes from a query (Type1/2/3 data sources)." +
|
|
552
|
-
"If the query
|
|
553
|
-
"then Clock >= 0 + timeout is always true, the time-lock is effectively void, and the disadvantaged party can bypass the time constraint immediately
|
|
1212
|
+
title: "Time-lock baseline query may return 0 causing >= comparison to always be true",
|
|
1213
|
+
description: "Guard uses logic_as_u256_greater_or_equal for time-lock comparison, and the compared baseline value comes from a query (Type1/2/3 data sources). " +
|
|
1214
|
+
"If the query SUCCEEDS but its stored value is 0 (e.g. a count that is 0 in the empty initial state, a user-submitted value of 0, or a Some(0) from an Option field), " +
|
|
1215
|
+
"then Clock >= 0 + timeout is always true, the time-lock is effectively void, and the disadvantaged party can bypass the time constraint immediately.\n\n" +
|
|
1216
|
+
"IMPORTANT — 'VM error = Guard failure' baseline: " +
|
|
1217
|
+
"if a query targets an absent object, unset Option, or missing collection entry, the WoWok native runtime aborts with a VM error (e.g. W_FIELD_NOT_FOUND) " +
|
|
1218
|
+
"and the Guard fails as a whole (does NOT silently return 0). Therefore the null-bypass risk ONLY applies to queries whose stored value can legitimately be 0 " +
|
|
1219
|
+
"while the query itself succeeds. Such queries are exactly those WITHOUT an `invariant` annotation in the SDK GUARDQUERY catalog.",
|
|
554
1220
|
affected_stakeholders: ["customer", "provider"],
|
|
555
|
-
scenario: "Time-lock invalidation: query returns 0, making the >= condition always pass",
|
|
556
|
-
mitigation: "1) Use a query
|
|
557
|
-
"
|
|
558
|
-
"
|
|
559
|
-
"
|
|
1221
|
+
scenario: "Time-lock invalidation: baseline query returns 0 (not VM error), making the >= condition always pass",
|
|
1222
|
+
mitigation: "1) Use a query whose return is guaranteed non-zero (look for `invariant` in guard-ins.ts — e.g. progress.current_time id=1272 with invariant='clock_derived'); " +
|
|
1223
|
+
"2) Add a precondition check query > 0 before the time-lock (for queries without an invariant, e.g. user-submitted baselines); " +
|
|
1224
|
+
"3) Avoid using count/index/find/user-submitted values as time-lock baselines — these can all legitimately return 0; " +
|
|
1225
|
+
"4) Distinguish 'VM error' (safe — Guard fails) from 'query returns 0' (unsafe — Guard bypassed). Only the latter is a null-bypass risk.",
|
|
1226
|
+
evidence,
|
|
560
1227
|
};
|
|
561
1228
|
},
|
|
562
1229
|
},
|
|
563
1230
|
{
|
|
564
1231
|
id: "R-X1-02",
|
|
565
1232
|
data_source_class: "cross_type",
|
|
566
|
-
trigger: "Guard uses logic_as_u256_greater
|
|
1233
|
+
trigger: "Guard uses logic_as_u256_greater with a query operand that has NO non-zero invariant (i.e. the query MAY legitimately return 0 when it succeeds), and the comparison does not distinguish 'query succeeded with 0' from 'query failed'",
|
|
567
1234
|
check: (ctx) => {
|
|
568
1235
|
const str = rootStr(ctx);
|
|
569
1236
|
if (!(str.includes("logic_as_u256_greater") && str.includes('"query"')))
|
|
570
1237
|
return null;
|
|
1238
|
+
if (rootAllQueriesNonZero(ctx.root))
|
|
1239
|
+
return null;
|
|
1240
|
+
const queryIds = extractQueryIds(ctx.root);
|
|
1241
|
+
const riskyQueries = queryIds.filter((id) => !QUERY_INVARIANT_MAP.has(id));
|
|
1242
|
+
const evidence = riskyQueries.length > 0
|
|
1243
|
+
? `Queries without non-zero invariant: ${riskyQueries
|
|
1244
|
+
.map((id) => {
|
|
1245
|
+
const q = GUARDQUERY.find((x) => x.id === id);
|
|
1246
|
+
return q ? `${q.id} (${q.name})` : `${id}`;
|
|
1247
|
+
})
|
|
1248
|
+
.join(", ")}`
|
|
1249
|
+
: "Query may return 0";
|
|
571
1250
|
return {
|
|
572
1251
|
id: "R-X1-02",
|
|
573
1252
|
data_source_class: "cross_type",
|
|
574
1253
|
level: "medium",
|
|
575
1254
|
category: "logic_gap",
|
|
576
|
-
title: "Numeric comparison
|
|
577
|
-
description: "Guard uses logic_as_u256_greater for numeric comparison, and operands
|
|
578
|
-
"If the query
|
|
579
|
-
"
|
|
1255
|
+
title: "Numeric comparison with a query that may legitimately return 0",
|
|
1256
|
+
description: "Guard uses logic_as_u256_greater for numeric comparison, and one of the operands comes from a query (Type1/2/3 data sources) that has NO non-zero invariant annotation. " +
|
|
1257
|
+
"If the query SUCCEEDS but its stored value is 0 (e.g. a count that is 0 in the empty initial state, a user-submitted value of 0, or a Some(0) from an Option field), " +
|
|
1258
|
+
"the comparison may produce an unexpected result (e.g. `query > 0` fails, or `query > some_threshold` is silently bypassed).\n\n" +
|
|
1259
|
+
"IMPORTANT — 'VM error = Guard failure' baseline: " +
|
|
1260
|
+
"if a query targets absent data (e.g. object does not exist, Option is None, collection entry is missing), the WoWok native runtime aborts with a VM error " +
|
|
1261
|
+
"(not 0). The Guard fails as a whole (does NOT silently return 0). Therefore this rule only flags queries whose stored value can be 0 while the query succeeds.",
|
|
580
1262
|
affected_stakeholders: ["customer", "provider"],
|
|
581
|
-
scenario: "Null misjudgment: query
|
|
582
|
-
mitigation: "1)
|
|
583
|
-
"2)
|
|
584
|
-
"3)
|
|
1263
|
+
scenario: "Null misjudgment: query succeeds with value 0, causing > 0 condition to unexpectedly fail (or > threshold to be bypassed)",
|
|
1264
|
+
mitigation: "1) Prefer queries with an `invariant` annotation (e.g. clock_derived / positive_lower_bound); " +
|
|
1265
|
+
"2) Add a precondition check (query > 0) before the main comparison when using count/index/find/user-submitted baselines; " +
|
|
1266
|
+
"3) Distinguish 'VM error' (safe — Guard fails) from 'query returns 0' (potentially unsafe). Only the latter is a null-bypass risk.",
|
|
1267
|
+
evidence,
|
|
585
1268
|
};
|
|
586
1269
|
},
|
|
587
1270
|
},
|
|
@@ -918,7 +1601,93 @@ const RISK_RULES = [
|
|
|
918
1601
|
};
|
|
919
1602
|
},
|
|
920
1603
|
},
|
|
1604
|
+
{
|
|
1605
|
+
id: "R-X1-14",
|
|
1606
|
+
data_source_class: "cross_type",
|
|
1607
|
+
trigger: "Scene-specific reentrancy: Guard pass triggers a state-changing side effect (fund distribution, vote count, Arb creation) that does NOT naturally invalidate the Guard condition. " +
|
|
1608
|
+
"Without an anti-reentrancy primitive, the same Signer can repeatedly pass the Guard. " +
|
|
1609
|
+
"Additionally detects pseudo-protection: primitive present but not properly wrapped in logic_not (for Bool queries) or logic_equal(0) (for count queries).",
|
|
1610
|
+
check: (ctx) => {
|
|
1611
|
+
const spec = REENTRANCY_SPECS.find((s) => s.sceneId === ctx.scene?.id);
|
|
1612
|
+
if (!spec)
|
|
1613
|
+
return null;
|
|
1614
|
+
const protection = detectReentrancyProtection(ctx.root, ctx.table, spec);
|
|
1615
|
+
if (protection.state === "protected")
|
|
1616
|
+
return null;
|
|
1617
|
+
if (protection.state === "pseudo") {
|
|
1618
|
+
return {
|
|
1619
|
+
id: "R-X1-14",
|
|
1620
|
+
data_source_class: "cross_type",
|
|
1621
|
+
level: "medium",
|
|
1622
|
+
category: "logic_gap",
|
|
1623
|
+
title: `Reentrancy pseudo-protection: anti-reentrancy primitive present but not properly wrapped (${spec.sceneId})`,
|
|
1624
|
+
description: `Scene: ${spec.sceneId}\n` +
|
|
1625
|
+
`The Guard root contains an anti-reentrancy primitive (${protection.foundPrimitiveName}), but it is NOT properly wrapped.\n` +
|
|
1626
|
+
"Proper wrapping rules:\n" +
|
|
1627
|
+
" - Bool-returning primitives (exists-style): must be wrapped in logic_not\n" +
|
|
1628
|
+
" - U64-returning primitives (count-style): must be compared to 0 via logic_equal\n" +
|
|
1629
|
+
" - Address-returning primitives: must be wrapped in logic_not (None check)\n\n" +
|
|
1630
|
+
"WHY THIS IS DANGEROUS:\n" +
|
|
1631
|
+
" The developer likely intended to prevent reentrancy but the wrapping is incorrect, " +
|
|
1632
|
+
"so the primitive does NOT actually block repeated Guard passes. This is arguably MORE " +
|
|
1633
|
+
"dangerous than absence — code reviewers see the primitive and assume protection is in place.\n\n" +
|
|
1634
|
+
`Scene rationale: ${spec.rationale}`,
|
|
1635
|
+
affected_stakeholders: deriveStakeholders(spec.sceneId),
|
|
1636
|
+
scenario: `Developer added anti-reentrancy primitive but used wrong wrapping; reentrancy exploit still possible in ${spec.sceneId}`,
|
|
1637
|
+
mitigation: "Fix the wrapping:\n" +
|
|
1638
|
+
" - For exists-style (Bool) primitives: wrap in logic_not — { type: 'logic_not', node: { type: 'query', query: <primitive>, ... } }\n" +
|
|
1639
|
+
" - For count-style (U64) primitives: compare to 0 — { type: 'logic_equal', nodes: [ { type: 'query', query: <primitive>, ... }, identifier(N) ] } where table[N].value = 0\n" +
|
|
1640
|
+
" - For address-style primitives: wrap in logic_not (None check)\n" +
|
|
1641
|
+
`Then wrap the protection in logic_and with the existing root conditions.\n\n` +
|
|
1642
|
+
`Full mitigation for ${spec.sceneId}:\n${spec.mitigation}`,
|
|
1643
|
+
evidence: protection.evidence,
|
|
1644
|
+
};
|
|
1645
|
+
}
|
|
1646
|
+
const level = spec.level;
|
|
1647
|
+
const stakeholders = deriveStakeholders(spec.sceneId);
|
|
1648
|
+
return {
|
|
1649
|
+
id: "R-X1-14",
|
|
1650
|
+
data_source_class: "cross_type",
|
|
1651
|
+
level,
|
|
1652
|
+
category: "logic_gap",
|
|
1653
|
+
title: level === "info"
|
|
1654
|
+
? `Reentrancy reminder: ${spec.sceneId} has no anti-reentrancy primitive (LOW risk)`
|
|
1655
|
+
: `Reentrancy risk: ${spec.sceneId} has NO anti-reentrancy protection (${level.toUpperCase()})`,
|
|
1656
|
+
description: `Scene: ${spec.sceneId}\n` +
|
|
1657
|
+
`Reentrancy risk level: ${level.toUpperCase()}\n\n` +
|
|
1658
|
+
`RATIONALE:\n${spec.rationale}\n\n` +
|
|
1659
|
+
(protection.evidence
|
|
1660
|
+
? `DETECTION:\n${protection.evidence}\n\n`
|
|
1661
|
+
: "") +
|
|
1662
|
+
(level === "critical"
|
|
1663
|
+
? "IMPACT: Without protection, the same Signer can repeatedly trigger the side effect (fund distribution / vote counting), causing direct fund loss or result manipulation. THIS IS A CRITICAL VULNERABILITY — the Guard MUST be rebuilt with an anti-reentrancy primitive before deployment.\n\n"
|
|
1664
|
+
: level === "high"
|
|
1665
|
+
? "IMPACT: Without protection, the system can be abused (multiple dispute cases, arbitration harassment). Strongly recommended to add protection.\n\n"
|
|
1666
|
+
: "IMPACT: Low by default; only relevant if business intent requires single-action enforcement. Review the rationale above.\n\n"),
|
|
1667
|
+
affected_stakeholders: stakeholders,
|
|
1668
|
+
scenario: level === "info"
|
|
1669
|
+
? `${spec.sceneId} lacks anti-reentrancy primitive; may or may not be needed depending on business intent`
|
|
1670
|
+
: `${spec.sceneId} lacks anti-reentrancy primitive; same Signer can repeatedly trigger the side effect`,
|
|
1671
|
+
mitigation: spec.mitigation,
|
|
1672
|
+
evidence: protection.evidence,
|
|
1673
|
+
};
|
|
1674
|
+
},
|
|
1675
|
+
},
|
|
921
1676
|
];
|
|
1677
|
+
function deriveStakeholders(sceneId) {
|
|
1678
|
+
switch (sceneId) {
|
|
1679
|
+
case "reward_guard":
|
|
1680
|
+
return ["claimant", "provider"];
|
|
1681
|
+
case "arbitration_voting_guard":
|
|
1682
|
+
return ["customer", "provider", "arbitrator"];
|
|
1683
|
+
case "arbitration_usage_guard":
|
|
1684
|
+
return ["customer", "provider"];
|
|
1685
|
+
case "service_order_allocators_guard":
|
|
1686
|
+
return ["customer", "provider"];
|
|
1687
|
+
default:
|
|
1688
|
+
return ["provider"];
|
|
1689
|
+
}
|
|
1690
|
+
}
|
|
922
1691
|
export function assessGuardRisks(params) {
|
|
923
1692
|
const ctx = {
|
|
924
1693
|
table: params.table,
|
|
@@ -926,6 +1695,7 @@ export function assessGuardRisks(params) {
|
|
|
926
1695
|
rely: params.rely,
|
|
927
1696
|
scene: params.scene,
|
|
928
1697
|
operation_type: params.operation_type,
|
|
1698
|
+
sharing_recipients: params.sharing_recipients,
|
|
929
1699
|
};
|
|
930
1700
|
const risks = [];
|
|
931
1701
|
for (const rule of RISK_RULES) {
|