@decentrys/protect 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 +82 -0
- package/dist/browser/decentrys-protect.js +901 -0
- package/dist/browser/decentrys-protect.mjs +876 -0
- package/dist/cache.d.ts +41 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +75 -0
- package/dist/cache.js.map +1 -0
- package/dist/classify.d.ts +58 -0
- package/dist/classify.d.ts.map +1 -0
- package/dist/classify.js +269 -0
- package/dist/classify.js.map +1 -0
- package/dist/client.d.ts +132 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +307 -0
- package/dist/client.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +24 -0
- package/dist/index.js.map +1 -0
- package/dist/model.d.ts +156 -0
- package/dist/model.d.ts.map +1 -0
- package/dist/model.js +80 -0
- package/dist/model.js.map +1 -0
- package/dist/simulation.d.ts +57 -0
- package/dist/simulation.d.ts.map +1 -0
- package/dist/simulation.js +23 -0
- package/dist/simulation.js.map +1 -0
- package/dist/transport.d.ts +61 -0
- package/dist/transport.d.ts.map +1 -0
- package/dist/transport.js +151 -0
- package/dist/transport.js.map +1 -0
- package/dist/wire.d.ts +63 -0
- package/dist/wire.d.ts.map +1 -0
- package/dist/wire.js +274 -0
- package/dist/wire.js.map +1 -0
- package/package.json +64 -0
- package/src/cache.test.ts +67 -0
- package/src/cache.ts +87 -0
- package/src/classify.test.ts +294 -0
- package/src/classify.ts +323 -0
- package/src/client.test.ts +224 -0
- package/src/client.ts +420 -0
- package/src/index.ts +7 -0
- package/src/model.ts +237 -0
- package/src/simulation.ts +71 -0
- package/src/transport.test.ts +129 -0
- package/src/transport.ts +203 -0
- package/src/wire.test.ts +172 -0
- package/src/wire.ts +321 -0
package/dist/wire.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"wire.js","sourceRoot":"","sources":["../src/wire.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@decentrys/protect",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Evidence-based on-chain risk assessment for wallets and dapps. Unknown is neutral, risk requires evidence, and nothing is ever called a scam without one.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Decentrys Labs",
|
|
7
|
+
"homepage": "https://decentrys.com",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/teamdecentrys-byte/Decentrys.git",
|
|
11
|
+
"directory": "sdk/protect"
|
|
12
|
+
},
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/teamdecentrys-byte/Decentrys/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"decentrys",
|
|
18
|
+
"blockchain",
|
|
19
|
+
"web3",
|
|
20
|
+
"security",
|
|
21
|
+
"crypto",
|
|
22
|
+
"wallet",
|
|
23
|
+
"transaction-security",
|
|
24
|
+
"risk-assessment",
|
|
25
|
+
"threat-intelligence",
|
|
26
|
+
"ethereum",
|
|
27
|
+
"solana",
|
|
28
|
+
"sui"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"main": "dist/index.js",
|
|
38
|
+
"types": "dist/index.d.ts",
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"types": "./dist/index.d.ts",
|
|
42
|
+
"import": "./dist/browser/decentrys-protect.mjs",
|
|
43
|
+
"default": "./dist/index.js"
|
|
44
|
+
},
|
|
45
|
+
"./browser": "./dist/browser/decentrys-protect.mjs"
|
|
46
|
+
},
|
|
47
|
+
"files": [
|
|
48
|
+
"dist",
|
|
49
|
+
"!dist/.tsbuildinfo*",
|
|
50
|
+
"src",
|
|
51
|
+
"LICENSE",
|
|
52
|
+
"README.md"
|
|
53
|
+
],
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"typescript": "^5.7.2",
|
|
56
|
+
"vitest": "^3.2.7"
|
|
57
|
+
},
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.json && npm run build:browser",
|
|
60
|
+
"typecheck": "tsc -p tsconfig.spec.json",
|
|
61
|
+
"test": "vitest run",
|
|
62
|
+
"build:browser": "../../node_modules/.bin/esbuild src/index.ts --bundle --format=esm --target=es2022 --outfile=dist/browser/decentrys-protect.mjs && ../../node_modules/.bin/esbuild src/index.ts --bundle --format=iife --global-name=DecentrysProtect --target=es2022 --outfile=dist/browser/decentrys-protect.js"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { TtlCache, cacheKey } from './cache';
|
|
3
|
+
|
|
4
|
+
function clock(start = 0) {
|
|
5
|
+
let t = start;
|
|
6
|
+
return { now: () => t, advance: (ms: number) => { t += ms; } };
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
describe('TtlCache', () => {
|
|
10
|
+
it('serves a value inside its TTL', () => {
|
|
11
|
+
const c = clock();
|
|
12
|
+
const cache = new TtlCache<string>({ ttlMs: 100, maxEntries: 10, now: c.now });
|
|
13
|
+
|
|
14
|
+
cache.set('a', 'value');
|
|
15
|
+
c.advance(99);
|
|
16
|
+
expect(cache.get('a')).toBe('value');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Threat intelligence changes. An entry that outlives the discovery of a
|
|
21
|
+
* drainer is worse than no cache at all.
|
|
22
|
+
*/
|
|
23
|
+
it('drops a value once its TTL passes', () => {
|
|
24
|
+
const c = clock();
|
|
25
|
+
const cache = new TtlCache<string>({ ttlMs: 100, maxEntries: 10, now: c.now });
|
|
26
|
+
|
|
27
|
+
cache.set('a', 'value');
|
|
28
|
+
c.advance(100);
|
|
29
|
+
expect(cache.get('a')).toBeUndefined();
|
|
30
|
+
expect(cache.size).toBe(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('evicts the least recently used entry when full', () => {
|
|
34
|
+
const cache = new TtlCache<string>({ ttlMs: 1_000, maxEntries: 2, now: () => 0 });
|
|
35
|
+
|
|
36
|
+
cache.set('a', '1');
|
|
37
|
+
cache.set('b', '2');
|
|
38
|
+
cache.get('a'); // 'a' becomes the most recent
|
|
39
|
+
cache.set('c', '3'); // evicts 'b'
|
|
40
|
+
|
|
41
|
+
expect(cache.get('a')).toBe('1');
|
|
42
|
+
expect(cache.get('b')).toBeUndefined();
|
|
43
|
+
expect(cache.get('c')).toBe('3');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('stores nothing when caching is disabled', () => {
|
|
47
|
+
const cache = new TtlCache<string>({ ttlMs: 0, maxEntries: 10, now: () => 0 });
|
|
48
|
+
cache.set('a', '1');
|
|
49
|
+
expect(cache.get('a')).toBeUndefined();
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
describe('cacheKey', () => {
|
|
54
|
+
/**
|
|
55
|
+
* Two subjects that differ only in a field the key forgot would share an
|
|
56
|
+
* assessment, and one of them would be wrong.
|
|
57
|
+
*/
|
|
58
|
+
it('separates the same address on different chains', () => {
|
|
59
|
+
const evm = cacheKey('address', ['ethereum', '0xabc']);
|
|
60
|
+
const other = cacheKey('address', ['polygon', '0xabc']);
|
|
61
|
+
expect(evm).not.toBe(other);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('is stable for the same inputs', () => {
|
|
65
|
+
expect(cacheKey('token', ['sui', '0x2::sui::SUI'])).toBe(cacheKey('token', ['sui', '0x2::sui::SUI']));
|
|
66
|
+
});
|
|
67
|
+
});
|
package/src/cache.ts
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small TTL cache with a bounded size.
|
|
3
|
+
*
|
|
4
|
+
* Wallets call `screenAddress` on the same handful of addresses constantly —
|
|
5
|
+
* the token they always trade, the exchange they always withdraw to, the
|
|
6
|
+
* router every swap goes through. Serving those from memory is the difference
|
|
7
|
+
* between a signing screen that appears instantly and one that flashes a
|
|
8
|
+
* spinner every time.
|
|
9
|
+
*
|
|
10
|
+
* Two deliberate limits:
|
|
11
|
+
*
|
|
12
|
+
* - **Bounded.** A long-lived extension background page would otherwise grow
|
|
13
|
+
* without end. Eviction is oldest-first, which is right for this shape of
|
|
14
|
+
* traffic and costs nothing to maintain.
|
|
15
|
+
* - **Short.** Threat intelligence changes. A cached "no critical risk"
|
|
16
|
+
* entry that outlives the discovery of a drainer is worse than no cache,
|
|
17
|
+
* so the default TTL is measured in minutes, not hours.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
interface Entry<T> {
|
|
21
|
+
value: T;
|
|
22
|
+
expiresAt: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CacheOptions {
|
|
26
|
+
ttlMs: number;
|
|
27
|
+
maxEntries: number;
|
|
28
|
+
now?: () => number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class TtlCache<T> {
|
|
32
|
+
private readonly entries = new Map<string, Entry<T>>();
|
|
33
|
+
private readonly now: () => number;
|
|
34
|
+
|
|
35
|
+
constructor(private readonly options: CacheOptions) {
|
|
36
|
+
this.now = options.now ?? (() => Date.now());
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
get(key: string): T | undefined {
|
|
40
|
+
const entry = this.entries.get(key);
|
|
41
|
+
if (!entry) return undefined;
|
|
42
|
+
|
|
43
|
+
if (entry.expiresAt <= this.now()) {
|
|
44
|
+
this.entries.delete(key);
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Re-insert so recency ordering reflects use, not just insertion.
|
|
49
|
+
this.entries.delete(key);
|
|
50
|
+
this.entries.set(key, entry);
|
|
51
|
+
return entry.value;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
set(key: string, value: T, ttlMs = this.options.ttlMs): void {
|
|
55
|
+
if (ttlMs <= 0 || this.options.maxEntries <= 0) return;
|
|
56
|
+
|
|
57
|
+
this.entries.delete(key);
|
|
58
|
+
this.entries.set(key, { value, expiresAt: this.now() + ttlMs });
|
|
59
|
+
|
|
60
|
+
while (this.entries.size > this.options.maxEntries) {
|
|
61
|
+
const oldest = this.entries.keys().next();
|
|
62
|
+
if (oldest.done) break;
|
|
63
|
+
this.entries.delete(oldest.value);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
delete(key: string): void {
|
|
68
|
+
this.entries.delete(key);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
clear(): void {
|
|
72
|
+
this.entries.clear();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
get size(): number {
|
|
76
|
+
return this.entries.size;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cache keys are built from the request, never from a hash of a stringified
|
|
82
|
+
* object: two subjects that differ only in a field the key forgot would
|
|
83
|
+
* otherwise share an assessment, and one of them would be wrong.
|
|
84
|
+
*/
|
|
85
|
+
export function cacheKey(kind: string, parts: Array<string | number | undefined | null>): string {
|
|
86
|
+
return [kind, ...parts.map((p) => (p === undefined || p === null ? '' : String(p)))].join('|');
|
|
87
|
+
}
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { DEFAULT_POLICY, applyPolicy, classify, unavailableAssessment } from './classify';
|
|
3
|
+
import { isAtLeast, type Evidence, type ThreatSignal } from './model';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* The non-negotiable cases.
|
|
7
|
+
*
|
|
8
|
+
* These encode the product's central claim — **unknown is neutral, risk
|
|
9
|
+
* requires evidence** — and they are written to fail loudly if anyone ever
|
|
10
|
+
* "improves" the model by scoring newness, thin liquidity or a missing audit.
|
|
11
|
+
* That change would be easy to make, would look like a security improvement,
|
|
12
|
+
* and would quietly turn Decentrys into a gatekeeper that taxes every new
|
|
13
|
+
* project while protecting incumbents.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const NOW = new Date('2026-09-06T00:00:00Z');
|
|
17
|
+
|
|
18
|
+
function evidence(overrides: Partial<Evidence> = {}): Evidence {
|
|
19
|
+
return {
|
|
20
|
+
id: 'ev_1', type: 'BYTECODE_MATCH', source: 'Decentrys',
|
|
21
|
+
observedAt: NOW.toISOString(), confidence: 0.99, analystVerified: false,
|
|
22
|
+
...overrides,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function signal(overrides: Partial<ThreatSignal> = {}): ThreatSignal {
|
|
27
|
+
return {
|
|
28
|
+
type: 'KNOWN_DRAINER_BYTECODE', severity: 'CRITICAL', confidence: 0.99,
|
|
29
|
+
explanation: 'Bytecode matches a confirmed drainer family.', hops: 0,
|
|
30
|
+
evidence: [evidence()], status: 'ACTIVE',
|
|
31
|
+
createdAt: NOW.toISOString(), lastSeen: NOW.toISOString(),
|
|
32
|
+
...overrides,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// --- 1 ----------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
describe('TEST 1 — a new token with no malicious indicators', () => {
|
|
39
|
+
it('is not high risk', () => {
|
|
40
|
+
const result = classify({
|
|
41
|
+
now: NOW,
|
|
42
|
+
historyStatus: 'LIMITED',
|
|
43
|
+
facts: [
|
|
44
|
+
{ type: 'CONTRACT_AGE_HOURS', value: 2, statement: 'Deployed 2 hours ago.', source: 'chain', observedAt: NOW.toISOString() },
|
|
45
|
+
{ type: 'SOURCE_VERIFIED', value: true, statement: 'Source code is verified.', source: 'explorer', observedAt: NOW.toISOString() },
|
|
46
|
+
],
|
|
47
|
+
capabilities: [
|
|
48
|
+
{ type: 'MINT_AUTHORITY_ACTIVE', severity: 'INFO', statement: 'The owner can mint additional supply.' },
|
|
49
|
+
],
|
|
50
|
+
threatSignals: [],
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
expect(isAtLeast(result.riskLevel, 'ELEVATED_RISK')).toBe(false);
|
|
54
|
+
expect(result.confirmedMalicious).toBe(false);
|
|
55
|
+
expect(result.riskLevel).toBe('INFORMATIONAL');
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
it('does not let two hours of age contribute to technical risk', () => {
|
|
59
|
+
const young = classify({ now: NOW, historyStatus: 'NONE', facts: [
|
|
60
|
+
{ type: 'CONTRACT_AGE_HOURS', value: 2, statement: 'Deployed 2 hours ago.', source: 'chain', observedAt: NOW.toISOString() },
|
|
61
|
+
] });
|
|
62
|
+
const old = classify({ now: NOW, historyStatus: 'ESTABLISHED', facts: [
|
|
63
|
+
{ type: 'CONTRACT_AGE_HOURS', value: 40_000, statement: 'Deployed 4 years ago.', source: 'chain', observedAt: NOW.toISOString() },
|
|
64
|
+
] });
|
|
65
|
+
|
|
66
|
+
// Age changes what we KNOW, never what we ACCUSE.
|
|
67
|
+
expect(young.components.technicalRisk).toBe(old.components.technicalRisk);
|
|
68
|
+
expect(young.riskLevel).toBe(old.riskLevel);
|
|
69
|
+
expect(young.components.historyConfidence).toBeLessThan(old.components.historyConfidence);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('states limited history as a fact about our coverage, not a warning', () => {
|
|
73
|
+
const result = classify({ now: NOW, historyStatus: 'LIMITED' });
|
|
74
|
+
expect(result.explanation.join(' ')).toMatch(/not a risk finding/i);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// --- 2 ----------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
describe('TEST 2 — a new anonymous project with a valid contract', () => {
|
|
81
|
+
it('is not malicious', () => {
|
|
82
|
+
const result = classify({
|
|
83
|
+
now: NOW,
|
|
84
|
+
historyStatus: 'LIMITED',
|
|
85
|
+
facts: [
|
|
86
|
+
{ type: 'DEPLOYER_HISTORY', value: 'LIMITED', statement: 'The deployer has little prior activity.', source: 'chain', observedAt: NOW.toISOString() },
|
|
87
|
+
{ type: 'TEAM_IDENTIFIED', value: false, statement: 'No public team identity was located.', source: 'osint', observedAt: NOW.toISOString() },
|
|
88
|
+
],
|
|
89
|
+
unknowns: [
|
|
90
|
+
{ field: 'ownership', reason: 'UNKNOWN', statement: 'Project ownership could not be determined.' },
|
|
91
|
+
],
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
expect(result.confirmedMalicious).toBe(false);
|
|
95
|
+
expect(isAtLeast(result.riskLevel, 'CAUTION')).toBe(false);
|
|
96
|
+
expect(result.components.threatIntelligenceRisk).toBe(0);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('says the unknown is unknown rather than converting it to risk', () => {
|
|
100
|
+
const result = classify({
|
|
101
|
+
now: NOW,
|
|
102
|
+
unknowns: [{ field: 'ownership', reason: 'UNKNOWN', statement: 'Ownership unknown.' }],
|
|
103
|
+
});
|
|
104
|
+
expect(result.explanation.join(' ')).toMatch(/does not contribute to risk/i);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// --- 3 ----------------------------------------------------------------------
|
|
109
|
+
|
|
110
|
+
describe('TEST 3 — a low-liquidity token', () => {
|
|
111
|
+
it('reports liquidity as a fact and nothing more', () => {
|
|
112
|
+
const result = classify({
|
|
113
|
+
now: NOW,
|
|
114
|
+
historyStatus: 'LIMITED',
|
|
115
|
+
facts: [
|
|
116
|
+
{ type: 'LIQUIDITY_USD', value: 1200, statement: 'Pool liquidity is $1,200.', source: 'dex', observedAt: NOW.toISOString() },
|
|
117
|
+
{ type: 'HOLDER_CONCENTRATION', value: 0.92, statement: 'The top holder holds 92% of supply.', source: 'chain', observedAt: NOW.toISOString() },
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
expect(isAtLeast(result.riskLevel, 'CAUTION')).toBe(false);
|
|
122
|
+
expect(result.confirmedMalicious).toBe(false);
|
|
123
|
+
// The facts are still surfaced — objectivity is not silence.
|
|
124
|
+
expect(result.facts).toHaveLength(2);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// --- 4 ----------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
describe('TEST 4 — no audit found', () => {
|
|
131
|
+
it('records the audit status without raising risk', () => {
|
|
132
|
+
const withAudit = classify({
|
|
133
|
+
now: NOW, historyStatus: 'MODERATE',
|
|
134
|
+
facts: [{ type: 'AUDIT_STATUS', value: 'FOUND', statement: 'An audit was located.', source: 'registry', observedAt: NOW.toISOString() }],
|
|
135
|
+
});
|
|
136
|
+
const without = classify({
|
|
137
|
+
now: NOW, historyStatus: 'MODERATE',
|
|
138
|
+
facts: [{ type: 'AUDIT_STATUS', value: 'NONE_FOUND', statement: 'No audit was located.', source: 'registry', observedAt: NOW.toISOString() }],
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(without.riskLevel).toBe(withAudit.riskLevel);
|
|
142
|
+
expect(without.components.technicalRisk).toBe(withAudit.components.technicalRisk);
|
|
143
|
+
});
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// --- 5 ----------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
describe('TEST 5 — a known drainer contract', () => {
|
|
149
|
+
it('reaches KNOWN_MALICIOUS when an analyst has verified the evidence', () => {
|
|
150
|
+
const result = classify({
|
|
151
|
+
now: NOW,
|
|
152
|
+
threatSignals: [signal({ evidence: [evidence({ analystVerified: true })] })],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(result.riskLevel).toBe('KNOWN_MALICIOUS');
|
|
156
|
+
expect(result.confirmedMalicious).toBe(true);
|
|
157
|
+
expect(result.threatSignals[0].evidence[0].id).toBe('ev_1');
|
|
158
|
+
expect(result.explanation.join(' ')).toMatch(/verified by an analyst/i);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('stops at CRITICAL_THREAT without analyst verification', () => {
|
|
162
|
+
// The accusation is the one output that needs a human behind it.
|
|
163
|
+
const result = classify({ now: NOW, threatSignals: [signal({ evidence: [evidence({ analystVerified: false })] })] });
|
|
164
|
+
|
|
165
|
+
expect(result.riskLevel).toBe('CRITICAL_THREAT');
|
|
166
|
+
expect(result.confirmedMalicious).toBe(false);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('never reaches KNOWN_MALICIOUS from an absence of reputation', () => {
|
|
170
|
+
const result = classify({
|
|
171
|
+
now: NOW, historyStatus: 'NONE',
|
|
172
|
+
facts: [{ type: 'LISTED_ON_EXCHANGES', value: false, statement: 'Not listed on any major exchange.', source: 'registry', observedAt: NOW.toISOString() }],
|
|
173
|
+
});
|
|
174
|
+
expect(result.riskLevel).not.toBe('KNOWN_MALICIOUS');
|
|
175
|
+
expect(result.confirmedMalicious).toBe(false);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// --- 6 ----------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
describe('TEST 6 — direct stolen-fund exposure', () => {
|
|
182
|
+
it('raises risk and carries the transaction provenance', () => {
|
|
183
|
+
const result = classify({
|
|
184
|
+
now: NOW,
|
|
185
|
+
threatSignals: [signal({
|
|
186
|
+
type: 'STOLEN_FUNDS_DIRECT', severity: 'HIGH', hops: 0, confidence: 0.94,
|
|
187
|
+
explanation: 'Received assets directly from a wallet attributed to incident INC-2026-0012.',
|
|
188
|
+
evidence: [evidence({ type: 'INCIDENT_ATTRIBUTION', txHash: '0xabc', analystVerified: true })],
|
|
189
|
+
})],
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(isAtLeast(result.riskLevel, 'ELEVATED_RISK')).toBe(true);
|
|
193
|
+
expect(result.threatSignals[0].evidence[0].txHash).toBe('0xabc');
|
|
194
|
+
expect(result.components.behavioralRisk).toBeGreaterThan(0);
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// --- 7 ----------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
describe('TEST 7 — an indirect, low-confidence association', () => {
|
|
201
|
+
it('reports it with its confidence and does not treat it as proven', () => {
|
|
202
|
+
const result = classify({
|
|
203
|
+
now: NOW,
|
|
204
|
+
threatSignals: [signal({
|
|
205
|
+
type: 'MIXER_INDIRECT', severity: 'MEDIUM', hops: 4, confidence: 0.31,
|
|
206
|
+
explanation: 'Four hops from an address labelled as a mixing service.',
|
|
207
|
+
})],
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// Present, visible, and explicitly not sufficient on its own.
|
|
211
|
+
expect(result.threatSignals).toHaveLength(1);
|
|
212
|
+
expect(result.confirmedMalicious).toBe(false);
|
|
213
|
+
expect(isAtLeast(result.riskLevel, 'ELEVATED_RISK')).toBe(false);
|
|
214
|
+
expect(result.explanation.join(' ')).toMatch(/too low\s+to raise the risk level/i);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('decays a distant signal in the intelligence component', () => {
|
|
218
|
+
const direct = classify({ now: NOW, threatSignals: [signal({ severity: 'HIGH', hops: 0, confidence: 0.8 })] });
|
|
219
|
+
const distant = classify({ now: NOW, threatSignals: [signal({ severity: 'HIGH', hops: 4, confidence: 0.8 })] });
|
|
220
|
+
|
|
221
|
+
expect(distant.components.threatIntelligenceRisk)
|
|
222
|
+
.toBeLessThan(direct.components.threatIntelligenceRisk);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
// --- decay, policy, availability -------------------------------------------
|
|
227
|
+
|
|
228
|
+
describe('temporal decay', () => {
|
|
229
|
+
it('will not let an expired signal raise the level', () => {
|
|
230
|
+
const result = classify({
|
|
231
|
+
now: NOW,
|
|
232
|
+
threatSignals: [signal({ expiresAt: '2024-01-01T00:00:00Z' })],
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
expect(result.threatSignals[0].status).toBe('STALE');
|
|
236
|
+
expect(result.riskLevel).not.toBe('CRITICAL_THREAT');
|
|
237
|
+
// Still reported. Suppressing it would hide a real past observation.
|
|
238
|
+
expect(result.explanation.join(' ')).toMatch(/stale/i);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('keeps a disputed signal visible but inert', () => {
|
|
242
|
+
const result = classify({ now: NOW, threatSignals: [signal({ status: 'DISPUTED_FACT' })] });
|
|
243
|
+
expect(result.riskLevel).not.toBe('CRITICAL_THREAT');
|
|
244
|
+
expect(result.threatSignals).toHaveLength(1);
|
|
245
|
+
});
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
describe('policy', () => {
|
|
249
|
+
it('blocks only what is confirmed malicious by default', () => {
|
|
250
|
+
// Transparency over blocking: the decision belongs to whoever holds the keys.
|
|
251
|
+
expect(DEFAULT_POLICY.KNOWN_MALICIOUS).toBe('block');
|
|
252
|
+
expect(DEFAULT_POLICY.CRITICAL_THREAT).toBe('require_confirmation');
|
|
253
|
+
expect(DEFAULT_POLICY.CAUTION).toBe('warn');
|
|
254
|
+
expect(DEFAULT_POLICY.INFORMATIONAL).toBe('inform');
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('lets an integrator be stricter without changing the assessment', () => {
|
|
258
|
+
const assessment = classify({ now: NOW, threatSignals: [signal({ severity: 'HIGH', confidence: 0.8 })] });
|
|
259
|
+
const institutional = applyPolicy(assessment, { ELEVATED_RISK: 'block' });
|
|
260
|
+
|
|
261
|
+
expect(institutional.action).toBe('block');
|
|
262
|
+
expect(assessment.riskLevel).toBe('ELEVATED_RISK');
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('always gives a reason with the action', () => {
|
|
266
|
+
const assessment = classify({ now: NOW, historyStatus: 'LIMITED' });
|
|
267
|
+
expect(applyPolicy(assessment).reason.length).toBeGreaterThan(0);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
describe('when Decentrys is unreachable', () => {
|
|
272
|
+
it('says nothing was checked rather than implying safety', () => {
|
|
273
|
+
const result = unavailableAssessment('warn', 'timeout', NOW);
|
|
274
|
+
|
|
275
|
+
expect(result.confidence).toBe(0);
|
|
276
|
+
expect(result.unknowns[0].reason).toBe('PROVIDER_UNAVAILABLE');
|
|
277
|
+
expect(result.explanation.join(' ')).toMatch(/no security check was performed/i);
|
|
278
|
+
// Critically, it does not claim the subject is fine.
|
|
279
|
+
expect(result.confirmedMalicious).toBe(false);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('explainability', () => {
|
|
284
|
+
it('never returns a classification with no explanation', () => {
|
|
285
|
+
for (const input of [
|
|
286
|
+
{},
|
|
287
|
+
{ historyStatus: 'ESTABLISHED' as const },
|
|
288
|
+
{ threatSignals: [signal()] },
|
|
289
|
+
{ capabilities: [{ type: 'PAUSE', severity: 'SIGNIFICANT' as const, statement: 'Transfers can be paused.' }] },
|
|
290
|
+
]) {
|
|
291
|
+
expect(classify({ ...input, now: NOW }).explanation.length).toBeGreaterThan(0);
|
|
292
|
+
}
|
|
293
|
+
});
|
|
294
|
+
});
|