import dash from dash import dcc, html from dash.dependencies import Output, Input from dash_iconify import DashIconify import plotly.graph_objects as go import numpy as np import datetime from waf_api import fetch_attack_logs COUNTRY_FLAG = { "US": "🇺🇸", "ID": "🇮🇩", "PH": "🇵🇭", "SG": "🇸🇬", "CN": "🇨🇳", "RU": "🇷🇺", "DE": "🇩🇪", "MY": "🇲🇾", "JP": "🇯🇵", "IN": "🇮🇳", "KR": "🇰🇷", "FR": "🇫🇷", } COUNTRY_FULL = { "US": "United States", "ID": "Indonesia", "PH": "Philippines", "SG": "Singapore", "CN": "China", "RU": "Russia", "DE": "Germany", "MY": "Malaysia", "JP": "Japan", "IN": "India", "KR": "Korea", "FR": "France" } TARGET_LAT, TARGET_LON = -1.5789, 101.3033 def patch_record(rec): cc = (rec.get('country', '')[:2] or "ID").upper() country = rec.get('country', 'Unknown') dt = rec.get('start_at', int(datetime.datetime.now().timestamp()*1000)) domain = rec.get('host', '-') deny = int(rec.get('deny_count', 0)) url_path = rec.get('url_path', '-') return { 'ip': rec.get('ip', '-'), 'country_code': cc, 'country': country, 'start_at': dt, 'deny_count': deny, 'domain': domain, 'url_path': url_path, } # ========== ANIMASI S-CURVE ========== # def s_curve_points(lat1, lon1, lat2, lon2, steps=28, arch=13): mid1_lat = (lat1 + lat2) / 2 + arch mid1_lon = (lon1 + lon2) / 2 - arch / 2 mid2_lat = (lat1 + lat2) / 2 - arch mid2_lon = (lon1 + lon2) / 2 + arch / 2 lats, lons = [], [] for t in np.linspace(0, 1, steps): lat = (1-t)**3*lat1 + 3*(1-t)**2*t*mid1_lat + 3*(1-t)*t**2*mid2_lat + t**3*lat2 lon = (1-t)**3*lon1 + 3*(1-t)**2*t*mid1_lon + 3*(1-t)*t**2*mid2_lon + t**3*lon2 lats.append(lat) lons.append(lon) return lats, lons def make_map_figure(records, anim_idx=0): fig = go.Figure() steps = 28 geo_map = { "US": (37.751, -97.822), "ID": (-6.2000, 106.8167), "PH": (13.41, 122.56), "SG": (1.3521, 103.8198), "CN": (35.8617, 104.1954), "RU": (61.5240, 105.3188), "DE": (51.1657, 10.4515), "MY": (4.2105, 101.9758), "JP": (36.2048, 138.2529), "IN": (20.5937, 78.9629), "KR": (35.9078, 127.7669), "FR": (46.6034, 1.8883) } np.random.seed(0) for i, rec in enumerate(records[:5]): cc = rec.get('country_code','') country = COUNTRY_FULL.get(cc, cc) lat, lon = geo_map.get(cc, (TARGET_LAT + np.random.uniform(-3, 3), TARGET_LON + np.random.uniform(-3, 3))) lats, lons = s_curve_points(lat, lon, TARGET_LAT, TARGET_LON, steps=steps, arch=13) # S-Curve garis animasi fig.add_trace(go.Scattergeo( lon=lons, lat=lats, mode='lines', line=dict(width=3, color='rgba(56,189,248,0.18)'), opacity=0.7, hoverinfo='none', showlegend=False )) # Efek burst/animasi bulatan berjalan di garis progress = anim_idx % steps fade = 6 for p in range(progress-fade, progress+1): if 0 <= p < steps: alpha = (p-progress+fade)/fade fig.add_trace(go.Scattergeo( lon=[lons[p]], lat=[lats[p]], mode='markers', marker=dict( size=11-(progress-p), color=f'rgba(56,189,248,{0.45+0.5*alpha})', line=dict(width=1, color='#fff') ), hoverinfo='none', showlegend=False )) # Titik attacker fig.add_trace(go.Scattergeo( lon=[lon], lat=[lat], mode='markers', marker=dict( size=17, color='#38bdf8', line=dict(width=3, color='#fff') ), hoverinfo='text', text=[f"{COUNTRY_FLAG.get(cc,'')} {country}
{rec['ip']}
{rec['domain']}"], showlegend=False )) # Target fig.add_trace(go.Scattergeo( lon=[TARGET_LON], lat=[TARGET_LAT], mode='markers+text', marker=dict( size=34, color="#2563eb", line=dict(width=6, color="#fff") ), text=["Pemkab Solok Selatan"], textfont=dict(size=18, color="#fff"), textposition="top center", hoverinfo='text', showlegend=False )) fig.update_layout( geo=dict( projection_type="natural earth", showland=True, landcolor="#162131", countrycolor="#254070", lakecolor="#19253b", bgcolor="#101728", showcountries=True, showframe=False, showcoastlines=True, ), paper_bgcolor="#101728", plot_bgcolor="#101728", height=670, margin=dict(r=0, l=0, t=0, b=0) ) return fig def make_top_ip_list(records): from collections import Counter ip_country = [(r['ip'], r.get('country_code',''), r.get('country','')) for r in records] top_ip = Counter([(ip, cc, country) for (ip, cc, country) in ip_country]).most_common(5) badge_class = ["ta-gold", "ta-silver", "ta-bronze", "ta-gray", "ta-red"] rows = [] for i, ((ip, cc, country), count) in enumerate(top_ip): rows.append( html.Div([ html.Span(str(i+1), className=f"ta-badge {badge_class[i]}", style={"flexShrink": "0"}), html.Span(COUNTRY_FLAG.get(cc, ""), className="ta-flag"), html.Span(country, className="ta-country", style={ "fontWeight": "900", "marginRight": "10px", "fontSize": "1.05em" }), html.Span(ip, className="ta-ip", style={ "fontWeight": "700", "fontSize": "1.07em", "color": "#223251", "marginRight": "10px" }), html.Span(str(count), className="ta-count", style={ "color": "#ef1444", "fontWeight": "900", "fontSize": "1.17em", "marginLeft": "auto", "marginRight": "8px" }), ], className="ta-row", style={ "background": "#f8fafd" if i % 2 == 0 else "#eef4fd", "borderRadius": "13px", "marginBottom": "8px", "padding": "7px 10px 7px 7px", "alignItems": "center", "minHeight": "36px", "display": "flex" }) ) return html.Div([ html.Div([ DashIconify(icon="mdi:ip-network-outline", width=26, color="#2052a8", style={"marginRight": "8px"}), html.Span("IP Addr", className="ta-title", style={'color': '#2052a8', 'fontWeight': '900', 'fontSize': '1.13em'}), html.Span("24 hours", style={'fontWeight':700, 'color': '#2052a8', 'marginLeft':'auto', 'fontSize':'1.03em'}), ], className="ta-header", style={"marginBottom": "14px", "alignItems": "center"}), *rows ], className="ta-card", style={ 'background': 'linear-gradient(115deg,#f9fbff 60%,#e3eeff 100%)', 'boxShadow': '0 4px 22px 0 #e3eefe66', 'borderRadius': '20px', "padding": "18px 8px 12px 11px" }) def make_attack_log_table(records): # Header table_header = html.Div([ html.Span("No", style={"width": "36px", "fontWeight": 800, "paddingLeft": "12px"}), html.Span("Domain", style={"width": "300px", "fontWeight": 800}), html.Span("IP Address", style={"width": "130px", "fontWeight": 800}), html.Span("Country", style={"width": "95px", "fontWeight": 800}), html.Span("Datetime", style={"width": "135px", "fontWeight": 800}), html.Span("Denied", style={"width": "70px", "fontWeight": 800, "textAlign": "center"}), html.Span("URL Path", style={"width": "260px", "fontWeight": 800}), ], style={ "display": "flex", "gap": "8px", "background": "#eaf3ff", "borderRadius": "13px 13px 0 0", "padding": "12px 0 10px 0", "fontSize": "1.08em" }) # Isi log (max 5 baris) table_rows = [] for idx, rec in enumerate(records[:5]): cc = rec.get('country_code', '') country = rec.get('country', cc) ts = datetime.datetime.fromtimestamp(rec.get('start_at', 0)//1000).strftime('%Y-%m-%d %H:%M:%S') domain = rec.get('domain', '-') domain_link = f"http://{domain}" if domain != "-" else "#" url_path = rec.get('url_path', '-') table_rows.append(html.Div([ html.Span(str(idx+1), style={"width": "36px", "fontWeight": 700, "paddingLeft": "12px"}), html.A(domain, href=domain_link, style={ "width": "300px", "fontWeight": 700, "color": "#2563eb", "textDecoration": "none", "overflow": "hidden", "textOverflow": "ellipsis", "whiteSpace": "nowrap" }, target="_blank"), html.Span(rec['ip'], style={"width": "130px", "fontWeight": 600, "color": "#223251"}), html.Span(f"{COUNTRY_FLAG.get(cc,'')} {country}", style={"width": "95px"}), html.Span(ts, style={"width": "135px"}), html.Span( str(rec.get('deny_count', 0)), style={ "width": "70px", "color": "#ef1444" if rec.get('deny_count', 0) > 0 else "#7890a9", "fontWeight": "900" if rec.get('deny_count', 0) > 0 else 700, "textAlign": "center", "fontSize": "1.1em" } ), html.Span(url_path, style={ "width": "260px", "fontFamily": "JetBrains Mono, monospace", "fontSize": "0.99em", "color": "#465f8c", "overflow": "hidden", "textOverflow": "ellipsis", "whiteSpace": "nowrap" }), ], style={ "display": "flex", "gap": "8px", "padding": "11px 0", "background": "#fff" if idx % 2 == 0 else "#f7fafd", "borderRadius": "7px", "fontSize": "1.04em", "alignItems": "center" })) return html.Div([ html.Div("Attack Log Realtime", style={ "fontWeight": 900, "color": "#2052a8", "fontSize": "1.21em", "margin": "7px 0 13px 2px" }), html.Div([table_header, *table_rows], style={ "borderRadius": "13px", "boxShadow": "0 2px 12px #bae6fd22", "background": "linear-gradient(115deg,#f9fbff 60%,#e3eeff 100%)" }) ], style={"margin": "32px 0 0 0", "padding": "0 4vw"}) def make_realtime_attack(records): elements = [] for rec in records[:5]: ip = rec['ip'] cc = rec.get('country_code','') country = COUNTRY_FULL.get(cc, cc) flag = COUNTRY_FLAG.get(cc, '') ts = datetime.datetime.fromtimestamp(rec.get('start_at', 0)//1000).strftime('%Y-%m-%d %H:%M:%S') deny_count = rec.get('deny_count', 0) elements.append( html.Div([ html.Div([ html.Span(flag, className="rt-flag"), html.Span(country, className="rt-country"), html.A(ip, href=f"https://ipinfo.io/{ip}", target="_blank", className="rt-ip-link"), ], className="rt-ipbox"), html.Span(ts, className="rt-time"), html.Span( str(deny_count), className="rt-deny-badge" if deny_count > 0 else "rt-deny-zero" ) ], className="rt-attack-item") ) return html.Div([ html.Div("Real-time Attack", className="rt-attack-title"), *elements ], className="realtime-attack-card") external_stylesheets = [ "https://fonts.googleapis.com/css2?family=Inter:wght@600;800&family=JetBrains+Mono:wght@400;600&display=swap", "/root/attack-map-dashboard/assets/modern-style.css", ] app = dash.Dash(__name__, external_stylesheets=external_stylesheets) app.layout = html.Div([ html.Div([ html.Img(src='https://upload.wikimedia.org/wikipedia/commons/5/50/Lambang_Kabupaten_Solok_Selatan.png', style={ 'height': '48px', 'margin-right':'18px', 'vertical-align':'middle'}), html.Span("DASHBOARD MAP ATTACK", style={'font-size':'2.2rem','font-weight':900, 'color':'#fff', 'letter-spacing':'0.08em'}), html.Div(id="clock", className="clock-blink", style={ 'font-family':'JetBrains Mono', 'color':'#fff', 'background':'#233257', 'font-size':'1.21em', 'border-radius':'13px', 'padding':'6px 26px', 'margin-left':'auto', 'box-shadow':'0 2px 8px #2226' }), ], style={ 'display':'flex', 'align-items':'center', 'background':'#19223c', 'padding':'17px 38px 14px 28px', 'border-radius':'0 0 30px 30px', 'box-shadow':'0 8px 36px 0 #1116', 'margin-bottom':'12px', 'min-width': '900px', 'max-width': '1700px', 'margin': '0 auto 16px auto' }), html.Div([ html.Div([ html.Div(id="top-ip-list"), html.Div(id='dashboard-stats'), ], className="left-panel", style={ 'min-width': '255px', 'max-width': '315px', 'padding':'10px 0 0 24px' }), html.Div([ dcc.Graph(id='attack-map', config={'displayModeBar': False}), html.Div(id="attack-log-table"), dcc.Interval(id='interval', interval=15000, n_intervals=0) ], className="map-panel", style={ 'flex': 2.2, 'min-width': '850px', 'max-width': '1900px', 'margin':'10px 24px 0 24px', 'background':'#101728', 'border-radius':'30px', 'box-shadow':'0 4px 20px 0 #2223', 'padding':'8px 8px 0 8px', 'height': '900px' }), html.Div(id='realtime-attack', className="right-panel", style={ 'min-width': '250px', 'max-width': '325px', 'padding':'10px 18px 0 0' }), ], className='main-flex', style={ 'display':'flex', 'background':'none', 'padding-bottom':'11px', 'max-width':'2200px', 'min-width':'900px', 'margin':'0 auto' }), ], style={ 'background':'#101728', 'min-height':'100vh', 'font-family':"Inter, 'JetBrains Mono', Arial, sans-serif", 'overflowX':'hidden' }) def get_dashboard_stats(records): uv = len(set([r['ip'] for r in records])) pv = len(records) blocked = sum(1 for r in records if r.get('deny_count', 0) > 0) return {'uv': uv, 'pv': pv, 'blocked': blocked} @app.callback( Output('dashboard-stats', 'children'), Output('top-ip-list', 'children'), Input('interval', 'n_intervals') ) def update_stats(n): data_api = fetch_attack_logs(page_size=10, page=1) records = [patch_record(r) for r in data_api] stats = get_dashboard_stats(records) return ( html.Div([ html.Div(className="stats-card", children=[ html.Div(className="stats-icon", children=DashIconify(icon="mdi:account-group", width=28, color="#fff")), html.Div(className="stats-info", children=[ html.Div("UV in 24 hours", className="stats-title"), html.Div(f"{stats['uv']:,}", className="stats-value"), ]) ]), html.Div(className="stats-card", children=[ html.Div(className="stats-icon blue", children=DashIconify(icon="mdi:eye-outline", width=28, color="#fff")), html.Div(className="stats-info", children=[ html.Div("PV in 24 hours", className="stats-title"), html.Div(f"{stats['pv']:,}", className="stats-value"), ]) ]), html.Div(className="stats-card", children=[ html.Div(className="stats-icon orange", children=DashIconify(icon="mdi:shield-off-outline", width=28, color="#fff")), html.Div(className="stats-info", children=[ html.Div("Blocked Real-Time", className="stats-title"), html.Div(f"{stats['blocked']:,}", className="stats-value"), ]) ]), ], className='stats-card-wrap'), make_top_ip_list(records) ) @app.callback( Output('attack-map', 'figure'), Output('realtime-attack', 'children'), Output('clock', 'children'), Output('attack-log-table', 'children'), Input('interval', 'n_intervals') ) def update_map(n): data_api = fetch_attack_logs(page_size=10, page=1) records = [patch_record(r) for r in data_api] fig = make_map_figure(records, n % 28) realtime = make_realtime_attack(records) now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") attack_log_table = make_attack_log_table(records) return fig, realtime, now, attack_log_table if __name__ == '__main__': app.run(debug=False, host='0.0.0.0', port=8050)