@goplus/agentguard 1.0.4 → 1.0.6
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/openclaw.plugin.json +21 -0
- package/package.json +4 -2
- package/skills/agentguard/SKILL.md +385 -0
- package/skills/agentguard/action-policies.md +234 -0
- package/skills/agentguard/evals.md +82 -0
- package/skills/agentguard/scan-rules.md +309 -0
- package/skills/agentguard/scripts/action-cli.ts +240 -0
- package/skills/agentguard/scripts/auto-scan.js +167 -0
- package/skills/agentguard/scripts/guard-hook.js +110 -0
- package/skills/agentguard/scripts/package.json +7 -0
- package/skills/agentguard/scripts/trust-cli.ts +134 -0
- package/skills/agentguard/web3-patterns.md +161 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GoPlus AgentGuard — SessionStart Auto-Scan Hook
|
|
5
|
+
*
|
|
6
|
+
* Runs on session startup to discover and scan newly installed skills.
|
|
7
|
+
* For each skill in ~/.claude/skills/:
|
|
8
|
+
* 1. Calculate artifact hash
|
|
9
|
+
* 2. Check trust registry — skip if already registered with same hash
|
|
10
|
+
* 3. Run quickScan for new/updated skills
|
|
11
|
+
* 4. Report results to stderr (scan-only, does NOT modify trust registry)
|
|
12
|
+
*
|
|
13
|
+
* OPT-IN: This script only runs when AGENTGUARD_AUTO_SCAN=1.
|
|
14
|
+
* Without this env var, the script exits immediately.
|
|
15
|
+
*
|
|
16
|
+
* To register scanned skills, use: /agentguard trust attest
|
|
17
|
+
*
|
|
18
|
+
* Exits 0 always (informational only, never blocks session startup).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { readdirSync, existsSync, appendFileSync, mkdirSync } from 'node:fs';
|
|
22
|
+
import { join } from 'node:path';
|
|
23
|
+
import { homedir } from 'node:os';
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Opt-in gate: only run when explicitly enabled
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
if (process.env.AGENTGUARD_AUTO_SCAN !== '1') {
|
|
30
|
+
process.exit(0);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Load AgentGuard engine
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
|
|
37
|
+
const agentguardPath = join(
|
|
38
|
+
import.meta.url.replace('file://', ''),
|
|
39
|
+
'..', '..', '..', '..', 'dist', 'index.js'
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
let createAgentGuard;
|
|
43
|
+
try {
|
|
44
|
+
const gs = await import(agentguardPath);
|
|
45
|
+
createAgentGuard = gs.createAgentGuard || gs.default;
|
|
46
|
+
} catch {
|
|
47
|
+
try {
|
|
48
|
+
const gs = await import('@goplus/agentguard');
|
|
49
|
+
createAgentGuard = gs.createAgentGuard || gs.default;
|
|
50
|
+
} catch {
|
|
51
|
+
// Can't load engine — exit silently
|
|
52
|
+
process.exit(0);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
// Config
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
const SKILLS_DIRS = [
|
|
61
|
+
join(homedir(), '.claude', 'skills'),
|
|
62
|
+
join(homedir(), '.openclaw', 'skills'),
|
|
63
|
+
];
|
|
64
|
+
const AGENTGUARD_DIR = join(homedir(), '.agentguard');
|
|
65
|
+
const AUDIT_PATH = join(AGENTGUARD_DIR, 'audit.jsonl');
|
|
66
|
+
|
|
67
|
+
function ensureDir() {
|
|
68
|
+
if (!existsSync(AGENTGUARD_DIR)) {
|
|
69
|
+
mkdirSync(AGENTGUARD_DIR, { recursive: true });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function writeAuditLog(entry) {
|
|
74
|
+
try {
|
|
75
|
+
ensureDir();
|
|
76
|
+
appendFileSync(AUDIT_PATH, JSON.stringify(entry) + '\n');
|
|
77
|
+
} catch {
|
|
78
|
+
// Non-critical
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// Discover skills
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Find all skill directories under ~/.claude/skills/ and ~/.openclaw/skills/
|
|
88
|
+
* A skill directory is one that contains a SKILL.md file.
|
|
89
|
+
*/
|
|
90
|
+
function discoverSkills() {
|
|
91
|
+
const skills = [];
|
|
92
|
+
for (const skillsDir of SKILLS_DIRS) {
|
|
93
|
+
if (!existsSync(skillsDir)) continue;
|
|
94
|
+
try {
|
|
95
|
+
const entries = readdirSync(skillsDir, { withFileTypes: true });
|
|
96
|
+
for (const entry of entries) {
|
|
97
|
+
if (!entry.isDirectory()) continue;
|
|
98
|
+
const skillDir = join(skillsDir, entry.name);
|
|
99
|
+
const skillMd = join(skillDir, 'SKILL.md');
|
|
100
|
+
if (existsSync(skillMd)) {
|
|
101
|
+
skills.push({ name: entry.name, path: skillDir });
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
} catch {
|
|
105
|
+
// Can't read skills dir
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return skills;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Main — scan-only mode (no trust registry mutations)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
async function main() {
|
|
116
|
+
const skills = discoverSkills();
|
|
117
|
+
if (skills.length === 0) {
|
|
118
|
+
process.exit(0);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const { scanner } = createAgentGuard();
|
|
122
|
+
|
|
123
|
+
let scanned = 0;
|
|
124
|
+
const results = [];
|
|
125
|
+
|
|
126
|
+
for (const skill of skills) {
|
|
127
|
+
// Skip self (agentguard)
|
|
128
|
+
if (skill.name === 'agentguard') continue;
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
const result = await scanner.quickScan(skill.path);
|
|
132
|
+
scanned++;
|
|
133
|
+
|
|
134
|
+
results.push({
|
|
135
|
+
name: skill.name,
|
|
136
|
+
risk_level: result.risk_level,
|
|
137
|
+
risk_tags: result.risk_tags,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Audit log — only record skill name, risk level, and tag names (no code/evidence)
|
|
141
|
+
writeAuditLog({
|
|
142
|
+
timestamp: new Date().toISOString(),
|
|
143
|
+
event: 'auto_scan',
|
|
144
|
+
skill_name: skill.name,
|
|
145
|
+
risk_level: result.risk_level,
|
|
146
|
+
risk_tags: result.risk_tags,
|
|
147
|
+
});
|
|
148
|
+
} catch {
|
|
149
|
+
// Skip skills that fail to scan
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Output summary to stderr (shown as status message)
|
|
154
|
+
if (scanned > 0) {
|
|
155
|
+
const lines = results.map(r =>
|
|
156
|
+
` ${r.name}: ${r.risk_level}${r.risk_tags.length ? ` [${r.risk_tags.join(', ')}]` : ''}`
|
|
157
|
+
);
|
|
158
|
+
process.stderr.write(
|
|
159
|
+
`GoPlus AgentGuard: scanned ${scanned} skill(s)\n${lines.join('\n')}\n` +
|
|
160
|
+
`Use /agentguard trust attest to register trusted skills.\n`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
process.exit(0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
main();
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GoPlus AgentGuard PreToolUse / PostToolUse Hook (Claude Code)
|
|
5
|
+
*
|
|
6
|
+
* Uses the common adapter + engine architecture.
|
|
7
|
+
* Reads Claude Code hook input from stdin, delegates to evaluateHook(),
|
|
8
|
+
* and outputs allow / deny / ask via Claude Code protocol.
|
|
9
|
+
*
|
|
10
|
+
* PreToolUse exit codes:
|
|
11
|
+
* 0 = allow (or JSON with permissionDecision)
|
|
12
|
+
* 2 = deny (stderr = reason shown to Claude)
|
|
13
|
+
*
|
|
14
|
+
* PostToolUse: appends audit log entry (async, always exits 0)
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
// Load AgentGuard engine + adapters
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
|
|
23
|
+
const agentguardPath = join(import.meta.url.replace('file://', ''), '..', '..', '..', '..', 'dist', 'index.js');
|
|
24
|
+
|
|
25
|
+
let createAgentGuard, ClaudeCodeAdapter, evaluateHook, loadConfig;
|
|
26
|
+
try {
|
|
27
|
+
const gs = await import(agentguardPath);
|
|
28
|
+
createAgentGuard = gs.createAgentGuard || gs.default;
|
|
29
|
+
ClaudeCodeAdapter = gs.ClaudeCodeAdapter;
|
|
30
|
+
evaluateHook = gs.evaluateHook;
|
|
31
|
+
loadConfig = gs.loadConfig;
|
|
32
|
+
} catch {
|
|
33
|
+
try {
|
|
34
|
+
const gs = await import('@goplus/agentguard');
|
|
35
|
+
createAgentGuard = gs.createAgentGuard || gs.default;
|
|
36
|
+
ClaudeCodeAdapter = gs.ClaudeCodeAdapter;
|
|
37
|
+
evaluateHook = gs.evaluateHook;
|
|
38
|
+
loadConfig = gs.loadConfig;
|
|
39
|
+
} catch {
|
|
40
|
+
process.stderr.write('GoPlus AgentGuard: unable to load engine, allowing action\n');
|
|
41
|
+
process.exit(0);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Read stdin
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
function readStdin() {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
let data = '';
|
|
52
|
+
process.stdin.setEncoding('utf-8');
|
|
53
|
+
process.stdin.on('data', (chunk) => (data += chunk));
|
|
54
|
+
process.stdin.on('end', () => {
|
|
55
|
+
try {
|
|
56
|
+
resolve(JSON.parse(data));
|
|
57
|
+
} catch {
|
|
58
|
+
resolve(null);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
setTimeout(() => resolve(null), 5000);
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Claude Code output helpers
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
function outputDeny(reason) {
|
|
70
|
+
process.stderr.write(reason + '\n');
|
|
71
|
+
process.exit(2);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function outputAsk(reason) {
|
|
75
|
+
console.log(JSON.stringify({
|
|
76
|
+
hookSpecificOutput: {
|
|
77
|
+
hookEventName: 'PreToolUse',
|
|
78
|
+
permissionDecision: 'ask',
|
|
79
|
+
permissionDecisionReason: reason,
|
|
80
|
+
},
|
|
81
|
+
}));
|
|
82
|
+
process.exit(0);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function outputAllow() {
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Main
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
async function main() {
|
|
94
|
+
const input = await readStdin();
|
|
95
|
+
if (!input) {
|
|
96
|
+
process.exit(0);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const adapter = new ClaudeCodeAdapter();
|
|
100
|
+
const config = loadConfig();
|
|
101
|
+
const agentguard = createAgentGuard();
|
|
102
|
+
|
|
103
|
+
const result = await evaluateHook(adapter, input, { config, agentguard });
|
|
104
|
+
|
|
105
|
+
if (result.decision === 'deny') outputDeny(result.reason || 'Action blocked');
|
|
106
|
+
else if (result.decision === 'ask') outputAsk(result.reason || 'Action requires confirmation');
|
|
107
|
+
else outputAllow();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
main();
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* GoPlus AgentGuard Trust CLI — lightweight wrapper for SkillRegistry operations.
|
|
5
|
+
*
|
|
6
|
+
* Usage:
|
|
7
|
+
* node trust-cli.ts lookup --id <id> --source <source> --version <version> --hash <hash>
|
|
8
|
+
* node trust-cli.ts attest --id <id> --source <source> --version <version> --hash <hash> --trust-level <level> [--preset <preset>] [--capabilities <json>] [--reviewed-by <name>] [--notes <text>] [--expires <iso>] [--force]
|
|
9
|
+
* node trust-cli.ts revoke [--source <source>] [--key <record_key>] --reason <reason>
|
|
10
|
+
* node trust-cli.ts list [--trust-level <level>] [--status <status>] [--source-pattern <pattern>]
|
|
11
|
+
* node trust-cli.ts hash --path <dir>
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { createAgentGuard, CAPABILITY_PRESETS, SkillScanner } from '@goplus/agentguard';
|
|
15
|
+
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const command = args[0];
|
|
18
|
+
|
|
19
|
+
function getArg(name: string): string | undefined {
|
|
20
|
+
const idx = args.indexOf(`--${name}`);
|
|
21
|
+
if (idx === -1 || idx + 1 >= args.length) return undefined;
|
|
22
|
+
return args[idx + 1];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hasFlag(name: string): boolean {
|
|
26
|
+
return args.includes(`--${name}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function main() {
|
|
30
|
+
const registryPath = getArg('registry-path');
|
|
31
|
+
const { registry } = createAgentGuard({ registryPath });
|
|
32
|
+
|
|
33
|
+
switch (command) {
|
|
34
|
+
case 'lookup': {
|
|
35
|
+
const skill = {
|
|
36
|
+
id: getArg('id') || '',
|
|
37
|
+
source: getArg('source') || '',
|
|
38
|
+
version_ref: getArg('version') || '',
|
|
39
|
+
artifact_hash: getArg('hash') || '',
|
|
40
|
+
};
|
|
41
|
+
const record = await registry.lookup(skill);
|
|
42
|
+
console.log(JSON.stringify(record, null, 2));
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
case 'attest': {
|
|
47
|
+
const skill = {
|
|
48
|
+
id: getArg('id') || '',
|
|
49
|
+
source: getArg('source') || '',
|
|
50
|
+
version_ref: getArg('version') || '',
|
|
51
|
+
artifact_hash: getArg('hash') || '',
|
|
52
|
+
};
|
|
53
|
+
const trustLevel = (getArg('trust-level') || 'restricted') as
|
|
54
|
+
| 'untrusted'
|
|
55
|
+
| 'restricted'
|
|
56
|
+
| 'trusted';
|
|
57
|
+
|
|
58
|
+
let capabilities;
|
|
59
|
+
const preset = getArg('preset');
|
|
60
|
+
if (preset && preset in CAPABILITY_PRESETS) {
|
|
61
|
+
capabilities =
|
|
62
|
+
CAPABILITY_PRESETS[preset as keyof typeof CAPABILITY_PRESETS];
|
|
63
|
+
} else if (getArg('capabilities')) {
|
|
64
|
+
capabilities = JSON.parse(getArg('capabilities')!);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const force = hasFlag('force');
|
|
68
|
+
const attestFn = force ? registry.forceAttest : registry.attest;
|
|
69
|
+
const result = await attestFn.call(registry, {
|
|
70
|
+
skill,
|
|
71
|
+
trust_level: trustLevel,
|
|
72
|
+
capabilities,
|
|
73
|
+
review: {
|
|
74
|
+
reviewed_by: getArg('reviewed-by') || 'cli',
|
|
75
|
+
reviewed_at: new Date().toISOString(),
|
|
76
|
+
notes: getArg('notes') || '',
|
|
77
|
+
},
|
|
78
|
+
expires_at: getArg('expires'),
|
|
79
|
+
});
|
|
80
|
+
console.log(JSON.stringify(result, null, 2));
|
|
81
|
+
break;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
case 'revoke': {
|
|
85
|
+
const source = getArg('source');
|
|
86
|
+
const key = getArg('key');
|
|
87
|
+
const reason = getArg('reason') || 'Revoked via CLI';
|
|
88
|
+
const result = await registry.revoke(
|
|
89
|
+
{ source, record_key: key },
|
|
90
|
+
reason
|
|
91
|
+
);
|
|
92
|
+
console.log(JSON.stringify(result, null, 2));
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
case 'list': {
|
|
97
|
+
const filters: Record<string, string> = {};
|
|
98
|
+
const trustLevel = getArg('trust-level');
|
|
99
|
+
const status = getArg('status');
|
|
100
|
+
const sourcePattern = getArg('source-pattern');
|
|
101
|
+
if (trustLevel) filters.trust_level = trustLevel;
|
|
102
|
+
if (status) filters.status = status;
|
|
103
|
+
if (sourcePattern) filters.source_pattern = sourcePattern;
|
|
104
|
+
|
|
105
|
+
const records = await registry.list(filters);
|
|
106
|
+
console.log(JSON.stringify(records, null, 2));
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
case 'hash': {
|
|
111
|
+
const dirPath = getArg('path');
|
|
112
|
+
if (!dirPath) {
|
|
113
|
+
console.error('Error: --path is required for hash');
|
|
114
|
+
process.exit(1);
|
|
115
|
+
}
|
|
116
|
+
const scanner = new SkillScanner({ useExternalScanner: false });
|
|
117
|
+
const hash = await scanner.calculateArtifactHash(dirPath);
|
|
118
|
+
console.log(JSON.stringify({ hash }));
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
default:
|
|
123
|
+
console.error(
|
|
124
|
+
'Usage: trust-cli.ts <lookup|attest|revoke|list|hash> [options]'
|
|
125
|
+
);
|
|
126
|
+
console.error('Run with --help for details.');
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
main().catch((err) => {
|
|
132
|
+
console.error(JSON.stringify({ error: err.message }));
|
|
133
|
+
process.exit(1);
|
|
134
|
+
});
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Web3 Vulnerability Patterns Reference
|
|
2
|
+
|
|
3
|
+
Detailed vulnerability patterns for the `web3` subcommand. Use this when performing Web3/smart contract security audits.
|
|
4
|
+
|
|
5
|
+
## Solidity Vulnerability Categories
|
|
6
|
+
|
|
7
|
+
### 1. Reentrancy (HIGH)
|
|
8
|
+
|
|
9
|
+
**Pattern**: External call before state update in the same function.
|
|
10
|
+
|
|
11
|
+
```solidity
|
|
12
|
+
// VULNERABLE: state updated after external call
|
|
13
|
+
function withdraw(uint amount) public {
|
|
14
|
+
require(balances[msg.sender] >= amount);
|
|
15
|
+
(bool success, ) = msg.sender.call{value: amount}(""); // external call
|
|
16
|
+
balances[msg.sender] -= amount; // state change AFTER call
|
|
17
|
+
}
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
**Detection**: Look for `.call{`, `.transfer(`, `.send(` followed by storage variable assignment (`var =`, `var +=`, `var -=`) within the same function body.
|
|
21
|
+
|
|
22
|
+
**Fix**: Use Checks-Effects-Interactions pattern or ReentrancyGuard.
|
|
23
|
+
|
|
24
|
+
### 2. Wallet Draining (CRITICAL)
|
|
25
|
+
|
|
26
|
+
**Patterns**:
|
|
27
|
+
- `approve()` with max uint256 value followed by `transferFrom()`
|
|
28
|
+
- `permit()` with far-future deadline enabling gasless approvals
|
|
29
|
+
- Approval granted to attacker-controlled address
|
|
30
|
+
|
|
31
|
+
**Detection**:
|
|
32
|
+
- `approve(address, type(uint256).max)` or `MaxUint256` or `2**256-1`
|
|
33
|
+
- `transferFrom` in same contract/flow as `approve`
|
|
34
|
+
- `permit(` with `deadline` parameter
|
|
35
|
+
|
|
36
|
+
### 3. Unlimited Approval (HIGH)
|
|
37
|
+
|
|
38
|
+
**Pattern**: Token approval for maximum amount, giving spender unlimited access.
|
|
39
|
+
|
|
40
|
+
```solidity
|
|
41
|
+
token.approve(spender, type(uint256).max);
|
|
42
|
+
token.setApprovalForAll(operator, true);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
**Risk**: If the spender contract is compromised, all approved tokens can be drained.
|
|
46
|
+
|
|
47
|
+
### 4. Self-Destruct (HIGH)
|
|
48
|
+
|
|
49
|
+
**Pattern**: `selfdestruct(address)` or deprecated `suicide(address)`.
|
|
50
|
+
|
|
51
|
+
**Risk**: Permanently destroys the contract, sending remaining ETH to the specified address. Can be exploited if access control is weak.
|
|
52
|
+
|
|
53
|
+
### 5. Signature Replay (HIGH)
|
|
54
|
+
|
|
55
|
+
**Pattern**: Using `ecrecover()` without nonce or chain ID protection.
|
|
56
|
+
|
|
57
|
+
```solidity
|
|
58
|
+
// VULNERABLE: no nonce, signature can be replayed
|
|
59
|
+
function execute(bytes memory signature, uint amount) public {
|
|
60
|
+
bytes32 hash = keccak256(abi.encodePacked(msg.sender, amount));
|
|
61
|
+
address signer = ecrecover(hash, v, r, s);
|
|
62
|
+
// Missing: nonce check, chain ID, contract address in hash
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Fix**: Include nonce, chainId, and contract address in signed message. Use EIP-712 typed data.
|
|
67
|
+
|
|
68
|
+
### 6. Flash Loan Attacks (MEDIUM)
|
|
69
|
+
|
|
70
|
+
**Patterns**:
|
|
71
|
+
- `flashLoan()`, `IFlashLoan`, `executeOperation()`
|
|
72
|
+
- AAVE/dYdX/Uniswap flash loan interfaces
|
|
73
|
+
|
|
74
|
+
**Risk**: Attacker borrows large amounts without collateral within a single transaction to manipulate prices, governance, or vulnerable protocols.
|
|
75
|
+
|
|
76
|
+
**Check**: Ensure price oracles use TWAP, governance has timelock, and lending markets have proper collateral checks.
|
|
77
|
+
|
|
78
|
+
### 7. Proxy Upgrade Risks (MEDIUM)
|
|
79
|
+
|
|
80
|
+
**Patterns**:
|
|
81
|
+
- `upgradeTo(address)`, `upgradeToAndCall(address, bytes)`
|
|
82
|
+
- `_setImplementation()`, `IMPLEMENTATION_SLOT`
|
|
83
|
+
- Transparent proxy / UUPS patterns
|
|
84
|
+
|
|
85
|
+
**Check**:
|
|
86
|
+
- Is upgrade function access-controlled?
|
|
87
|
+
- Is there a timelock on upgrades?
|
|
88
|
+
- Can the proxy be upgraded to a malicious implementation?
|
|
89
|
+
|
|
90
|
+
### 8. Hidden Transfers (MEDIUM)
|
|
91
|
+
|
|
92
|
+
**Pattern**: ETH or token transfers hidden in functions not named transfer/send.
|
|
93
|
+
|
|
94
|
+
```solidity
|
|
95
|
+
// Transfer hidden in an innocuous-looking function
|
|
96
|
+
function updateConfig(address _new) public {
|
|
97
|
+
payable(_new).transfer(address(this).balance); // hidden drain
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Detection**: `.transfer(`, `.call{value:}` in functions not named `transfer`, `_transfer`, `send`, `withdraw`.
|
|
102
|
+
|
|
103
|
+
### 9. Price Oracle Manipulation
|
|
104
|
+
|
|
105
|
+
**Patterns**:
|
|
106
|
+
- Single-block spot price from DEX (e.g., `getReserves()` used directly for pricing)
|
|
107
|
+
- No TWAP (Time-Weighted Average Price) implementation
|
|
108
|
+
- Price feed without staleness check
|
|
109
|
+
|
|
110
|
+
**Check**: Look for Uniswap `getReserves()`, Chainlink `latestRoundData()` without `updatedAt` staleness check.
|
|
111
|
+
|
|
112
|
+
### 10. Access Control Issues
|
|
113
|
+
|
|
114
|
+
**Patterns**:
|
|
115
|
+
- Public/external functions that modify critical state without access modifiers
|
|
116
|
+
- Missing `onlyOwner`, `onlyRole`, or similar guards
|
|
117
|
+
- `tx.origin` used for authentication (vulnerable to phishing)
|
|
118
|
+
|
|
119
|
+
**Check**:
|
|
120
|
+
- All state-changing functions should have appropriate access control
|
|
121
|
+
- `tx.origin` should never be used for authorization (use `msg.sender`)
|
|
122
|
+
- Owner/admin functions should be behind multisig or timelock
|
|
123
|
+
|
|
124
|
+
## JavaScript/TypeScript Web3 Patterns
|
|
125
|
+
|
|
126
|
+
### Private Key Exposure
|
|
127
|
+
|
|
128
|
+
- Hardcoded private keys: `0x` followed by 64 hex characters in quotes
|
|
129
|
+
- `PRIVATE_KEY` in environment variable assignments
|
|
130
|
+
- Private key in config files, .env files committed to repo
|
|
131
|
+
|
|
132
|
+
### Mnemonic Exposure
|
|
133
|
+
|
|
134
|
+
- BIP-39 seed phrases (12-24 words) in source code
|
|
135
|
+
- `seed_phrase`, `mnemonic`, `recovery_phrase` variable assignments
|
|
136
|
+
|
|
137
|
+
### Unsafe RPC Usage
|
|
138
|
+
|
|
139
|
+
- Sending signed transactions without user confirmation
|
|
140
|
+
- Using `eth_sendTransaction` with hardcoded parameters
|
|
141
|
+
- Missing gas limit estimation (risk of out-of-gas)
|
|
142
|
+
|
|
143
|
+
### Front-End Risks
|
|
144
|
+
|
|
145
|
+
- Phishing patterns: UI mimicking wallet approval screens
|
|
146
|
+
- Hidden iframe/overlay for approval harvesting
|
|
147
|
+
- Approval transactions disguised as other operations
|
|
148
|
+
|
|
149
|
+
## GoPlus Security Checks (Optional)
|
|
150
|
+
|
|
151
|
+
If GoPlus API is available, these additional checks can be performed:
|
|
152
|
+
|
|
153
|
+
| Check | API | Description |
|
|
154
|
+
|-------|-----|-------------|
|
|
155
|
+
| Token Security | `tokenSecurity(chainId, address)` | Honeypot, tax, owner control |
|
|
156
|
+
| Address Security | `addressSecurity(chainId, address)` | Blacklisted, phishing, mixer |
|
|
157
|
+
| Approval Risk | `approvalSecurity(chainId, address)` | Risky approvals |
|
|
158
|
+
| Phishing Site | `phishingSite(url)` | Known phishing URL |
|
|
159
|
+
| Transaction Simulation | `simulateTransaction(...)` | Pre-execution risk analysis |
|
|
160
|
+
|
|
161
|
+
Requires `GOPLUS_API_KEY` and `GOPLUS_API_SECRET` environment variables.
|