Initial Docker deployment of LSB Steganography Flask app
Browse files- Dockerfile +11 -0
- app.py +166 -0
- final_bin_best.pth +3 -0
- final_bin_le.pkl +3 -0
- final_multi_best.pth +3 -0
- final_multi_le.pkl +3 -0
- final_scaler.pkl +3 -0
- requirements.txt +6 -0
- templates/index.html +223 -0
Dockerfile
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM python:3.10
|
| 2 |
+
|
| 3 |
+
WORKDIR /app
|
| 4 |
+
|
| 5 |
+
COPY . /app
|
| 6 |
+
|
| 7 |
+
RUN pip install --no-cache-dir -r requirements.txt
|
| 8 |
+
|
| 9 |
+
EXPOSE 7860
|
| 10 |
+
|
| 11 |
+
CMD ["python", "app.py"]
|
app.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from flask import Flask, render_template, request, jsonify
|
| 2 |
+
import torch
|
| 3 |
+
import torch.nn as nn
|
| 4 |
+
from PIL import Image
|
| 5 |
+
import numpy as np
|
| 6 |
+
import io
|
| 7 |
+
import pickle
|
| 8 |
+
from scipy.stats import entropy
|
| 9 |
+
|
| 10 |
+
app = Flask(__name__)
|
| 11 |
+
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024
|
| 12 |
+
|
| 13 |
+
# --------------------
|
| 14 |
+
# Model definitions
|
| 15 |
+
# --------------------
|
| 16 |
+
class BinaryDNN(nn.Module):
|
| 17 |
+
def __init__(self):
|
| 18 |
+
super().__init__()
|
| 19 |
+
self.net = nn.Sequential(
|
| 20 |
+
nn.Linear(196614, 384), nn.BatchNorm1d(384), nn.ReLU(), nn.Dropout(0.45),
|
| 21 |
+
nn.Linear(384, 192), nn.BatchNorm1d(192), nn.ReLU(), nn.Dropout(0.35),
|
| 22 |
+
nn.Linear(192, 96), nn.BatchNorm1d(96), nn.ReLU(), nn.Dropout(0.25),
|
| 23 |
+
nn.Linear(96, 2))
|
| 24 |
+
def forward(self, x): return self.net(x)
|
| 25 |
+
|
| 26 |
+
class MultiDNN(nn.Module):
|
| 27 |
+
def __init__(self):
|
| 28 |
+
super().__init__()
|
| 29 |
+
self.net = nn.Sequential(
|
| 30 |
+
nn.Linear(196614, 384), nn.BatchNorm1d(384), nn.ReLU(), nn.Dropout(0.45),
|
| 31 |
+
nn.Linear(384, 192), nn.BatchNorm1d(192), nn.ReLU(), nn.Dropout(0.35),
|
| 32 |
+
nn.Linear(192, 96), nn.BatchNorm1d(96), nn.ReLU(), nn.Dropout(0.25),
|
| 33 |
+
nn.Linear(96, 4))
|
| 34 |
+
def forward(self, x): return self.net(x)
|
| 35 |
+
|
| 36 |
+
# --------------------
|
| 37 |
+
# Load artifacts ONCE
|
| 38 |
+
# --------------------
|
| 39 |
+
with open('final_scaler.pkl', 'rb') as f:
|
| 40 |
+
scaler = pickle.load(f)
|
| 41 |
+
with open('final_bin_le.pkl', 'rb') as f:
|
| 42 |
+
binary_le = pickle.load(f)
|
| 43 |
+
with open('final_multi_le.pkl', 'rb') as f:
|
| 44 |
+
multi_le = pickle.load(f)
|
| 45 |
+
|
| 46 |
+
device = torch.device('cpu')
|
| 47 |
+
binary_model = BinaryDNN()
|
| 48 |
+
binary_model.load_state_dict(torch.load('final_bin_best.pth', map_location=device))
|
| 49 |
+
binary_model.eval()
|
| 50 |
+
multi_model = MultiDNN()
|
| 51 |
+
multi_model.load_state_dict(torch.load('final_multi_best.pth', map_location=device))
|
| 52 |
+
multi_model.eval()
|
| 53 |
+
|
| 54 |
+
# --------------------
|
| 55 |
+
# V3 Feature extraction
|
| 56 |
+
# --------------------
|
| 57 |
+
def extract_lsb_feature(img, k=2):
|
| 58 |
+
mask = (1 << k) - 1
|
| 59 |
+
return (img & mask).flatten()
|
| 60 |
+
|
| 61 |
+
def extract_randomness_features(lsb_feat):
|
| 62 |
+
chunk_size = 512
|
| 63 |
+
lsb_reshaped = lsb_feat.reshape(-1, chunk_size)
|
| 64 |
+
ent_values = []
|
| 65 |
+
for block in lsb_reshaped[::10]:
|
| 66 |
+
hist = np.bincount(block.astype(np.int32), minlength=4)
|
| 67 |
+
hist = hist / max(hist.sum(), 1)
|
| 68 |
+
ent = entropy(hist + 1e-10)
|
| 69 |
+
ent_values.append(ent)
|
| 70 |
+
mean_entropy = np.mean(ent_values)
|
| 71 |
+
sample = lsb_feat[:5000].astype(np.int32)
|
| 72 |
+
transitions = np.sum(np.abs(np.diff(sample)))
|
| 73 |
+
transition_rate = transitions / max(len(sample) - 1, 1)
|
| 74 |
+
hist_full = np.bincount(lsb_feat.astype(np.int32), minlength=4)
|
| 75 |
+
expected = len(lsb_feat) / 4
|
| 76 |
+
chi2_stat = np.sum((hist_full - expected) ** 2 / expected) if expected > 0 else 0
|
| 77 |
+
return np.array([mean_entropy, transition_rate, chi2_stat], dtype=np.float32)
|
| 78 |
+
|
| 79 |
+
def extract_url_pattern_features(lsb_feat):
|
| 80 |
+
chunk_size = 4
|
| 81 |
+
num_chunks = len(lsb_feat) // chunk_size
|
| 82 |
+
lsb_trimmed = lsb_feat[:num_chunks * chunk_size].astype(np.int32)
|
| 83 |
+
byte_values = []
|
| 84 |
+
for i in range(0, len(lsb_trimmed), chunk_size):
|
| 85 |
+
val = (lsb_trimmed[i] << 6) | (lsb_trimmed[i+1] << 4) | (lsb_trimmed[i+2] << 2) | lsb_trimmed[i+3]
|
| 86 |
+
byte_values.append(val)
|
| 87 |
+
byte_values = np.array(byte_values)
|
| 88 |
+
url_char_count = np.sum((byte_values >= 10) & (byte_values <= 15))
|
| 89 |
+
html_char_count = np.sum((byte_values >= 0) & (byte_values <= 5))
|
| 90 |
+
unique_ratio = len(np.unique(byte_values)) / max(len(byte_values), 1)
|
| 91 |
+
return np.array([
|
| 92 |
+
url_char_count / max(len(byte_values), 1),
|
| 93 |
+
html_char_count / max(len(byte_values), 1),
|
| 94 |
+
unique_ratio
|
| 95 |
+
], dtype=np.float32)
|
| 96 |
+
|
| 97 |
+
def extract_features(image, k=2):
|
| 98 |
+
image = image.resize((256, 256), Image.LANCZOS)
|
| 99 |
+
if image.mode != 'RGB':
|
| 100 |
+
image = image.convert('RGB')
|
| 101 |
+
img_array = np.array(image)
|
| 102 |
+
lsb_feat = extract_lsb_feature(img_array, k=k)
|
| 103 |
+
rand_feat = extract_randomness_features(lsb_feat)
|
| 104 |
+
url_feat = extract_url_pattern_features(lsb_feat)
|
| 105 |
+
combined = np.concatenate([lsb_feat.astype(np.float32), rand_feat, url_feat])
|
| 106 |
+
return combined
|
| 107 |
+
|
| 108 |
+
# --------------------
|
| 109 |
+
# Flask endpoints
|
| 110 |
+
# --------------------
|
| 111 |
+
@app.route('/')
|
| 112 |
+
def index():
|
| 113 |
+
return render_template('index.html')
|
| 114 |
+
|
| 115 |
+
@app.route('/predict', methods=['POST'])
|
| 116 |
+
def predict():
|
| 117 |
+
try:
|
| 118 |
+
file = request.files['file']
|
| 119 |
+
image = Image.open(io.BytesIO(file.read()))
|
| 120 |
+
features = extract_features(image)
|
| 121 |
+
features_normalized = scaler.transform(features.reshape(1, -1)).astype(np.float32)
|
| 122 |
+
features_tensor = torch.FloatTensor(features_normalized)
|
| 123 |
+
|
| 124 |
+
with torch.no_grad():
|
| 125 |
+
binary_output = binary_model(features_tensor)
|
| 126 |
+
binary_probs = torch.softmax(binary_output, dim=1).cpu().numpy()[0]
|
| 127 |
+
binary_pred_idx = int(np.argmax(binary_probs))
|
| 128 |
+
binary_class = binary_le.classes_[binary_pred_idx]
|
| 129 |
+
|
| 130 |
+
if binary_class == 'stego':
|
| 131 |
+
with torch.no_grad():
|
| 132 |
+
multi_output = multi_model(features_tensor)
|
| 133 |
+
multi_probs = torch.softmax(multi_output, dim=1).cpu().numpy()[0]
|
| 134 |
+
multi_pred_idx = int(np.argmax(multi_probs))
|
| 135 |
+
payload_type = multi_le.classes_[multi_pred_idx]
|
| 136 |
+
else:
|
| 137 |
+
payload_type = 'clean'
|
| 138 |
+
multi_probs = np.zeros(len(multi_le.classes_))
|
| 139 |
+
multi_probs[multi_le.transform(['clean'])[0]] = 1.0
|
| 140 |
+
|
| 141 |
+
result = {
|
| 142 |
+
'binary_prediction': {
|
| 143 |
+
'class': binary_class,
|
| 144 |
+
'confidence': float(binary_probs[binary_pred_idx]),
|
| 145 |
+
'probabilities': {c: float(binary_probs[binary_le.transform([c])[0]]) for c in binary_le.classes_}
|
| 146 |
+
},
|
| 147 |
+
'multiclass_prediction': {
|
| 148 |
+
'payload_type': payload_type,
|
| 149 |
+
'probabilities': {c: float(multi_probs[i]) for i, c in enumerate(multi_le.classes_)}
|
| 150 |
+
}
|
| 151 |
+
}
|
| 152 |
+
return jsonify(result)
|
| 153 |
+
except Exception as e:
|
| 154 |
+
import traceback
|
| 155 |
+
print('Error:', str(e))
|
| 156 |
+
traceback.print_exc()
|
| 157 |
+
return jsonify({'error': str(e)}), 500
|
| 158 |
+
|
| 159 |
+
@app.errorhandler(413)
|
| 160 |
+
def request_entity_too_large(error):
|
| 161 |
+
return jsonify({'error': 'File size exceeds 5MB'}), 413
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
if __name__ == "__main__":
|
| 165 |
+
port = int(os.environ.get("PORT", 7860))
|
| 166 |
+
app.run(debug=True, host="0.0.0.0", port=port)
|
final_bin_best.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:937140babfa952b004688c3bb98665434afd2b901658e5e8ff4de0438c107d90
|
| 3 |
+
size 302389710
|
final_bin_le.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:90b3bd41467e805a270b7d2b1ca04eb1ea9d31670d16bf5c4022f81f5c3c6d9f
|
| 3 |
+
size 276
|
final_multi_best.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2d10330083813ade2f03ff8365b24abdc732f5728f1470372390863f748073ee
|
| 3 |
+
size 302390532
|
final_multi_le.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b16da8d351473e04c4cf7ebcac502f4f7410e7832ec44db0f2e2f1c8d6ddf93b
|
| 3 |
+
size 316
|
final_scaler.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:0a115a9cf586486270825e41369e3d3e6f2367f6f49c16f1d583d6ac626015a2
|
| 3 |
+
size 4719234
|
requirements.txt
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
flask
|
| 2 |
+
torch
|
| 3 |
+
numpy
|
| 4 |
+
scipy
|
| 5 |
+
pillow
|
| 6 |
+
scikit-learn
|
templates/index.html
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<title>Multi-Payload LSB Steganalysis Demo</title>
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
| 7 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css?family=Inter:600,700&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
body {
|
| 11 |
+
min-height: 100vh;
|
| 12 |
+
background: linear-gradient(120deg,#090e1a 0%,#1e293b 75%,#64748b 100%);
|
| 13 |
+
font-family: 'Inter', Arial, sans-serif;
|
| 14 |
+
margin:0;padding:0;
|
| 15 |
+
overflow-x: hidden;
|
| 16 |
+
}
|
| 17 |
+
.main-card {
|
| 18 |
+
background: rgba(255,255,255,0.92);
|
| 19 |
+
border-radius: 2rem;
|
| 20 |
+
box-shadow: 0 12px 40px rgba(20,32,64,0.21);
|
| 21 |
+
margin: 2.2rem auto;
|
| 22 |
+
max-width: 790px;
|
| 23 |
+
width: 98vw;
|
| 24 |
+
padding: 2.7rem 2.2rem 2.7rem 2.2rem;
|
| 25 |
+
transition: box-shadow 0.18s;
|
| 26 |
+
position: relative;
|
| 27 |
+
backdrop-filter: blur(2px);
|
| 28 |
+
}
|
| 29 |
+
.header-title {
|
| 30 |
+
font-size: 2.4rem;
|
| 31 |
+
font-weight: 900;
|
| 32 |
+
color: #2563eb;
|
| 33 |
+
text-align: center;
|
| 34 |
+
letter-spacing: .016em;
|
| 35 |
+
margin-bottom: 0.18em;
|
| 36 |
+
text-shadow: 0 3px 12px #64748b26;
|
| 37 |
+
}
|
| 38 |
+
.header-desc {
|
| 39 |
+
text-align: center;
|
| 40 |
+
color: #334155;
|
| 41 |
+
font-size: 1.22rem;
|
| 42 |
+
font-weight: 600;
|
| 43 |
+
margin-bottom: 1.5em;
|
| 44 |
+
letter-spacing: 0.07em;
|
| 45 |
+
}
|
| 46 |
+
/* Glass info panel */
|
| 47 |
+
.info-glass {
|
| 48 |
+
background: rgba(229,240,255,0.82);
|
| 49 |
+
border-radius: 1.1rem;
|
| 50 |
+
box-shadow: 0 2px 18px #0797c522, 0 0.5px 3.5px #ffd70033;
|
| 51 |
+
padding: 1.3em 1.3em 0.9em 1.3em;
|
| 52 |
+
margin-bottom: 1.45em;
|
| 53 |
+
}
|
| 54 |
+
.info-title { color: #2563eb; font-size: 1.12em; font-weight: 700; margin-bottom: 0.7em;}
|
| 55 |
+
.info-snip { color: #23304f; font-size: 0.97em; margin-bottom: 0.3em;}
|
| 56 |
+
.info-payloads td, .info-payloads th {padding: 0.26em .8em;}
|
| 57 |
+
.info-collapsible { cursor: pointer; font-size: 1.07rem; color: #334155; background:none;border:none;font-weight:700;}
|
| 58 |
+
.info-collapsible:after {content: "▼";font-size:.9em;margin-left:.5em;}
|
| 59 |
+
.info-collapsible.collapsed:after {content:"►";}
|
| 60 |
+
.info-content {display:block;transition: max-height .3s cubic-bezier(.32,1.8,.75,1);}
|
| 61 |
+
.info-content.collapsed {display:none;}
|
| 62 |
+
.dropzone { border: 3px dashed #2563eb; border-radius: 1.2rem; text-align: center; background: #f5faff; padding: 2.3rem 1.1rem; cursor: pointer; transition: border-color 0.22s, background 0.2s, box-shadow .21s;}
|
| 63 |
+
.dropzone:hover { background: #dbeafe; border-color: #3b82f6; box-shadow: 0 3px 26px #2563eb33; }
|
| 64 |
+
.preview-img { max-width: 250px; border-radius: 1rem; margin-top: 1rem; box-shadow: 0 0 16px #3b82f655; background: #fff;}
|
| 65 |
+
.result-badge { font-size: 1.2em; font-weight: 800; padding: 0.45em 1.6em; border-radius: 2em; letter-spacing: 0.03em; display:inline-block; margin-bottom:0.3em; box-shadow: 0 3px 10px #0002;}
|
| 66 |
+
.prob-bar-wrap { display: flex; align-items: center; gap:0.45em; margin-bottom: 0.6em;}
|
| 67 |
+
.prob-bar-label { width:80px; font-weight:700; font-size:1.09em; color:#222b3a; letter-spacing:.01em; text-transform:uppercase; text-align:right;}
|
| 68 |
+
.prob-bar { flex: 1; height: 29px; border-radius: 1em; background: #e0e7ef; overflow: hidden; position:relative; margin-right:6px;}
|
| 69 |
+
.prob-bar-inner { height:100%;display:flex;align-items:center; font-weight:700;color:#fff;font-size:1.17em;padding-left:1em; border-radius:1em;transition:width 0.55s cubic-bezier(.32,1.2,.69,1);}
|
| 70 |
+
.prob-bar-value { min-width:52px;text-align:right;font-weight:600;font-size:1.05em;margin-left:4px; color:#1e293b;}
|
| 71 |
+
.icon-big { font-size: 2.2em; vertical-align: sub; margin-right: 0.23em;}
|
| 72 |
+
.icon-clean { color: #1dd8a2;}
|
| 73 |
+
.icon-stego { color: #ef4444;}
|
| 74 |
+
/* Subtle watermark background icon */
|
| 75 |
+
.bg-watermark {
|
| 76 |
+
position: absolute; bottom: 14px; right: 18px; z-index:0; opacity:.12; font-size:5.5em; pointer-events:none;
|
| 77 |
+
}
|
| 78 |
+
@media(max-width: 900px){ .main-card{ max-width:99vw; padding:1.3rem}}
|
| 79 |
+
@media(max-width: 520px){ .prob-bar-label{width:54px;font-size:0.99em}}
|
| 80 |
+
</style>
|
| 81 |
+
</head>
|
| 82 |
+
<body>
|
| 83 |
+
<div class="container-fluid">
|
| 84 |
+
<div class="main-card">
|
| 85 |
+
<!-- Watermark icon -->
|
| 86 |
+
<div class="bg-watermark"><span class="icon-big icon-stego">🔒</span></div>
|
| 87 |
+
<!-- Header section -->
|
| 88 |
+
<div class="header-title">Multi-Payload LSB Steganalysis Demo</div>
|
| 89 |
+
<div class="header-desc">
|
| 90 |
+
Analyze PNG images for hidden data using advanced<br>Least Significant Bit detection and AI.<br>
|
| 91 |
+
Upload an image and instantly detect steganographic payloads!
|
| 92 |
+
</div>
|
| 93 |
+
<!-- Info section -->
|
| 94 |
+
<div class="info-glass mb-4">
|
| 95 |
+
<button class="info-collapsible" onclick="toggleInfo()">About & Knowledge </button>
|
| 96 |
+
<div id="infoPanel" class="info-content collapsed">
|
| 97 |
+
<div class="info-title">What is Steganography?</div>
|
| 98 |
+
<div class="info-snip">Steganography is the science of hiding information within digital media so it is invisible to regular inspection. Images, audio, and video files are common carriers.</div>
|
| 99 |
+
<div class="info-title mt-2">What are LSBs and Payloads?</div>
|
| 100 |
+
<div class="info-snip">LSB (Least Significant Bit) steganography modifies the lowest-value bits in an image (usually the last 1 or 2 of each pixel) to embed secret data called the payload.</div>
|
| 101 |
+
<div class="info-title mt-2">What Payload Types Are Detected?</div>
|
| 102 |
+
<table class="info-payloads mb-1 small">
|
| 103 |
+
<tr><th>Payload</th><th>Meaning</th></tr>
|
| 104 |
+
<tr><td>HTML</td><td>Hidden HTML code</td></tr>
|
| 105 |
+
<tr><td>JS</td><td>Hidden JavaScript code</td></tr>
|
| 106 |
+
<tr><td>PS</td><td>Hidden Powershell script</td></tr>
|
| 107 |
+
<tr><td>Clean</td><td>No payload / genuine image</td></tr>
|
| 108 |
+
</table>
|
| 109 |
+
<div class="info-title mt-2">How does the model decide?</div>
|
| 110 |
+
<div class="info-snip mb-2">The neural network analyzes subtle changes in the 2 least significant bits of every image pixel, plus additional statistics, to predict if a payload is present and its type. Confidence is shown for each class.</div>
|
| 111 |
+
</div>
|
| 112 |
+
</div>
|
| 113 |
+
<!-- Upload & results UI -->
|
| 114 |
+
<div class="dropzone" id="dropzone">
|
| 115 |
+
<span class="fs-3">🖼️</span>
|
| 116 |
+
<div class="mt-2 mb-1 fw-semibold">Upload PNG image or drag & drop</div>
|
| 117 |
+
<div class="text-muted small mb-0">Max 5MB | Secure PyTorch Model</div>
|
| 118 |
+
<input type="file" id="fileInput" accept="image/png" style="display:none;">
|
| 119 |
+
</div>
|
| 120 |
+
<div id="preview" style="display:none;" class="text-center mt-3">
|
| 121 |
+
<img id="previewImg" class="preview-img mb-2" alt="Preview"/>
|
| 122 |
+
<div id="filename" class="small text-muted mt-1"></div>
|
| 123 |
+
</div>
|
| 124 |
+
<button id="analyzeBtn" class="btn btn-primary w-100 mt-3 fw-bold" disabled>Analyze Image</button>
|
| 125 |
+
<div id="loading" style="display:none;" class="text-center mt-3">
|
| 126 |
+
<div class="spinner-border text-primary" style="width:2.1rem;height:2.1rem;"></div>
|
| 127 |
+
<p class="mt-2">Analyzing image...</p>
|
| 128 |
+
</div>
|
| 129 |
+
<div id="results" style="display:none;" class="mt-4"></div>
|
| 130 |
+
</div>
|
| 131 |
+
</div>
|
| 132 |
+
<script>
|
| 133 |
+
let selectedFile = null;
|
| 134 |
+
const dropzone = document.getElementById('dropzone');
|
| 135 |
+
const fileInput = document.getElementById('fileInput');
|
| 136 |
+
const preview = document.getElementById('preview');
|
| 137 |
+
const previewImg = document.getElementById('previewImg');
|
| 138 |
+
const filenameDiv = document.getElementById('filename');
|
| 139 |
+
const analyzeBtn = document.getElementById('analyzeBtn');
|
| 140 |
+
const loading = document.getElementById('loading');
|
| 141 |
+
const results = document.getElementById('results');
|
| 142 |
+
const infoPanel = document.getElementById('infoPanel');
|
| 143 |
+
|
| 144 |
+
dropzone.onclick = () => fileInput.click();
|
| 145 |
+
dropzone.ondragover = (e) => { e.preventDefault(); dropzone.style.background = "#dbeafe"; };
|
| 146 |
+
dropzone.ondragleave = () => dropzone.style.background = "#f5faff";
|
| 147 |
+
dropzone.ondrop = (e) => {
|
| 148 |
+
e.preventDefault(); dropzone.style.background = "#dbeafe";
|
| 149 |
+
if (e.dataTransfer.files.length) handleFile(e.dataTransfer.files[0]);
|
| 150 |
+
};
|
| 151 |
+
fileInput.onchange = (e) => { if (e.target.files.length) handleFile(e.target.files[0]); };
|
| 152 |
+
function handleFile(file) {
|
| 153 |
+
if (!file.name.toLowerCase().endsWith('.png')) { alert('Please select a PNG image.'); return; }
|
| 154 |
+
selectedFile = file; analyzeBtn.disabled = false;
|
| 155 |
+
preview.style.display = 'block';
|
| 156 |
+
filenameDiv.textContent = `${file.name} (${(file.size/1024).toFixed(1)} KB)`;
|
| 157 |
+
const reader = new FileReader();
|
| 158 |
+
reader.onload = e => { previewImg.src = e.target.result; }; reader.readAsDataURL(file);
|
| 159 |
+
results.style.display = 'none';
|
| 160 |
+
}
|
| 161 |
+
analyzeBtn.onclick = async () => {
|
| 162 |
+
if (!selectedFile) return;
|
| 163 |
+
loading.style.display = 'block'; results.style.display = 'none'; analyzeBtn.disabled = true;
|
| 164 |
+
const formData = new FormData(); formData.append('file', selectedFile);
|
| 165 |
+
try {
|
| 166 |
+
const resp = await fetch('/predict', { method:'POST', body:formData });
|
| 167 |
+
const data = await resp.json();
|
| 168 |
+
if (resp.ok) displayResults(data); else alert(data.error || 'Prediction failed');
|
| 169 |
+
} catch (e) { alert('Network error: ' + e.message); }
|
| 170 |
+
loading.style.display = 'none'; analyzeBtn.disabled = false;
|
| 171 |
+
};
|
| 172 |
+
// Collapsible info
|
| 173 |
+
function toggleInfo(){
|
| 174 |
+
infoPanel.classList.toggle('collapsed');
|
| 175 |
+
document.querySelector('.info-collapsible').classList.toggle('collapsed');
|
| 176 |
+
}
|
| 177 |
+
// Custom icons for clean/stego
|
| 178 |
+
const CLEAN_EMOJI = '<span class="icon-big icon-clean">🔓</span>';
|
| 179 |
+
const STEGO_EMOJI = '<span class="icon-big icon-stego">🔒</span>';
|
| 180 |
+
function displayResults(data) {
|
| 181 |
+
let html = '';
|
| 182 |
+
const b = data.binary_prediction, m = data.multiclass_prediction;
|
| 183 |
+
html += `<div class="mb-2 text-center">
|
| 184 |
+
<span class="result-badge"
|
| 185 |
+
style="background:${b.class==='clean'?'#14b8a6':'#ef4444'};color:#fff">
|
| 186 |
+
${b.class==='clean' ? CLEAN_EMOJI+'CLEAN' : STEGO_EMOJI+'STEGO'}
|
| 187 |
+
</span>
|
| 188 |
+
<div class="small mt-1 mb-2" style="color:#34425a;">Binary prediction (Clean vs. Stego)</div>
|
| 189 |
+
<div class="prob-bar-wrap">
|
| 190 |
+
<span class="prob-bar-label">CLEAN</span>
|
| 191 |
+
<div class="prob-bar"><div class="prob-bar-inner" style="width:${(b.probabilities.clean*100).toFixed(1)}%;background:#10b981;"></div></div>
|
| 192 |
+
<span class="prob-bar-value">${(b.probabilities.clean*100).toFixed(1)}%</span>
|
| 193 |
+
</div>
|
| 194 |
+
<div class="prob-bar-wrap">
|
| 195 |
+
<span class="prob-bar-label">STEGO</span>
|
| 196 |
+
<div class="prob-bar"><div class="prob-bar-inner" style="width:${(b.probabilities.stego*100).toFixed(1)}%;background:#ef4444;"></div></div>
|
| 197 |
+
<span class="prob-bar-value">${(b.probabilities.stego*100).toFixed(1)}%</span>
|
| 198 |
+
</div>
|
| 199 |
+
</div>`;
|
| 200 |
+
if (b.class === 'stego') {
|
| 201 |
+
html += `<div class="mb-2 text-center">
|
| 202 |
+
<span class="result-badge" style="background:#3b82f6;color:#fff">Payload: ${m.payload_type.toUpperCase()}</span>
|
| 203 |
+
<div class="small mb-2" style="color:#2a3140;">Multi-class payload classification</div>`;
|
| 204 |
+
const colors = {html:'#6366f1',js:'#f97316',ps:'#14b8a6',clean:'#06b6d4'};
|
| 205 |
+
Object.entries(m.probabilities)
|
| 206 |
+
.sort((a,b)=>b[1]-a[1])
|
| 207 |
+
.forEach(([k,v]) => {
|
| 208 |
+
let color = colors[k] || '#818cf8';
|
| 209 |
+
html += `<div class="prob-bar-wrap">
|
| 210 |
+
<span class="prob-bar-label">${k.toUpperCase()}</span>
|
| 211 |
+
<div class="prob-bar">
|
| 212 |
+
<div class="prob-bar-inner" style="width:${(v*100).toFixed(1)}%;background:${color};"></div>
|
| 213 |
+
</div>
|
| 214 |
+
<span class="prob-bar-value">${(v*100).toFixed(1)}%</span>
|
| 215 |
+
</div>`;
|
| 216 |
+
});
|
| 217 |
+
html += `</div>`;
|
| 218 |
+
}
|
| 219 |
+
results.innerHTML = html; results.style.display = 'block';
|
| 220 |
+
}
|
| 221 |
+
</script>
|
| 222 |
+
</body>
|
| 223 |
+
</html>
|