@decentrys/sentinel-sdk 0.1.0
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/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/browser/decentrys-sentinel.js +377 -0
- package/dist/browser/decentrys-sentinel.mjs +352 -0
- package/dist/client.d.ts +118 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +218 -0
- package/dist/client.js.map +1 -0
- package/dist/from-audit.d.ts +70 -0
- package/dist/from-audit.d.ts.map +1 -0
- package/dist/from-audit.js +207 -0
- package/dist/from-audit.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +77 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +27 -0
- package/dist/model.js.map +1 -0
- package/package.json +62 -0
- package/src/client.ts +289 -0
- package/src/from-audit.test.ts +131 -0
- package/src/from-audit.ts +253 -0
- package/src/index.ts +3 -0
- package/src/model.ts +85 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Decentrys Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# @decentrys/sentinel-sdk
|
|
2
|
+
|
|
3
|
+
Monitoring for deployed contracts, wallets and treasuries.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @decentrys/sentinel-sdk
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Use
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
import { Sentinel, rulesFromAudit } from '@decentrys/sentinel-sdk';
|
|
15
|
+
|
|
16
|
+
const sentinel = new Sentinel({ apiKey: 'dk_live_...' });
|
|
17
|
+
|
|
18
|
+
await sentinel.registerContract({ projectId, chain: 'ethereum', address });
|
|
19
|
+
|
|
20
|
+
// Turn what an audit learned into what the system watches.
|
|
21
|
+
for (const { rule } of rulesFromAudit({ projectId, capabilities })) {
|
|
22
|
+
await sentinel.createRule(rule);
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## It throws, deliberately
|
|
27
|
+
|
|
28
|
+
Unlike `@decentrys/protect`, this client raises errors. It is management —
|
|
29
|
+
registering a contract, writing a rule. Swallowing a failure would leave an
|
|
30
|
+
operator believing they are monitored when they are not, which is the more
|
|
31
|
+
dangerous silence.
|
|
32
|
+
|
|
33
|
+
`reportEvent()` is the exception: it runs on your hot path and returns a
|
|
34
|
+
boolean, because a monitoring call must not be able to fail the thing it is
|
|
35
|
+
monitoring.
|
|
36
|
+
|
|
37
|
+
## Audit handover
|
|
38
|
+
|
|
39
|
+
`rulesFromAudit()` watches the upgrade *happening*, never the contract *being*
|
|
40
|
+
upgradeable. A capability is not a fault. Capabilities with no plausible
|
|
41
|
+
response generate no rule, and `unmappedCapabilities()` tells you which — a
|
|
42
|
+
rule nobody can act on trains a team to close alerts unread.
|
|
43
|
+
|
|
44
|
+
**Secret keys only.** A publishable key is refused at construction.
|
|
45
|
+
|
|
46
|
+
## Licence
|
|
47
|
+
|
|
48
|
+
MIT © Decentrys Labs
|
|
49
|
+
|
|
50
|
+
## Links
|
|
51
|
+
|
|
52
|
+
- [decentrys.com](https://decentrys.com) · [SDK overview](https://decentrys.com/sdk) · [Developer API](https://decentrys.com/developers)
|
|
53
|
+
- Source: [github.com/teamdecentrys-byte/Decentrys](https://github.com/teamdecentrys-byte/Decentrys)
|
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var DecentrysSentinel = (() => {
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
20
|
+
|
|
21
|
+
// src/index.ts
|
|
22
|
+
var index_exports = {};
|
|
23
|
+
__export(index_exports, {
|
|
24
|
+
ALERT_SEVERITIES: () => ALERT_SEVERITIES,
|
|
25
|
+
SDK_VERSION: () => SDK_VERSION,
|
|
26
|
+
Sentinel: () => Sentinel,
|
|
27
|
+
SentinelError: () => SentinelError,
|
|
28
|
+
TARGET_TYPES: () => TARGET_TYPES,
|
|
29
|
+
rulesFromAudit: () => rulesFromAudit,
|
|
30
|
+
unmappedCapabilities: () => unmappedCapabilities
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
// src/model.ts
|
|
34
|
+
var ALERT_SEVERITIES = ["INFO", "LOW", "MEDIUM", "HIGH", "CRITICAL"];
|
|
35
|
+
var TARGET_TYPES = [
|
|
36
|
+
"CONTRACT",
|
|
37
|
+
"WALLET",
|
|
38
|
+
"TREASURY",
|
|
39
|
+
"MULTISIG",
|
|
40
|
+
"LP_POOL",
|
|
41
|
+
"ORACLE",
|
|
42
|
+
"BRIDGE",
|
|
43
|
+
"GOVERNANCE"
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// src/client.ts
|
|
47
|
+
var SDK_VERSION = "0.1.0";
|
|
48
|
+
var DEFAULT_BASE_URL = "https://api.decentrys.com";
|
|
49
|
+
var DEFAULT_TIMEOUT_MS = 1e4;
|
|
50
|
+
var SentinelError = class extends Error {
|
|
51
|
+
constructor(status, message, code) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.status = status;
|
|
54
|
+
this.code = code;
|
|
55
|
+
this.name = "SentinelError";
|
|
56
|
+
}
|
|
57
|
+
status;
|
|
58
|
+
code;
|
|
59
|
+
};
|
|
60
|
+
var Sentinel = class {
|
|
61
|
+
baseUrl;
|
|
62
|
+
apiKey;
|
|
63
|
+
timeoutMs;
|
|
64
|
+
fetchImpl;
|
|
65
|
+
constructor(config) {
|
|
66
|
+
if (!config.apiKey?.trim()) {
|
|
67
|
+
throw new Error("Sentinel: an apiKey is required. Create one at https://decentrys.com/developers.");
|
|
68
|
+
}
|
|
69
|
+
if (config.apiKey.startsWith("dk_pub_")) {
|
|
70
|
+
throw new Error(
|
|
71
|
+
"Sentinel: that is a publishable key. Monitoring is managed server-side with a secret key \u2014 a publishable key ships inside clients and cannot be trusted to configure detection."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
this.apiKey = config.apiKey;
|
|
75
|
+
this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
|
|
76
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
77
|
+
this.fetchImpl = config.fetch ?? resolveFetch();
|
|
78
|
+
}
|
|
79
|
+
// --- Targets ------------------------------------------------------------
|
|
80
|
+
/** Watch a deployed contract. */
|
|
81
|
+
registerContract(input) {
|
|
82
|
+
return this.addTarget(input, "CONTRACT");
|
|
83
|
+
}
|
|
84
|
+
registerWallet(input) {
|
|
85
|
+
return this.addTarget(input, "WALLET");
|
|
86
|
+
}
|
|
87
|
+
registerTreasury(input) {
|
|
88
|
+
return this.addTarget(input, "TREASURY");
|
|
89
|
+
}
|
|
90
|
+
registerTarget(input, targetType) {
|
|
91
|
+
return this.addTarget(input, targetType);
|
|
92
|
+
}
|
|
93
|
+
listTargets(projectId) {
|
|
94
|
+
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
|
95
|
+
return this.request("GET", `/v1/monitoring/targets${query}`);
|
|
96
|
+
}
|
|
97
|
+
async setTargetEnabled(targetId, enabled) {
|
|
98
|
+
return this.request("PATCH", `/v1/monitoring/targets/${targetId}`, { enabled });
|
|
99
|
+
}
|
|
100
|
+
async removeTarget(targetId) {
|
|
101
|
+
await this.request("DELETE", `/v1/monitoring/targets/${targetId}`);
|
|
102
|
+
}
|
|
103
|
+
// --- Rules --------------------------------------------------------------
|
|
104
|
+
createRule(input) {
|
|
105
|
+
return this.request("POST", "/v1/monitoring/rules", input);
|
|
106
|
+
}
|
|
107
|
+
updateRule(ruleId, changes) {
|
|
108
|
+
return this.request("PATCH", `/v1/monitoring/rules/${ruleId}`, changes);
|
|
109
|
+
}
|
|
110
|
+
async deleteRule(ruleId) {
|
|
111
|
+
await this.request("DELETE", `/v1/monitoring/rules/${ruleId}`);
|
|
112
|
+
}
|
|
113
|
+
listRules(projectId) {
|
|
114
|
+
const query = projectId ? `?projectId=${encodeURIComponent(projectId)}` : "";
|
|
115
|
+
return this.request("GET", `/v1/monitoring/rules${query}`);
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Evaluate a rule against facts without arming it.
|
|
119
|
+
*
|
|
120
|
+
* A rule that has never been tested against a fact set is a rule nobody
|
|
121
|
+
* knows the behaviour of, and finding out during an incident is the worst
|
|
122
|
+
* possible time.
|
|
123
|
+
*/
|
|
124
|
+
testRule(ruleId, facts) {
|
|
125
|
+
return this.request("POST", `/v1/monitoring/rules/${ruleId}/test`, { facts });
|
|
126
|
+
}
|
|
127
|
+
/** The vocabulary rules are built from: fields, operators, actions, templates. */
|
|
128
|
+
catalog() {
|
|
129
|
+
return this.request("GET", "/v1/monitoring/catalog");
|
|
130
|
+
}
|
|
131
|
+
// --- Alerts -------------------------------------------------------------
|
|
132
|
+
listAlerts(input = {}) {
|
|
133
|
+
const params = new URLSearchParams();
|
|
134
|
+
if (input.status) params.set("status", input.status);
|
|
135
|
+
if (input.severity) params.set("severity", input.severity);
|
|
136
|
+
if (input.limit) params.set("limit", String(input.limit));
|
|
137
|
+
const query = params.toString();
|
|
138
|
+
return this.request("GET", `/v1/alerts${query ? `?${query}` : ""}`);
|
|
139
|
+
}
|
|
140
|
+
getAlert(alertId) {
|
|
141
|
+
return this.request("GET", `/v1/alerts/${alertId}`);
|
|
142
|
+
}
|
|
143
|
+
acknowledgeAlert(alertId, note) {
|
|
144
|
+
return this.request("PATCH", `/v1/alerts/${alertId}`, {
|
|
145
|
+
status: "ACKNOWLEDGED",
|
|
146
|
+
...note ? { note } : {}
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
// --- Reporting ----------------------------------------------------------
|
|
150
|
+
/**
|
|
151
|
+
* Report an event from your own system.
|
|
152
|
+
*
|
|
153
|
+
* The one method here that never throws. It is called from a customer's hot
|
|
154
|
+
* path — a deploy script, a treasury movement, a governance execution — and
|
|
155
|
+
* a monitoring call must not be able to fail the thing it is monitoring.
|
|
156
|
+
* The boolean says whether it was recorded, so a caller who cares can check.
|
|
157
|
+
*/
|
|
158
|
+
async reportEvent(event) {
|
|
159
|
+
try {
|
|
160
|
+
await this.request("POST", "/v1/monitoring/events", event);
|
|
161
|
+
return true;
|
|
162
|
+
} catch {
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
// --- Internals ----------------------------------------------------------
|
|
167
|
+
addTarget(input, targetType) {
|
|
168
|
+
return this.request("POST", "/v1/monitoring/targets", {
|
|
169
|
+
projectId: input.projectId,
|
|
170
|
+
chainKey: input.chain,
|
|
171
|
+
address: input.address,
|
|
172
|
+
targetType,
|
|
173
|
+
...input.label ? { label: input.label } : {}
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
async request(method, path, body) {
|
|
177
|
+
const controller = new AbortController();
|
|
178
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
179
|
+
try {
|
|
180
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
181
|
+
method,
|
|
182
|
+
headers: {
|
|
183
|
+
"content-type": "application/json",
|
|
184
|
+
"x-api-key": this.apiKey,
|
|
185
|
+
"user-agent": `decentrys-sentinel/${SDK_VERSION}`
|
|
186
|
+
},
|
|
187
|
+
...body === void 0 ? {} : { body: JSON.stringify(body) },
|
|
188
|
+
signal: controller.signal
|
|
189
|
+
});
|
|
190
|
+
const text = await response.text();
|
|
191
|
+
if (!response.ok) {
|
|
192
|
+
const parsed2 = safeParse(text);
|
|
193
|
+
throw new SentinelError(
|
|
194
|
+
response.status,
|
|
195
|
+
typeof parsed2?.message === "string" ? parsed2.message : `Decentrys returned HTTP ${response.status}.`,
|
|
196
|
+
typeof parsed2?.code === "string" ? parsed2.code : void 0
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (!text) return void 0;
|
|
200
|
+
const parsed = safeParse(text);
|
|
201
|
+
if (parsed === null) throw new SentinelError(response.status, "The response was not valid JSON.");
|
|
202
|
+
return "data" in parsed && !("total" in parsed) && !("page" in parsed) ? parsed.data : parsed;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
if (error instanceof SentinelError) throw error;
|
|
205
|
+
if (controller.signal.aborted) {
|
|
206
|
+
throw new SentinelError(0, `No response within ${this.timeoutMs}ms.`);
|
|
207
|
+
}
|
|
208
|
+
throw new SentinelError(0, error instanceof Error ? error.message : "Request failed.");
|
|
209
|
+
} finally {
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
function safeParse(text) {
|
|
215
|
+
try {
|
|
216
|
+
const parsed = JSON.parse(text);
|
|
217
|
+
return typeof parsed === "object" && parsed !== null ? parsed : null;
|
|
218
|
+
} catch {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
function resolveFetch() {
|
|
223
|
+
const candidate = globalThis.fetch;
|
|
224
|
+
if (typeof candidate !== "function") {
|
|
225
|
+
throw new Error("Sentinel: no global fetch was found. Pass one via `new Sentinel({ fetch })`.");
|
|
226
|
+
}
|
|
227
|
+
return candidate.bind(globalThis);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/from-audit.ts
|
|
231
|
+
var DEFAULT_ACTIONS = [{ type: "ALERT", config: {} }];
|
|
232
|
+
var RULES = {
|
|
233
|
+
UPGRADEABLE: {
|
|
234
|
+
name: "Contract logic replaced",
|
|
235
|
+
description: "The proxy now points at different code. Every finding in the audit describes the previous implementation and stops applying the moment this fires.",
|
|
236
|
+
severity: "CRITICAL",
|
|
237
|
+
triggerType: "contract.upgraded",
|
|
238
|
+
conditions: { op: "eq", field: "upgrade.changed", value: true }
|
|
239
|
+
},
|
|
240
|
+
ADMIN_CONTROL: {
|
|
241
|
+
name: "Admin address changed",
|
|
242
|
+
description: "Whoever can upgrade or configure this contract is now a different account.",
|
|
243
|
+
severity: "CRITICAL",
|
|
244
|
+
triggerType: "contract.admin_changed",
|
|
245
|
+
conditions: { op: "eq", field: "admin.changed", value: true }
|
|
246
|
+
},
|
|
247
|
+
OWNERSHIP: {
|
|
248
|
+
name: "Ownership transferred",
|
|
249
|
+
description: "Owner-only functions identified during the audit are now controlled by someone else.",
|
|
250
|
+
severity: "HIGH",
|
|
251
|
+
triggerType: "contract.ownership_transferred",
|
|
252
|
+
conditions: { op: "eq", field: "ownership.changed", value: true }
|
|
253
|
+
},
|
|
254
|
+
MINT_AUTHORITY: {
|
|
255
|
+
name: "Supply increased",
|
|
256
|
+
description: "The mint authority the audit identified was used. Legitimate for many designs \u2014 this reports it so holders are not the last to know.",
|
|
257
|
+
severity: "HIGH",
|
|
258
|
+
triggerType: "token.supply_changed",
|
|
259
|
+
conditions: {
|
|
260
|
+
op: "AND",
|
|
261
|
+
conditions: [
|
|
262
|
+
{ op: "eq", field: "token.supplyIncreased", value: true },
|
|
263
|
+
// A percentage rather than an absolute: a fixed figure is meaningless
|
|
264
|
+
// across tokens with different supplies and decimals.
|
|
265
|
+
{ op: "gt", field: "token.supplyChangePercent", value: 1 }
|
|
266
|
+
]
|
|
267
|
+
}
|
|
268
|
+
},
|
|
269
|
+
PAUSABLE: {
|
|
270
|
+
name: "Pause state changed",
|
|
271
|
+
description: "The contract was paused or unpaused. Pausing is usually a defence, and knowing it happened is how you find out an incident started without you.",
|
|
272
|
+
severity: "HIGH",
|
|
273
|
+
triggerType: "contract.paused",
|
|
274
|
+
conditions: { op: "eq", field: "contract.pauseChanged", value: true }
|
|
275
|
+
},
|
|
276
|
+
BLACKLIST: {
|
|
277
|
+
name: "Transfer restriction applied",
|
|
278
|
+
description: "An address was restricted from transferring. Reported because it is invisible on-chain otherwise.",
|
|
279
|
+
severity: "MEDIUM",
|
|
280
|
+
triggerType: "contract.function_called",
|
|
281
|
+
conditions: { op: "contains", field: "event.types", value: "BLACKLIST" }
|
|
282
|
+
},
|
|
283
|
+
FORCED_BALANCE_CHANGE: {
|
|
284
|
+
name: "Balance moved without the holder acting",
|
|
285
|
+
description: "A permanent delegate or equivalent authority moved tokens from an account that did not sign for it. Legitimate for some regulated designs, and always worth knowing about.",
|
|
286
|
+
severity: "CRITICAL",
|
|
287
|
+
triggerType: "contract.function_called",
|
|
288
|
+
conditions: { op: "contains", field: "event.types", value: "FORCED_TRANSFER" }
|
|
289
|
+
},
|
|
290
|
+
GOVERNANCE: {
|
|
291
|
+
name: "Governance proposal executed",
|
|
292
|
+
description: "An executed proposal can change anything the audit assumed was fixed.",
|
|
293
|
+
severity: "HIGH",
|
|
294
|
+
triggerType: "governance.executed",
|
|
295
|
+
conditions: { op: "eq", field: "governance.executed", value: true }
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
var ALIASES = {
|
|
299
|
+
// The canonical names the Protect layer emits. Chain-specific spellings —
|
|
300
|
+
// Sui's TreasuryCap, Solana's mintAuthority, an EIP-1967 slot — are already
|
|
301
|
+
// normalised to these before an audit hands anything over, so mapping raw
|
|
302
|
+
// chain vocabulary here would be mapping names that never arrive.
|
|
303
|
+
UPGRADEABLE: "UPGRADEABLE",
|
|
304
|
+
DELEGATED_EXECUTION: "UPGRADEABLE",
|
|
305
|
+
MINT_AUTHORITY: "MINT_AUTHORITY",
|
|
306
|
+
PAUSABLE: "PAUSABLE",
|
|
307
|
+
ACCOUNT_FREEZE: "BLACKLIST",
|
|
308
|
+
FORCED_BALANCE_CHANGE: "FORCED_BALANCE_CHANGE",
|
|
309
|
+
GOVERNANCE: "GOVERNANCE",
|
|
310
|
+
// Accepted because an audit report may use them in prose, and rejecting a
|
|
311
|
+
// reviewer's own wording would silently drop a rule they expected.
|
|
312
|
+
PROXY: "UPGRADEABLE",
|
|
313
|
+
UPGRADE_AUTHORITY: "UPGRADEABLE",
|
|
314
|
+
ADMIN: "ADMIN_CONTROL",
|
|
315
|
+
ADMIN_CONTROL: "ADMIN_CONTROL",
|
|
316
|
+
OWNER: "OWNERSHIP",
|
|
317
|
+
OWNERSHIP: "OWNERSHIP",
|
|
318
|
+
MINT: "MINT_AUTHORITY",
|
|
319
|
+
PAUSE: "PAUSABLE",
|
|
320
|
+
FREEZE_AUTHORITY: "BLACKLIST",
|
|
321
|
+
BLACKLIST: "BLACKLIST"
|
|
322
|
+
};
|
|
323
|
+
function rulesFromAudit(input) {
|
|
324
|
+
const actions = input.actions?.length ? input.actions : DEFAULT_ACTIONS;
|
|
325
|
+
const seen = /* @__PURE__ */ new Set();
|
|
326
|
+
const generated = [];
|
|
327
|
+
for (const capability of input.capabilities) {
|
|
328
|
+
const key = ALIASES[capability.type.toUpperCase()];
|
|
329
|
+
if (!key || seen.has(key)) continue;
|
|
330
|
+
seen.add(key);
|
|
331
|
+
const template = RULES[key];
|
|
332
|
+
if (!template) continue;
|
|
333
|
+
generated.push({
|
|
334
|
+
capability: capability.type,
|
|
335
|
+
rule: {
|
|
336
|
+
projectId: input.projectId,
|
|
337
|
+
name: template.name,
|
|
338
|
+
description: capability.grantedBy ? `${template.description} Granted by ${capability.grantedBy}, identified during the audit.` : `${template.description} Identified during the audit.`,
|
|
339
|
+
severity: template.severity,
|
|
340
|
+
triggerType: template.triggerType,
|
|
341
|
+
conditions: template.conditions,
|
|
342
|
+
actions,
|
|
343
|
+
// Five minutes. Long enough that one event in a loop does not page a
|
|
344
|
+
// team repeatedly, short enough that a second, genuinely separate
|
|
345
|
+
// occurrence still gets through.
|
|
346
|
+
cooldownSeconds: 300
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
if (typeof input.treasuryThresholdUsd === "number" && input.treasuryThresholdUsd > 0) {
|
|
351
|
+
generated.push({
|
|
352
|
+
capability: "TREASURY",
|
|
353
|
+
rule: {
|
|
354
|
+
projectId: input.projectId,
|
|
355
|
+
name: "Large treasury outflow",
|
|
356
|
+
description: `An outbound movement above $${input.treasuryThresholdUsd.toLocaleString()}, the threshold agreed during the audit.`,
|
|
357
|
+
severity: "HIGH",
|
|
358
|
+
triggerType: "treasury.transfer",
|
|
359
|
+
conditions: {
|
|
360
|
+
op: "AND",
|
|
361
|
+
conditions: [
|
|
362
|
+
{ op: "eq", field: "transfer.direction", value: "OUT" },
|
|
363
|
+
{ op: "gt", field: "transfer.valueUsd", value: input.treasuryThresholdUsd }
|
|
364
|
+
]
|
|
365
|
+
},
|
|
366
|
+
actions,
|
|
367
|
+
cooldownSeconds: 300
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
return generated;
|
|
372
|
+
}
|
|
373
|
+
function unmappedCapabilities(capabilities) {
|
|
374
|
+
return capabilities.filter((capability) => !ALIASES[capability.type.toUpperCase()]).map((capability) => capability.type);
|
|
375
|
+
}
|
|
376
|
+
return __toCommonJS(index_exports);
|
|
377
|
+
})();
|