From 2e511ca4283113a66d96e0d9b847fd86c8bca95e Mon Sep 17 00:00:00 2001 From: Simon Cloutier Date: Sun, 6 Apr 2025 14:39:32 -0400 Subject: [PATCH] getting there --- app/Dockerfile | 2 +- app/db_interface.py | 312 +++++++++++------- app/static/styles.css | 23 ++ app/templates/stats.html | 106 ++++-- .../__pycache__/user_views.cpython-39.pyc | Bin 1869 -> 1879 bytes app/views/stats_views.py | 116 +++++-- app/views/user_views.py | 4 +- 7 files changed, 387 insertions(+), 176 deletions(-) diff --git a/app/Dockerfile b/app/Dockerfile index e46e9af..acfbda5 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -29,4 +29,4 @@ COPY . . EXPOSE 8080 # Run the app via Gunicorn -CMD ["gunicorn", "--bind", "0.0.0.0:8080", "wsgi:app"] +CMD ["gunicorn", "--bind", "0.0.0.0:8080", "wsgi:app", "--timeout", "120", "--workers", "2"] diff --git a/app/db_interface.py b/app/db_interface.py index 7af9983..d55588e 100644 --- a/app/db_interface.py +++ b/app/db_interface.py @@ -1,6 +1,6 @@ from flask import current_app import mysql.connector -import datetime +from datetime import datetime, timedelta, timezone import requests import time import os @@ -139,149 +139,83 @@ def delete_user(mac_address): conn.close() -def get_latest_auth_logs(reply_type=None, limit=5, time_range=None): +def get_latest_auth_logs(reply_type=None, limit=5, time_range=None, offset=0): conn = get_connection() cursor = conn.cursor(dictionary=True) - # Determine the time filter based on the time_range - if time_range: - now = datetime.now(pytz.timezone(current_app.config.get('APP_TIMEZONE', 'UTC'))) - if time_range == 'last_minute': - time_filter = now - timedelta(minutes=1) - elif time_range == 'last_5_minutes': - time_filter = now - timedelta(minutes=5) - elif time_range == 'last_10_minutes': - time_filter = now - timedelta(minutes=10) - elif time_range == 'last_hour': - time_filter = now - timedelta(hours=1) - elif time_range == 'last_6_hours': - time_filter = now - timedelta(hours=6) - elif time_range == 'last_12_hours': - time_filter = now - timedelta(hours=12) - elif time_range == 'last_day': - time_filter = now - timedelta(days=1) - elif time_range == 'last_30_days': - time_filter = now - timedelta(days=30) - else: # 'all' case - time_filter = None + now = datetime.now(pytz.timezone(current_app.config.get('APP_TIMEZONE', 'UTC'))) + time_filter = None - if time_filter: - cursor.execute(""" - SELECT * FROM auth_logs - WHERE reply = %s AND timestamp >= %s - ORDER BY timestamp DESC - LIMIT %s - """, (reply_type, time_filter, limit)) - else: - cursor.execute(""" - SELECT * FROM auth_logs - WHERE reply = %s - ORDER BY timestamp DESC - LIMIT %s - """, (reply_type, limit)) + if time_range and time_range != 'all': + delta = { + 'last_minute': timedelta(minutes=1), + 'last_5_minutes': timedelta(minutes=5), + 'last_10_minutes': timedelta(minutes=10), + 'last_hour': timedelta(hours=1), + 'last_6_hours': timedelta(hours=6), + 'last_12_hours': timedelta(hours=12), + 'last_day': timedelta(days=1), + 'last_30_days': timedelta(days=30) + }.get(time_range) + if delta: + time_filter = now - delta + + if time_filter: + cursor.execute(""" + SELECT * FROM auth_logs + WHERE reply = %s AND timestamp >= %s + ORDER BY timestamp DESC + LIMIT %s OFFSET %s + """, (reply_type, time_filter, limit, offset)) else: cursor.execute(""" SELECT * FROM auth_logs WHERE reply = %s ORDER BY timestamp DESC - LIMIT %s - """, (reply_type, limit)) + LIMIT %s OFFSET %s + """, (reply_type, limit, offset)) logs = cursor.fetchall() cursor.close() conn.close() return logs - - -def get_vendor_info(mac, insert_if_found=True): +def count_auth_logs(reply_type=None, time_range=None): conn = get_connection() - cursor = conn.cursor(dictionary=True) - prefix = mac.lower().replace(":", "").replace("-", "")[:6] + cursor = conn.cursor() - print(f">>> Looking up MAC: {mac} → Prefix: {prefix}") - print("→ Searching in local database...") - cursor.execute("SELECT vendor_name, status FROM mac_vendors WHERE mac_prefix = %s", (prefix,)) - row = cursor.fetchone() + now = datetime.now(pytz.timezone(current_app.config.get('APP_TIMEZONE', 'UTC'))) + time_filter = None - if row: - print(f"✓ Found locally: {row['vendor_name']} (Status: {row['status']})") - cursor.close() - conn.close() - return { - "mac": mac, - "vendor": row['vendor_name'], - "source": "local", - "status": row['status'] - } + if time_range and time_range != 'all': + delta = { + 'last_minute': timedelta(minutes=1), + 'last_5_minutes': timedelta(minutes=5), + 'last_10_minutes': timedelta(minutes=10), + 'last_hour': timedelta(hours=1), + 'last_6_hours': timedelta(hours=6), + 'last_12_hours': timedelta(hours=12), + 'last_day': timedelta(days=1), + 'last_30_days': timedelta(days=30) + }.get(time_range) + if delta: + time_filter = now - delta - print("✗ Not found locally, querying API...") + if time_filter: + cursor.execute(""" + SELECT COUNT(*) FROM auth_logs + WHERE reply = %s AND timestamp >= %s + """, (reply_type, time_filter)) + else: + cursor.execute(""" + SELECT COUNT(*) FROM auth_logs + WHERE reply = %s + """, (reply_type,)) - url_template = current_app.config.get("OUI_API_URL", "https://api.maclookup.app/v2/macs/{}") - api_key = current_app.config.get("OUI_API_KEY", "") - headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} - - try: - url = url_template.format(prefix) - print(f"→ Querying API: {url}") - response = requests.get(url, headers=headers) - - if response.status_code == 200: - data = response.json() - vendor = data.get("company", "").strip() - if vendor: - print(f"✓ Found from API: {vendor}") - if insert_if_found: - cursor.execute(""" - INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated) - VALUES (%s, %s, 'found', NOW(), NOW()) - ON DUPLICATE KEY UPDATE - vendor_name = VALUES(vendor_name), - status = 'found', - last_checked = NOW(), - last_updated = NOW() - """, (prefix, vendor)) - conn.commit() - print("→ Inserted into database (found).") - return { - "mac": mac, - "vendor": vendor, - "source": "api", - "status": "found" - } - - elif response.status_code == 404: - print("✗ API returned 404 - vendor not found.") - if insert_if_found: - cursor.execute(""" - INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated) - VALUES (%s, %s, 'not_found', NOW(), NOW()) - ON DUPLICATE KEY UPDATE - vendor_name = VALUES(vendor_name), - status = 'not_found', - last_checked = NOW(), - last_updated = NOW() - """, (prefix, "not found")) - conn.commit() - print("→ Inserted into database (not_found).") - return { - "mac": mac, - "vendor": "", - "source": "api", - "status": "not_found" - } - - else: - print(f"✗ API error: {response.status_code}") - return {"mac": mac, "vendor": "", "error": f"API error: {response.status_code}"} - - except Exception as e: - print(f"✗ Exception while querying API: {e}") - return {"mac": mac, "vendor": "", "error": str(e)} - - finally: - cursor.close() - conn.close() + count = cursor.fetchone()[0] + cursor.close() + conn.close() + return count def get_summary_counts(): conn = get_connection() @@ -458,3 +392,133 @@ def get_user_by_mac(mac_address): cursor.close() conn.close() return user + +def get_known_mac_vendors(): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + cursor.execute("SELECT mac_prefix, vendor_name, status FROM mac_vendors") + entries = cursor.fetchall() + cursor.close() + conn.close() + + return { + row['mac_prefix'].lower(): { + 'vendor': row['vendor_name'], + 'status': row['status'] + } + for row in entries + } + +def get_vendor_info(mac, insert_if_found=True): + conn = get_connection() + cursor = conn.cursor(dictionary=True) + prefix = mac.lower().replace(":", "").replace("-", "")[:6] + + print(f">>> Looking up MAC: {mac} → Prefix: {prefix}") + print("→ Searching in local database...") + cursor.execute("SELECT vendor_name, status FROM mac_vendors WHERE mac_prefix = %s", (prefix,)) + row = cursor.fetchone() + + if row: + print(f"✓ Found locally: {row['vendor_name']} (Status: {row['status']})") + cursor.close() + conn.close() + return { + "mac": mac, + "vendor": row['vendor_name'], + "source": "local", + "status": row['status'] + } + + print("✗ Not found locally, querying API...") + + url_template = current_app.config.get("OUI_API_URL", "https://api.maclookup.app/v2/macs/{}") + api_key = current_app.config.get("OUI_API_KEY", "") + headers = {"Authorization": f"Bearer {api_key}"} if api_key else {} + + try: + url = url_template.format(prefix) + print(f"→ Querying API: {url}") + response = requests.get(url, headers=headers) + + if response.status_code == 200: + data = response.json() + vendor = data.get("company", "").strip() + + if vendor: + print(f"✓ Found from API: {vendor}") + + # Always insert found results, even if insert_if_found=False + cursor.execute(""" + INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated) + VALUES (%s, %s, 'found', NOW(), NOW()) + ON DUPLICATE KEY UPDATE + vendor_name = VALUES(vendor_name), + status = 'found', + last_checked = NOW(), + last_updated = NOW() + """, (prefix, vendor)) + print(f"→ Inserted vendor: {vendor} → rowcount: {cursor.rowcount}") + conn.commit() + + return { + "mac": mac, + "vendor": vendor, + "source": "api", + "status": "found" + } + + else: + print("⚠️ API returned empty company field. Treating as not_found.") + # 🛠 Always insert not_found, even if insert_if_found=False + cursor.execute(""" + INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated) + VALUES (%s, %s, 'not_found', NOW(), NOW()) + ON DUPLICATE KEY UPDATE + vendor_name = VALUES(vendor_name), + status = 'not_found', + last_checked = NOW(), + last_updated = NOW() + """, (prefix, "not found")) + print(f"→ Inserted not_found for {prefix} → rowcount: {cursor.rowcount}") + conn.commit() + return { + "mac": mac, + "vendor": "", + "source": "api", + "status": "not_found" + } + + elif response.status_code == 404: + print("✗ API returned 404 - vendor not found.") + # 🛠 Always insert not_found + cursor.execute(""" + INSERT INTO mac_vendors (mac_prefix, vendor_name, status, last_checked, last_updated) + VALUES (%s, %s, 'not_found', NOW(), NOW()) + ON DUPLICATE KEY UPDATE + vendor_name = VALUES(vendor_name), + status = 'not_found', + last_checked = NOW(), + last_updated = NOW() + """, (prefix, "not found")) + print(f"→ Inserted not_found (404) for {prefix} → rowcount: {cursor.rowcount}") + conn.commit() + return { + "mac": mac, + "vendor": "", + "source": "api", + "status": "not_found" + } + + else: + print(f"✗ API error: {response.status_code}") + return {"mac": mac, "vendor": "", "error": f"API error: {response.status_code}"} + + except Exception as e: + print(f"✗ Exception while querying API: {e}") + return {"mac": mac, "vendor": "", "error": str(e)} + + finally: + cursor.close() + conn.close() + diff --git a/app/static/styles.css b/app/static/styles.css index 39820b3..69f8129 100644 --- a/app/static/styles.css +++ b/app/static/styles.css @@ -294,3 +294,26 @@ form.inline-form { flex: 0 0 auto; padding: 6px; } + +.pagination { + margin-top: 0.75rem; + text-align: center; +} + +.pagination a, +.pagination span.current-page { + display: inline-block; + padding: 4px 10px; + margin: 0 3px; + border: 1px solid var(--accent); + border-radius: 4px; + color: var(--fg); + background-color: transparent; + text-decoration: none; +} + +.pagination span.current-page { + font-weight: bold; + background-color: var(--accent); + color: black; +} \ No newline at end of file diff --git a/app/templates/stats.html b/app/templates/stats.html index 50a9849..c474090 100644 --- a/app/templates/stats.html +++ b/app/templates/stats.html @@ -3,10 +3,9 @@ {% block content %}
-

Authentication Stats

-
+ @@ -99,35 +119,73 @@ {{ entry.description or '' }} {% endif %} - - {{ entry.vendor }} + {{ entry.vendor or '...' }} {{ entry.ago }} - {% if not entry.already_exists %} - - - - -
+
+ + + +
{% else %} - Already exists in VLAN {{ entry.existing_vlan or 'unknown' }} + Already exists in VLAN {{ entry.existing_vlan or 'unknown' }} {% endif %} {% endfor %} - {% else %} - No data available. - {% endif %} + {% if total_pages_fallback > 1 %} + + {% endif %}
+ - {# closes .stats-page #} + {% endblock %} diff --git a/app/views/__pycache__/user_views.cpython-39.pyc b/app/views/__pycache__/user_views.cpython-39.pyc index f6f19ed3590c97d1e0ced14c463d1230bc268cea..ad91d5ab5ce0eddb6f64d4e0831de16dfea527d7 100644 GIT binary patch delta 39 tcmX@hcb$(nk(ZZ?0SFGhc$a>2Bd;|Rw?JZ9VrEWaQci08W=|#-Rsi 0 else 0.5 + + # Lowercase cleaned prefixes + prefixes_to_lookup = {} + for mac in macs: + prefix = mac.lower().replace(":", "").replace("-", "")[:6] + prefixes_to_lookup[prefix] = mac # Use last MAC that used this prefix + + known_vendors = get_known_mac_vendors() # local DB cache + vendor_cache = {} # cache during this request + + for prefix, mac in prefixes_to_lookup.items(): + if prefix in known_vendors: + results[mac] = known_vendors[prefix]['vendor'] + continue + + if prefix in vendor_cache: + print(f"→ Prefix {prefix} already queried in this request, skipping.") + results[mac] = vendor_cache[prefix] + continue + + info = get_vendor_info(mac) # will insert into DB + vendor_name = info.get('vendor', '') + vendor_cache[prefix] = vendor_name + results[mac] = vendor_name + + sleep(delay) # throttle + + return jsonify(results) + + diff --git a/app/views/user_views.py b/app/views/user_views.py index f166e2a..2925092 100644 --- a/app/views/user_views.py +++ b/app/views/user_views.py @@ -7,8 +7,8 @@ user = Blueprint('user', __name__, url_prefix='/user') @user.route('/') def user_list(): users = get_all_users() - groups = get_all_groups() - return render_template('user_list.html', users=users, groups=groups) + available_groups = get_all_groups() + return render_template('user_list.html', users=users, available_groups=available_groups) @user.route('/add', methods=['POST'])