Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .github/workflows/security-audit.yml
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The workflow should include a step to install all Python dependencies required by security_audit.py. If you add more dependencies in the future, using a requirements.txt file may be better for maintainability.

Add fail-fast: false under jobs if you plan to add more jobs in the future for better workflow management.

Optionally, add a step to check Python code formatting (e.g., with black or flake8) for code quality.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Security Audit

on:
push:
paths:
- '_data/chains/**'
- 'scripts/security_audit.py'
pull_request:
paths:
- '_data/chains/**'
- 'scripts/security_audit.py'

jobs:
audit:
runs-on: ubuntu-latest

steps:
- name: Checkout code
uses: actions/checkout@v3

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'

- name: Install dependencies
run: pip install requests

- name: Run security audit script
run: python scripts/security_audit.py
6 changes: 6 additions & 0 deletions _data/contributors/mrthang0597.json
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The GitHub username "Mrthang0597" has an uppercase "M", but GitHub usernames are case-insensitive and typically written in lowercase. For consistency, you might want to change "github": "Mrthang0597" to "github": "mrthang0597".

The field "contribution": "metadata update or data contribution" is fine, but if possible, specify the exact type of contribution for clarity (e.g., "metadata update").

Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"name": "MrThang0597",
"github": "Mrthang0597",
"contribution": "metadata update or data contribution",
"date": "2025-10-12"
}
86 changes: 86 additions & 0 deletions scripts/security_audit.py
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In is_valid_ipfs(cid), the regex assumes all CIDs are 46 alphanumeric chars. However, IPFS CIDs can be both v0 (Qm…) and v1 (bafy…). Consider using a more flexible CID validation or a library for robustness.

In audit_chain(), the variable cid is used for both chainId and as an IPFS CID. Consider renaming one for clarity.

The script loads icon metadata files as JSON but assumes they are lists. Consider catching exceptions for malformed files for robustness.

Requests is imported but never used; you can remove import requests unless you plan to use it.

Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import json
import os
import glob
import re
import requests
from urllib.parse import urlparse

CHAINS_DIR = "_data/chains"
WHITELIST_RPC_DOMAINS = ["infura.io", "alchemyapi.io", "ankr.com", "cloudflare-eth.com"]

def is_http_url_insecure(url):
return url.startswith("http://")

def domain_from_url(url):
try:
return urlparse(url).netloc.lower()
except Exception:
return None

def is_untrusted_domain(url):
domain = domain_from_url(url)
if domain:
return not any(d in domain for d in WHITELIST_RPC_DOMAINS)
return True

def is_valid_ipfs(cid):
return re.fullmatch(r"[A-Za-z0-9]{46}", cid) is not None

def audit_chain(chain, filename):
findings = []
cid = chain.get("chainId")
name = chain.get("name")

if cid == 1 and name != "Ethereum Mainnet":
findings.append(f"[ERROR] {filename}: ChainId 1 must be Ethereum Mainnet")

for url in chain.get("rpc", []):
if is_http_url_insecure(url):
findings.append(f"[ERROR] {filename}: Insecure RPC URL: {url}")
if is_untrusted_domain(url):
findings.append(f"[WARNING] {filename}: RPC domain not in trusted list: {url}")

if "explorers" in chain:
for exp in chain["explorers"]:
url = exp.get("url")
if url and is_http_url_insecure(url):
findings.append(f"[WARNING] {filename}: Explorer URL not HTTPS: {url}")

if "icon" in chain:
icon_path = f"_data/icons/{chain['icon']}.json"
if os.path.exists(icon_path):
with open(icon_path, "r", encoding="utf-8") as f:
icon_data = json.load(f)
for icon_entry in icon_data:
url = icon_entry.get("url", "")
if url.startswith("ipfs://"):
cid = url.replace("ipfs://", "")
if not is_valid_ipfs(cid):
findings.append(f"[ERROR] {filename}: Invalid IPFS CID: {cid}")
elif is_http_url_insecure(url):
findings.append(f"[WARNING] {filename}: Insecure icon URL: {url}")
else:
findings.append(f"[ERROR] {filename}: Icon metadata not found: {icon_path}")

return findings

def run_audit():
print("🔍 Running metadata security audit...")
files = glob.glob(os.path.join(CHAINS_DIR, "*.json"))
total_findings = 0

for filepath in files:
with open(filepath, "r", encoding="utf-8") as f:
data = json.load(f)
findings = audit_chain(data, filepath)
for f in findings:
print(f)
total_findings += len(findings)

if total_findings == 0:
print("✅ No security issues found.")
else:
print(f"⚠️ Found {total_findings} security issues.")

if __name__ == "__main__":
run_audit()