File size: 11,284 Bytes
c29c418
 
 
 
 
 
 
3840a2c
08a6d59
48419e8
c29c418
 
 
 
3840a2c
c29c418
 
 
08a6d59
 
 
 
 
3840a2c
 
 
 
 
c29c418
3840a2c
 
c29c418
 
 
3840a2c
 
 
 
08a6d59
 
 
 
 
 
 
 
 
 
 
 
 
3840a2c
 
48419e8
08a6d59
 
 
 
 
3840a2c
 
08a6d59
48419e8
3840a2c
08a6d59
 
 
 
 
 
3840a2c
08a6d59
 
3840a2c
08a6d59
 
 
3840a2c
08a6d59
 
48419e8
08a6d59
48419e8
 
 
 
 
 
 
 
 
 
08a6d59
 
48419e8
3840a2c
 
08a6d59
 
 
 
3840a2c
 
08a6d59
3840a2c
 
08a6d59
 
 
 
 
 
 
 
 
 
 
c29c418
 
 
 
 
 
 
 
 
 
3840a2c
 
 
 
 
c29c418
 
08a6d59
48419e8
08a6d59
 
48419e8
08a6d59
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48419e8
 
 
 
08a6d59
c29c418
3840a2c
 
 
 
 
 
 
 
 
 
 
 
 
48419e8
 
 
 
 
 
 
 
 
 
 
 
c29c418
 
3840a2c
 
48419e8
 
08a6d59
 
48419e8
08a6d59
3840a2c
c29c418
3840a2c
c29c418
3840a2c
c29c418
3840a2c
5023bdf
3840a2c
 
 
 
 
 
 
 
 
 
 
48419e8
3840a2c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c29c418
3840a2c
48419e8
 
3840a2c
48419e8
 
 
 
 
 
 
3840a2c
48419e8
 
3840a2c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c29c418
3840a2c
 
c29c418
 
3840a2c
c29c418
3840a2c
c29c418
 
3840a2c
c29c418
5023bdf
 
3840a2c
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List
import pandas as pd
import numpy as np
import joblib
import logging
import os
import sys
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.ensemble import IsolationForest

app = FastAPI(title="Isolation Forest Anomaly Detection")

# Configure logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# Log environment info for debugging
logger.info(f"Python version: {sys.version}")
logger.info(f"Current working directory: {os.getcwd()}")
logger.info(f"Directory contents: {os.listdir('.')}")

# Model paths - make them more flexible for different environments
MODEL_PATH = os.getenv("MODEL_PATH", "./isoforest_dos.pkl")
SCALER_PATH = os.getenv("SCALER_PATH", "./scaler_dos.pkl")
ENCODER_PATH = os.getenv("ENCODER_PATH", "./encoder_dos.pkl")

THRESHOLD = 0.4153
NUM_FEATS = ["inter_arrival_time", "packet_rate", "packet_length", "length_per_rate", 
             "packet_rate_mean", "packet_rate_var", "packet_rate_skew"]
CAT_FEATS = ["protocol"]
FEATURES = NUM_FEATS + CAT_FEATS

# Global variables to store loaded models
isoforest = None
scaler = None
encoder = None
models_loaded = False

def check_sklearn_versions():
    """Check and log sklearn version compatibility"""
    try:
        import sklearn
        logger.info(f"Scikit-learn version: {sklearn.__version__}")
        import numpy
        logger.info(f"NumPy version: {numpy.__version__}")
        return True
    except Exception as e:
        logger.error(f"Version check failed: {e}")
        return False

def load_models():
    """Load models with better error handling and fallback for encoder"""
    global isoforest, scaler, encoder, models_loaded
    
    # Check versions first
    if not check_sklearn_versions():
        return False
    
    try:
        # Check if files exist and log their details
        for path, name in [(MODEL_PATH, "model"), (SCALER_PATH, "scaler")]:
            if not os.path.exists(path):
                logger.error(f"{name} file not found at {path}")
                logger.info(f"Available files: {[f for f in os.listdir('.') if f.endswith('.pkl')]}")
                return False
            else:
                file_size = os.path.getsize(path)
                logger.info(f"{name} file found at {path} (size: {file_size} bytes)")
        
        # Try to load models with more specific error handling
        logger.info("Loading isolation forest model...")
        isoforest = joblib.load(MODEL_PATH)
        logger.info("✓ Isolation forest loaded")
        
        logger.info("Loading scaler...")
        scaler = joblib.load(SCALER_PATH)
        logger.info("✓ Scaler loaded")
        
        # Try to load encoder, but use fallback if it fails
        logger.info("Loading encoder...")
        try:
            if os.path.exists(ENCODER_PATH):
                encoder = joblib.load(ENCODER_PATH)
                logger.info("✓ Encoder loaded")
            else:
                logger.warning("Encoder file not found, will use fallback encoding")
                encoder = None
        except Exception as e:
            logger.warning(f"Failed to load encoder: {str(e)}. Will use fallback encoding")
            encoder = None
        
        models_loaded = True
        logger.info("Models loaded successfully")
        return True
        
    except ImportError as e:
        logger.error(f"Import error while loading models: {str(e)}")
        logger.error("This might be a version compatibility issue")
        return False
    except Exception as e:
        logger.error(f"Failed to load model or preprocessors: {str(e)}")
        logger.error(f"Error type: {type(e).__name__}")
        return False

# Add startup event
@app.on_event("startup")
async def startup_event():
    """Load models on startup"""
    global models_loaded
    logger.info("Starting model loading...")
    models_loaded = load_models()
    if models_loaded:
        logger.info("✓ Startup complete - models loaded successfully")
    else:
        logger.error("✗ Startup failed - models not loaded")

class NetworkData(BaseModel):
    inter_arrival_time: float
    packet_length: float
    protocol: str

class PredictionResponse(BaseModel):
    anomaly: int
    anomaly_score: float

@app.get("/")
async def root():
    """Root endpoint"""
    return {"message": "Isolation Forest Anomaly Detection API", "status": "running"}

@app.get("/health")
async def health_check():
    """Health check endpoint with more details"""
    if not models_loaded or isoforest is None or scaler is None:
        return {
            "status": "unhealthy", 
            "reason": "Critical models not loaded",
            "models_loaded": models_loaded,
            "isoforest_loaded": isoforest is not None,
            "scaler_loaded": scaler is not None,
            "encoder_loaded": encoder is not None
        }
    return {"status": "healthy", "models_loaded": True}

@app.get("/debug")
async def debug_info():
    """Debug endpoint to check environment"""
    import sklearn
    import numpy
    return {
        "sklearn_version": sklearn.__version__,
        "numpy_version": numpy.__version__,
        "working_directory": os.getcwd(),
        "files": os.listdir('.'),
        "pkl_files": [f for f in os.listdir('.') if f.endswith('.pkl')],
        "models_loaded": models_loaded,
        "isoforest_loaded": isoforest is not None,
        "scaler_loaded": scaler is not None,
        "encoder_loaded": encoder is not None
    }

def safe_clip_and_log(series, lower=None, upper=None):
    """Safely clip and apply log1p transformation"""
    if lower is not None:
        series = series.clip(lower=lower)
    if upper is not None:
        # Calculate quantile safely
        try:
            upper_val = series.quantile(0.98) if upper == "quantile_98" else upper
            series = series.clip(upper=upper_val)
        except:
            pass  # If quantile calculation fails, skip upper clipping
    return np.log1p(series)

def fallback_encode_protocol(protocols):
    """Fallback encoding for protocol column"""
    protocol_map = {
        "tcp": 0, 
        "udp": 1, 
        "icmp": 2, 
        "http": 3,
        "https": 4,
        "unknown": 5
    }
    return [protocol_map.get(p.lower(), 5) for p in protocols]

@app.post("/predict", response_model=List[PredictionResponse])
async def predict(data: List[NetworkData]):
    """Predict anomalies in network data"""
    
    # Check if critical models are loaded
    if not models_loaded or isoforest is None or scaler is None:
        raise HTTPException(
            status_code=503, 
            detail="Critical models not loaded. Service unavailable. Check /health for details."
        )
    
    try:
        # Convert input data to DataFrame
        df = pd.DataFrame([d.dict() for d in data])
        original_len = len(df)
        
        # Ensure minimum 5 rows for rolling calculations
        if len(df) < 5:
            padding_rows = 5 - len(df)
            padding_df = pd.DataFrame(
                [[0.001, 64, "tcp"]] * padding_rows, 
                columns=["inter_arrival_time", "packet_length", "protocol"]
            )
            df = pd.concat([padding_df, df], ignore_index=True)
        
        # Feature engineering with better error handling
        df["inter_arrival_time"] = safe_clip_and_log(df["inter_arrival_time"], lower=0.001)
        
        # Calculate packet_rate
        df["packet_rate"] = 1 / np.exp(df["inter_arrival_time"])
        df["packet_rate"] = safe_clip_and_log(df["packet_rate"])
        
        # Process packet_length
        df["packet_length"] = safe_clip_and_log(
            df["packet_length"].clip(lower=0), 
            upper="quantile_98"
        )
        
        # Calculate length_per_rate
        df["length_per_rate"] = np.exp(df["packet_length"]) / np.exp(df["packet_rate"])
        df["length_per_rate"] = safe_clip_and_log(df["length_per_rate"], upper="quantile_98")
        
        # Rolling statistics with better handling
        packet_rate_exp = np.exp(df["packet_rate"])
        
        # Rolling mean
        rolling_mean = packet_rate_exp.rolling(window=5, min_periods=1).mean()
        df["packet_rate_mean"] = safe_clip_and_log(rolling_mean.fillna(packet_rate_exp.median()))
        
        # Rolling variance
        rolling_std = packet_rate_exp.rolling(window=5, min_periods=1).std()
        df["packet_rate_var"] = safe_clip_and_log(
            rolling_std.fillna(packet_rate_exp.std() if packet_rate_exp.std() > 0 else 0.1)
        )
        
        # Rolling skewness
        rolling_skew = packet_rate_exp.rolling(window=5, min_periods=1).skew()
        df["packet_rate_skew"] = rolling_skew.fillna(0)
        # Handle negative skewness for log transformation
        df["packet_rate_skew"] = np.log1p(df["packet_rate_skew"] - df["packet_rate_skew"].min() + 0.001)
        
        # Process categorical features
        df["protocol"] = df["protocol"].astype(str).fillna("unknown")
        
        # Transform categorical features with fallback
        df_encoded = df.copy()
        try:
            if encoder is not None:
                df_encoded[CAT_FEATS] = encoder.transform(df[CAT_FEATS])
                logger.info("Used trained encoder")
            else:
                # Use fallback encoding
                df_encoded["protocol"] = fallback_encode_protocol(df["protocol"])
                logger.info("Used fallback encoding")
        except Exception as e:
            logger.warning(f"Encoding failed: {str(e)}. Using fallback encoding.")
            df_encoded["protocol"] = fallback_encode_protocol(df["protocol"])
        
        # Select features and scale
        X = df_encoded[FEATURES]
        
        # Handle any remaining NaN values
        X = X.fillna(0)
        
        try:
            X_scaled = scaler.transform(X)
        except Exception as e:
            logger.warning(f"Scaling failed: {str(e)}. Using unscaled features.")
            X_scaled = X.values
        
        # Predict anomalies
        try:
            anomaly_scores = -isoforest.score_samples(X_scaled)
            anomalies = (anomaly_scores > THRESHOLD).astype(int)
        except Exception as e:
            logger.error(f"Prediction failed: {str(e)}")
            # Fallback: return all as normal
            anomaly_scores = np.zeros(len(X_scaled))
            anomalies = np.zeros(len(X_scaled), dtype=int)
        
        # Return only the original data predictions (skip padding)
        start_idx = len(df) - original_len
        response = [
            PredictionResponse(anomaly=int(anomaly), anomaly_score=float(score))
            for anomaly, score in zip(anomalies[start_idx:], anomaly_scores[start_idx:])
        ]
        
        logger.info(f"Processed {original_len} records successfully")
        return response
        
    except Exception as e:
        logger.error(f"Prediction error: {str(e)}")
        raise HTTPException(status_code=500, detail=f"Prediction failed: {str(e)}")

if __name__ == "__main__":
    import uvicorn
    port = int(os.getenv("PORT", 8000))
    uvicorn.run(app, host="0.0.0.0", port=port)