Alvlt commited on
Commit
ac704e3
·
1 Parent(s): 9b7adad

Deploy CVE prediction API

Browse files
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.11-slim
2
+
3
+ # Set up a new user named "user" with user ID 1000
4
+ RUN useradd -m -u 1000 user
5
+
6
+ # Switch to the "user" user
7
+ USER user
8
+
9
+ # Set home to the user's home directory
10
+ ENV HOME=/home/user \
11
+ PATH=/home/user/.local/bin:$PATH
12
+
13
+ # Set the working directory to the user's home directory
14
+ WORKDIR $HOME/app
15
+
16
+ # Try and run pip command after setting the user with `USER user` to avoid permission issues with Python
17
+ RUN pip install --no-cache-dir --upgrade pip
18
+
19
+ # Copy requirements first
20
+ COPY --chown=user requirements.txt requirements.txt
21
+ RUN pip install --no-cache-dir --upgrade -r requirements.txt
22
+
23
+ # Copy the current directory contents into the container at $HOME/app setting the owner to the user
24
+ COPY --chown=user . $HOME/app
25
+
26
+ EXPOSE 7860
27
+
28
+ CMD ["uvicorn", "src.api.main:app", "--host", "0.0.0.0", "--port", "7860"]
models/model_final_xgboost_v2.pkl ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e8c99976f10fa9b6fd4cca035d4fd2c3c7a7130c2a43a49a5860603c19ac824
3
+ size 11407321
requirements.txt ADDED
@@ -0,0 +1,9 @@
 
 
 
 
 
 
 
 
 
 
1
+ fastapi==0.138.2
2
+ uvicorn[standard]==0.49.0
3
+ xgboost==3.2.0
4
+ pandas==2.3.3
5
+ scikit-learn==1.9.0
6
+ joblib
7
+ requests
8
+ python-dotenv
9
+ pydantic
src/api/main.py ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ sys.path.append(os.path.join(os.path.dirname(__file__), "..", ".."))
4
+
5
+ import joblib
6
+ import pandas as pd
7
+ import requests
8
+ from dotenv import load_dotenv
9
+ from fastapi import FastAPI, HTTPException
10
+
11
+ from src.data.features import extract_features
12
+ from src.api.schemas import CVERequest, PredictionResponse
13
+
14
+ load_dotenv()
15
+ api_key = os.getenv("NVD_API_KEY")
16
+
17
+ # We load our model
18
+ MODEL_PATH = os.path.join(os.path.dirname(__file__), "..", "..", "models", "model_final_xgboost_v2.pkl")
19
+ model = joblib.load(MODEL_PATH)
20
+
21
+ # We define our columns and the Threshold
22
+ CATEGORICAL_COLUMNS = [
23
+ "attack_vector", "attack_complexity", "privileges_required",
24
+ "user_interaction", "confidentiality_impact", "integrity_impact",
25
+ "availability_impact", "cwe", "scope", "component_type", "vendor"
26
+ ]
27
+ THRESHOLD = 0.9898
28
+
29
+ app = FastAPI(title="Prediction of CVE exploitation")
30
+
31
+ # the function we developed in 04_API_pipeline.ipynb
32
+ # Used to retrieve a CVE
33
+ def fetch_single_cve(cve_id: str, timeout: int = 30) -> dict:
34
+ url = "https://services.nvd.nist.gov/rest/json/cves/2.0"
35
+ params = {"cveId": cve_id}
36
+ headers = {"apiKey": api_key}
37
+
38
+ r = requests.get(url, params=params, headers=headers, timeout=timeout)
39
+ r.raise_for_status()
40
+ data = r.json()
41
+
42
+ vulnerabilities = data.get("vulnerabilities", [])
43
+ if not vulnerabilities:
44
+ raise ValueError(f"CVE {cve_id} introuvable dans NVD")
45
+
46
+ return vulnerabilities[0]
47
+
48
+
49
+ @app.get("/")
50
+ def root():
51
+ return {"message": "Go to /docs for usage"}
52
+
53
+ # the other function developed in 04_API_pipeline.ipynb
54
+ # Use to make prediction
55
+ @app.post("/predict", response_model=PredictionResponse)
56
+ def predict(request: CVERequest):
57
+ try:
58
+ cve_raw = fetch_single_cve(request.cve_id)
59
+ except ValueError as e:
60
+ raise HTTPException(status_code=404, detail=str(e))
61
+ except requests.RequestException as e:
62
+ raise HTTPException(status_code=502, detail=f"Erreur NVD: {e}")
63
+
64
+ features = extract_features(cve_raw)
65
+ features["description_length_reconstructed"] = features["description_length"]
66
+
67
+ feature_columns = model.feature_names_in_
68
+ X = pd.DataFrame([features])[list(feature_columns)]
69
+
70
+ for col in CATEGORICAL_COLUMNS:
71
+ if col in X.columns:
72
+ X[col] = X[col].astype("category")
73
+
74
+ proba = model.predict_proba(X)[:, 1][0]
75
+
76
+ return PredictionResponse(
77
+ cve_id=request.cve_id,
78
+ probability=float(proba),
79
+ prediction=int(proba >= THRESHOLD)
80
+ )
src/api/schemas.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+
3
+ # Just the ID is sufficient, as the API itself handles fetching the data from the NVD
4
+ class CVERequest(BaseModel):
5
+ cve_id: str
6
+
7
+ class PredictionResponse(BaseModel):
8
+ cve_id: str
9
+ probability: float
10
+ prediction: int
src/data/features.py ADDED
@@ -0,0 +1,118 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ features.py
3
+ Extracting and Cleaning Data from the NVD API
4
+ """
5
+
6
+ import logging
7
+ from datetime import datetime
8
+
9
+ # We are in a .py so we dont't use 'print' but 'logger'
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def extract_features(cve: dict) -> dict:
14
+ """
15
+ Function to extract and clean data from the NVD API
16
+
17
+ Args:
18
+ cve: The NVD API returns a dictionary containing the raw CVE.
19
+
20
+ Returns:
21
+ Non-nested dictionary ready for model training
22
+ returns 'None' if the CVE dictionary is invalid.
23
+ """
24
+ cve_id = cve["cve"]["id"]
25
+ published = cve["cve"]["published"]
26
+ # '.get' to provide a fallback in case the data is missing
27
+ source_identifier = cve["cve"].get("sourceIdentifier")
28
+ vulnerability_status = cve["cve"].get("vulnStatus")
29
+ description = cve["cve"].get("descriptions", [])
30
+ # Our model cannot read a description
31
+ # But a detailed—and therefore lengthy—description indicates a well-documented vulnerability, which makes it more vulnerable
32
+ description_words = " ".join([d.get("value", "") for d in description if d.get("lang") == "en"])
33
+ description_length = len(description_words)
34
+ reference = cve["cve"].get("references", [])
35
+
36
+ # metrics:
37
+ # security, if the metrics are missing:
38
+ metrics = cve["cve"].get("metrics", {})
39
+ weaknesses = cve["cve"].get("weaknesses", [])
40
+ cvss_v31 = metrics.get("cvssMetricV31", []) # "cvssMetricV31" is a list
41
+ cvss_data = cvss_v31[0]["cvssData"] if cvss_v31 else {}
42
+ configurations_data = cve["cve"].get("configurations", [])
43
+
44
+
45
+ base_score = cvss_data.get("baseScore")
46
+ attack_vector = cvss_data.get("attackVector")
47
+ attack_complexity = cvss_data.get("attackComplexity")
48
+ privileges_required = cvss_data.get("privilegesRequired")
49
+ user_interaction = cvss_data.get("userInteraction")
50
+ confidentiality_impact = cvss_data.get("confidentialityImpact")
51
+ integrity_impact = cvss_data.get("integrityImpact")
52
+ availability_impact = cvss_data.get("availabilityImpact")
53
+ # We are adding back these two metrics, which had been excluded due to probable collinearity
54
+ exploitability_score = cvss_v31[0].get("exploitabilityScore") if cvss_v31 else None
55
+ impact_score = cvss_v31[0].get("impactScore") if cvss_v31 else None
56
+
57
+ # addition of content type to weakness
58
+ # empty dictionary if there is no dictionary
59
+ weakness_data = weaknesses[0] if weaknesses else {}
60
+ description_list = weakness_data.get("description", [])
61
+ # Empty dictionary if no elements
62
+ description_data = description_list[0] if description_list else {}
63
+ cwe = description_data.get("value")
64
+
65
+ # Adding scope
66
+ scope = cvss_data.get("scope")
67
+
68
+ # Adding configuration
69
+ # Directory tree up to "criteria"
70
+ config_0 = configurations_data[0] if configurations_data else {}
71
+ nodes = config_0.get("nodes", [])
72
+ node_0 = nodes[0] if nodes else {}
73
+ cpe_matches = node_0.get("cpeMatch", [])
74
+ cpe_0 = cpe_matches[0] if cpe_matches else {}
75
+ criteria = cpe_0.get("criteria")
76
+ criteria_parts = criteria.split(":") if criteria else []
77
+ # If there is an application, hardware, OS
78
+ component_type = criteria_parts[2] if len(criteria_parts) > 2 else None
79
+ # The vendor
80
+ vendor = criteria_parts[3] if len(criteria_parts) > 3 else None
81
+
82
+ # Date
83
+ publication_date = datetime.strptime(published, "%Y-%m-%dT%H:%M:%S.%f") # %f for milliseconds
84
+ age_in_days = (datetime.now() - publication_date).days
85
+
86
+ reference_count = len(reference)
87
+ # We're looking for websites that contain code capable of exploiting vulnerabilities, such as exploit-db.com or GitHub
88
+ # If “exploits” are published on these sites, the likelihood of being hacked is much higher
89
+ has_exploit_reference = any (
90
+ "exploit" in ref.get("url", "") or "github" in ref.get("url", "")
91
+ for ref in reference
92
+ )
93
+
94
+ return {
95
+ "cve_id": cve_id,
96
+ "published": published,
97
+ "source_identifier": source_identifier,
98
+ "vulnerability_status": vulnerability_status,
99
+ "description_length" : description_length,
100
+ "description_words" : description_words,
101
+ "base_score": base_score,
102
+ "attack_vector": attack_vector,
103
+ "attack_complexity": attack_complexity,
104
+ "privileges_required": privileges_required,
105
+ "user_interaction": user_interaction,
106
+ "confidentiality_impact": confidentiality_impact,
107
+ "integrity_impact": integrity_impact,
108
+ "availability_impact": availability_impact,
109
+ "age_in_days": age_in_days,
110
+ "reference_count": reference_count,
111
+ "has_exploit_reference": has_exploit_reference,
112
+ "cwe": cwe,
113
+ "scope": scope,
114
+ "component_type": component_type,
115
+ "vendor": vendor,
116
+ "exploitability_score": exploitability_score,
117
+ "impact_score": impact_score
118
+ }