YashMK89 commited on
Commit
16d29c7
·
verified ·
1 Parent(s): aba2d10

update code with advance model

Browse files
Files changed (1) hide show
  1. pages/malicious_url.py +448 -98
pages/malicious_url.py CHANGED
@@ -1,98 +1,448 @@
1
- # pages/malicious_url.py
2
-
3
- import streamlit as st
4
- import numpy as np
5
- import pandas as pd
6
- import tensorflow as tf
7
- from urllib.parse import urlparse
8
- import re
9
- import joblib
10
-
11
- @st.cache_resource
12
- def load_model_and_scaler():
13
- model = tf.keras.models.load_model("models/malicious_url_model.h5")
14
- scaler = joblib.load("models/scaler.pkl")
15
- return model, scaler
16
-
17
- model, scaler = load_model_and_scaler()
18
-
19
- def extract_features(url):
20
- try:
21
- parsed_url = urlparse(str(url))
22
- features = {
23
- 'url_length': len(str(url)),
24
- 'hostname_length': len(parsed_url.hostname) if parsed_url.hostname else 0,
25
- 'path_length': len(parsed_url.path) if parsed_url.path else 0,
26
- 'query_length': len(parsed_url.query) if parsed_url.query else 0,
27
- 'fragment_length': len(parsed_url.fragment) if parsed_url.fragment else 0,
28
- 'num_dots': str(url).count('.'),
29
- 'num_hyphens': str(url).count('-'),
30
- 'num_at': str(url).count('@'),
31
- 'num_question': str(url).count('?'),
32
- 'num_ampersand': str(url).count('&'),
33
- 'num_equals': str(url).count('='),
34
- 'num_exclamation': str(url).count('!'),
35
- 'num_slash': str(url).count('/'),
36
- 'num_plus': str(url).count('+'),
37
- 'num_asterisk': str(url).count('*'),
38
- 'num_underscore': str(url).count('_'),
39
- 'num_hash': str(url).count('#'),
40
- 'num_dollar': str(url).count('$'),
41
- 'num_percent': str(url).count('%'),
42
- 'is_https': 1 if parsed_url.scheme == 'https' else 0,
43
- 'has_http_in_hostname': 1 if parsed_url.hostname and 'http' in parsed_url.hostname else 0,
44
- 'hostname_is_ip': 1 if parsed_url.hostname and re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', parsed_url.hostname) else 0,
45
- 'path_depth': str(url).count('/') - 2 if url and urlparse(str(url)).scheme in ['http', 'https'] and str(url).count('/') > 2 else 0
46
- }
47
- return pd.Series(features)
48
- except Exception:
49
- return pd.Series({
50
- 'url_length': 0, 'hostname_length': 0, 'path_length': 0,
51
- 'query_length': 0, 'fragment_length': 0, 'num_dots': 0,
52
- 'num_hyphens': 0, 'num_at': 0, 'num_question': 0,
53
- 'num_ampersand': 0, 'num_equals': 0, 'num_exclamation': 0,
54
- 'num_slash': 0, 'num_plus': 0, 'num_asterisk': 0,
55
- 'num_underscore': 0, 'num_hash': 0, 'num_dollar': 0,
56
- 'num_percent': 0, 'is_https': 0, 'has_http_in_hostname': 0,
57
- 'hostname_is_ip': 0, 'path_depth': 0
58
- })
59
-
60
- X_columns = [
61
- 'url_length', 'hostname_length', 'path_length', 'query_length',
62
- 'fragment_length', 'num_dots', 'num_hyphens', 'num_at',
63
- 'num_question', 'num_ampersand', 'num_equals', 'num_exclamation',
64
- 'num_slash', 'num_plus', 'num_asterisk', 'num_underscore',
65
- 'num_hash', 'num_dollar', 'num_percent', 'is_https',
66
- 'has_http_in_hostname', 'hostname_is_ip', 'path_depth'
67
- ]
68
-
69
- def app():
70
- st.title("🔗 Malicious URL Detector")
71
- st.markdown("Enter a URL below to check if it's likely malicious.")
72
-
73
- url_input = st.text_input(
74
- "🔗 Enter a URL:",
75
- placeholder="e.g., https://example.com",
76
- help="Type any URL you want to analyze"
77
- )
78
-
79
- if st.button("🔍 Analyze URL"):
80
- if not url_input.strip():
81
- st.warning("Please enter a valid URL.")
82
- else:
83
- with st.spinner("Analyzing..."):
84
- features = extract_features(url_input)
85
- df_new = pd.DataFrame([features])
86
- X_new = df_new[X_columns]
87
- X_new.fillna(-1, inplace=True)
88
- X_scaled = scaler.transform(X_new)
89
- prediction = model.predict(X_scaled)
90
- prob = float(prediction[0][0])
91
-
92
- if prob > 0.5:
93
- st.error(f"⚠️ This URL is likely **malicious**. Confidence: `{prob:.4f}`")
94
- else:
95
- st.success(f"✅ This URL appears to be **safe**. Confidence: `{1 - prob:.4f}`")
96
-
97
- st.markdown("---")
98
- st.markdown("💡 *Model trained on URL-based features like length, special characters, domain patterns, etc.*")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import streamlit as st
2
+ import re
3
+ import math
4
+ import time
5
+ import os
6
+ import joblib
7
+ import numpy as np
8
+ import pandas as pd
9
+ import torch
10
+ import tensorflow as tf
11
+ from collections import Counter
12
+ from tensorflow.keras.models import load_model
13
+ from tensorflow.keras.preprocessing.sequence import pad_sequences
14
+ import tldextract
15
+ from rapidfuzz import fuzz, process
16
+
17
+ # Set page config
18
+ st.set_page_config(
19
+ page_title="URL Threat Detector",
20
+ page_icon="🛡️",
21
+ layout="wide",
22
+ initial_sidebar_state="expanded"
23
+ )
24
+
25
+ # Disable GPU usage for TensorFlow and PyTorch
26
+ os.environ['CUDA_VISIBLE_DEVICES'] = '-1'
27
+ tf.config.set_visible_devices([], 'GPU')
28
+
29
+ # Global configuration
30
+ MAX_LEN = 200
31
+ FIXED_FEATURE_COLS = [
32
+ 'url_length', 'domain_length', 'subdomain_count', 'path_depth',
33
+ 'param_count', 'has_ip', 'has_executable', 'has_double_extension',
34
+ 'hex_encoded', 'digit_ratio', 'special_char_ratio', 'entropy',
35
+ 'is_safe_domain', 'is_uncommon_tld'
36
+ ]
37
+
38
+ # Enhanced domain and TLD lists
39
+ SAFE_DOMAINS = {
40
+ 'google.com', 'google.co.in', 'google.co.uk', 'google.fr', 'google.de',
41
+ 'amazon.com', 'amazon.in', 'amazon.co.uk', 'amazon.de', 'amazon.fr',
42
+ 'wikipedia.org', 'github.com', 'python.org', 'irs.gov', 'adobe.com',
43
+ 'steampowered.com', 'imdb.com', 'weather.com', 'archive.org', 'cdc.gov',
44
+ 'microsoft.com', 'apple.com', 'youtube.com', 'facebook.com', 'twitter.com',
45
+ 'linkedin.com', 'instagram.com', 'netflix.com', 'reddit.com', 'stackoverflow.com',
46
+ 'google.com', 'amazon.in', 'linkedin.com'
47
+ }
48
+
49
+ COMMON_TLDS = {
50
+ 'com', 'org', 'net', 'gov', 'edu', 'mil', 'co', 'io', 'ai', 'in',
51
+ 'uk', 'us', 'ca', 'au', 'de', 'fr', 'es', 'it', 'nl', 'jp', 'cn',
52
+ 'br', 'mx', 'ru', 'ch', 'se', 'no', 'dk', 'fi', 'be', 'at', 'nz'
53
+ }
54
+
55
+ # Initialize tldextract
56
+ tld_extractor = tldextract.TLDExtract()
57
+
58
+ @st.cache_resource
59
+ def load_char_mapping():
60
+ char_to_idx_path = 'char_to_idx.pkl'
61
+ if not os.path.exists(char_to_idx_path):
62
+ st.error(f"Character mapping file not found: {char_to_idx_path}")
63
+ return None
64
+ return joblib.load(char_to_idx_path)
65
+
66
+ @st.cache_resource
67
+ def load_all_models():
68
+ """Load models with CPU optimization"""
69
+ models = {}
70
+ model_dir = "models"
71
+
72
+ if not os.path.exists(model_dir):
73
+ os.makedirs(model_dir)
74
+ st.warning(f"Created model directory: {model_dir}")
75
+
76
+ # Hybrid models
77
+ hybrid_models = {
78
+ 'hybrid': 'hybrid_model.h5',
79
+ 'hybrid_fold1': 'best_hybrid_fold1.h5',
80
+ 'hybrid_fold2': 'best_hybrid_fold2.h5'
81
+ }
82
+
83
+ for name, file in hybrid_models.items():
84
+ path = os.path.join(model_dir, file)
85
+ if os.path.exists(path):
86
+ try:
87
+ models[name] = load_model(path)
88
+ st.success(f"Loaded {name}")
89
+ except Exception as e:
90
+ st.error(f"Error loading {name}: {str(e)}")
91
+ else:
92
+ st.warning(f"Model file not found: {path}")
93
+
94
+ # Traditional models
95
+ traditional_models = {
96
+ 'random_forest': 'random_forest_model.pkl',
97
+ 'xgboost': 'xgboost_model.pkl',
98
+ }
99
+
100
+ for name, file in traditional_models.items():
101
+ path = os.path.join(model_dir, file)
102
+ if os.path.exists(path):
103
+ try:
104
+ models[name] = joblib.load(path)
105
+ st.success(f"Loaded {name}")
106
+ except Exception as e:
107
+ st.error(f"Error loading {name}: {str(e)}")
108
+ else:
109
+ st.warning(f"Model file not found: {path}")
110
+
111
+ return models
112
+
113
+ def normalize_url(url):
114
+ """Safer URL normalization"""
115
+ try:
116
+ is_https = url.lower().startswith('https://')
117
+ url = url.lower()
118
+
119
+ prefixes = ['http://', 'ftp://', 'www.', 'ww2.', 'web.']
120
+ for prefix in prefixes:
121
+ if url.startswith(prefix):
122
+ url = url[len(prefix):]
123
+
124
+ if is_https:
125
+ url = "https://" + url
126
+
127
+ url = url.split('#')[0]
128
+
129
+ if '?' in url:
130
+ base, query = url.split('?', 1)
131
+ if not any(sd in base for sd in SAFE_DOMAINS):
132
+ params = [p for p in query.split('&') if '=' in p]
133
+ essential_params = [p for p in params if any(
134
+ kw in p for kw in ['id=', 'ref=', 'token='])]
135
+ url = base + ('?' + '&'.join(essential_params)
136
+ return re.sub(r'/{2,}', '/', url)
137
+ except Exception:
138
+ return url
139
+
140
+ def extract_url_components(url):
141
+ """Robust URL parsing"""
142
+ try:
143
+ extracted = tld_extractor(url)
144
+ subdomain = extracted.subdomain
145
+ domain = extracted.domain
146
+ suffix = extracted.suffix
147
+
148
+ path = ""
149
+ query = ""
150
+ if "/" in url:
151
+ path_start = url.find("/", url.find("//") + 2) if "//" in url else url.find("/")
152
+ if path_start != -1:
153
+ path_query = url[path_start:]
154
+ if "?" in path_query:
155
+ path, query = path_query.split("?", 1)
156
+ else:
157
+ path = path_query
158
+
159
+ if not domain and subdomain:
160
+ domain_parts = subdomain.split('.')
161
+ if len(domain_parts) > 1:
162
+ domain = domain_parts[-1]
163
+ subdomain = '.'.join(domain_parts[:-1])
164
+
165
+ return {
166
+ 'subdomain': subdomain,
167
+ 'domain': domain,
168
+ 'suffix': suffix,
169
+ 'path': path,
170
+ 'query': query
171
+ }
172
+ except:
173
+ return {
174
+ 'subdomain': '',
175
+ 'domain': '',
176
+ 'suffix': '',
177
+ 'path': '',
178
+ 'query': ''
179
+ }
180
+
181
+ def calculate_entropy(s):
182
+ """Compute Shannon entropy"""
183
+ if not s:
184
+ return 0
185
+ try:
186
+ p, lns = Counter(s), float(len(s))
187
+ return -sum(count/lns * math.log(count/lns, 2) for count in p.values())
188
+ except:
189
+ return 0
190
+
191
+ def fuzzy_domain_match(domain):
192
+ """Safe domain matching"""
193
+ if domain in SAFE_DOMAINS:
194
+ return True
195
+
196
+ domain_parts = domain.split('.')
197
+ if len(domain_parts) > 2:
198
+ base_domain = '.'.join(domain_parts[-2:])
199
+ if base_domain in SAFE_DOMAINS:
200
+ return True
201
+
202
+ best_match, score, _ = process.extractOne(domain, SAFE_DOMAINS, scorer=fuzz.WRatio)
203
+ return score > 85
204
+
205
+ def extract_robust_features(url):
206
+ """Feature extraction optimized for CPU"""
207
+ try:
208
+ clean_url = re.sub(r'[^\x00-\x7F]+', '', str(url))
209
+ normalized = normalize_url(clean_url)
210
+ components = extract_url_components(clean_url)
211
+ full_domain = f"{components['domain']}.{components['suffix']}" if components['suffix'] else components['domain']
212
+
213
+ # Structural features
214
+ url_length = len(clean_url)
215
+ domain_length = len(components['domain'])
216
+ subdomain_count = len(components['subdomain'].split('.')) if components['subdomain'] else 0
217
+ path_depth = components['path'].count('/') if components['path'] else 0
218
+ param_count = len(components['query'].split('&')) if components['query'] else 0
219
+
220
+ # Security features
221
+ has_ip = 1 if re.match(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$', components['domain']) else 0
222
+ has_executable = 1 if re.search(r'\.(exe|js|jar|bat|sh|py|dll)$', components['path'], re.I) else 0
223
+ has_double_extension = 1 if re.search(r'\.\w+\.\w+$', components['path'], re.I) else 0
224
+ hex_encoded = 1 if re.search(r'%[0-9a-f]{2}', normalized, re.I) else 0
225
+
226
+ # Lexical features
227
+ digit_count = sum(c.isdigit() for c in normalized)
228
+ special_chars = sum(not (c.isalnum() or c in ' ./-') for c in normalized)
229
+
230
+ digit_ratio = digit_count / url_length if url_length > 0 else 0
231
+ special_char_ratio = special_chars / url_length if url_length > 0 else 0
232
+ entropy = calculate_entropy(normalized)
233
+
234
+ # Domain reputation
235
+ is_safe_domain = 1 if fuzzy_domain_match(full_domain) else 0
236
+ is_uncommon_tld = 1 if components['suffix'] and components['suffix'] not in COMMON_TLDS else 0
237
+
238
+ if url.startswith('https://') and full_domain in SAFE_DOMAINS:
239
+ is_safe_domain = 1
240
+
241
+ return {
242
+ 'url_length': url_length,
243
+ 'domain_length': domain_length,
244
+ 'subdomain_count': subdomain_count,
245
+ 'path_depth': path_depth,
246
+ 'param_count': param_count,
247
+ 'has_ip': has_ip,
248
+ 'has_executable': has_executable,
249
+ 'has_double_extension': has_double_extension,
250
+ 'hex_encoded': hex_encoded,
251
+ 'digit_ratio': digit_ratio,
252
+ 'special_char_ratio': special_char_ratio,
253
+ 'entropy': entropy,
254
+ 'is_safe_domain': is_safe_domain,
255
+ 'is_uncommon_tld': is_uncommon_tld
256
+ }
257
+ except Exception as e:
258
+ st.error(f"Feature extraction error: {str(e)}")
259
+ return {col: 0 for col in FIXED_FEATURE_COLS}
260
+
261
+ def preprocess_url(url, char_to_idx):
262
+ """URL preprocessing for CPU"""
263
+ try:
264
+ clean_url = re.sub(r'[^\x00-\x7F]+', '', str(url))
265
+ normalized = normalize_url(clean_url)
266
+ features = extract_robust_features(clean_url)
267
+ feature_vector = np.array([features.get(col, 0) for col in FIXED_FEATURE_COLS]).reshape(1, -1)
268
+
269
+ char_seq = [char_to_idx.get(c, 0) for c in normalized]
270
+ char_seq = pad_sequences([char_seq], maxlen=MAX_LEN, padding='post', truncating='post')
271
+
272
+ return char_seq, feature_vector, features
273
+ except Exception as e:
274
+ st.error(f"Preprocessing error: {str(e)}")
275
+ return np.zeros((1, MAX_LEN)), np.zeros((1, len(FIXED_FEATURE_COLS))), {}
276
+
277
+ def weighted_ensemble_predict(models, char_seq, feature_vector, features):
278
+ """Ensemble prediction for CPU"""
279
+ predictions = []
280
+ weights = {
281
+ 'hybrid': 0.25,
282
+ 'hybrid_fold1': 0.20,
283
+ 'hybrid_fold2': 0.20,
284
+ 'xgboost': 0.35
285
+ }
286
+
287
+ if features.get('is_safe_domain', 0) == 1:
288
+ return 0.01, [('safe_domain_override', 0.01)]
289
+
290
+ for model_name, model in models.items():
291
+ if model_name in weights:
292
+ try:
293
+ if 'hybrid' in model_name:
294
+ proba = model.predict([char_seq, feature_vector], verbose=0)[0][0]
295
+ else:
296
+ adjusted_features = feature_vector[:, :14] if feature_vector.shape[1] > 14 else feature_vector
297
+ proba = model.predict_proba(adjusted_features)[0][1]
298
+ predictions.append((model_name, proba))
299
+ except Exception as e:
300
+ st.error(f"Prediction error in {model_name}: {str(e)}")
301
+
302
+ if predictions:
303
+ weighted_sum = sum(p * weights.get(name, 0) for name, p in predictions)
304
+ total_weight = sum(weights.get(name, 0) for name, _ in predictions)
305
+ avg_proba = weighted_sum / total_weight if total_weight > 0 else sum(p for _, p in predictions) / len(predictions)
306
+ else:
307
+ avg_proba = 0.5
308
+
309
+ return avg_proba, predictions
310
+
311
+ def analyze_single_url(url, char_to_idx, models):
312
+ """Analyze a single URL"""
313
+ with st.spinner(f"Analyzing URL: {url[:50]}..."):
314
+ start_time = time.time()
315
+
316
+ char_seq, feature_vector, features = preprocess_url(url, char_to_idx)
317
+ ensemble_proba, model_predictions = weighted_ensemble_predict(
318
+ models, char_seq, feature_vector, features)
319
+
320
+ processing_time = time.time() - start_time
321
+
322
+ st.subheader("Analysis Results")
323
+ col1, col2 = st.columns([1, 2])
324
+
325
+ with col1:
326
+ if ensemble_proba >= 0.5:
327
+ st.error(f"🔴 **Threat Detected!** (Probability: {ensemble_proba:.4f})")
328
+ else:
329
+ st.success(f"🟢 **Safe URL** (Probability: {ensemble_proba:.4f})")
330
+
331
+ st.metric("Processing Time", f"{processing_time*1000:.2f} ms")
332
+
333
+ st.subheader("Key Features")
334
+ st.json({
335
+ "URL Length": features.get('url_length', 0),
336
+ "Domain Length": features.get('domain_length', 0),
337
+ "Subdomains": features.get('subdomain_count', 0),
338
+ "Path Depth": features.get('path_depth', 0),
339
+ "Parameters": features.get('param_count', 0),
340
+ "Contains IP": bool(features.get('has_ip', 0)),
341
+ "Contains Executable": bool(features.get('has_executable', 0)),
342
+ "Double Extension": bool(features.get('has_double_extension', 0)),
343
+ "Hex Encoded": bool(features.get('hex_encoded', 0)),
344
+ "Safe Domain": bool(features.get('is_safe_domain', 0)),
345
+ "Uncommon TLD": bool(features.get('is_uncommon_tld', 0)),
346
+ "Entropy": features.get('entropy', 0)
347
+ })
348
+
349
+ with col2:
350
+ st.subheader("Model Predictions")
351
+ model_df = pd.DataFrame(model_predictions, columns=['Model', 'Probability'])
352
+ model_df['Prediction'] = model_df['Probability'].apply(
353
+ lambda x: "MALICIOUS" if x >= 0.5 else "SAFE")
354
+
355
+ st.bar_chart(model_df.set_index('Model')['Probability'])
356
+
357
+ st.write("Detailed Model Results:")
358
+ for model_name, proba in model_predictions:
359
+ pred = "MALICIOUS" if proba >= 0.5 else "SAFE"
360
+ st.write(f"- **{model_name}**: {proba:.4f} ({pred})")
361
+
362
+ def analyze_batch_urls(urls, char_to_idx, models):
363
+ """Analyze multiple URLs"""
364
+ results = []
365
+ progress_bar = st.progress(0)
366
+ status_text = st.empty()
367
+
368
+ for i, url in enumerate(urls):
369
+ status_text.text(f"Processing {i+1}/{len(urls)}: {url[:50]}...")
370
+ progress_bar.progress((i + 1) / len(urls))
371
+
372
+ try:
373
+ char_seq, feature_vector, features = preprocess_url(url, char_to_idx)
374
+ ensemble_proba, _ = weighted_ensemble_predict(models, char_seq, feature_vector, features)
375
+ results.append({
376
+ 'URL': url,
377
+ 'Threat Probability': ensemble_proba,
378
+ 'Classification': "MALICIOUS" if ensemble_proba >= 0.5 else "SAFE"
379
+ })
380
+ except Exception as e:
381
+ st.error(f"Error processing {url}: {str(e)}")
382
+
383
+ if results:
384
+ results_df = pd.DataFrame(results)
385
+ st.dataframe(results_df)
386
+
387
+ csv = results_df.to_csv(index=False).encode('utf-8')
388
+ st.download_button(
389
+ "Download Results",
390
+ csv,
391
+ "url_analysis_results.csv",
392
+ "text/csv",
393
+ key='download-csv'
394
+ )
395
+
396
+ def main():
397
+ st.title("🛡️ URL Threat Detector (CPU Version)")
398
+ st.markdown("""
399
+ This tool analyzes URLs using machine learning models to detect potential threats.
400
+ Optimized for CPU-only environments.
401
+ """)
402
+
403
+ with st.sidebar:
404
+ st.header("About")
405
+ st.markdown("""
406
+ - **Models**: Hybrid CNN+MLP, XGBoost
407
+ - **Features**: URL structure, lexical patterns, domain reputation
408
+ - **Environment**: CPU-only
409
+ """)
410
+
411
+ st.header("Example URLs")
412
+ st.code("https://paypal-security-alert.com/login")
413
+ st.code("https://github.com/features/actions")
414
+
415
+ # Load resources
416
+ with st.spinner("Loading models..."):
417
+ char_to_idx = load_char_mapping()
418
+ models = load_all_models()
419
+
420
+ if not char_to_idx or not models:
421
+ st.error("Failed to load required resources. Please check the model files.")
422
+ return
423
+
424
+ # URL input
425
+ st.subheader("Single URL Analysis")
426
+ url_input = st.text_input("Enter URL to analyze:",
427
+ placeholder="https://example.com")
428
+
429
+ if st.button("Analyze URL") and url_input:
430
+ analyze_single_url(url_input, char_to_idx, models)
431
+
432
+ # Batch analysis
433
+ st.subheader("Batch Analysis")
434
+ uploaded_file = st.file_uploader("Upload a text file with URLs (one per line)",
435
+ type=['txt', 'csv'])
436
+
437
+ if uploaded_file is not None:
438
+ urls = [line.decode('utf-8').strip() for line in uploaded_file if line.strip()]
439
+ if urls and st.button("Analyze All URLs"):
440
+ analyze_batch_urls(urls, char_to_idx, models)
441
+
442
+ if __name__ == "__main__":
443
+ # Configure TensorFlow logging
444
+ os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'
445
+ tf.get_logger().setLevel('ERROR')
446
+
447
+ # Run the app
448
+ main()