Files
Flock_SCAN-fix/modules/s3_url_catcher.py
T

368 lines
12 KiB
Python

#!/usr/bin/env python3
"""
s3_url_catcher.py — Extract signed S3 URLs from captured camera traffic.
When cameras send alert webhooks or the admin UI loads images, we can
capture signed S3 URLs from the traffic. These URLs have expiry windows
(typically 7 days for Flock alert images) and give us:
- LPR capture images (license plates)
- Camera snapshots
- Evidence of deployment locations
This module processes captured HTTP payloads (from tap mode, PCAP, or
HTTP response bodies) and extracts:
- flock-hibiki-inbox.s3.amazonaws.com URLs
- Generic S3 signed URLs
- hotspot/flocksafety.com image URLs
- Metadata: timestamp, device, organization
"""
import re
import json
from datetime import datetime
from urllib.parse import urlparse, parse_qs
# ── S3 and Flock image URL patterns ──
S3_SIGNED_URL = re.compile(
r'https?://[a-zA-Z0-9.-]*flock-hibiki-inbox[a-zA-Z0-9.-]*\.s3\.amazonaws\.com/[^"\'\s<>]+',
re.I
)
S3_GENERIC = re.compile(
r'https?://[a-zA-Z0-9._-]+\.s3\.amazonaws\.com/[^"\'\s<>]+',
re.I
)
FLOCK_IMAGE_URL = re.compile(
r'https?://[a-zA-Z0-9._-]*flocksafety\.com/[^"\'\s<>]*\.(?:jpg|jpeg|png|gif|webp)[^"\'\s<>]*',
re.I
)
FLOCK_DETAILS_URL = re.compile(
r'https?://hotlist\.flocksafety\.com/[^"\'\s<>]+',
re.I
)
# S3 URL with X-Amz-Signature = signed
SIGNED_URL_PATTERN = re.compile(
r'https?://[^"\'\s<>]+\?X-Amz-Signature=[a-f0-9]{64}[^"\'\s<>]*',
re.I
)
# Image URL from JSON payloads (like webhook bodies)
JSON_IMAGE_URL = re.compile(
r'"(?:imageUrl|image_url|url|preview|thumbnail)"\s*:\s*"(https?://[^"]+\.(?:jpg|jpeg|png))"',
re.I
)
def extract_s3_urls(data, source="unknown"):
"""
Extract all S3/flock image URLs from a text body (HTTP response,
PCAP payload, etc.).
Returns list of dicts with url, type, expiry_info, source.
"""
results = []
if not data:
return results
seen_urls = set()
# 1. Flock hibiki-inbox S3 (known camera image bucket)
for m in S3_SIGNED_URL.finditer(data):
url = m.group(0)
if url in seen_urls:
continue
seen_urls.add(url)
info = {
"url": url,
"type": "FLOCK_S3_IMAGE",
"bucket": "flock-hibiki-inbox",
"source": source,
"signed": "X-Amz-Signature" in url,
"expiry": extract_expiry(url),
}
results.append(info)
# 2. Other S3 buckets
for m in S3_GENERIC.finditer(data):
url = m.group(0)
if url in seen_urls:
continue
seen_urls.add(url)
bucket = urlparse(url).hostname.split(".")[0] if urlparse(url).hostname else "unknown"
info = {
"url": url,
"type": "S3_GENERIC",
"bucket": bucket,
"source": source,
"signed": "X-Amz-Signature" in url,
"expiry": extract_expiry(url),
}
results.append(info)
# 3. Signed URLs (non-S3 but with X-Amz-Signature)
for m in SIGNED_URL_PATTERN.finditer(data):
url = m.group(0)
if url in seen_urls:
continue
seen_urls.add(url)
info = {
"url": url,
"type": "SIGNED_URL",
"bucket": urlparse(url).hostname if urlparse(url).hostname else "unknown",
"source": source,
"signed": True,
"expiry": extract_expiry(url),
}
results.append(info)
# 4. Flock hotlist/capture URLs
for m in FLOCK_IMAGE_URL.finditer(data):
url = m.group(0)
if url in seen_urls:
continue
seen_urls.add(url)
info = {
"url": url,
"type": "FLOCK_HOTLIST_IMAGE",
"source": source,
"signed": False,
}
results.append(info)
return results
def extract_expiry(url):
"""Extract X-Amz-Expires or Expires parameter from a signed URL."""
try:
parsed = urlparse(url)
params = parse_qs(parsed.query)
if "X-Amz-Expires" in params:
expires_seconds = int(params["X-Amz-Expires"][0])
return f"{expires_seconds}s ({expires_seconds//3600}h)"
if "Expires" in params:
return params["Expires"][0]
# Check for ISO expiry in URL
m = re.search(r'imageExpiration["\']\s*:\s*["\']([^"\']+)["\']', url)
if m:
return m.group(1)
except Exception:
pass
return None
def extract_from_webhook_payload(body, source="unknown"):
"""
Parse a Flock webhook JSON payload for image URLs and metadata.
Returns structured data with device info and S3 URLs.
"""
results = []
try:
payload = json.loads(body) if isinstance(body, str) else body
except json.JSONDecodeError:
return results
if not isinstance(payload, dict):
return results
# Extract image URL
image_url = payload.get("imageUrl")
if image_url:
results.append({
"url": image_url,
"type": "WEBHOOK_IMAGE",
"source": source,
"signed": "X-Amz-Signature" in image_url,
"expiry": payload.get("imageExpiration"),
"metadata": {
"device_name": payload.get("deviceName"),
"device_id": payload.get("deviceExternalId"),
"plate": payload.get("ocr", {}).get("label"),
"state": payload.get("ocr", {}).get("state"),
"timestamp": payload.get("eventTime"),
"latitude": payload.get("deviceLat"),
"longitude": payload.get("deviceLong"),
"network": payload.get("networkName"),
"details_url": payload.get("detailsUrl"),
},
})
# Details URL
details = payload.get("detailsUrl")
if details:
results.append({
"url": details,
"type": "WEBHOOK_DETAILS",
"source": source,
"metadata": {"plate": payload.get("ocr", {}).get("label")},
})
return results
def scan_traffic_for_s3(http_responses=None, pcap_payloads=None, output_file=None):
"""
Scan captured HTTP responses and raw PCAP payloads for S3 URLs.
http_responses: list of {url, body, headers, source}
pcap_payloads: list of raw strings from traffic tap
Returns deduplicated S3 findings.
"""
all_urls = []
seen = set()
if http_responses:
for resp in http_responses:
body = resp.get("body", "")
source = resp.get("source", resp.get("url", "unknown"))
# Extract from body
for url_info in extract_s3_urls(body, source=source):
key = url_info["url"]
if key not in seen:
seen.add(key)
all_urls.append(url_info)
# Try webhook JSON parsing
for wh_info in extract_from_webhook_payload(body, source=source):
key = wh_info["url"]
if key not in seen:
seen.add(key)
all_urls.append(wh_info)
if pcap_payloads:
for payload in pcap_payloads:
if isinstance(payload, str):
source_data = "pcap_raw"
for url_info in extract_s3_urls(payload, source=source_data):
key = url_info["url"]
if key not in seen:
seen.add(key)
all_urls.append(url_info)
# Write to output file
if output_file and all_urls:
write_s3_report(all_urls, output_file)
return all_urls
def write_s3_report(urls, output_path):
"""Write S3 URL findings to a file."""
timestamp = datetime.now().isoformat()
with open(output_path, "w") as f:
f.write("╔══════════════════════════════════════════════════════════════╗\n")
f.write("║ FLOCK_scan — Captured S3 Image URLs ║\n")
f.write(f"║ Generated: {timestamp}\n")
f.write("╚══════════════════════════════════════════════════════════════╝\n\n")
if not urls:
f.write("No S3 URLs found.\n")
return 0
f.write(f"Total unique URLs: {len(urls)}\n\n")
# Group by type
for url_info in urls:
f.write(f"{'─' * 70}\n")
f.write(f" {url_info['type']}\n")
f.write(f" URL: {url_info['url']}\n")
if url_info.get("expiry"):
f.write(f" Exp: {url_info['expiry']}\n")
if url_info.get("bucket"):
f.write(f" Buck: {url_info['bucket']}\n")
if url_info.get("source"):
f.write(f" From: {url_info['source']}\n")
meta = url_info.get("metadata")
if meta:
plate = meta.get("plate")
if plate:
f.write(f" Plate: {plate}\n")
dev = meta.get("device_name")
if dev:
f.write(f" Cam: {dev}\n")
loc = meta.get("latitude")
if loc:
f.write(f" GPS: {loc}, {meta.get('longitude')}\n")
ts = meta.get("timestamp")
if ts:
f.write(f" Time: {ts}\n")
f.write("\n")
f.write(f"{'═' * 70}\n")
f.write(f"Total: {len(urls)} S3 URLs captured\n")
return len(urls)
def format_s3_findings_terminal(urls):
"""Format S3 URL findings for terminal display."""
lines = []
if not urls:
return " No S3 URLs captured."
lines.append(f" \033[93m{len(urls)} S3/image URLs captured:\033[0m")
for u in urls[:15]: # Show max 15
url_short = u["url"][:100]
expiry = u.get("expiry", "")
plate = u.get("metadata", {}).get("plate", "")
icon = "\033[91m[S3_IMG]\033[0m" if u["type"] == "WEBHOOK_IMAGE" else "\033[94m[URL]\033[0m"
line = f" {icon} {url_short}"
if expiry:
line += f" \033[90m(exp: {expiry})\033[0m"
if plate:
line += f" \033[93m[{plate}]\033[0m"
lines.append(line)
if len(urls) > 15:
lines.append(f" \033[90m... and {len(urls) - 15} more\033[0m")
return "\n".join(lines)
# ═══════════════════════════════════════════════════════════════════
# CLI TEST
# ═══════════════════════════════════════════════════════════════════
if __name__ == "__main__":
sample_webhook = json.dumps({
"deviceLat": 33.045,
"deviceLong": -80.106,
"deviceName": "LR#007 College Park Rd @ N Main St NB",
"deviceExternalId": "949692b4-3110-413e-a93c-52fbc5a9885a",
"imageUrl": "https://flock-hibiki-inbox.s3.us-east-1.amazonaws.com/policy/ORG/raw/CAPTURE.jpg?X-Amz-Signature=abc123&X-Amz-Expires=604800",
"imageExpiration": "2025-03-19T14:59:16Z",
"detailsUrl": "https://hotlist.flocksafety.com/img/tar-TOKEN",
"ocr": {"label": "ABC123", "state": "south_carolina"},
"eventTime": "2025-03-12T14:59:05.000Z",
})
urls = scan_traffic_for_s3(
http_responses=[{"body": sample_webhook, "source": "webhook_test"}]
)
print(format_s3_findings_terminal(urls))