Simon
Model evealuation (#10)
a9f0e5a
Raw
History Blame Contribute Delete
6.78 kB
"""URLScan.io API client for URL analysis."""
import os
import time
from typing import Dict, Any, Optional
from dotenv import load_dotenv
import requests
# Load environment variables from .env file
load_dotenv()
class URLScanError(Exception):
"""Custom exception for URLScan API errors."""
pass
class URLScanClient:
"""Client for interacting with URLScan.io API."""
BASE_URL = "https://urlscan.io/api/v1"
def __init__(self, api_key: Optional[str] = None):
"""
Initialize URLScan client.
Args:
api_key: URLScan.io API key. If not provided, will try to get from
URLSCAN_API_KEY environment variable.
Raises:
URLScanError: If no API key is provided or found in environment.
"""
self.api_key = api_key or os.getenv("URLSCAN_API_KEY")
if not self.api_key:
raise URLScanError(
"No API key provided. Set URLSCAN_API_KEY environment variable "
"or pass api_key parameter."
)
self.session = requests.Session()
self.session.headers.update({
"API-Key": self.api_key,
"Content-Type": "application/json"
})
def submit_url(
self,
url: str,
visibility: str = "public",
tags: Optional[list] = None
) -> Dict[str, Any]:
"""
Submit a URL for scanning.
Args:
url: The URL to scan
visibility: Visibility of the scan ('public', 'unlisted', or 'private')
tags: Optional list of tags for categorization
Returns:
Dictionary containing scan submission response with 'uuid' and 'api' fields
Raises:
URLScanError: If submission fails
"""
endpoint = f"{self.BASE_URL}/scan/"
payload = {
"url": url,
"visibility": visibility
}
if tags:
payload["tags"] = tags
try:
response = self.session.post(endpoint, json=payload)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if response.status_code == 429:
raise URLScanError("Rate limit exceeded. Please wait before retrying.")
elif response.status_code == 400:
raise URLScanError(f"Bad request: {response.text}")
else:
raise URLScanError(f"HTTP error occurred: {e}")
except requests.exceptions.RequestException as e:
raise URLScanError(f"Request failed: {e}")
def get_result(self, uuid: str) -> Dict[str, Any]:
"""
Get scan results by UUID.
Args:
uuid: The scan UUID returned from submit_url
Returns:
Dictionary containing scan results
Raises:
URLScanError: If retrieval fails
"""
endpoint = f"{self.BASE_URL}/result/{uuid}/"
try:
response = self.session.get(endpoint)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
if response.status_code == 404:
raise URLScanError(
f"Scan not found or not ready yet. UUID: {uuid}"
)
else:
raise URLScanError(f"HTTP error occurred: {e}")
except requests.exceptions.RequestException as e:
raise URLScanError(f"Request failed: {e}")
def submit_and_wait(
self,
url: str,
visibility: str = "public",
tags: Optional[list] = None,
max_wait: int = 60,
poll_interval: int = 5
) -> Dict[str, Any]:
"""
Submit a URL and wait for results.
Args:
url: The URL to scan
visibility: Visibility of the scan
tags: Optional list of tags
max_wait: Maximum time to wait for results (seconds)
poll_interval: Time between polling attempts (seconds)
Returns:
Dictionary containing scan results
Raises:
URLScanError: If submission or retrieval fails, or timeout occurs
"""
# Submit URL
submission = self.submit_url(url, visibility, tags)
uuid = submission.get("uuid")
if not uuid:
raise URLScanError("No UUID returned from submission")
# Wait for results
elapsed = 0
while elapsed < max_wait:
try:
time.sleep(poll_interval)
elapsed += poll_interval
result = self.get_result(uuid)
return result
except URLScanError as e:
if "not found or not ready" in str(e):
# Scan not ready yet, continue waiting
continue
else:
# Other error, raise it
raise
raise URLScanError(
f"Timeout waiting for scan results. UUID: {uuid}. "
f"You can retrieve results later using get_result('{uuid}')"
)
def search(self, query: str, size: int = 100) -> Dict[str, Any]:
"""
Search URLScan.io database.
Args:
query: Search query (e.g., 'domain:example.com')
size: Number of results to return (max 10000)
Returns:
Dictionary containing search results
Raises:
URLScanError: If search fails
"""
endpoint = f"{self.BASE_URL}/search/"
params = {
"q": query,
"size": min(size, 10000)
}
try:
response = self.session.get(endpoint, params=params)
response.raise_for_status()
return response.json()
except requests.exceptions.HTTPError as e:
raise URLScanError(f"HTTP error occurred: {e}")
except requests.exceptions.RequestException as e:
raise URLScanError(f"Request failed: {e}")
def get_verdict(self, uuid: str) -> Optional[str]:
"""
Get the verdict (malicious/safe) for a scan.
Args:
uuid: The scan UUID
Returns:
Verdict string ('malicious', 'safe', or None if not available)
Raises:
URLScanError: If retrieval fails
"""
result = self.get_result(uuid)
# Extract verdict from results
verdicts = result.get("verdicts", {})
overall = verdicts.get("overall", {})
if overall.get("malicious", False):
return "malicious"
elif overall.get("score", 0) == 0:
return "safe"
else:
return None