""" 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"", 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("""
AI Safety + Static Analysis

🔒 Smart Contract Security Scanner

Inspect Solidity code for high-signal vulnerability patterns, explain the risk, and produce remediation notes.

Reentrancy Authorization flaws Audit-ready output
""", 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. """)