File size: 6,175 Bytes
64a1c9c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
"""
Smart Contract Security Scanner
Rule-based Solidity audit assistant with transparent findings and remediation notes.
"""

from pathlib import Path
import re

import pandas as pd
import plotly.express as px
import streamlit as st


st.set_page_config(page_title="Smart Contract Security Scanner", page_icon="๐Ÿ”’", layout="wide")


def load_shared_css() -> None:
    current_dir = Path(__file__).resolve().parent
    candidates = [
        current_dir / "shared" / "styles.css",
        current_dir.parent / "shared" / "styles.css",
    ]
    css_path = next(path for path in candidates if path.exists())
    st.markdown(f"<style>{css_path.read_text(encoding='utf-8')}</style>", unsafe_allow_html=True)


load_shared_css()


SAMPLE_CONTRACT = """pragma solidity ^0.8.0;

contract Vault {
    mapping(address => uint256) public balances;
    address public owner;

    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount);
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok);
        balances[msg.sender] -= amount;
    }

    function emergencyTransfer(address target, bytes memory data) public {
        require(tx.origin == owner);
        target.delegatecall(data);
    }
}
"""


RULES = [
    {
        "id": "reentrancy-call-before-state-update",
        "pattern": r"\.call\{value:\s*[^}]+\}\([^)]*\)",
        "severity": "High",
        "reason": "External value transfer can re-enter before state is updated.",
        "fix": "Apply checks-effects-interactions, update balances before the call, or use ReentrancyGuard.",
    },
    {
        "id": "tx-origin-auth",
        "pattern": r"tx\.origin",
        "severity": "High",
        "reason": "tx.origin authentication can be phished through intermediary contracts.",
        "fix": "Use msg.sender and explicit role-based authorization.",
    },
    {
        "id": "delegatecall",
        "pattern": r"\bdelegatecall\b",
        "severity": "Critical",
        "reason": "delegatecall executes target code in this contract storage context.",
        "fix": "Avoid delegatecall unless target code is immutable, audited, and tightly authorized.",
    },
    {
        "id": "missing-events",
        "pattern": r"function\s+\w+[\s\S]{0,180}(balances\[|owner\s*=)",
        "severity": "Medium",
        "reason": "State-changing operations should emit events for monitoring and incident response.",
        "fix": "Emit events for withdrawals, ownership changes, and privileged actions.",
    },
    {
        "id": "unchecked-low-level-call",
        "pattern": r"\.send\(|\.call\(",
        "severity": "Medium",
        "reason": "Low-level calls need explicit success handling and safe control flow.",
        "fix": "Check return values and prefer typed interfaces when possible.",
    },
]

SEVERITY_WEIGHT = {"Low": 1, "Medium": 2, "High": 4, "Critical": 6}


def line_number(source: str, index: int) -> int:
    return source[:index].count("\n") + 1


def scan_contract(source: str) -> pd.DataFrame:
    findings = []
    for rule in RULES:
        for match in re.finditer(rule["pattern"], source, re.IGNORECASE):
            findings.append({
                "rule": rule["id"],
                "severity": rule["severity"],
                "line": line_number(source, match.start()),
                "evidence": source[match.start():match.end()].replace("\n", " ")[:120],
                "reason": rule["reason"],
                "fix": rule["fix"],
            })
    return pd.DataFrame(findings)


def risk_score(findings: pd.DataFrame) -> int:
    if findings.empty:
        return 0
    raw = sum(SEVERITY_WEIGHT[item] for item in findings["severity"])
    return min(100, int(raw / 18 * 100))


st.markdown("""
<div class="hero">
  <div class="hf-badge">AI Safety + Static Analysis</div>
  <h1>๐Ÿ”’ Smart Contract Security Scanner</h1>
  <p>Inspect Solidity code for high-signal vulnerability patterns, explain the risk, and produce remediation notes.</p>
  <div class="pill-row">
    <span class="hf-chip">Reentrancy</span>
    <span class="hf-chip">Authorization flaws</span>
    <span class="hf-chip">Audit-ready output</span>
  </div>
</div>
""", unsafe_allow_html=True)

with st.sidebar:
    st.markdown("### Scope")
    st.info("This Space is a transparent audit assistant, not a formal verification engine. It is useful for education, first-pass triage, and building labeled vulnerability datasets.")
    include_medium = st.checkbox("Show medium severity findings", value=True)

source = st.text_area("Solidity source", value=SAMPLE_CONTRACT, height=360)
findings = scan_contract(source)
if not include_medium and not findings.empty:
    findings = findings[findings["severity"].isin(["High", "Critical"])]

score = risk_score(findings)
metric_cols = st.columns(3)
metric_cols[0].metric("Risk score", f"{score}/100")
metric_cols[1].metric("Findings", len(findings))
metric_cols[2].metric("Critical/High", int(findings["severity"].isin(["Critical", "High"]).sum()) if not findings.empty else 0)

tab1, tab2, tab3 = st.tabs(["Findings", "Risk Breakdown", "How To Extend"])

with tab1:
    if findings.empty:
        st.success("No configured rule triggered. Add more rules before treating this as safe.")
    else:
        st.dataframe(findings, use_container_width=True, hide_index=True)

with tab2:
    if findings.empty:
        st.info("No chart to show yet.")
    else:
        counts = findings.groupby("severity").size().reset_index(name="count")
        fig = px.bar(
            counts,
            x="severity",
            y="count",
            color="severity",
            color_discrete_map={"Critical": "#111827", "High": "#e8935c", "Medium": "#b8a9d9", "Low": "#7accff"},
            title="Finding count by severity",
        )
        st.plotly_chart(fig, use_container_width=True)

with tab3:
    st.markdown("""
### HF-Native Extension Path

- Publish scanned snippets and labels as a Hugging Face Dataset.
- Fine-tune or evaluate a code model on vulnerability explanations.
- Add Slither/Mythril output as additional features.
- Use a Space to compare rule-based, model-based, and hybrid triage.
""")