sammoftah's picture
Deploy Smart Contract Security Scanner
64a1c9c verified
"""
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.
""")