deepaudits 0.2.0__py3-none-any.whl
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.
- deepaudits/__init__.py +22 -0
- deepaudits/__main__.py +4 -0
- deepaudits/agents/__init__.py +13 -0
- deepaudits/agents/base_agent.py +108 -0
- deepaudits/agents/economic_attacker.py +7 -0
- deepaudits/agents/exploit_hunter.py +7 -0
- deepaudits/agents/judge.py +105 -0
- deepaudits/agents/protocol_analyst.py +7 -0
- deepaudits/agents/solidity_reviewer.py +7 -0
- deepaudits/analyzer/__init__.py +5 -0
- deepaudits/analyzer/html_report.py +188 -0
- deepaudits/analyzer/preprocessor.py +136 -0
- deepaudits/analyzer/report_builder.py +177 -0
- deepaudits/cli.py +432 -0
- deepaudits/config.py +87 -0
- deepaudits/memory/__init__.py +3 -0
- deepaudits/memory/audit_memory.py +83 -0
- deepaudits/models.py +117 -0
- deepaudits/orchestrator.py +271 -0
- deepaudits/prompts/__init__.py +0 -0
- deepaudits/prompts/economic_attacker.txt +63 -0
- deepaudits/prompts/exploit_hunter.txt +70 -0
- deepaudits/prompts/judge.txt +73 -0
- deepaudits/prompts/protocol_analyst.txt +63 -0
- deepaudits/prompts/solidity_reviewer.txt +64 -0
- deepaudits/tools/__init__.py +24 -0
- deepaudits/tools/ast_parser.py +46 -0
- deepaudits/tools/attack_tree.py +60 -0
- deepaudits/tools/blind_spot_analyzer.py +208 -0
- deepaudits/tools/cache_manager.py +75 -0
- deepaudits/tools/cross_protocol_scanner.py +75 -0
- deepaudits/tools/dashboard.py +102 -0
- deepaudits/tools/economic_analyzer.py +72 -0
- deepaudits/tools/fetcher.py +109 -0
- deepaudits/tools/fix_engine.py +93 -0
- deepaudits/tools/invariant_engine.py +107 -0
- deepaudits/tools/poc_engine.py +77 -0
- deepaudits/tools/poc_generator.py +55 -0
- deepaudits/tools/policy_engine.py +55 -0
- deepaudits/tools/qna_engine.py +80 -0
- deepaudits/tools/slither_runner.py +43 -0
- deepaudits/tools/web3_detector.py +36 -0
- deepaudits/vulns/fingerprints.json +137 -0
- deepaudits-0.2.0.dist-info/METADATA +1596 -0
- deepaudits-0.2.0.dist-info/RECORD +49 -0
- deepaudits-0.2.0.dist-info/WHEEL +5 -0
- deepaudits-0.2.0.dist-info/entry_points.txt +2 -0
- deepaudits-0.2.0.dist-info/licenses/LICENSE +21 -0
- deepaudits-0.2.0.dist-info/top_level.txt +1 -0
deepaudits/__init__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
from .models import (
|
|
2
|
+
Severity, Finding, AgentOutput, DebateRound, AuditReport,
|
|
3
|
+
BlindSpot, AnalysisGap, AttackPath, AttackTree, Invariant,
|
|
4
|
+
EconomicAnalysis, PolicyCheck, QnAResponse,
|
|
5
|
+
)
|
|
6
|
+
from .config import DeepAuditsConfig, load_config, save_global_config
|
|
7
|
+
from .orchestrator import DeepAuditsOrchestrator
|
|
8
|
+
from .memory import AuditMemory
|
|
9
|
+
|
|
10
|
+
__title__ = "deepaudits"
|
|
11
|
+
__version__ = "0.2.0"
|
|
12
|
+
__author__ = "Nishan Mishra"
|
|
13
|
+
__github__ = "https://github.com/nishanm15"
|
|
14
|
+
__license__ = "MIT"
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"Severity", "Finding", "AgentOutput", "DebateRound", "AuditReport",
|
|
18
|
+
"BlindSpot", "AnalysisGap", "AttackPath", "AttackTree", "Invariant",
|
|
19
|
+
"EconomicAnalysis", "PolicyCheck", "QnAResponse",
|
|
20
|
+
"DeepAuditsConfig", "load_config", "save_global_config",
|
|
21
|
+
"DeepAuditsOrchestrator", "AuditMemory",
|
|
22
|
+
]
|
deepaudits/__main__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .exploit_hunter import ExploitHunter
|
|
2
|
+
from .solidity_reviewer import SolidityReviewer
|
|
3
|
+
from .economic_attacker import EconomicAttacker
|
|
4
|
+
from .protocol_analyst import ProtocolAnalyst
|
|
5
|
+
from .judge import Judge
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"ExploitHunter",
|
|
9
|
+
"SolidityReviewer",
|
|
10
|
+
"EconomicAttacker",
|
|
11
|
+
"ProtocolAnalyst",
|
|
12
|
+
"Judge"
|
|
13
|
+
]
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import json
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import Dict, Any, Optional
|
|
5
|
+
from pydantic import ValidationError
|
|
6
|
+
from deepaudits.models import AgentOutput, Finding
|
|
7
|
+
from litellm import acompletion
|
|
8
|
+
|
|
9
|
+
class BaseAgent:
|
|
10
|
+
name: str = "BaseAgent"
|
|
11
|
+
role_description: str = ""
|
|
12
|
+
prompt_template: str = ""
|
|
13
|
+
|
|
14
|
+
def __init__(self, config):
|
|
15
|
+
self.config = config
|
|
16
|
+
|
|
17
|
+
async def analyze(self, contract_code: str, context: dict, verbose: bool = False) -> Optional[AgentOutput]:
|
|
18
|
+
prompt = self._build_prompt(contract_code, context)
|
|
19
|
+
|
|
20
|
+
for attempt in range(4):
|
|
21
|
+
try:
|
|
22
|
+
response_text = await self._call_llm(prompt, verbose)
|
|
23
|
+
return self._parse_response(response_text)
|
|
24
|
+
except (json.JSONDecodeError, ValidationError) as e:
|
|
25
|
+
if attempt == 3:
|
|
26
|
+
return None
|
|
27
|
+
prompt += f"\n\nCRITICAL ERROR: Your previous response caused an error: {str(e)}. You MUST return ONLY valid JSON matching the exact schema."
|
|
28
|
+
except Exception as e:
|
|
29
|
+
if attempt == 3:
|
|
30
|
+
return None
|
|
31
|
+
await asyncio.sleep(2)
|
|
32
|
+
return None
|
|
33
|
+
|
|
34
|
+
def _build_prompt(self, contract_code: str, context: dict) -> str:
|
|
35
|
+
try:
|
|
36
|
+
with open(self.prompt_template, "r", encoding="utf-8") as f:
|
|
37
|
+
template = f.read()
|
|
38
|
+
except FileNotFoundError:
|
|
39
|
+
template = "System prompt not found."
|
|
40
|
+
|
|
41
|
+
slither_ctx = context.get("slither_summary") or "Not available"
|
|
42
|
+
ast_ctx = json.dumps(context.get("ast_summary", {}), indent=2)
|
|
43
|
+
|
|
44
|
+
# Multi-pass context injection
|
|
45
|
+
findings_context = ""
|
|
46
|
+
if context.get("previous_findings"):
|
|
47
|
+
findings_context = "\n".join([f"- {f.title} (Severity: {f.severity}, Confidence: {f.confidence})" for f in context["previous_findings"]])
|
|
48
|
+
findings_context = f"""
|
|
49
|
+
=============================================================
|
|
50
|
+
PREVIOUS FINDINGS (Pass 1)
|
|
51
|
+
=============================================================
|
|
52
|
+
{findings_context}
|
|
53
|
+
"""
|
|
54
|
+
|
|
55
|
+
full_prompt = f"""{template}
|
|
56
|
+
|
|
57
|
+
=============================================================
|
|
58
|
+
CONTEXT
|
|
59
|
+
=============================================================
|
|
60
|
+
Target Type: {context.get('contract_type', 'Unknown')}
|
|
61
|
+
Profile: {context.get('profile', 'general')}
|
|
62
|
+
|
|
63
|
+
SLITHER ANALYSIS:
|
|
64
|
+
{slither_ctx}
|
|
65
|
+
|
|
66
|
+
AST SUMMARY:
|
|
67
|
+
{ast_ctx}
|
|
68
|
+
{findings_context}
|
|
69
|
+
|
|
70
|
+
=============================================================
|
|
71
|
+
CONTRACT CODE
|
|
72
|
+
=============================================================
|
|
73
|
+
{contract_code}
|
|
74
|
+
"""
|
|
75
|
+
return full_prompt
|
|
76
|
+
|
|
77
|
+
async def _call_llm(self, prompt: str, verbose: bool) -> str:
|
|
78
|
+
kwargs = {
|
|
79
|
+
"model": self.config.model,
|
|
80
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
81
|
+
"max_tokens": self.config.max_tokens_per_agent,
|
|
82
|
+
}
|
|
83
|
+
if self.config.api_key:
|
|
84
|
+
kwargs["api_key"] = self.config.api_key
|
|
85
|
+
if self.config.api_base:
|
|
86
|
+
kwargs["api_base"] = self.config.api_base
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
response = await acompletion(**kwargs)
|
|
90
|
+
return response.choices[0].message.content
|
|
91
|
+
except Exception as e:
|
|
92
|
+
# If the model fails, try a fallback
|
|
93
|
+
if self.config.model != "gpt-3.5-turbo":
|
|
94
|
+
print(f"Model {self.config.model} failed, trying fallback...")
|
|
95
|
+
kwargs["model"] = "gpt-3.5-turbo"
|
|
96
|
+
response = await acompletion(**kwargs)
|
|
97
|
+
return response.choices[0].message.content
|
|
98
|
+
raise
|
|
99
|
+
|
|
100
|
+
def _parse_response(self, text: str) -> AgentOutput:
|
|
101
|
+
json_pattern = re.compile(r'\{.*\}', re.DOTALL)
|
|
102
|
+
match = json_pattern.search(text)
|
|
103
|
+
if match:
|
|
104
|
+
json_str = match.group(0)
|
|
105
|
+
data = json.loads(json_str)
|
|
106
|
+
return AgentOutput(**data)
|
|
107
|
+
else:
|
|
108
|
+
raise json.JSONDecodeError("No JSON object could be decoded", text, 0)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .base_agent import BaseAgent
|
|
3
|
+
|
|
4
|
+
class EconomicAttacker(BaseAgent):
|
|
5
|
+
name = "EconomicAttacker"
|
|
6
|
+
role_description = "Specialized DeFi researcher looking for tokenomics and oracle vulnerabilities."
|
|
7
|
+
prompt_template = os.path.join(os.path.dirname(__file__), "..", "prompts", "economic_attacker.txt")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .base_agent import BaseAgent
|
|
3
|
+
|
|
4
|
+
class ExploitHunter(BaseAgent):
|
|
5
|
+
name = "ExploitHunter"
|
|
6
|
+
role_description = "Specialized smart contract security researcher looking for code-level exploits."
|
|
7
|
+
prompt_template = os.path.join(os.path.dirname(__file__), "..", "prompts", "exploit_hunter.txt")
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import asyncio
|
|
4
|
+
from typing import List, Dict, Any
|
|
5
|
+
from deepaudits.models import AgentOutput, DebateRound, AuditReport, Finding
|
|
6
|
+
from deepaudits.agents.base_agent import BaseAgent
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
|
|
9
|
+
class Judge(BaseAgent):
|
|
10
|
+
name = "Judge"
|
|
11
|
+
role_description = "Lead auditor who synthesizes findings and resolves debates."
|
|
12
|
+
|
|
13
|
+
def __init__(self, config):
|
|
14
|
+
super().__init__(config)
|
|
15
|
+
self.prompt_template = os.path.join(os.path.dirname(__file__), "..", "prompts", "judge.txt")
|
|
16
|
+
|
|
17
|
+
async def evaluate(self, agent_outputs: List[AgentOutput], debate_rounds: List[DebateRound], context: dict, verbose: bool = False) -> AuditReport:
|
|
18
|
+
all_findings_str = ""
|
|
19
|
+
for out in agent_outputs:
|
|
20
|
+
all_findings_str += f"\n--- Agent: {out.agent_name} ---\n"
|
|
21
|
+
for f in out.findings:
|
|
22
|
+
all_findings_str += json.dumps(f.model_dump(), indent=2) + "\n"
|
|
23
|
+
|
|
24
|
+
debate_str = ""
|
|
25
|
+
for round in debate_rounds:
|
|
26
|
+
debate_str += f"\nRound {round.round_number}:\n"
|
|
27
|
+
debate_str += json.dumps(round.model_dump(), indent=2) + "\n"
|
|
28
|
+
|
|
29
|
+
try:
|
|
30
|
+
with open(self.prompt_template, "r", encoding="utf-8") as f:
|
|
31
|
+
template = f.read()
|
|
32
|
+
except FileNotFoundError:
|
|
33
|
+
template = "Judge prompt not found."
|
|
34
|
+
|
|
35
|
+
prompt = f"""
|
|
36
|
+
{template}
|
|
37
|
+
|
|
38
|
+
=============================================================
|
|
39
|
+
ALL AGENT FINDINGS
|
|
40
|
+
=============================================================
|
|
41
|
+
{all_findings_str}
|
|
42
|
+
|
|
43
|
+
=============================================================
|
|
44
|
+
DEBATE ROUNDS
|
|
45
|
+
=============================================================
|
|
46
|
+
{debate_str}
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
response_text = ""
|
|
50
|
+
for attempt in range(4):
|
|
51
|
+
try:
|
|
52
|
+
response_text = await self._call_llm(prompt, verbose)
|
|
53
|
+
return self._parse_judge_response(response_text, agent_outputs, debate_rounds, context)
|
|
54
|
+
except Exception as e:
|
|
55
|
+
if attempt == 3:
|
|
56
|
+
raise Exception(f"Judge failed after 4 attempts: {e}")
|
|
57
|
+
prompt += f"\n\nCRITICAL: Your previous response was invalid. Error: {str(e)}. You MUST return ONLY valid JSON."
|
|
58
|
+
await asyncio.sleep(2)
|
|
59
|
+
|
|
60
|
+
raise Exception("Judge failed")
|
|
61
|
+
|
|
62
|
+
def _parse_judge_response(self, text: str, agent_outputs: List[AgentOutput], debate_rounds: List[DebateRound], context: dict) -> AuditReport:
|
|
63
|
+
import re
|
|
64
|
+
json_pattern = re.compile(r'\{.*\}', re.DOTALL)
|
|
65
|
+
match = json_pattern.search(text)
|
|
66
|
+
if not match:
|
|
67
|
+
raise json.JSONDecodeError("No JSON object could be decoded", text, 0)
|
|
68
|
+
|
|
69
|
+
data = json.loads(match.group(0))
|
|
70
|
+
|
|
71
|
+
findings = [Finding(**f) for f in data.get("findings", [])]
|
|
72
|
+
|
|
73
|
+
audit_score = 100
|
|
74
|
+
counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "informational": 0}
|
|
75
|
+
|
|
76
|
+
for f in findings:
|
|
77
|
+
sev = f.severity.value if hasattr(f.severity, 'value') else f.severity
|
|
78
|
+
counts[sev] += 1
|
|
79
|
+
|
|
80
|
+
audit_score -= min(counts["critical"] * 25, 50)
|
|
81
|
+
audit_score -= min(counts["high"] * 10, 30)
|
|
82
|
+
audit_score -= min(counts["medium"] * 3, 15)
|
|
83
|
+
audit_score -= min(counts["low"] * 1, 5)
|
|
84
|
+
audit_score = max(audit_score, 0)
|
|
85
|
+
|
|
86
|
+
agents_used = list(set([out.agent_name for out in agent_outputs]))
|
|
87
|
+
|
|
88
|
+
return AuditReport(
|
|
89
|
+
target=context.get("target", "unknown"),
|
|
90
|
+
timestamp=datetime.utcnow().isoformat(),
|
|
91
|
+
contract_type=context.get("contract_type", "Unknown"),
|
|
92
|
+
profile=context.get("profile", "general"),
|
|
93
|
+
agents_used=agents_used,
|
|
94
|
+
total_findings=len(findings),
|
|
95
|
+
critical=counts["critical"],
|
|
96
|
+
high=counts["high"],
|
|
97
|
+
medium=counts["medium"],
|
|
98
|
+
low=counts["low"],
|
|
99
|
+
informational=counts["informational"],
|
|
100
|
+
findings=findings,
|
|
101
|
+
debate_rounds=debate_rounds,
|
|
102
|
+
slither_output=context.get("slither_summary"),
|
|
103
|
+
executive_summary=data.get("executive_summary", ""),
|
|
104
|
+
audit_score=audit_score
|
|
105
|
+
)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .base_agent import BaseAgent
|
|
3
|
+
|
|
4
|
+
class ProtocolAnalyst(BaseAgent):
|
|
5
|
+
name = "ProtocolAnalyst"
|
|
6
|
+
role_description = "System architect looking for logic errors and trust assumption violations."
|
|
7
|
+
prompt_template = os.path.join(os.path.dirname(__file__), "..", "prompts", "protocol_analyst.txt")
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from .base_agent import BaseAgent
|
|
3
|
+
|
|
4
|
+
class SolidityReviewer(BaseAgent):
|
|
5
|
+
name = "SolidityReviewer"
|
|
6
|
+
role_description = "Specialized smart contract engineer focusing on code quality and language pitfalls."
|
|
7
|
+
prompt_template = os.path.join(os.path.dirname(__file__), "..", "prompts", "solidity_reviewer.txt")
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List
|
|
4
|
+
from deepaudits.models import AuditReport
|
|
5
|
+
|
|
6
|
+
class HTMLReportBuilder:
|
|
7
|
+
def build(self, report: AuditReport, out_dir: str) -> str:
|
|
8
|
+
out_path = Path(out_dir)
|
|
9
|
+
out_path.mkdir(parents=True, exist_ok=True)
|
|
10
|
+
safe = str(report.target).split("/")[-1].replace("\\", "_").replace(":", "_")
|
|
11
|
+
ts = report.timestamp.replace(":", "").replace("-", "").replace(".", "_")
|
|
12
|
+
base = f"deepaudits-{safe}-{ts}"
|
|
13
|
+
fp = out_path / f"{base}.html"
|
|
14
|
+
with open(fp, "w") as f:
|
|
15
|
+
f.write(self._html(report))
|
|
16
|
+
return str(fp)
|
|
17
|
+
|
|
18
|
+
def _html(self, report: AuditReport) -> str:
|
|
19
|
+
sev_order = {"critical":0,"high":1,"medium":2,"low":3,"informational":4}
|
|
20
|
+
sorted_f = sorted(report.findings, key=lambda x: (sev_order.get(x.severity.value if hasattr(x.severity,'value') else x.severity,5), -x.confidence))
|
|
21
|
+
|
|
22
|
+
sc = "#ff4444" if report.audit_score < 50 else "#ff8800" if report.audit_score < 80 else "#44cc44"
|
|
23
|
+
colors = {"critical":"#ff4444","high":"#ff8800","medium":"#ffcc00","low":"#4488ff","informational":"#aaa"}
|
|
24
|
+
|
|
25
|
+
rows = ""
|
|
26
|
+
for f in sorted_f:
|
|
27
|
+
sev = f.severity.value if hasattr(f.severity,'value') else f.severity
|
|
28
|
+
c = colors.get(sev,"#fff")
|
|
29
|
+
rows += f"""
|
|
30
|
+
<tr>
|
|
31
|
+
<td>{f.id}</td>
|
|
32
|
+
<td><span style="color:{c};font-weight:bold">{sev.upper()}</span></td>
|
|
33
|
+
<td>{f.confidence}%</td>
|
|
34
|
+
<td>{f.title}</td>
|
|
35
|
+
<td>{f.agent}</td>
|
|
36
|
+
<td>{f.location}</td>
|
|
37
|
+
<td><button onclick="tog('{f.id}')">View</button></td>
|
|
38
|
+
</tr>
|
|
39
|
+
<tr id="d-{f.id}" style="display:none;background:#1a1a2e">
|
|
40
|
+
<td colspan="7"><strong>Description:</strong> {f.description}<br>
|
|
41
|
+
<strong>Impact:</strong> {f.impact}<br>
|
|
42
|
+
<strong>Exploit Path:</strong> {f.exploit_path}<br>
|
|
43
|
+
<strong>Recommendation:</strong> {f.recommendation}
|
|
44
|
+
{f'<br><strong>PoC:</strong><pre>{f.poc_code}</pre>' if f.poc_code else ''}</td>
|
|
45
|
+
</tr>"""
|
|
46
|
+
|
|
47
|
+
# Blind spots
|
|
48
|
+
bs_rows = ""
|
|
49
|
+
for bs in report.blind_spots:
|
|
50
|
+
sev = bs.severity.value if hasattr(bs.severity,'value') else bs.severity
|
|
51
|
+
c = colors.get(sev,"#fff")
|
|
52
|
+
bs_rows += f"<tr><td>{bs.id}</td><td style='color:{c}'>{sev.upper()}</td><td>{bs.title}</td><td>{bs.reason}</td><td>{bs.location}</td></tr>"
|
|
53
|
+
|
|
54
|
+
# Invariants
|
|
55
|
+
inv_rows = ""
|
|
56
|
+
for inv in report.invariants:
|
|
57
|
+
icon = {"verified":"✅","broken":"❌","unverifiable":"⚠️"}.get(inv.status,"⚪")
|
|
58
|
+
inv_rows += f"<tr><td>{icon}</td><td>{inv.description}</td><td style='color:{'red' if inv.status=='broken' else 'green' if inv.status=='verified' else '#ffcc00'}'>{inv.status.upper()}</td></tr>"
|
|
59
|
+
|
|
60
|
+
# Economic
|
|
61
|
+
econ_rows = ""
|
|
62
|
+
for ea in report.economic_analyses:
|
|
63
|
+
vc = "red" if "not profitable" in ea.verdict.lower() else "green" if "highly profitable" in ea.verdict.lower() else "#ffcc00"
|
|
64
|
+
econ_rows += f"<tr><td>{ea.finding_id}</td><td>{ea.capital_required}</td><td>{ea.gas_cost}</td><td>{ea.potential_profit}</td><td>{ea.risk_reward_ratio}</td><td style='color:{vc}'>{ea.verdict}</td></tr>"
|
|
65
|
+
|
|
66
|
+
# Attack tree
|
|
67
|
+
at_html = ""
|
|
68
|
+
for tree in report.attack_tree:
|
|
69
|
+
at_html += f"<h3>🎯 {tree.goal}</h3>"
|
|
70
|
+
for path in tree.paths:
|
|
71
|
+
icon = {"confirmed":"🟢","suspected":"🟡","blocked":"🔴"}.get(path.status,"⚪")
|
|
72
|
+
at_html += f"<div style='margin:8px 0;padding:8px;background:#1a1a2e;border-radius:4px'>{icon} <b>{path.goal}</b> ({path.status})"
|
|
73
|
+
for i, s in enumerate(path.steps, 1):
|
|
74
|
+
at_html += f"<br> {i}. {s}"
|
|
75
|
+
at_html += "</div>"
|
|
76
|
+
|
|
77
|
+
# Policy checks
|
|
78
|
+
pc_rows = ""
|
|
79
|
+
for pc in report.policy_checks:
|
|
80
|
+
icon = {"passed":"✅","violated":"❌","unverifiable":"⚠️"}.get(pc.status,"⚪")
|
|
81
|
+
color = "green" if pc.status=="passed" else "red" if pc.status=="violated" else "#ffcc00"
|
|
82
|
+
pc_rows += f"<tr><td>{icon}</td><td>{pc.policy_text}</td><td style='color:{color}'>{pc.status.upper()}</td><td>{pc.details}</td></tr>"
|
|
83
|
+
|
|
84
|
+
html = f"""<!DOCTYPE html>
|
|
85
|
+
<html lang="en">
|
|
86
|
+
<head>
|
|
87
|
+
<meta charset="UTF-8">
|
|
88
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
89
|
+
<title>DeepAudits Report</title>
|
|
90
|
+
<style>
|
|
91
|
+
*{{margin:0;padding:0;box-sizing:border-box}}
|
|
92
|
+
body{{font-family:'Segoe UI',sans-serif;background:#0a0a1a;color:#e0e0e0;padding:20px}}
|
|
93
|
+
.container{{max-width:1300px;margin:0 auto}}
|
|
94
|
+
h1{{color:#00d4ff;margin-bottom:10px}}
|
|
95
|
+
h2{{color:#00d4ff;margin:25px 0 10px;border-bottom:1px solid #333;padding-bottom:5px}}
|
|
96
|
+
.score-box{{display:inline-block;padding:20px 40px;border-radius:10px;font-size:48px;font-weight:bold;text-align:center;margin:20px 0}}
|
|
97
|
+
.score-box .label{{font-size:14px;color:#aaa}}
|
|
98
|
+
.summary-grid{{display:grid;grid-template-columns:repeat(auto-fit,minmax(120px,1fr));gap:10px;margin:15px 0}}
|
|
99
|
+
.summary-item{{background:#1a1a2e;padding:15px;border-radius:8px;text-align:center}}
|
|
100
|
+
.summary-item .count{{font-size:28px;font-weight:bold}}
|
|
101
|
+
.summary-item .label{{font-size:12px;color:#888}}
|
|
102
|
+
table{{width:100%;border-collapse:collapse;margin:15px 0;background:#1a1a2e;border-radius:8px;overflow:hidden}}
|
|
103
|
+
th,td{{padding:10px 15px;text-align:left;border-bottom:1px solid #333}}
|
|
104
|
+
th{{background:#16213e;color:#00d4ff;font-weight:600}}
|
|
105
|
+
tr:hover{{background:#1a1a3e}}
|
|
106
|
+
button{{background:#00d4ff;color:#000;border:none;padding:5px 10px;border-radius:4px;cursor:pointer;font-weight:bold}}
|
|
107
|
+
button:hover{{background:#00b8e6}}
|
|
108
|
+
pre{{background:#111;padding:10px;border-radius:4px;overflow-x:auto;margin:8px 0}}
|
|
109
|
+
.exec-summary{{background:#1a1a2e;padding:20px;border-radius:8px;line-height:1.6;margin:15px 0}}
|
|
110
|
+
.meta{{color:#888;font-size:14px}}
|
|
111
|
+
.tab{{overflow:hidden;margin:15px 0}}
|
|
112
|
+
.tab button{{background:#16213e;color:#00d4ff;float:left;border:none;outline:none;cursor:pointer;padding:12px 20px;transition:0.3s;border-radius:4px 4px 0 0;margin-right:2px}}
|
|
113
|
+
.tab button:hover{{background:#1e2d50}}
|
|
114
|
+
.tab button.active{{background:#00d4ff;color:#000}}
|
|
115
|
+
.tabcontent{{display:none;padding:20px;background:#1a1a2e;border-radius:0 4px 4px 4px}}
|
|
116
|
+
@media(max-width:768px){{th,td{{font-size:12px;padding:6px}}}}
|
|
117
|
+
</style>
|
|
118
|
+
</head>
|
|
119
|
+
<body>
|
|
120
|
+
<div class="container">
|
|
121
|
+
<h1>🔍 DeepAudits Security Report</h1>
|
|
122
|
+
<p class="meta"><strong>Target:</strong> {report.target} | <strong>Score:</strong> {report.audit_score}/100 | <strong>{report.contract_type}</strong></p>
|
|
123
|
+
|
|
124
|
+
<div style="text-align:center">
|
|
125
|
+
<div class="score-box" style="background:{sc}22;border:3px solid {sc}">
|
|
126
|
+
<span style="color:{sc}">{report.audit_score}</span>
|
|
127
|
+
<div class="label">SECURITY SCORE</div></div></div>
|
|
128
|
+
|
|
129
|
+
<h2>📊 Findings</h2>
|
|
130
|
+
<div class="summary-grid">
|
|
131
|
+
<div class="summary-item"><div class="count" style="color:#ff4444">{report.critical}</div><div class="label">Critical</div></div>
|
|
132
|
+
<div class="summary-item"><div class="count" style="color:#ff8800">{report.high}</div><div class="label">High</div></div>
|
|
133
|
+
<div class="summary-item"><div class="count" style="color:#ffcc00">{report.medium}</div><div class="label">Medium</div></div>
|
|
134
|
+
<div class="summary-item"><div class="count" style="color:#4488ff">{report.low}</div><div class="label">Low</div></div>
|
|
135
|
+
<div class="summary-item"><div class="count" style="color:#aaa">{report.informational}</div><div class="label">Info</div></div>
|
|
136
|
+
<div class="summary-item"><div class="count" style="color:#00d4ff">{report.total_findings}</div><div class="label">Total</div></div>
|
|
137
|
+
</div>
|
|
138
|
+
|
|
139
|
+
<h2>📝 Executive Summary</h2>
|
|
140
|
+
<div class="exec-summary">{report.executive_summary}</div>
|
|
141
|
+
|
|
142
|
+
<div class="tab">
|
|
143
|
+
<button class="tablinks active" onclick="openTab(event,'findings')">🔎 Findings {report.total_findings}</button>
|
|
144
|
+
<button class="tablinks" onclick="openTab(event,'blindspots')">⚠️ Blind Spots {len(report.blind_spots)}</button>
|
|
145
|
+
<button class="tablinks" onclick="openTab(event,'invariants')">🧮 Invariants {len(report.invariants)}</button>
|
|
146
|
+
<button class="tablinks" onclick="openTab(event,'economic')">💰 Economics {len(report.economic_analyses)}</button>
|
|
147
|
+
<button class="tablinks" onclick="openTab(event,'attacktree')">🗺️ Attack Tree</button>
|
|
148
|
+
{"<button class='tablinks' onclick=\"openTab(event,'policies')\">📋 Policies</button>" if report.policy_checks else ""}
|
|
149
|
+
</div>
|
|
150
|
+
|
|
151
|
+
<div id="findings" class="tabcontent" style="display:block">
|
|
152
|
+
<table><thead><tr><th>ID</th><th>Severity</th><th>Conf</th><th>Title</th><th>Agent</th><th>Location</th><th></th></tr></thead>
|
|
153
|
+
<tbody>{rows}</tbody></table></div>
|
|
154
|
+
|
|
155
|
+
<div id="blindspots" class="tabcontent">
|
|
156
|
+
{"<table><tr><th>ID</th><th>Sev</th><th>Title</th><th>Reason</th><th>Location</th></tr>"+bs_rows+"</table>" if report.blind_spots else "<p>No blind spots identified.</p>"}
|
|
157
|
+
{("<h3>Analysis Gaps</h3><ul>"+''.join(f"<li><b>{g.title}</b> ({g.location}): {g.description}</li>" for g in report.analysis_gaps)+"</ul>") if report.analysis_gaps else ""}
|
|
158
|
+
</div>
|
|
159
|
+
|
|
160
|
+
<div id="invariants" class="tabcontent">
|
|
161
|
+
{"<table><tr><th></th><th>Invariant</th><th>Status</th></tr>"+inv_rows+"</table>" if report.invariants else "<p>No invariants inferred.</p>"}
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<div id="economic" class="tabcontent">
|
|
165
|
+
{"<table><tr><th>ID</th><th>Capital</th><th>Gas</th><th>Profit</th><th>Risk/Reward</th><th>Verdict</th></tr>"+econ_rows+"</table>" if report.economic_analyses else "<p>No economic analysis performed.</p>"}
|
|
166
|
+
</div>
|
|
167
|
+
|
|
168
|
+
<div id="attacktree" class="tabcontent">
|
|
169
|
+
{at_html if report.attack_tree else "<p>No attack trees generated.</p>"}
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
{"<div id='policies' class='tabcontent'><table><tr><th></th><th>Policy</th><th>Status</th><th>Details</th></tr>"+pc_rows+"</table></div>" if report.policy_checks else ""}
|
|
173
|
+
|
|
174
|
+
<h2>🛠 Agents Used</h2>
|
|
175
|
+
<p>{", ".join(report.agents_used)}</p>
|
|
176
|
+
|
|
177
|
+
<h2>⚙️ Slither</h2>
|
|
178
|
+
<pre>{report.slither_output or "Skipped."}</pre>
|
|
179
|
+
|
|
180
|
+
<p class="meta" style="margin-top:30px;text-align:center">DeepAudits v0.2.0</p>
|
|
181
|
+
</div>
|
|
182
|
+
<script>
|
|
183
|
+
function tog(id){{var e=document.getElementById('d-'+id);e.style.display=e.style.display==='none'?'table-row':'none'}}
|
|
184
|
+
function openTab(e,t){{var i,x=document.getElementsByClassName('tabcontent');for(i=0;i<x.length;i++)x[i].style.display='none';var b=document.getElementsByClassName('tablinks');for(i=0;i<b.length;i++)b[i].classList.remove('active');document.getElementById(t).style.display='block';e.currentTarget.classList.add('active')}}
|
|
185
|
+
</script>
|
|
186
|
+
</body>
|
|
187
|
+
</html>"""
|
|
188
|
+
return html
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import hashlib
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import List, Tuple, Dict
|
|
5
|
+
|
|
6
|
+
class Preprocessor:
|
|
7
|
+
def __init__(self, cache_dir: str = ".deepaudits/cache"):
|
|
8
|
+
self.cache_dir = Path(cache_dir)
|
|
9
|
+
self.cache_dir.mkdir(parents=True, exist_ok=True)
|
|
10
|
+
|
|
11
|
+
def _compute_hash(self, source_code: str) -> str:
|
|
12
|
+
"""Compute hash of source code for caching."""
|
|
13
|
+
return hashlib.md5(source_code.encode()).hexdigest()
|
|
14
|
+
|
|
15
|
+
def _get_cache_path(self, file_hash: str) -> Path:
|
|
16
|
+
return self.cache_dir / f"ast_{file_hash}.json"
|
|
17
|
+
|
|
18
|
+
def chunk_contract(self, source_code: str, max_lines: int = 2000, overlap: int = 200) -> List[Dict[str, str]]:
|
|
19
|
+
"""Smart chunking: chunk by functions, not just lines."""
|
|
20
|
+
# First, try to extract functions
|
|
21
|
+
functions = self._extract_functions_with_context(source_code)
|
|
22
|
+
if functions:
|
|
23
|
+
return functions
|
|
24
|
+
|
|
25
|
+
# Fallback: line-based chunking
|
|
26
|
+
lines = source_code.splitlines()
|
|
27
|
+
if len(lines) <= max_lines:
|
|
28
|
+
return [{"type": "code", "content": source_code}]
|
|
29
|
+
|
|
30
|
+
chunks = []
|
|
31
|
+
i = 0
|
|
32
|
+
while i < len(lines):
|
|
33
|
+
chunk_lines = lines[i:i + max_lines]
|
|
34
|
+
chunks.append({"type": "code", "content": "\n".join(chunk_lines)})
|
|
35
|
+
i += (max_lines - overlap)
|
|
36
|
+
return chunks
|
|
37
|
+
|
|
38
|
+
def _extract_functions_with_context(self, source_code: str) -> List[Dict[str, str]]:
|
|
39
|
+
"""Extract functions with state variable context for smart chunking."""
|
|
40
|
+
lines = source_code.splitlines()
|
|
41
|
+
|
|
42
|
+
# Extract state variables from the contract
|
|
43
|
+
state_vars = self._extract_state_variables(source_code)
|
|
44
|
+
|
|
45
|
+
# Extract all function definitions with their full body
|
|
46
|
+
function_pattern = re.compile(
|
|
47
|
+
r'(//.*?\n)?' # Optional comment before function
|
|
48
|
+
r'(function\s+([a-zA-Z0-9_]+)\s*\((.*?)\)\s*' # Function signature
|
|
49
|
+
r'((?:public|external|internal|private|view|pure|payable|override|virtual|abstract|\s)*)' # Modifiers
|
|
50
|
+
r'\s*(?:returns\s*\(.*?\))?\s*\{', # Optional return and opening brace
|
|
51
|
+
re.MULTILINE | re.DOTALL
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
matches = list(function_pattern.finditer(source_code))
|
|
55
|
+
if not matches:
|
|
56
|
+
return []
|
|
57
|
+
|
|
58
|
+
chunks = []
|
|
59
|
+
for match in matches:
|
|
60
|
+
func_start = match.start()
|
|
61
|
+
func_signature = match.group(0)
|
|
62
|
+
|
|
63
|
+
# Find the matching closing brace
|
|
64
|
+
brace_count = 1
|
|
65
|
+
pos = match.end()
|
|
66
|
+
while pos < len(source_code) and brace_count > 0:
|
|
67
|
+
if source_code[pos] == '{':
|
|
68
|
+
brace_count += 1
|
|
69
|
+
elif source_code[pos] == '}':
|
|
70
|
+
brace_count -= 1
|
|
71
|
+
pos += 1
|
|
72
|
+
|
|
73
|
+
func_body = source_code[func_start:pos]
|
|
74
|
+
|
|
75
|
+
# Build context: state variables that are used in this function
|
|
76
|
+
used_vars = self._get_used_state_vars(func_body, state_vars)
|
|
77
|
+
|
|
78
|
+
# Create chunk with function body and relevant context
|
|
79
|
+
chunk_content = self._build_chunk(func_signature, func_body, used_vars, state_vars)
|
|
80
|
+
|
|
81
|
+
chunks.append({
|
|
82
|
+
"type": "function",
|
|
83
|
+
"function_name": match.group(3),
|
|
84
|
+
"content": chunk_content
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return chunks
|
|
88
|
+
|
|
89
|
+
def _extract_state_variables(self, source_code: str) -> Dict[str, str]:
|
|
90
|
+
"""Extract state variables with their types."""
|
|
91
|
+
state_vars = {}
|
|
92
|
+
|
|
93
|
+
# Pattern for state variable declarations
|
|
94
|
+
# uint256 public balance;
|
|
95
|
+
# mapping(address => uint256) public balances;
|
|
96
|
+
# address private owner;
|
|
97
|
+
var_patterns = [
|
|
98
|
+
# Simple type declaration
|
|
99
|
+
r'(uint[0-9]*|int[0-9]*|address|bool|string|bytes[0-9]*|mapping\([^)]+\))\s+(?:public|private|internal)?\s*([a-zA-Z0-9_]+)\s*[;=]',
|
|
100
|
+
# Array declaration
|
|
101
|
+
r'(uint[0-9]*|int[0-9]*|address|bool|string|bytes[0-9]*)\[\]\s+(?:public|private|internal)?\s*([a-zA-Z0-9_]+)\s*[;=]',
|
|
102
|
+
]
|
|
103
|
+
|
|
104
|
+
for pattern in var_patterns:
|
|
105
|
+
for match in re.finditer(pattern, source_code):
|
|
106
|
+
var_type = match.group(1)
|
|
107
|
+
var_name = match.group(2)
|
|
108
|
+
state_vars[var_name] = var_type
|
|
109
|
+
|
|
110
|
+
return state_vars
|
|
111
|
+
|
|
112
|
+
def _get_used_state_vars(self, func_body: str, state_vars: Dict[str, str]) -> List[str]:
|
|
113
|
+
"""Find which state variables are used in a function."""
|
|
114
|
+
used = []
|
|
115
|
+
for var_name in state_vars:
|
|
116
|
+
# Check if variable is referenced in the function body
|
|
117
|
+
# Use word boundary to avoid partial matches
|
|
118
|
+
if re.search(r'\b' + re.escape(var_name) + r'\b', func_body):
|
|
119
|
+
used.append(var_name)
|
|
120
|
+
return used
|
|
121
|
+
|
|
122
|
+
def _build_chunk(self, func_signature: str, func_body: str, used_vars: List[str], all_vars: Dict[str, str]) -> str:
|
|
123
|
+
"""Build a chunk with function and relevant state variable context."""
|
|
124
|
+
context_parts = []
|
|
125
|
+
|
|
126
|
+
# Add relevant state variables
|
|
127
|
+
if used_vars:
|
|
128
|
+
context_parts.append("// Relevant State Variables:\n")
|
|
129
|
+
for var in used_vars:
|
|
130
|
+
if var in all_vars:
|
|
131
|
+
context_parts.append(f"// {all_vars[var]} {var};\n")
|
|
132
|
+
|
|
133
|
+
# Add the function body
|
|
134
|
+
context_parts.append(func_body)
|
|
135
|
+
|
|
136
|
+
return "".join(context_parts)
|