athena-mcp 1.0.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/README.md +477 -0
- package/install.js +327 -0
- package/mcp/servers.json +100 -0
- package/mcp/tools/README.md +64 -0
- package/mcp/tools/__init__.py +1 -0
- package/mcp/tools/aderyn_runner.py +226 -0
- package/mcp/tools/eas_attest.py +404 -0
- package/mcp/tools/evidence_chain.py +363 -0
- package/mcp/tools/exploit_simulator.py +545 -0
- package/mcp/tools/fuzz_runner.py +440 -0
- package/mcp/tools/gev_analyzer.py +362 -0
- package/mcp/tools/halmos_runner.py +408 -0
- package/mcp/tools/incremental_auditor.py +441 -0
- package/mcp/tools/knowledge_base.py +378 -0
- package/mcp/tools/poc_generator.py +479 -0
- package/mcp/tools/protocol_scanner.py +456 -0
- package/mcp/tools/repair_validator.py +421 -0
- package/mcp/tools/slither_runner.py +221 -0
- package/package.json +52 -0
- package/requirements.txt +20 -0
- package/skills/glm-audit-skill/SKILL.md +73 -0
- package/skills/glm-audit-skill/references/audit-agents/access-control-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/asymmetry-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/boundary-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/economic-security-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/execution-trace-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/first-principles-agent.md +42 -0
- package/skills/glm-audit-skill/references/audit-agents/flow-gap-agent.md +38 -0
- package/skills/glm-audit-skill/references/audit-agents/invariant-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/math-precision-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/numerical-gap-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/periphery-agent.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/shared-rules.md +37 -0
- package/skills/glm-audit-skill/references/audit-agents/trust-gap-agent.md +39 -0
- package/skills/glm-audit-skill/references/judging.md +45 -0
- package/skills/glm-audit-skill/references/report-formatting.md +22 -0
- package/skills/glm-audit-skill/references/senior-auditor-sop.md +34 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Server: Aderyn Runner
|
|
4
|
+
Wraps Aderyn (Rust-based) static analysis for Solidity smart contracts.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import tempfile
|
|
10
|
+
import subprocess
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
16
|
+
logger = logging.getLogger("aderyn_runner")
|
|
17
|
+
|
|
18
|
+
TOOL_NAME = "aderyn_runner"
|
|
19
|
+
TOOL_VERSION = "1.0.0"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def build_tool_definitions() -> list:
|
|
23
|
+
return [
|
|
24
|
+
{
|
|
25
|
+
"name": "aderyn_analyze",
|
|
26
|
+
"description": "Run Aderyn static analysis on a Solidity project. Returns findings grouped by severity (high, low, informational, optimization).",
|
|
27
|
+
"inputSchema": {
|
|
28
|
+
"type": "object",
|
|
29
|
+
"properties": {
|
|
30
|
+
"project_path": {
|
|
31
|
+
"type": "string",
|
|
32
|
+
"description": "Path to the Solidity project root directory (must contain foundry.toml or hardhat.config)"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
"required": ["project_path"]
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def run_aderyn(project_path: str) -> dict:
|
|
42
|
+
"""Execute Aderyn and parse JSON output."""
|
|
43
|
+
project_path = os.path.abspath(project_path)
|
|
44
|
+
|
|
45
|
+
if not os.path.isdir(project_path):
|
|
46
|
+
return {"success": False, "error": f"Project directory not found: {project_path}"}
|
|
47
|
+
|
|
48
|
+
tmp_dir = tempfile.mkdtemp()
|
|
49
|
+
json_output_path = os.path.join(tmp_dir, "report.json")
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
cmd = ["aderyn", project_path, "--output", json_output_path]
|
|
53
|
+
|
|
54
|
+
logger.info(f"Running aderyn: {' '.join(cmd)}")
|
|
55
|
+
|
|
56
|
+
proc = await asyncio.create_subprocess_exec(
|
|
57
|
+
*cmd,
|
|
58
|
+
stdout=asyncio.subprocess.PIPE,
|
|
59
|
+
stderr=asyncio.subprocess.PIPE
|
|
60
|
+
)
|
|
61
|
+
stdout, stderr = await proc.communicate()
|
|
62
|
+
|
|
63
|
+
if proc.returncode != 0:
|
|
64
|
+
return {
|
|
65
|
+
"success": False,
|
|
66
|
+
"error": f"Aderyn exited with code {proc.returncode}",
|
|
67
|
+
"stderr": stderr.decode("utf-8", errors="replace").strip()
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# Parse JSON output
|
|
71
|
+
if not os.path.isfile(json_output_path):
|
|
72
|
+
# Aderyn may output with a different name
|
|
73
|
+
possible_names = ["report.json", "aderyn-report.json", "output.json"]
|
|
74
|
+
for name in possible_names:
|
|
75
|
+
alt_path = os.path.join(tmp_dir, name)
|
|
76
|
+
if os.path.isfile(alt_path):
|
|
77
|
+
json_output_path = alt_path
|
|
78
|
+
break
|
|
79
|
+
else:
|
|
80
|
+
return {
|
|
81
|
+
"success": False,
|
|
82
|
+
"error": "Aderyn did not produce JSON output",
|
|
83
|
+
"stderr": stderr.decode("utf-8", errors="replace").strip(),
|
|
84
|
+
"stdout": stdout.decode("utf-8", errors="replace").strip()
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
with open(json_output_path, "r") as f:
|
|
88
|
+
raw_output = json.load(f)
|
|
89
|
+
|
|
90
|
+
# Normalize Aderyn output structure
|
|
91
|
+
# Aderyn groups issues by severity
|
|
92
|
+
def extract_issues(issue_list):
|
|
93
|
+
issues = []
|
|
94
|
+
for issue in (issue_list or []):
|
|
95
|
+
issues.append({
|
|
96
|
+
"title": issue.get("title", ""),
|
|
97
|
+
"description": issue.get("description", ""),
|
|
98
|
+
"detector_name": issue.get("detector_name", ""),
|
|
99
|
+
"severity": issue.get("severity", ""),
|
|
100
|
+
"instances": [
|
|
101
|
+
{
|
|
102
|
+
"contract_path": inst.get("contract_path", ""),
|
|
103
|
+
"line_no": inst.get("line_no", 0),
|
|
104
|
+
"src": inst.get("src", ""),
|
|
105
|
+
"code_snippet": inst.get("code_snippet", "")
|
|
106
|
+
}
|
|
107
|
+
for inst in issue.get("instances", [])
|
|
108
|
+
],
|
|
109
|
+
"instance_count": len(issue.get("instances", []))
|
|
110
|
+
})
|
|
111
|
+
return issues
|
|
112
|
+
|
|
113
|
+
high_issues = extract_issues(raw_output.get("high_issues", {}).get("issues", []))
|
|
114
|
+
low_issues = extract_issues(raw_output.get("low_issues", {}).get("issues", []))
|
|
115
|
+
informational_issues = extract_issues(raw_output.get("informational_issues", {}).get("issues", []))
|
|
116
|
+
optimization_issues = extract_issues(raw_output.get("optimization_issues", {}).get("issues", []))
|
|
117
|
+
|
|
118
|
+
total_issues = (
|
|
119
|
+
len(high_issues) + len(low_issues) +
|
|
120
|
+
len(informational_issues) + len(optimization_issues)
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
"success": True,
|
|
125
|
+
"project": project_path,
|
|
126
|
+
"summary": {
|
|
127
|
+
"total_issues": total_issues,
|
|
128
|
+
"high_severity": len(high_issues),
|
|
129
|
+
"low_severity": len(low_issues),
|
|
130
|
+
"informational": len(informational_issues),
|
|
131
|
+
"optimization": len(optimization_issues)
|
|
132
|
+
},
|
|
133
|
+
"high_issues": high_issues,
|
|
134
|
+
"low_issues": low_issues,
|
|
135
|
+
"informational_issues": informational_issues,
|
|
136
|
+
"optimization_issues": optimization_issues
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
except FileNotFoundError:
|
|
140
|
+
return {"success": False, "error": "Aderyn not found. Install with: cargo install aderyn"}
|
|
141
|
+
except json.JSONDecodeError as e:
|
|
142
|
+
return {"success": False, "error": f"Failed to parse Aderyn JSON output: {str(e)}"}
|
|
143
|
+
except Exception as e:
|
|
144
|
+
logger.exception("Unexpected error in aderyn_runner")
|
|
145
|
+
return {"success": False, "error": f"Unexpected error: {str(e)}"}
|
|
146
|
+
finally:
|
|
147
|
+
# Cleanup temp dir
|
|
148
|
+
import shutil
|
|
149
|
+
if os.path.exists(tmp_dir):
|
|
150
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
async def execute_tool(tool_name: str, arguments: dict) -> dict:
|
|
154
|
+
if tool_name == "aderyn_analyze":
|
|
155
|
+
project_path = arguments.get("project_path", "")
|
|
156
|
+
return await run_aderyn(project_path)
|
|
157
|
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
async def handle_request(request: dict) -> dict:
|
|
161
|
+
method = request.get("method")
|
|
162
|
+
params = request.get("params", {})
|
|
163
|
+
|
|
164
|
+
if method == "initialize":
|
|
165
|
+
return {
|
|
166
|
+
"protocolVersion": "2024-11-05",
|
|
167
|
+
"capabilities": {"tools": {}},
|
|
168
|
+
"serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
|
|
169
|
+
}
|
|
170
|
+
elif method == "tools/list":
|
|
171
|
+
return {"tools": build_tool_definitions()}
|
|
172
|
+
elif method == "tools/call":
|
|
173
|
+
tool_name = params.get("name")
|
|
174
|
+
arguments = params.get("arguments", {})
|
|
175
|
+
result = await execute_tool(tool_name, arguments)
|
|
176
|
+
return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
|
|
177
|
+
elif method == "ping":
|
|
178
|
+
return {}
|
|
179
|
+
return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
async def main():
|
|
183
|
+
reader = asyncio.StreamReader()
|
|
184
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
185
|
+
loop = asyncio.get_event_loop()
|
|
186
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
187
|
+
|
|
188
|
+
logger.info(f"{TOOL_NAME} MCP server started")
|
|
189
|
+
|
|
190
|
+
while True:
|
|
191
|
+
line = await reader.readline()
|
|
192
|
+
if not line:
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
line_str = line.decode("utf-8").strip()
|
|
196
|
+
if not line_str:
|
|
197
|
+
continue
|
|
198
|
+
|
|
199
|
+
try:
|
|
200
|
+
request = json.loads(line_str)
|
|
201
|
+
response = await handle_request(request)
|
|
202
|
+
response["jsonrpc"] = "2.0"
|
|
203
|
+
response["id"] = request.get("id")
|
|
204
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
205
|
+
sys.stdout.flush()
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
error_resp = {
|
|
208
|
+
"jsonrpc": "2.0",
|
|
209
|
+
"id": None,
|
|
210
|
+
"error": {"code": -32700, "message": "Parse error"}
|
|
211
|
+
}
|
|
212
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
213
|
+
sys.stdout.flush()
|
|
214
|
+
except Exception as e:
|
|
215
|
+
logger.exception("Error handling request")
|
|
216
|
+
error_resp = {
|
|
217
|
+
"jsonrpc": "2.0",
|
|
218
|
+
"id": request.get("id") if "request" in dir() else None,
|
|
219
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"}
|
|
220
|
+
}
|
|
221
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
222
|
+
sys.stdout.flush()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
if __name__ == "__main__":
|
|
226
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
MCP Server: EAS Attestation
|
|
4
|
+
Handles on-chain attestation via Ethereum Attestation Service (EAS) on Sepolia testnet.
|
|
5
|
+
"""
|
|
6
|
+
import sys
|
|
7
|
+
import json
|
|
8
|
+
import asyncio
|
|
9
|
+
import logging
|
|
10
|
+
import os
|
|
11
|
+
import hashlib
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
|
|
15
|
+
logger = logging.getLogger("eas_attest")
|
|
16
|
+
|
|
17
|
+
TOOL_NAME = "eas_attest"
|
|
18
|
+
TOOL_VERSION = "1.0.0"
|
|
19
|
+
|
|
20
|
+
# EAS Sepolia configuration
|
|
21
|
+
EAS_CONTRACT_ADDRESS = "0xC2679fBD37d54388Ce493F1DB75320D236e1815e"
|
|
22
|
+
EAS_SCHEMA_REGISTRY = "0x0a7E2Ff94F05A4a6dCa5eF0D47c1e39e8F8e5fC0"
|
|
23
|
+
SEPOLIA_CHAIN_ID = 11155111
|
|
24
|
+
|
|
25
|
+
# Audit result schema UID (example - replace with actual registered schema)
|
|
26
|
+
AUDIT_RESULT_SCHEMA_UID = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"
|
|
27
|
+
|
|
28
|
+
# Common vulnerability severities
|
|
29
|
+
SEVERITY_MAP = {
|
|
30
|
+
"critical": 4,
|
|
31
|
+
"high": 3,
|
|
32
|
+
"medium": 2,
|
|
33
|
+
"low": 1,
|
|
34
|
+
"informational": 0
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_tool_definitions() -> list:
|
|
39
|
+
return [
|
|
40
|
+
{
|
|
41
|
+
"name": "submit_attestation",
|
|
42
|
+
"description": "Submit an audit attestation to the Ethereum Attestation Service (EAS) on Sepolia testnet. Creates an on-chain record of the audit results including found vulnerabilities and their severities.",
|
|
43
|
+
"inputSchema": {
|
|
44
|
+
"type": "object",
|
|
45
|
+
"properties": {
|
|
46
|
+
"contract_address": {
|
|
47
|
+
"type": "string",
|
|
48
|
+
"description": "The Ethereum address of the audited smart contract"
|
|
49
|
+
},
|
|
50
|
+
"vulnerabilities": {
|
|
51
|
+
"type": "array",
|
|
52
|
+
"items": {
|
|
53
|
+
"type": "object",
|
|
54
|
+
"properties": {
|
|
55
|
+
"type": {"type": "string", "description": "Vulnerability type (e.g., 'reentrancy', 'overflow')"},
|
|
56
|
+
"severity": {"type": "string", "enum": ["critical", "high", "medium", "low", "informational"]},
|
|
57
|
+
"description": {"type": "string"},
|
|
58
|
+
"location": {"type": "string", "description": "File and line number"},
|
|
59
|
+
"remediation": {"type": "string", "description": "Suggested fix"}
|
|
60
|
+
},
|
|
61
|
+
"required": ["type", "severity"]
|
|
62
|
+
},
|
|
63
|
+
"description": "List of vulnerabilities found during the audit"
|
|
64
|
+
},
|
|
65
|
+
"audit_mode": {
|
|
66
|
+
"type": "string",
|
|
67
|
+
"enum": ["full", "quick", "targeted"],
|
|
68
|
+
"description": "Type of audit performed (default: full)",
|
|
69
|
+
"default": "full"
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
"required": ["contract_address", "vulnerabilities"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def compute_audit_hash(contract_address: str, vulnerabilities: list) -> str:
|
|
79
|
+
"""Compute deterministic hash of audit results for verification."""
|
|
80
|
+
audit_data = {
|
|
81
|
+
"contract": contract_address.lower(),
|
|
82
|
+
"vulnerabilities": sorted(
|
|
83
|
+
[
|
|
84
|
+
{
|
|
85
|
+
"type": v.get("type", ""),
|
|
86
|
+
"severity": v.get("severity", ""),
|
|
87
|
+
"description": v.get("description", "")
|
|
88
|
+
}
|
|
89
|
+
for v in vulnerabilities
|
|
90
|
+
],
|
|
91
|
+
key=lambda x: x["type"]
|
|
92
|
+
),
|
|
93
|
+
"timestamp": datetime.utcnow().strftime("%Y-%m-%d")
|
|
94
|
+
}
|
|
95
|
+
data_str = json.dumps(audit_data, sort_keys=True)
|
|
96
|
+
return "0x" + hashlib.sha256(data_str.encode()).hexdigest()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def compute_audit_grade(vulnerabilities: list) -> str:
|
|
100
|
+
"""Compute audit grade based on vulnerability severities."""
|
|
101
|
+
if not vulnerabilities:
|
|
102
|
+
return "A+"
|
|
103
|
+
|
|
104
|
+
max_severity = 0
|
|
105
|
+
for vuln in vulnerabilities:
|
|
106
|
+
severity = vuln.get("severity", "informational").lower()
|
|
107
|
+
sev_value = SEVERITY_MAP.get(severity, 0)
|
|
108
|
+
max_severity = max(max_severity, sev_value)
|
|
109
|
+
|
|
110
|
+
# Count by severity
|
|
111
|
+
counts = {}
|
|
112
|
+
for vuln in vulnerabilities:
|
|
113
|
+
severity = vuln.get("severity", "informational").lower()
|
|
114
|
+
counts[severity] = counts.get(severity, 0) + 1
|
|
115
|
+
|
|
116
|
+
# Grade calculation
|
|
117
|
+
if max_severity >= 4: # critical
|
|
118
|
+
return "F"
|
|
119
|
+
elif max_severity >= 3: # high
|
|
120
|
+
if counts.get("high", 0) >= 3:
|
|
121
|
+
return "D"
|
|
122
|
+
return "C"
|
|
123
|
+
elif max_severity >= 2: # medium
|
|
124
|
+
if counts.get("medium", 0) >= 3:
|
|
125
|
+
return "C"
|
|
126
|
+
return "B"
|
|
127
|
+
elif max_severity >= 1: # low
|
|
128
|
+
return "B+"
|
|
129
|
+
else: # informational only
|
|
130
|
+
return "A"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
async def submit_eas_attestation(
|
|
134
|
+
contract_address: str,
|
|
135
|
+
vulnerabilities: list,
|
|
136
|
+
audit_mode: str = "full"
|
|
137
|
+
) -> dict:
|
|
138
|
+
"""Submit attestation to EAS on Sepolia."""
|
|
139
|
+
|
|
140
|
+
# Validate contract address
|
|
141
|
+
if not contract_address.startswith("0x") or len(contract_address) != 42:
|
|
142
|
+
return {"success": False, "error": "Invalid contract address format"}
|
|
143
|
+
|
|
144
|
+
# Compute audit hash and grade
|
|
145
|
+
audit_hash = compute_audit_hash(contract_address, vulnerabilities)
|
|
146
|
+
grade = compute_audit_grade(vulnerabilities)
|
|
147
|
+
|
|
148
|
+
# Build attestation data
|
|
149
|
+
attestation_data = {
|
|
150
|
+
"schema": AUDIT_RESULT_SCHEMA_UID,
|
|
151
|
+
"recipient": contract_address,
|
|
152
|
+
"expirationTime": 0,
|
|
153
|
+
"revocable": True,
|
|
154
|
+
"refUID": "0x0000000000000000000000000000000000000000000000000000000000000000",
|
|
155
|
+
"data": {
|
|
156
|
+
"contract_address": contract_address,
|
|
157
|
+
"audit_hash": audit_hash,
|
|
158
|
+
"grade": grade,
|
|
159
|
+
"audit_mode": audit_mode,
|
|
160
|
+
"vulnerability_count": len(vulnerabilities),
|
|
161
|
+
"vulnerabilities": vulnerabilities,
|
|
162
|
+
"timestamp": datetime.utcnow().isoformat(),
|
|
163
|
+
"auditor": "GLM-Audit-Agent-v1"
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Check for private key in environment
|
|
168
|
+
private_key = os.environ.get("SEPOLIA_PRIVATE_KEY", "")
|
|
169
|
+
rpc_url = os.environ.get("SEPOLIA_RPC_URL", "https://rpc.sepolia.org")
|
|
170
|
+
|
|
171
|
+
if private_key:
|
|
172
|
+
# Real on-chain attestation
|
|
173
|
+
try:
|
|
174
|
+
return await _send_onchain_attestation(attestation_data, private_key, rpc_url)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
logger.error(f"On-chain attestation failed: {e}")
|
|
177
|
+
return {
|
|
178
|
+
"success": False,
|
|
179
|
+
"error": f"On-chain attestation failed: {str(e)}",
|
|
180
|
+
"attestation_data": attestation_data
|
|
181
|
+
}
|
|
182
|
+
else:
|
|
183
|
+
# Mock mode - generate attestation without sending to chain
|
|
184
|
+
logger.info("No private key configured, generating mock attestation")
|
|
185
|
+
return _generate_mock_attestation(attestation_data)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
async def _send_onchain_attestation(attestation_data: dict, private_key: str, rpc_url: str) -> dict:
|
|
189
|
+
"""Send real attestation to EAS on Sepolia."""
|
|
190
|
+
try:
|
|
191
|
+
from web3 import Web3
|
|
192
|
+
from eth_account import Account
|
|
193
|
+
|
|
194
|
+
w3 = Web3(Web3.HTTPProvider(rpc_url))
|
|
195
|
+
if not w3.is_connected():
|
|
196
|
+
return {"success": False, "error": f"Cannot connect to {rpc_url}"}
|
|
197
|
+
|
|
198
|
+
account = Account.from_key(private_key)
|
|
199
|
+
balance = w3.eth.get_balance(account.address)
|
|
200
|
+
if balance < w3.to_wei(0.001, "ether"):
|
|
201
|
+
return {
|
|
202
|
+
"success": False,
|
|
203
|
+
"error": f"Insufficient balance: {w3.from_wei(balance, 'ether')} ETH. Need at least 0.001 ETH for gas."
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# EAS contract ABI (minimal for attest)
|
|
207
|
+
eas_abi = [
|
|
208
|
+
{
|
|
209
|
+
"inputs": [
|
|
210
|
+
{
|
|
211
|
+
"components": [
|
|
212
|
+
{"name": "schema", "type": "bytes32"},
|
|
213
|
+
{"name": "recipient", "type": "address"},
|
|
214
|
+
{"name": "expirationTime", "type": "uint64"},
|
|
215
|
+
{"name": "revocable", "type": "bool"},
|
|
216
|
+
{"name": "refUID", "type": "bytes32"},
|
|
217
|
+
{"name": "data", "type": "bytes"},
|
|
218
|
+
{"name": "value", "type": "uint256"}
|
|
219
|
+
],
|
|
220
|
+
"name": "request",
|
|
221
|
+
"type": "tuple"
|
|
222
|
+
}
|
|
223
|
+
],
|
|
224
|
+
"name": "attest",
|
|
225
|
+
"outputs": [{"name": "", "type": "bytes32"}],
|
|
226
|
+
"stateMutability": "payable",
|
|
227
|
+
"type": "function"
|
|
228
|
+
}
|
|
229
|
+
]
|
|
230
|
+
|
|
231
|
+
eas_contract = w3.eth.contract(
|
|
232
|
+
address=Web3.to_checksum_address(EAS_CONTRACT_ADDRESS),
|
|
233
|
+
abi=eas_abi
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Encode attestation data
|
|
237
|
+
data_bytes = json.dumps(attestation_data["data"]).encode("utf-8")
|
|
238
|
+
|
|
239
|
+
# Build transaction
|
|
240
|
+
attestation_request = (
|
|
241
|
+
bytes.fromhex(attestation_data["schema"][2:]),
|
|
242
|
+
Web3.to_checksum_address(attestation_data["recipient"]),
|
|
243
|
+
attestation_data["expirationTime"],
|
|
244
|
+
attestation_data["revocable"],
|
|
245
|
+
bytes.fromhex(attestation_data["refUID"][2:]),
|
|
246
|
+
data_bytes,
|
|
247
|
+
0
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
tx = eas_contract.functions.attest(attestation_request).build_transaction({
|
|
251
|
+
"from": account.address,
|
|
252
|
+
"nonce": w3.eth.get_transaction_count(account.address),
|
|
253
|
+
"gas": 200000,
|
|
254
|
+
"gasPrice": w3.eth.gas_price,
|
|
255
|
+
"value": 0
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
signed_tx = w3.eth.account.sign_transaction(tx, private_key)
|
|
259
|
+
tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)
|
|
260
|
+
|
|
261
|
+
# Wait for receipt
|
|
262
|
+
receipt = w3.eth.wait_for_transaction_receipt(tx_hash, timeout=120)
|
|
263
|
+
|
|
264
|
+
return {
|
|
265
|
+
"success": True,
|
|
266
|
+
"mode": "onchain",
|
|
267
|
+
"tx_hash": receipt.transactionHash.hex(),
|
|
268
|
+
"block_number": receipt.blockNumber,
|
|
269
|
+
"gas_used": receipt.gasUsed,
|
|
270
|
+
"contract_address": contract_address,
|
|
271
|
+
"grade": attestation_data["data"]["grade"],
|
|
272
|
+
"audit_hash": attestation_data["data"]["audit_hash"],
|
|
273
|
+
"eas_contract": EAS_CONTRACT_ADDRESS,
|
|
274
|
+
"network": "sepolia",
|
|
275
|
+
"explorer_url": f"https://sepolia.etherscan.io/tx/{receipt.transactionHash.hex()}"
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
except ImportError:
|
|
279
|
+
return {
|
|
280
|
+
"success": False,
|
|
281
|
+
"error": "web3 and eth-account packages required for on-chain attestation. Install with: pip install web3 eth-account"
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _generate_mock_attestation(attestation_data: dict) -> dict:
|
|
286
|
+
"""Generate mock attestation for testing."""
|
|
287
|
+
mock_tx_hash = hashlib.sha256(
|
|
288
|
+
json.dumps(attestation_data, sort_keys=True).encode()
|
|
289
|
+
).hexdigest()
|
|
290
|
+
|
|
291
|
+
data = attestation_data["data"]
|
|
292
|
+
|
|
293
|
+
return {
|
|
294
|
+
"success": True,
|
|
295
|
+
"mode": "mock",
|
|
296
|
+
"note": "This is a mock attestation. Set SEPOLIA_PRIVATE_KEY and SEPOLIA_RPC_URL for on-chain attestation.",
|
|
297
|
+
"tx_hash": f"0x{mock_tx_hash}",
|
|
298
|
+
"contract_address": data["contract_address"],
|
|
299
|
+
"grade": data["grade"],
|
|
300
|
+
"audit_hash": data["audit_hash"],
|
|
301
|
+
"audit_mode": data["audit_mode"],
|
|
302
|
+
"vulnerability_summary": {
|
|
303
|
+
"total": data["vulnerability_count"],
|
|
304
|
+
"by_severity": _count_by_severity(data["vulnerabilities"])
|
|
305
|
+
},
|
|
306
|
+
"eas_contract": EAS_CONTRACT_ADDRESS,
|
|
307
|
+
"network": "sepolia",
|
|
308
|
+
"timestamp": data["timestamp"]
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _count_by_severity(vulnerabilities: list) -> dict:
|
|
313
|
+
"""Count vulnerabilities by severity level."""
|
|
314
|
+
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "informational": 0}
|
|
315
|
+
for vuln in vulnerabilities:
|
|
316
|
+
severity = vuln.get("severity", "informational").lower()
|
|
317
|
+
if severity in counts:
|
|
318
|
+
counts[severity] += 1
|
|
319
|
+
return counts
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
async def execute_tool(tool_name: str, arguments: dict) -> dict:
|
|
323
|
+
if tool_name == "submit_attestation":
|
|
324
|
+
contract_address = arguments.get("contract_address", "")
|
|
325
|
+
vulnerabilities = arguments.get("vulnerabilities", [])
|
|
326
|
+
audit_mode = arguments.get("audit_mode", "full")
|
|
327
|
+
|
|
328
|
+
if not contract_address:
|
|
329
|
+
return {"success": False, "error": "contract_address is required"}
|
|
330
|
+
if not vulnerabilities:
|
|
331
|
+
return {"success": False, "error": "vulnerabilities array is required"}
|
|
332
|
+
|
|
333
|
+
return await submit_eas_attestation(contract_address, vulnerabilities, audit_mode)
|
|
334
|
+
|
|
335
|
+
return {"success": False, "error": f"Unknown tool: {tool_name}"}
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
async def handle_request(request: dict) -> dict:
|
|
339
|
+
method = request.get("method")
|
|
340
|
+
params = request.get("params", {})
|
|
341
|
+
|
|
342
|
+
if method == "initialize":
|
|
343
|
+
return {
|
|
344
|
+
"protocolVersion": "2024-11-05",
|
|
345
|
+
"capabilities": {"tools": {}},
|
|
346
|
+
"serverInfo": {"name": TOOL_NAME, "version": TOOL_VERSION}
|
|
347
|
+
}
|
|
348
|
+
elif method == "tools/list":
|
|
349
|
+
return {"tools": build_tool_definitions()}
|
|
350
|
+
elif method == "tools/call":
|
|
351
|
+
tool_name = params.get("name")
|
|
352
|
+
arguments = params.get("arguments", {})
|
|
353
|
+
result = await execute_tool(tool_name, arguments)
|
|
354
|
+
return {"content": [{"type": "text", "text": json.dumps(result, indent=2)}]}
|
|
355
|
+
elif method == "ping":
|
|
356
|
+
return {}
|
|
357
|
+
return {"error": {"code": -32601, "message": f"Method not found: {method}"}}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
async def main():
|
|
361
|
+
reader = asyncio.StreamReader()
|
|
362
|
+
protocol = asyncio.StreamReaderProtocol(reader)
|
|
363
|
+
loop = asyncio.get_event_loop()
|
|
364
|
+
await loop.connect_read_pipe(lambda: protocol, sys.stdin)
|
|
365
|
+
|
|
366
|
+
logger.info(f"{TOOL_NAME} MCP server started")
|
|
367
|
+
|
|
368
|
+
while True:
|
|
369
|
+
line = await reader.readline()
|
|
370
|
+
if not line:
|
|
371
|
+
break
|
|
372
|
+
|
|
373
|
+
line_str = line.decode("utf-8").strip()
|
|
374
|
+
if not line_str:
|
|
375
|
+
continue
|
|
376
|
+
|
|
377
|
+
try:
|
|
378
|
+
request = json.loads(line_str)
|
|
379
|
+
response = await handle_request(request)
|
|
380
|
+
response["jsonrpc"] = "2.0"
|
|
381
|
+
response["id"] = request.get("id")
|
|
382
|
+
sys.stdout.write(json.dumps(response) + "\n")
|
|
383
|
+
sys.stdout.flush()
|
|
384
|
+
except json.JSONDecodeError:
|
|
385
|
+
error_resp = {
|
|
386
|
+
"jsonrpc": "2.0",
|
|
387
|
+
"id": None,
|
|
388
|
+
"error": {"code": -32700, "message": "Parse error"}
|
|
389
|
+
}
|
|
390
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
391
|
+
sys.stdout.flush()
|
|
392
|
+
except Exception as e:
|
|
393
|
+
logger.exception("Error handling request")
|
|
394
|
+
error_resp = {
|
|
395
|
+
"jsonrpc": "2.0",
|
|
396
|
+
"id": request.get("id") if "request" in dir() else None,
|
|
397
|
+
"error": {"code": -32603, "message": f"Internal error: {str(e)}"}
|
|
398
|
+
}
|
|
399
|
+
sys.stdout.write(json.dumps(error_resp) + "\n")
|
|
400
|
+
sys.stdout.flush()
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
if __name__ == "__main__":
|
|
404
|
+
asyncio.run(main())
|