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.
Files changed (49) hide show
  1. deepaudits/__init__.py +22 -0
  2. deepaudits/__main__.py +4 -0
  3. deepaudits/agents/__init__.py +13 -0
  4. deepaudits/agents/base_agent.py +108 -0
  5. deepaudits/agents/economic_attacker.py +7 -0
  6. deepaudits/agents/exploit_hunter.py +7 -0
  7. deepaudits/agents/judge.py +105 -0
  8. deepaudits/agents/protocol_analyst.py +7 -0
  9. deepaudits/agents/solidity_reviewer.py +7 -0
  10. deepaudits/analyzer/__init__.py +5 -0
  11. deepaudits/analyzer/html_report.py +188 -0
  12. deepaudits/analyzer/preprocessor.py +136 -0
  13. deepaudits/analyzer/report_builder.py +177 -0
  14. deepaudits/cli.py +432 -0
  15. deepaudits/config.py +87 -0
  16. deepaudits/memory/__init__.py +3 -0
  17. deepaudits/memory/audit_memory.py +83 -0
  18. deepaudits/models.py +117 -0
  19. deepaudits/orchestrator.py +271 -0
  20. deepaudits/prompts/__init__.py +0 -0
  21. deepaudits/prompts/economic_attacker.txt +63 -0
  22. deepaudits/prompts/exploit_hunter.txt +70 -0
  23. deepaudits/prompts/judge.txt +73 -0
  24. deepaudits/prompts/protocol_analyst.txt +63 -0
  25. deepaudits/prompts/solidity_reviewer.txt +64 -0
  26. deepaudits/tools/__init__.py +24 -0
  27. deepaudits/tools/ast_parser.py +46 -0
  28. deepaudits/tools/attack_tree.py +60 -0
  29. deepaudits/tools/blind_spot_analyzer.py +208 -0
  30. deepaudits/tools/cache_manager.py +75 -0
  31. deepaudits/tools/cross_protocol_scanner.py +75 -0
  32. deepaudits/tools/dashboard.py +102 -0
  33. deepaudits/tools/economic_analyzer.py +72 -0
  34. deepaudits/tools/fetcher.py +109 -0
  35. deepaudits/tools/fix_engine.py +93 -0
  36. deepaudits/tools/invariant_engine.py +107 -0
  37. deepaudits/tools/poc_engine.py +77 -0
  38. deepaudits/tools/poc_generator.py +55 -0
  39. deepaudits/tools/policy_engine.py +55 -0
  40. deepaudits/tools/qna_engine.py +80 -0
  41. deepaudits/tools/slither_runner.py +43 -0
  42. deepaudits/tools/web3_detector.py +36 -0
  43. deepaudits/vulns/fingerprints.json +137 -0
  44. deepaudits-0.2.0.dist-info/METADATA +1596 -0
  45. deepaudits-0.2.0.dist-info/RECORD +49 -0
  46. deepaudits-0.2.0.dist-info/WHEEL +5 -0
  47. deepaudits-0.2.0.dist-info/entry_points.txt +2 -0
  48. deepaudits-0.2.0.dist-info/licenses/LICENSE +21 -0
  49. 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,4 @@
1
+ from deepaudits.cli import app
2
+
3
+ if __name__ == "__main__":
4
+ app()
@@ -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,5 @@
1
+ from .preprocessor import Preprocessor
2
+ from .report_builder import ReportBuilder
3
+ from .html_report import HTMLReportBuilder
4
+
5
+ __all__ = ["Preprocessor", "ReportBuilder", "HTMLReportBuilder"]
@@ -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>&nbsp;&nbsp;{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)