Threat_Hunter / tools /exploit_tool.py
EricChen2005's picture
Deploy ThreatHunter - AMD MI300X + Qwen2.5-32B
c8d30bc
Raw History Blame Contribute Delete
14.6 kB
# tools/exploit_tool.py
# ๅŠŸ่ƒฝ๏ผšGitHub Exploit / PoC ๆœๅฐ‹ Tool
# Harness ๆ”ฏๆŸฑ๏ผšGraceful Degradation๏ผˆ้™็ดš็€‘ๅธƒ๏ผ‰+ Observability๏ผˆๅŽŸๅญๅŒ–ๆ—ฅ่ชŒ๏ผ‰
# ๆ“ๆœ‰่€…๏ผšๆˆๅ“ก C๏ผˆAnalyst Agent Pipeline๏ผ‰
#
# ไฝฟ็”จๆ–นๅผ๏ผš
# from tools.exploit_tool import search_exploits
#
# ๆžถๆง‹ๅฎšไฝ๏ผš
# Analyst Agent ็š„ใ€Œ็ฌฌไบŒ้šปๆ‰‹ใ€โ€” ๆœๅฐ‹ CVE ็š„ๅ…ฌ้–‹ Exploit/PoC
# ๆœ‰ๅ…ฌ้–‹ exploit = ๆ”ปๆ“Š้–€ๆชปๆฅตไฝŽ = ้ขจ้šชๆŒ‡ๆจ™ HIGH
import json
import os
import time
import hashlib
import logging
from datetime import datetime, timezone
import requests
logger = logging.getLogger("ThreatHunter")
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๅธธๆ•ธ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
GITHUB_API_BASE = "https://api.github.com/search/repositories"
RESULTS_PER_PAGE = 10 # ้™ๅˆถ็ตๆžœๆ•ธ้‡๏ผŒ้ฟๅ… context ้Ž้•ท
REQUEST_TIMEOUT = 20 # ็ง’
# Rate limit ๆŽงๅˆถ
# GitHub ็„ก่ช่ญ‰ 10 req/min๏ผŒๆœ‰่ช่ญ‰ 30 req/min
RATE_LIMIT_WITH_TOKEN = 2.0 # ๆœ‰ Token: 30 req/min โ†’ 2s ้–“้š”๏ผˆไฟๅฎˆ๏ผ‰
RATE_LIMIT_WITHOUT_TOKEN = 6.0 # ็„ก Token: 10 req/min โ†’ 6s ้–“้š”๏ผˆไฟๅฎˆ๏ผ‰
MAX_RETRIES = 2
# ้›ข็ทšๅฟซๅ–
CACHE_DIR = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "data")
CACHE_TTL = 3600 * 24 # 24 ๅฐๆ™‚้ŽๆœŸ
# ไธŠๆฌก่ซ‹ๆฑ‚ๆ™‚้–“๏ผˆๆจก็ต„็ดš rate limiter๏ผ‰
_last_request_time = 0.0
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ่ผ”ๅŠฉๅ‡ฝๅผ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def _get_github_token() -> str:
"""ๅ–ๅพ— GitHub Token๏ผˆๅ„ชๅ…ˆๅพž config๏ผŒๅ‚™้ธ็’ฐๅขƒ่ฎŠๆ•ธ๏ผ‰"""
try:
from config import GITHUB_TOKEN
if GITHUB_TOKEN:
return GITHUB_TOKEN
except ImportError:
pass
return os.getenv("GITHUB_TOKEN", "")
def _get_cache_path(cve_id: str) -> str:
"""ๅ–ๅพ—้›ข็ทšๅฟซๅ–ๆช”ๆกˆ่ทฏๅพ‘"""
safe_name = hashlib.md5(cve_id.encode()).hexdigest()[:12]
return os.path.join(CACHE_DIR, f"exploit_cache_{cve_id}_{safe_name}.json")
def _read_cache(cve_id: str) -> dict | None:
"""่ฎ€ๅ–้›ข็ทšๅฟซๅ–๏ผŒ้ŽๆœŸๆˆ–ไธๅญ˜ๅœจๅ›žๅ‚ณ None"""
cache_path = _get_cache_path(cve_id)
try:
if os.path.exists(cache_path):
with open(cache_path, "r", encoding="utf-8") as f:
cached = json.load(f)
cached_time = cached.get("_cached_at", 0)
if time.time() - cached_time < CACHE_TTL:
logger.info("[OK] Exploit cache hit: %s", cve_id)
return cached
else:
logger.info("[INFO] Exploit cache expired: %s", cve_id)
except (json.JSONDecodeError, IOError) as e:
logger.warning("[WARN] Exploit cache read failed: %s", e)
return None
def _write_cache(cve_id: str, data: dict) -> None:
"""ๅฏซๅ…ฅ้›ข็ทšๅฟซๅ–"""
try:
os.makedirs(CACHE_DIR, exist_ok=True)
cache_path = _get_cache_path(cve_id)
data["_cached_at"] = time.time()
with open(cache_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
except (IOError, PermissionError) as e:
logger.warning("[WARN] Exploit cache write failed: %s", e)
def _rate_limit() -> None:
"""Rate limiter โ€” ็ขบไฟไธ่ถ…้Ž GitHub API ้™้€Ÿ"""
global _last_request_time
token = _get_github_token()
interval = RATE_LIMIT_WITH_TOKEN if token else RATE_LIMIT_WITHOUT_TOKEN
elapsed = time.time() - _last_request_time
if elapsed < interval:
wait = interval - elapsed
logger.info("[WAIT] GitHub rate limit: waiting %.1fs", wait)
time.sleep(wait)
_last_request_time = time.time()
def _classify_exploit_type(name: str, description: str) -> str:
"""
ๆ นๆ“š repo ๅ็จฑๅ’Œๆ่ฟฐๅˆคๆ–ท exploit ้กžๅž‹ใ€‚
ๅˆ†้กž่ฆๅ‰‡๏ผš
- ๅซ scanner/detect/check โ†’ scanner
- ๅซ weaponized/exploit-db/metasploit โ†’ weaponized
- ๅ…ถไป–๏ผˆpoc/proof/test/demo ็ญ‰๏ผ‰โ†’ poc
"""
text = f"{name} {description}".lower()
# ๆŽƒๆๅ™จ / ๅตๆธฌๅทฅๅ…ท
scanner_keywords = ["scanner", "detect", "check", "scan", "checker", "finder"]
for kw in scanner_keywords:
if kw in text:
return "scanner"
# ๆญฆๅ™จๅŒ– exploit
weaponized_keywords = ["weaponized", "exploit-db", "metasploit", "payload", "shellcode", "reverse shell"]
for kw in weaponized_keywords:
if kw in text:
return "weaponized"
# ้ ่จญ็‚บ PoC
return "poc"
def _determine_risk_indicator(exploit_count: int, api_available: bool) -> str:
"""
ๆ นๆ“š exploit ๆ•ธ้‡ๅ’Œ API ๅฏ็”จๆ€งๅˆคๅฎš้ขจ้šชๆŒ‡ๆจ™ใ€‚
้‚่ผฏ๏ผš
exploit_count > 0 โ†’ "HIGH"๏ผˆๆœ‰ๅ…ฌ้–‹ exploit = ้ซ˜้ขจ้šช๏ผ‰
exploit_count == 0 ไธ” API ๅฏ็”จ โ†’ "LOW"๏ผˆ็ขบ่ชๆฒ’ๆœ‰ๅ…ฌ้–‹ exploit๏ผ‰
API ไธๅฏ็”จ โ†’ "UNKNOWN (API limited)"
"""
if not api_available:
return "UNKNOWN (API limited)"
if exploit_count > 0:
return "HIGH"
return "LOW"
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# ๆ ธๅฟƒๆŸฅ่ฉข้‚่ผฏ
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
def _query_github_api(cve_id: str) -> dict | None:
"""
ๅ‘ผๅซ GitHub Search API๏ผŒๅ›žๅ‚ณๅŽŸๅง‹ JSON response dictใ€‚
ๅคฑๆ•—ๅ›žๅ‚ณ Noneใ€‚
ๅŒ…ๅซ๏ผš
- Rate limiting
- ้‡่ฉฆๆฉŸๅˆถ๏ผˆๆœ€ๅคš MAX_RETRIES ๆฌก๏ผ‰
- Timeout ่™•็†
"""
token = _get_github_token()
headers = {
"Accept": "application/vnd.github.v3+json",
}
if token:
headers["Authorization"] = f"token {token}"
# ๆœๅฐ‹็ญ–็•ฅ๏ผš็”จ CVE ID ไฝœ็‚บ้—œ้ตๅญ— + exploit ้™ๅฎš
params = {
"q": f"{cve_id} exploit",
"sort": "stars",
"order": "desc",
"per_page": RESULTS_PER_PAGE,
}
for attempt in range(1, MAX_RETRIES + 1):
_rate_limit()
try:
logger.info("[QUERY] GitHub API search: %s (attempt %d)", cve_id, attempt)
response = requests.get(
GITHUB_API_BASE,
params=params,
headers=headers,
timeout=REQUEST_TIMEOUT,
)
if response.status_code == 200:
return response.json()
if response.status_code in (403, 429):
# Rate limit ่ขซ่งธ็™ผ
retry_after = response.headers.get("Retry-After", "60")
logger.warning(
"[WARN] GitHub API %d (rate limited), Retry-After: %ss",
response.status_code, retry_after
)
time.sleep(min(int(retry_after), 30)) # ๆœ€ๅคš็ญ‰ 30 ็ง’
continue
if response.status_code >= 500:
logger.warning("[WARN] GitHub API %d (server error)", response.status_code)
time.sleep(2)
continue
# ๅ…ถไป–้Œฏ่ชค็ขผ
logger.warning("[WARN] GitHub API returned %d: %s", response.status_code, response.text[:200])
return None
except requests.exceptions.Timeout:
logger.warning("[WARN] GitHub API timeout (%ds)", REQUEST_TIMEOUT)
continue
except requests.exceptions.ConnectionError:
logger.warning("[WARN] GitHub API connection failed (network issue)")
continue
except requests.exceptions.RequestException as e:
logger.warning("[WARN] GitHub API request error: %s", e)
return None
return None # ๆ‰€ๆœ‰้‡่ฉฆ้ƒฝๅคฑๆ•—
def _parse_github_response(raw: dict, cve_id: str) -> dict:
"""
ๅฐ‡ GitHub Search API ๅŽŸๅง‹ response ่ฝ‰ๆ›็‚บ Tool ่ผธๅ‡บๆ ผๅผใ€‚
่ฝ‰ๆ› mapping๏ผš
response.items[].full_name โ†’ repo_name
response.items[].html_url โ†’ url
response.items[].stargazers_count โ†’ stars
response.items[].language โ†’ language
response.items[].updated_at โ†’ last_updated
response.items[].description โ†’ description
"""
exploits = []
items = raw.get("items", [])
for repo in items:
repo_name = repo.get("full_name", "")
description = repo.get("description", "") or ""
exploit_type = _classify_exploit_type(repo_name, description)
# ๆๅ–ๆœ€ๅพŒๆ›ดๆ–ฐๆ—ฅๆœŸ๏ผˆๅชๅ–ๆ—ฅๆœŸ้ƒจๅˆ†๏ผ‰
updated_at = repo.get("updated_at", "")
if "T" in updated_at:
updated_at = updated_at.split("T")[0]
exploits.append({
"repo_name": repo_name,
"url": repo.get("html_url", ""),
"stars": repo.get("stargazers_count", 0),
"language": repo.get("language", "") or "Unknown",
"last_updated": updated_at,
"description": description[:300], # ๆˆชๆ–ท้Ž้•ทๆ่ฟฐ
"type": exploit_type,
})
exploit_count = len(exploits)
risk_indicator = _determine_risk_indicator(exploit_count, api_available=True)
return {
"cve_id": cve_id,
"source": "GitHub API",
"exploit_count": exploit_count,
"exploits": exploits,
"risk_indicator": risk_indicator,
}
def _search_exploits_impl(cve_id: str) -> str:
"""
search_exploits ็š„ๆ ธๅฟƒๅฏฆไฝœ๏ผˆ่ˆ‡ CrewAI @tool ่งฃ่€ฆ๏ผŒๆ–นไพฟๅ–ฎๅ…ƒๆธฌ่ฉฆ๏ผ‰ใ€‚
้™็ดš็€‘ๅธƒ๏ผš
1. ๆŸฅ GitHub Search API โ†’ ๆˆๅŠŸๅ‰‡ๅฟซๅ–
2. API ๅคฑๆ•—๏ผˆ403/429 rate limit๏ผ‰โ†’ ่ฎ€้›ข็ทšๅฟซๅ–
3. ๅฟซๅ–ไนŸๆฒ’ๆœ‰ โ†’ ๅ›žๅ‚ณ exploit_count: 0, risk_indicator: "UNKNOWN"
4. ไปปไฝ•ๆœช้ ๆœŸ้Œฏ่ชค โ†’ ๅ›žๅ‚ณๅฎ‰ๅ…จ็š„้ ่จญ็ตๆžœ๏ผˆ็ต•ไธ crash๏ผ‰
"""
try:
# ๆธ…็† CVE ID๏ผˆๅŽป็ฉบ็™ฝใ€็ตฑไธ€ๅคงๅฏซ๏ผ‰
cve_id = cve_id.strip().upper()
if not cve_id:
logger.warning("[WARN] Exploit Tool received empty CVE ID input")
return json.dumps({
"cve_id": "",
"source": "GitHub API (error)",
"exploit_count": 0,
"exploits": [],
"risk_indicator": "UNKNOWN",
"error": "No CVE ID provided",
}, ensure_ascii=False, indent=2)
logger.info("[QUERY] Exploit search: %s", cve_id)
# ๅ˜—่ฉฆ GitHub API ๆŸฅ่ฉข
raw = _query_github_api(cve_id)
if raw is not None:
result = _parse_github_response(raw, cve_id)
# ๅฏซๅ…ฅๅฟซๅ–ไพ›้›ข็ทšไฝฟ็”จ
_write_cache(cve_id, result)
logger.info(
"[OK] Exploit search success: %s -> %d exploit(s), risk_indicator=%s",
cve_id, result['exploit_count'], result['risk_indicator']
)
return json.dumps(result, ensure_ascii=False, indent=2)
# API ๅคฑๆ•— โ†’ ๅ˜—่ฉฆ่ฎ€ๅฟซๅ–
cached = _read_cache(cve_id)
if cached:
cached.pop("_cached_at", None)
cached["source"] = "GitHub API (cache)"
cached["error"] = f"GitHub API unavailable, using cached data for '{cve_id}'"
logger.info("[OK] Exploit using cache: %s", cve_id)
return json.dumps(cached, ensure_ascii=False, indent=2)
# ๅฎŒๅ…จๆฒ’ๆœ‰่ณ‡ๆ–™
empty_result = {
"cve_id": cve_id,
"source": "GitHub API (unavailable)",
"exploit_count": 0,
"exploits": [],
"risk_indicator": "UNKNOWN (API limited)",
"error": f"GitHub API unavailable and no cache for '{cve_id}'",
}
logger.info("[INFO] Exploit no data for: %s", cve_id)
return json.dumps(empty_result, ensure_ascii=False, indent=2)
except Exception as e:
# ๆœ€ๅพŒไธ€้“้˜ฒ็ทš๏ผšไปปไฝ•ๆœช้ ๆœŸ้Œฏ่ชค้ƒฝไธ่ƒฝ่ฎ“ Agent crash
logger.error("[FAIL] Exploit Tool unexpected error: %s", e, exc_info=True)
error_result = {
"cve_id": cve_id if 'cve_id' in dir() else "",
"source": "GitHub API (error)",
"exploit_count": 0,
"exploits": [],
"risk_indicator": "UNKNOWN",
"error": f"Unexpected error: {str(e)}",
}
return json.dumps(error_result, ensure_ascii=False, indent=2)
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# CrewAI @tool ๅŒ…่ฃ๏ผˆAgent ๅ‘ผๅซ็”จ๏ผ‰
# โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
# โš ๏ธ ้‡่ฆ๏ผšไฝฟ็”จใ€Œๅปถ้ฒ่ผ‰ๅ…ฅใ€ๆจกๅผ๏ผˆLazyToolLoader๏ผ‰
# ๅŽŸๅ› ๏ผš้ฟๅ…ๅœจ import ้šŽๆฎตๅฐฑ่งธ็™ผ CrewAI ็š„ tool ่จปๅ†Š
def _create_tool():
"""ๅปถ้ฒๅปบ็ซ‹ CrewAI Tool๏ผŒๅƒ…ๅœจ Agent ๅฏฆ้š›ไฝฟ็”จๆ™‚ๆ‰ import"""
from crewai.tools import tool
@tool("search_exploits")
def search_exploits(cve_id: str) -> str:
"""ๆœๅฐ‹ๆŒ‡ๅฎš CVE ็š„ๅ…ฌ้–‹ Exploit ๅ’Œ PoC ็จ‹ๅผ็ขผ๏ผˆ้€้Ž GitHub API๏ผ‰ใ€‚
่ผธๅ…ฅๅ–ฎไธ€ CVE ID๏ผˆๅฆ‚ "CVE-2021-44228"๏ผ‰๏ผŒๅ›žๅ‚ณๅ…ฌ้–‹ exploit ็š„ๆ•ธ้‡ใ€้€ฃ็ตใ€ๆ˜Ÿๆ•ธ็ญ‰่ณ‡่จŠใ€‚
ๆœ‰ๅ…ฌ้–‹ exploit = ๆ”ปๆ“Š้–€ๆชปๆฅตไฝŽ = ้ขจ้šชๆŒ‡ๆจ™ HIGHใ€‚"""
return _search_exploits_impl(cve_id)
return search_exploits
# โ”€โ”€ ๅปถ้ฒ่ผ‰ๅ…ฅๆฉŸๅˆถ๏ผˆ่ˆ‡ nvd_tool.py ็›ธๅŒๆจกๅผ๏ผ‰โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
class _LazyToolLoader:
def __init__(self):
self._tool = None
def _load(self):
if self._tool is None:
self._tool = _create_tool()
@property
def search_exploits(self):
self._load()
return self._tool
_loader = _LazyToolLoader()
def __getattr__(name):
"""ๆจก็ต„ๅฑค็ดš __getattr__๏ผŒๆ”ฏๆด from tools.exploit_tool import search_exploits"""
if name == "search_exploits":
return _loader.search_exploits
raise AttributeError(f"module 'tools.exploit_tool' has no attribute {name!r}")