ML-Based TLS Security Risk Scoring
Train a hybrid ML pipeline to classify TLS sessions and detect downgrade attacks.
Introduction
30 Second Summary
Every time you visit a website or send an email, your device negotiates an encrypted connection with a server. Some of those connections use strong, modern cryptography. Others quietly fall back to weak, outdated protocols that attackers can exploit.
In this project, you will build a dual-model machine learning pipeline that scores TLS sessions by risk level and detects suspicious downgrade attack patterns that a single classifier would miss.
What You'll Build
You will feed your pipeline JSON records describing TLS sessions and watch it classify each server's cryptographic strength, rank which attributes drive risk the most, and flag anomalous patterns that warrant investigation.
By the end of this project, you'll have:
- A feature engineering pipeline that transforms raw TLS session data into 9 numerical features, including a composite weakness score that captures how multiple weak signals combine.
- A synthetic data generator that creates 2,000 labeled training samples spanning weak, moderate, and strong TLS configuration profiles.
- A Gradient Boosting classifier that achieves over 90% test accuracy distinguishing risk levels, with feature importance rankings showing which TLS attributes drive risk the most.
- An Isolation Forest anomaly detector that catches subtle TLS downgrade attack patterns the supervised classifier confidently misses.
- Secret Mission: Build a Bayesian fusion scoring engine that combines both models into a ranked risk report with severity tiers, escalating downgrade attacks from medium to critical.
Are there any prerequisites?
This project assumes you have completed Project 1 (PCAP Email TLS Handshake Analyzer) or have equivalent understanding of TLS handshake data. Familiarity with scikit-learn and the basics of supervised and unsupervised ML will help you get the most out of this project.
Before We Start
Before we dive in, let's lock in what you're about to build and why it matters for your hackathon submission.
Set Up Your ML Environment
Project 1's PCAP analyzer extracts raw TLS session data from network traffic. To transform those raw sessions into actionable security intelligence, you need ML models that can score risk and detect anomalies.
This step creates the project foundation. You will set up a Python virtual environment, install the ML libraries, and define the data contract that bridges Project 1's output with this pipeline's input.
In this step, get ready to:
- Create a virtual environment and install scikit-learn and pandas.
- Define the TLS session schema with strength mappings for protocols, ciphers, and signatures.
- Run the schema and verify three sample TLS sessions print correctly.
Create the project directory and virtual environment
Every Python ML project starts with an isolated environment. A virtual environment keeps this project's dependencies separate from anything else on your machine.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and move into it by running these commands:
mkdir tls-risk-scoring
cd tls-risk-scoring
- Create a virtual environment and activate it. The commands differ by operating system.
macOS
- Create the virtual environment by running:
python3 -m venv venv
- Confirm the venv folder was created by running:
ls
You should see a venv folder listed.
- Activate the virtual environment by running:
source venv/bin/activate
Windows
- Create the virtual environment by running:
python -m venv venv
- Confirm the venv folder was created by running:
dir
You should see a venv folder listed.
- Activate the virtual environment by running:
venv\Scripts\activate
Your terminal prompt now shows (venv) at the start. This confirms the virtual environment is active.
Don't see (venv) in your prompt?
Make sure you ran the activate command, not just the python -m venv venv creation command. Creating and activating are two separate steps.
On Windows PowerShell, you may see an execution policy error. Run Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser first, then retry the activate command.
Help me activate my Python virtual environment.
Install the ML dependencies
- Install scikit-learn and pandas by running:
pip install scikit-learn==1.9.0 pandas==3.0.5
What are these libraries?
- scikit-learn provides the ML models you will train: a Gradient Boosting classifier for risk scoring and an Isolation Forest for anomaly detection.
- pandas handles the tabular data. It turns your raw TLS session features into DataFrames that scikit-learn can consume.
- Verify scikit-learn installed correctly by running:
python -c "import sklearn; print(sklearn.__version__)"
You should see 1.9.0 printed.
Seeing a ModuleNotFoundError?
Make sure your virtual environment is active. Your prompt should show (venv) at the start. If it does not, re-run the activate command from the previous substep.
If pip install itself failed, check that you are using the exact version pins: scikit-learn==1.9.0 and pandas==3.0.5.
Help me fix my scikit-learn installation.
Define the TLS session schema
The schema file is the data contract between Project 1's PCAP analyzer and this ML pipeline. It maps raw TLS fields like protocol version and cipher family into ordinal numbers that ML models can work with.
- Create a file called schema.py in your tls-risk-scoring folder.
- Add the TLS version and cipher strength mappings by pasting the following code:
"""TLS Session Schema - Contract between Project 1 (PCAP Analyzer) and Project 2 (ML Risk Scoring)."""
TLS_VERSION_ORDER = {
"SSLv3": 1,
"TLS 1.0": 2,
"TLS 1.1": 3,
"TLS 1.2": 4,
"TLS 1.3": 5,
}
CIPHER_STRENGTH = {
"RC4": 1,
"DES": 1,
"3DES": 2,
"AES_128_CBC": 3,
"AES_256_CBC": 3,
"AES_128_GCM": 4,
"AES_256_GCM": 5,
"CHACHA20_POLY1305": 5,
}
What do these mappings do?
These ordinal values let the ML model treat TLS versions and cipher families as ordered features. SSLv3 gets 1 (weakest) and TLS 1.3 gets 5 (strongest), so the model learns that higher numbers mean stronger security.
The same logic applies to ciphers. RC4 and DES are broken ciphers (scored 1), while AES_256_GCM and CHACHA20_POLY1305 are modern and strong (scored 5).
- Below the CIPHER_STRENGTH dictionary, add the signature algorithm and PFS key exchange mappings:
SIG_ALGORITHM_STRENGTH = {
"MD5withRSA": 1,
"SHA1withRSA": 2,
"SHA256withRSA": 3,
"SHA384withRSA": 4,
"SHA256withECDSA": 4,
"SHA384withECDSA": 5,
}
PFS_KEY_EXCHANGES = {"ECDHE", "DHE"}
What is PFS?
Perfect Forward Secrecy (PFS) means that even if a server's private key is compromised later, past session traffic cannot be decrypted. ECDHE and DHE are key exchange algorithms that provide PFS. RSA key exchange does not.
The feature pipeline will use this set to create a binary feature: 1 if the session uses PFS, 0 if it does not.
- Save schema.py.
- Verify the mappings load correctly by running:
python -c "from schema import TLS_VERSION_ORDER; print(len(TLS_VERSION_ORDER), 'TLS versions mapped')"
You should see 5 TLS versions mapped printed.
Seeing a SyntaxError or ImportError?
Check that the closing braces for each dictionary are on their own line. A missing } or a trailing comma after the last entry inside the braces can cause a syntax error.
Make sure your terminal is in the tls-risk-scoring directory. Run ls to confirm schema.py is listed.
Help me debug my schema.py import error.
- Below PFS_KEY_EXCHANGES, start the SAMPLE_SESSIONS list with the first two sample sessions by adding this code:
SAMPLE_SESSIONS = [
{
"session_id": "sess_001",
"tls_version": "TLS 1.3",
"cipher_family": "AES_256_GCM",
"key_exchange": "ECDHE",
"cert_key_size": 4096,
"cert_sig_algorithm": "SHA384withECDSA",
"cert_chain_length": 3,
"has_expired_cert": False,
"has_self_signed": False,
"server_name": "mail.example.com",
},
{
"session_id": "sess_002",
"tls_version": "SSLv3",
"cipher_family": "RC4",
"key_exchange": "RSA",
"cert_key_size": 1024,
"cert_sig_algorithm": "SHA1withRSA",
"cert_chain_length": 1,
"has_expired_cert": True,
"has_self_signed": True,
"server_name": "legacy.example.com",
},
What do these sessions represent?
Each session dictionary mimics the JSON output from Project 1's PCAP analyzer. sess_001 is a strong configuration: TLS 1.3, AES-256-GCM, ECDHE for PFS, a 4096-bit key, and a valid certificate chain of 3.
sess_002 is dangerously weak: SSLv3, the broken RC4 cipher, no PFS, a tiny 1024-bit key, an expired certificate, and a self-signed chain of 1. This is the kind of session the ML model needs to flag immediately.
- Directly below sess_002's closing brace and comma, add the third session and close the list:
{
"session_id": "sess_003",
"tls_version": "TLS 1.2",
"cipher_family": "AES_128_GCM",
"key_exchange": "ECDHE",
"cert_key_size": 2048,
"cert_sig_algorithm": "SHA256withRSA",
"cert_chain_length": 2,
"has_expired_cert": False,
"has_self_signed": False,
"server_name": "smtp.example.com",
},
]
Why three different profiles?
sess_003 sits in the middle: TLS 1.2, AES-128-GCM, PFS enabled, 2048-bit key. It is not broken, but it is not best-in-class either. Having strong, weak, and moderate samples lets you visually verify that the feature pipeline scores them differently.
- Save schema.py.
- Verify all three sessions loaded by running:
python -c "from schema import SAMPLE_SESSIONS; print(len(SAMPLE_SESSIONS), 'sample sessions')"
You should see 3 sample sessions printed.
- At the bottom of schema.py, add the runner block that prints each session's details:
if __name__ == "__main__":
print("=== TLS Session Schema ===\n")
for session in SAMPLE_SESSIONS:
print(f"Session: {session['session_id']}")
for key, value in session.items():
if key != "session_id":
print(f" {key}: {value}")
print()
print(f"Total sample sessions: {len(SAMPLE_SESSIONS)}")
What does this runner block do?
The if __name__ == "__main__" guard runs this code only when you execute the file directly with python schema.py. It loops through each sample session and prints every field so you can visually inspect the data contract.
When other scripts import from schema.py (like the feature pipeline in the next step), this printing code does not execute. Only the dictionaries and constants are imported.
- Save schema.py.
Before you run the script, think about what you expect to see. You defined three sessions with different TLS configurations. What should the output look like?
- Run the schema script:
python schema.py
You should see all three sessions printed with their full field details. sess_001 shows TLS 1.3 and AES_256_GCM (strong). sess_002 shows SSLv3 and RC4 (weak). sess_003 shows TLS 1.2 and AES_128_GCM (moderate). The final line reads Total sample sessions: 3.
Seeing a NameError or empty output?
If you see NameError: name 'SAMPLE_SESSIONS' is not defined, check that the SAMPLE_SESSIONS list is defined at module level (not indented inside another block). The runner block references it directly.
If the script runs but prints nothing, make sure the if __name__ == "__main__" line is flush left with no indentation. Python only runs that block when the file is executed directly.
Help me debug my schema.py output.
✔️ Awesome, I've got everything!
Great. Double check that you have saved schema.py and all three sessions printed correctly.
ⓧ I'd like to double check the full code
Scroll up through your schema.py and confirm these sections are present, in this order:
- The module docstring at the very top.
- TLS_VERSION_ORDER dictionary (SSLv3 through TLS 1.3).
- CIPHER_STRENGTH dictionary (RC4 through CHACHA20_POLY1305).
- SIG_ALGORITHM_STRENGTH dictionary (MD5withRSA through SHA384withECDSA).
- PFS_KEY_EXCHANGES set containing ECDHE and DHE.
- SAMPLE_SESSIONS list with three session dictionaries (sess_001, sess_002, sess_003).
- The if __name__ == "__main__" runner block at the bottom.
Your ML environment is ready and the data contract is defined. Next up, you will build the feature engineering pipeline that transforms these raw session fields into the numerical features your ML models need.
Build the Feature Engineering Pipeline
Your schema.py contract is ready, and the three sample sessions are printing correctly. The next challenge is that ML models cannot read raw JSON fields like "TLS 1.3" or "AES_256_GCM". They need numbers.
In this step, you will build a feature engineering pipeline that transforms each TLS session into 9 numerical features. These features capture cryptographic strength, certificate health, and a composite weakness score. The quality of these features determines how well the models perform in later steps.
In this step, get ready to:
- Build an extract_features() function that converts TLS session dicts into a numerical matrix.
- Derive a composite weakness score that combines individual weakness signals into one metric.
- Test the pipeline on the three sample sessions from schema.py.
Create features.py with individual feature extraction
Each TLS session field maps to a different type of numerical feature. Protocol versions and cipher families become ordinal scores using the lookup tables in schema.py. Boolean fields like certificate expiry become 0/1 binary features.
- Create a new file called features.py in your tls-risk-scoring folder and add the following code:
"""Feature engineering pipeline for TLS session risk scoring."""
import pandas as pd
from schema import (
TLS_VERSION_ORDER,
CIPHER_STRENGTH,
SIG_ALGORITHM_STRENGTH,
PFS_KEY_EXCHANGES,
)
def extract_features(sessions: list[dict]) -> pd.DataFrame:
"""Transform raw TLS session dicts into a numerical feature matrix."""
records = []
for s in sessions:
# Look up ordinal scores from the schema mappings
tls_score = TLS_VERSION_ORDER.get(s["tls_version"], 0)
cipher_score = CIPHER_STRENGTH.get(s["cipher_family"], 0)
sig_score = SIG_ALGORITHM_STRENGTH.get(s["cert_sig_algorithm"], 0)
# Binary and normalized features
has_pfs = 1 if s["key_exchange"] in PFS_KEY_EXCHANGES else 0
key_size_norm = s["cert_key_size"] / 4096
expired = 1 if s["has_expired_cert"] else 0
self_signed = 1 if s["has_self_signed"] else 0
chain_len = s["cert_chain_length"]
What does this code do?
Each line inside the loop converts one raw session field into a number the model can learn from:
- tls_score, cipher_score, and sig_score use the ordinal mappings from schema.py to rank protocol, cipher, and signature strength on a 1-5 scale.
- has_pfs checks whether the key exchange supports Perfect Forward Secrecy (ECDHE or DHE). 1 means yes, 0 means no.
- key_size_norm normalizes the certificate key size by dividing by 4096 (the strongest common key size). A 2048-bit key becomes 0.5.
- expired and self_signed convert booleans to 0/1 binary values.
The function is not complete yet. Save features.py and continue to the next chunk, where you will add the composite weakness score that ties all of these individual signals together.
Add the composite weakness score and output dictionary
Individual features tell you one thing at a time. A composite weakness score combines them all into a single number that captures the overall risk posture of a session. Research on security ML confirms that composite features often outperform raw features because they capture the interaction between multiple weak signals.
- In features.py, find the line that says chain_len = s["cert_chain_length"], then add the following code below it:
# Combine all weaknesses into a single composite score
weakness_score = (
(5 - tls_score)
+ (5 - cipher_score)
+ (5 - sig_score)
+ (1 - has_pfs) * 3
+ (1 - key_size_norm) * 2
+ expired * 4
+ self_signed * 3
)
# Build the feature record for this session
records.append({
"tls_version_score": tls_score,
"cipher_strength": cipher_score,
"sig_algorithm_strength": sig_score,
"has_pfs": has_pfs,
"key_size_normalized": round(key_size_norm, 4),
"cert_expired": expired,
"cert_self_signed": self_signed,
"cert_chain_length": chain_len,
"composite_weakness": round(weakness_score, 4),
})
return pd.DataFrame(records)
How does the composite weakness score work?
The formula subtracts each feature's score from the maximum (5), so a weaker attribute produces a higher weakness contribution. Missing PFS is weighted at 3x because it removes forward secrecy protection entirely. Expired certs carry 4x weight because they indicate an unmaintained server.
The records.append() call packages all 9 features into a dictionary. The function returns a pandas DataFrame with one row per session and one column per feature.
- Save features.py.
- Verify the function loads without errors by running this command:
python -c "from features import extract_features; from schema import SAMPLE_SESSIONS; print(extract_features(SAMPLE_SESSIONS).shape)"
You should see (3, 9) printed. That confirms your function processes 3 sessions into 9 features each.
Seeing a SyntaxError or IndentationError?
- The composite weakness block must be indented at the same level as the other variable assignments inside the for s in sessions: loop (8 spaces or 2 tabs).
- Make sure return pd.DataFrame(records) is indented one level less than the loop body (4 spaces). It belongs inside the function, but outside the for loop.
- Check that you did not accidentally mix tabs and spaces. Python requires consistent indentation throughout a file.
Help me debug my extract_features function in features.py
Add the main block to run the pipeline
The extract_features() function works, but right now you can only test it through a one-liner. A __main__ block lets you run the file directly and see the full feature matrix printed in a readable table.
- In features.py, add the following code at the very bottom of the file, below the extract_features() function:
if __name__ == "__main__":
from schema import SAMPLE_SESSIONS
# Run the pipeline on the three sample sessions
df = extract_features(SAMPLE_SESSIONS)
print("=== Feature Engineering Pipeline ===\n")
print("Feature Matrix:")
print(df.to_string(index=False))
print(f"\nShape: {df.shape}")
print(f"Features: {list(df.columns)}")
What does this code do?
- The if __name__ == "__main__": guard ensures the code below only runs when you execute the file directly. Other scripts can import extract_features() without triggering the print statements.
- df.to_string(index=False) prints the DataFrame as a clean table without row numbers, making it easier to read all 9 feature columns side by side.
- Save features.py.
Before you run the pipeline, think about what you expect. The SSLv3/RC4 session (sess_002) uses the weakest protocol and cipher in the schema. Do you think its composite_weakness score will be much higher than the TLS 1.3 session, or only slightly higher?
- Run the feature engineering pipeline by running this command:
python features.py
You should see a 3-row, 9-column feature matrix. The strong TLS 1.3 session has a composite_weakness near 0, while the weak SSLv3 session has a much higher score. That spread is exactly what a classifier needs to distinguish risk levels.
Seeing a ModuleNotFoundError?
- If the error says No module named 'schema', make sure you are running the command from inside the tls-risk-scoring folder where schema.py lives.
- If the error mentions pandas, make sure your virtual environment is still activated. You should see (venv) at the start of your terminal prompt.
Help me fix a ModuleNotFoundError when running features.py
✔️ Awesome, I've got everything!
Great. Double check you have saved features.py.
ⓧ I'd like to double check the full code
Scroll up and confirm your features.py contains these three sections in order:
- The imports block at the top, importing pandas and the four schema mappings (TLS_VERSION_ORDER, CIPHER_STRENGTH, SIG_ALGORITHM_STRENGTH, PFS_KEY_EXCHANGES).
- The extract_features() function with 8 individual feature lines, the weakness_score calculation, the records.append() dictionary with 9 keys, and return pd.DataFrame(records).
- The if __name__ == "__main__": block at the bottom that imports SAMPLE_SESSIONS, calls extract_features(), and prints the matrix with shape and column names.
If anything is missing, scroll up to the corresponding chunk and re-add it. Then save and run python features.py again.
Your feature engineering pipeline is done. You can now convert any TLS session into 9 numbers that capture its cryptographic strength. Next up, you will generate 2,000 synthetic training samples so the classifier has enough data to learn the difference between strong, moderate, and weak configurations.
Generate Synthetic Training Data
Your feature pipeline can transform any TLS session into nine numerical features. Training an ML classifier on those features requires thousands of labeled examples, and you do not have thousands of real labeled TLS sessions.
In this step, you will build a synthetic data generator that creates 2,000 realistic TLS session records. Three configuration profiles will produce sessions spanning dangerously weak crypto through properly hardened setups, each tagged with a risk label.
In this step, get ready to:
- Define three TLS configuration profiles covering weak, moderate, and strong cryptographic setups.
- Build a dataset generator that produces 2,000 labeled sessions with reproducible randomness.
Create the configuration profiles
Each profile is a dictionary that describes a category of TLS server. The generator will randomly sample from these dictionaries to build individual sessions.
- Create a new file called synthetic_data.py in your tls-risk-scoring folder and add the imports and the first configuration profile by pasting this code:
"""Generate synthetic TLS session dataset for training risk models."""
import random
import numpy as np
from features import extract_features
WEAK_CONFIGS = {
"tls_versions": ["SSLv3", "TLS 1.0"],
"cipher_families": ["RC4", "DES", "3DES"],
"key_exchanges": ["RSA"],
"cert_key_sizes": [512, 1024],
"sig_algorithms": ["MD5withRSA", "SHA1withRSA"],
"expired_prob": 0.6,
"self_signed_prob": 0.5,
"chain_lengths": [1],
}
What does this code do?
random and numpy handle randomized session generation with reproducible seeds. extract_features is the pipeline you built in the previous step.
WEAK_CONFIGS defines the most vulnerable TLS profile: outdated protocols (SSLv3, TLS 1.0), broken ciphers (RC4, DES), no forward secrecy, small keys, and a 60% chance of expired certificates. These represent the servers you most want your classifier to flag.
- Save synthetic_data.py. The next chunk adds the remaining two profiles.
- Add the moderate and strong configuration profiles below WEAK_CONFIGS by pasting this code:
MODERATE_CONFIGS = {
"tls_versions": ["TLS 1.1", "TLS 1.2"],
"cipher_families": ["AES_128_CBC", "AES_256_CBC"],
"key_exchanges": ["RSA", "DHE"],
"cert_key_sizes": [2048],
"sig_algorithms": ["SHA256withRSA"],
"expired_prob": 0.1,
"self_signed_prob": 0.1,
"chain_lengths": [2, 3],
}
STRONG_CONFIGS = {
"tls_versions": ["TLS 1.2", "TLS 1.3"],
"cipher_families": ["AES_128_GCM", "AES_256_GCM", "CHACHA20_POLY1305"],
"key_exchanges": ["ECDHE"],
"cert_key_sizes": [2048, 4096],
"sig_algorithms": [
"SHA256withRSA",
"SHA384withRSA",
"SHA256withECDSA",
"SHA384withECDSA",
],
"expired_prob": 0.0,
"self_signed_prob": 0.0,
"chain_lengths": [2, 3, 4],
}
What do these profiles represent?
MODERATE_CONFIGS represents servers running TLS 1.1-1.2 with CBC ciphers and mixed key exchange. These sessions have some weaknesses but are not dangerously exposed.
STRONG_CONFIGS mirrors properly hardened servers: TLS 1.2-1.3, AEAD ciphers (AES-GCM, CHACHA20), ECDHE-only key exchange, and certificates that never expire or self-sign.
- Save synthetic_data.py.
- Verify the profiles load correctly by running this command:
python -c "from synthetic_data import WEAK_CONFIGS, MODERATE_CONFIGS, STRONG_CONFIGS; print('Profiles loaded:', len(WEAK_CONFIGS), 'keys each')"
You should see Profiles loaded: 9 keys each printed in your terminal.
Seeing an import error?
- Make sure features.py and schema.py are in the same tls-risk-scoring folder as synthetic_data.py.
- Check that your virtual environment is still active. You should see (venv) at the start of your terminal prompt.
- Still stuck? Help me debug an import error in synthetic_data.py. It cannot find features.py or schema.py.
Build the dataset generator
The profiles define what each category of server looks like. Now you need two functions: one that creates a single session from a profile, and one that orchestrates the full dataset.
- In synthetic_data.py, add the generate_session() function below STRONG_CONFIGS by pasting this code:
def generate_session(config: dict, session_id: int) -> dict:
"""Generate a single synthetic TLS session from a config profile."""
return {
"session_id": f"synth_{session_id:04d}",
"tls_version": random.choice(config["tls_versions"]),
"cipher_family": random.choice(config["cipher_families"]),
"key_exchange": random.choice(config["key_exchanges"]),
"cert_key_size": random.choice(config["cert_key_sizes"]),
"cert_sig_algorithm": random.choice(config["sig_algorithms"]),
"cert_chain_length": random.choice(config["chain_lengths"]),
"has_expired_cert": random.random() < config["expired_prob"],
"has_self_signed": random.random() < config["self_signed_prob"],
"server_name": f"server-{session_id}.example.com",
}
What does this function do?
generate_session() takes a config profile and randomly picks one value from each list in the profile. random.choice() selects from lists like tls_versions, while random.random() generates a float to compare against probability thresholds like expired_prob.
The output matches the session dict format from schema.py, so extract_features() can process it directly.
- Save synthetic_data.py.
- Test the function by generating a single weak session with this command:
python -c "from synthetic_data import generate_session, WEAK_CONFIGS; print(generate_session(WEAK_CONFIGS, 0))"
You should see a single session dict with fields like tls_version set to either SSLv3 or TLS 1.0, and cipher_family set to one of the weak ciphers.
Seeing a NameError?
- Check that generate_session() is defined after STRONG_CONFIGS, not inside one of the config dictionaries.
- Make sure the function starts with def generate_session at the top level (no extra indentation).
- Still stuck? Help me debug a NameError when calling generate_session in synthetic_data.py.
- Add the generate_dataset() function below generate_session() by pasting this code:
def generate_dataset(
n_weak: int = 500,
n_moderate: int = 500,
n_strong: int = 1000,
seed: int = 42,
) -> tuple[list[dict], np.ndarray]:
"""Generate a labeled synthetic dataset."""
random.seed(seed)
np.random.seed(seed)
sessions = []
labels = []
sid = 0
for _ in range(n_weak):
sessions.append(generate_session(WEAK_CONFIGS, sid))
labels.append("high_risk")
sid += 1
for _ in range(n_moderate):
sessions.append(generate_session(MODERATE_CONFIGS, sid))
labels.append("medium_risk")
sid += 1
for _ in range(n_strong):
sessions.append(generate_session(STRONG_CONFIGS, sid))
labels.append("low_risk")
sid += 1
return sessions, np.array(labels)
What does this function do?
- seed=42 locks the random number generators so every learner gets the same dataset.
- 500 weak sessions get the high_risk label. 500 moderate sessions get medium_risk. 1,000 strong sessions get low_risk.
- The 1:1:2 ratio intentionally overrepresents normal traffic. In real networks, most connections are properly configured. Matching that distribution helps the model learn what "normal" looks like.
- Save synthetic_data.py. The next chunk adds the runner that prints distribution statistics.
- Add the __main__ block at the bottom of synthetic_data.py by pasting this code:
if __name__ == "__main__":
sessions, labels = generate_dataset()
features_df = extract_features(sessions)
print("=== Synthetic Dataset Generator ===\n")
print(f"Generated {len(sessions)} synthetic sessions")
print("\nLabel distribution:")
unique, counts = np.unique(labels, return_counts=True)
for label, count in zip(unique, counts):
print(f" {label}: {count}")
print(f"\nFeature matrix shape: {features_df.shape}")
print("\nFeature statistics:")
print(features_df.describe().round(3).to_string())
What does the runner do?
- generate_dataset() creates all 2,000 sessions, then extract_features() transforms them into the 9-column feature matrix.
- The label distribution printout confirms the expected 500/500/1,000 split across risk tiers.
- The feature statistics (mean, std, min, max) verify that the features span realistic ranges across all three profiles.
- Save synthetic_data.py.
Before you run this, what do you expect the label distribution to look like? How many sessions should appear in each risk tier?
- Run the generator by running this command:
python synthetic_data.py
You should see output confirming 2,000 sessions generated with this label distribution: 500 high_risk, 500 medium_risk, and 1,000 low_risk. Below that, you will see a feature statistics table showing the mean, standard deviation, min, and max for all nine features.
Not seeing 2,000 sessions?
- If you see a TypeError about tuple, check that generate_dataset() returns sessions, np.array(labels) (two values separated by a comma).
- If the label counts do not match 500/500/1,000, check the default parameter values in the generate_dataset() function signature. n_weak should be 500, n_moderate should be 500, and n_strong should be 1000.
- Still stuck? Help me debug my synthetic_data.py. It is not producing the expected 2,000 sessions or the label distribution is wrong.
✔️ Awesome, I've got everything!
Great. Make sure you have saved synthetic_data.py.
ⓧ I'd like to double check the full code
Scroll up and confirm each of these sections is present in synthetic_data.py, in this order:
- Imports (random, numpy, extract_features).
- WEAK_CONFIGS dictionary with SSLv3/TLS 1.0, RC4/DES/3DES, RSA-only, 512-1024 bit keys.
- MODERATE_CONFIGS dictionary with TLS 1.1-1.2, AES-CBC ciphers, mixed RSA/DHE, 2048-bit keys.
- STRONG_CONFIGS dictionary with TLS 1.2-1.3, AES-GCM/CHACHA20, ECDHE-only, 2048-4096 bit keys.
- generate_session() function returning a session dict built from random.choice() calls.
- generate_dataset() function with defaults n_weak=500, n_moderate=500, n_strong=1000, seed=42.
- __main__ block that calls generate_dataset() and prints label distribution and feature statistics.
Why synthetic data instead of real captures?
Real labeled TLS datasets large enough for ML training are rare and often restricted. Synthetic generation lets you control the label distribution, ensure coverage of rare configurations (like SSLv3 servers), and reproduce results exactly with a fixed seed.
In production, you would train on real PCAP data from Project 1 and use this synthetic generator for augmentation or testing.
You now have 2,000 labeled TLS sessions ready for training. Next up, you will feed this dataset into a Gradient Boosting classifier and see which TLS features drive risk the most.
Train the Gradient Boosting Risk Classifier
Your synthetic dataset of 2,000 labeled TLS sessions is ready. Each session carries a risk label and 9 numerical features that capture its cryptographic profile.
The question now is whether a machine learning model can learn the boundary between weak and strong TLS configurations from these features alone. You will train a Gradient Boosting classifier that sorts sessions into risk tiers and reveals which TLS attributes matter most.
In this step, get ready to:
- Build a train_risk_classifier() function that prepares data, trains a Gradient Boosting model, and evaluates its performance.
- Run the classifier and review its accuracy, ROC-AUC score, and ranked feature importances.
- Save the trained model and label encoder to disk for reuse in later steps.
Create the risk classifier training function
The training function takes raw sessions and labels, transforms them into features, splits the data, trains the model, and packages the evaluation results.
- Create a new file called risk_classifier.py in your tls-risk-scoring folder.
- Add the imports and the data preparation portion of the train_risk_classifier() function by pasting this code:
"""Gradient Boosting risk classifier for TLS session scoring."""
import pandas as pd
import joblib
from sklearn.ensemble import GradientBoostingClassifier
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, roc_auc_score
from sklearn.preprocessing import LabelEncoder
from synthetic_data import generate_dataset
from features import extract_features
def train_risk_classifier(
sessions: list[dict],
labels,
) -> tuple:
"""Train a Gradient Boosting classifier on TLS session features."""
features_df = extract_features(sessions)
le = LabelEncoder()
y_encoded = le.fit_transform(labels)
X_train, X_test, y_train, y_test = train_test_split(
features_df.values,
y_encoded,
test_size=0.2,
random_state=42,
stratify=y_encoded,
)
What does this code do?
- extract_features() reuses your pipeline from the previous step to convert raw session dicts into a 9-column feature matrix.
- LabelEncoder maps string labels (high_risk, medium_risk, low_risk) to integers that the classifier needs.
- train_test_split reserves 20% of the data for testing. The stratify parameter ensures each risk level appears in the same proportion in both training and test sets.
The function is not complete yet. The next chunk adds the model training logic.
- Still in risk_classifier.py, add this code directly below the train_test_split closing parenthesis, inside the train_risk_classifier() function:
clf = GradientBoostingClassifier(
n_estimators=200,
learning_rate=0.1,
max_depth=4,
subsample=0.8,
random_state=42,
)
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
y_proba = clf.predict_proba(X_test)
report = classification_report(
y_test,
y_pred,
target_names=le.classes_,
output_dict=True,
)
roc_auc = roc_auc_score(y_test, y_proba, multi_class="ovr")
What does this code do?
- GradientBoostingClassifier builds an ensemble of 200 decision trees. Each tree corrects the errors of the previous ones.
- subsample=0.8 trains each tree on a random 80% of the data, reducing overfitting.
- classification_report computes precision, recall, and F1 score for each risk class.
- roc_auc_score with multi_class="ovr" measures how well the model separates each class from all others. A score of 1.0 is perfect separation.
Why Gradient Boosting?
Gradient Boosting builds trees sequentially, with each tree focused on correcting the previous tree's mistakes. This makes it effective at learning non-linear boundaries between risk levels.
It handles mixed feature types (ordinal scores, binary flags, continuous values) without needing normalization. The built-in feature_importances_ attribute tells you which features drive the model's decisions, giving you interpretable security insight alongside the prediction.
One more chunk completes the function. The next block packages the results and returns them.
- Still inside the train_risk_classifier() function, add this code directly below the roc_auc line:
importances = dict(zip(
features_df.columns,
clf.feature_importances_,
))
results = {
"classification_report": report,
"roc_auc": roc_auc,
"feature_importances": importances,
"test_accuracy": clf.score(X_test, y_test),
}
return clf, le, results
What does this code do?
- feature_importances_ extracts how much each of the 9 features contributed to the model's classification decisions.
- The results dictionary bundles the classification report, ROC-AUC score, feature importances, and test accuracy into a single return value.
- The function returns the trained classifier, label encoder, and results so the caller can use them without retraining.
- Save risk_classifier.py.
Run the classifier and save the trained model
The function is complete. Now add a __main__ block that generates training data, runs the classifier, prints the evaluation, and saves the model to disk.
- Add this __main__ block at the bottom of risk_classifier.py, below the function:
if __name__ == "__main__":
sessions, labels = generate_dataset()
clf, le, results = train_risk_classifier(sessions, labels)
print("=== Gradient Boosting Risk Classifier ===\n")
print(f"Test Accuracy: {results['test_accuracy']:.4f}")
print(f"ROC-AUC (OVR): {results['roc_auc']:.4f}")
print("\nClassification Report:")
report_df = pd.DataFrame(results["classification_report"]).T
print(report_df.round(3).to_string())
print("\nFeature Importances (descending):")
sorted_imp = sorted(
results["feature_importances"].items(),
key=lambda x: x[1],
reverse=True,
)
for feat, imp in sorted_imp:
bar = "#" * int(imp * 50)
print(f" {feat:25s} {imp:.4f} {bar}")
joblib.dump(clf, "risk_classifier.pkl")
joblib.dump(le, "label_encoder.pkl")
print("\nModel saved to risk_classifier.pkl")
What does this code do?
- It generates the synthetic dataset and passes it to train_risk_classifier() to train and evaluate the model in one call.
- The classification report prints precision, recall, and F1 for each risk level. Feature importances appear as an ASCII bar chart so you can see which TLS attributes drive risk at a glance.
- joblib.dump saves the trained classifier to risk_classifier.pkl and the label encoder to label_encoder.pkl so you can reload them later without retraining.
- Save risk_classifier.py.
Before you run this, what accuracy do you expect? The synthetic data has clear separation between weak and strong profiles, but some moderate and strong configurations share similar features.
- Run the classifier by executing this command:
python risk_classifier.py
You should see test accuracy above 90% and a ROC-AUC score close to 1.0. The classification report shows precision, recall, and F1 for each risk level.
The feature importances list shows composite_weakness and tls_version_score near the top. Your engineered composite feature and protocol version are the strongest risk signals.
Seeing an error?
- If you see ModuleNotFoundError, make sure your virtual environment is activated. Check for (venv) in your terminal prompt.
- If you see IndentationError, confirm that every line inside the function is indented with exactly 4 spaces. Mixing tabs and spaces causes this error.
- If you see ImportError for generate_dataset or extract_features, confirm that synthetic_data.py and features.py are in the same folder as risk_classifier.py.
Help me debug my risk classifier script.
✔️ Awesome, I've got everything!
Great. Double check you have saved risk_classifier.py and that risk_classifier.pkl and label_encoder.pkl now appear in your project folder.
ⓧ I'd like to double check the full code
The full risk_classifier.py file is built from the four code chunks above. Scroll up and confirm each chunk appears in order:
- Chunk 1: File docstring, imports, and the function from def train_risk_classifier through train_test_split.
- Chunk 2: GradientBoostingClassifier init, clf.fit, predictions, classification_report, and roc_auc_score.
- Chunk 3: importances dict, results dict, and return statement.
- Chunk 4: The __main__ block that generates data, trains, prints results, and saves models.
Make sure there are no extra blank lines or missing indentation between chunks.
The classifier performs well on known risk patterns. In the next step, you will discover its blind spot. Subtle TLS downgrade attack patterns that look individually acceptable will fool the classifier completely. You will use an unsupervised model to catch what the classifier misses.
Catch Downgrade Attacks with Isolation Forest
Your Gradient Boosting classifier scored over 90% accuracy on the test set. But a supervised model can only flag risk patterns it has seen in training data.
A TLS downgrade attack combines individually strong features (TLS 1.2, AES-256-GCM, valid certificate) with suspicious signals like RSA key exchange and a certificate chain length of 1. The classifier sees each feature as acceptable and confidently labels the session medium_risk. In real security work, that missed detection gets you breached. In this step, you will expose that gap and close it with an Isolation Forest.
In this step, get ready to:
- Craft TLS downgrade attack sessions that fool the supervised classifier.
- Expose the classifier's blind spot on feature combinations it has never seen.
- Train an Isolation Forest on normal traffic to catch novel downgrade patterns.
Craft the downgrade attack sessions
A downgrade attack forces a TLS connection to use weaker settings than the server normally supports. The tricky part is that each individual feature can look perfectly reasonable on its own.
- Create a new file called anomaly_detector.py in your tls-risk-scoring folder, and add the file header and imports by pasting this code:
"""Isolation Forest anomaly detector for TLS downgrade pattern detection."""
import joblib
from sklearn.ensemble import IsolationForest
from synthetic_data import generate_dataset
from features import extract_features
from risk_classifier import train_risk_classifier
What do these imports do?
- IsolationForest is the unsupervised anomaly detector you will train on normal traffic patterns.
- joblib serializes the trained model to a .pkl file, just like the classifier step.
- The local imports pull in your dataset generator, feature pipeline, and risk classifier from earlier steps.
- Save anomaly_detector.py.
- Confirm the file exists by running:
ls
- You should see anomaly_detector.py listed alongside schema.py, features.py, synthetic_data.py, and risk_classifier.py.
Now you will define two crafted TLS sessions that simulate a downgrade attack. Each session uses individually strong features paired with a suspicious combination.
- In anomaly_detector.py, add the following DOWNGRADE_SESSIONS list below the imports:
DOWNGRADE_SESSIONS = [
{
"session_id": "downgrade_001",
"tls_version": "TLS 1.2",
"cipher_family": "AES_256_GCM",
"key_exchange": "RSA",
"cert_key_size": 2048,
"cert_sig_algorithm": "SHA256withRSA",
"cert_chain_length": 1,
"has_expired_cert": False,
"has_self_signed": False,
"server_name": "suspicious.example.com",
},
{
"session_id": "downgrade_002",
"tls_version": "TLS 1.2",
"cipher_family": "AES_128_GCM",
"key_exchange": "RSA",
"cert_key_size": 2048,
"cert_sig_algorithm": "SHA256withRSA",
"cert_chain_length": 1,
"has_expired_cert": False,
"has_self_signed": False,
"server_name": "mitm-target.example.com",
},
]
What makes these sessions suspicious?
Look at each feature individually: TLS 1.2, AES-256-GCM, SHA256, 2048-bit key, valid certificate. Every single one looks strong.
The red flags are in the combination. RSA key exchange means no Perfect Forward Secrecy, and a certificate chain length of 1 means no intermediate CA. In the training data, every low_risk session uses ECDHE and has chain lengths of 2 or more. This combination never appears in normal traffic.
- Save anomaly_detector.py. You will test these sessions against the classifier in the next substep.
Expose the classifier's blind spot
Your classifier achieved >90% accuracy on the test set. Time to see how it handles sessions specifically designed to exploit the gap between individual feature quality and overall session safety.
- In anomaly_detector.py, add the following if __name__ block below the DOWNGRADE_SESSIONS list:
if __name__ == "__main__":
sessions, labels = generate_dataset()
print("=== Step 1: Classifier on downgrade patterns ===\n")
clf, le, results = train_risk_classifier(sessions, labels)
downgrade_features = extract_features(DOWNGRADE_SESSIONS)
clf_predictions = le.inverse_transform(clf.predict(downgrade_features.values))
clf_probas = clf.predict_proba(downgrade_features.values)
print("Classifier predictions on downgrade patterns:")
for i, session in enumerate(DOWNGRADE_SESSIONS):
print(f"\n Session: {session['session_id']}")
print(
f" TLS: {session['tls_version']}, "
f"Cipher: {session['cipher_family']}, "
f"KeyEx: {session['key_exchange']}, "
f"Chain: {session['cert_chain_length']}"
)
print(f" Classifier says: {clf_predictions[i]}")
proba_dict = dict(zip(le.classes_, clf_probas[i].round(3)))
print(f" Probabilities: {proba_dict}")
What does this code do?
- generate_dataset() and train_risk_classifier() rebuild the classifier from scratch so the test is self-contained.
- extract_features(DOWNGRADE_SESSIONS) runs the two crafted sessions through your feature pipeline.
- The loop prints each session's features, the classifier's label, and its probability distribution across all three risk classes.
- Save anomaly_detector.py.
Before you run this, think about what the classifier will predict. Both downgrade sessions use TLS 1.2 and AES-GCM ciphers. Do you think it will flag them as high risk, medium risk, or low risk?
- Run the script by executing:
python anomaly_detector.py
The classifier labels both downgrade sessions as medium_risk. Each individual feature looks acceptable, so the classifier has no reason to sound the alarm. This is exactly the gap that gets organizations breached in the real world.
Seeing an import error or no output?
- Make sure your virtual environment is active. You should see (venv) in your terminal prompt.
- Confirm that schema.py, features.py, synthetic_data.py, and risk_classifier.py are all in the same directory as anomaly_detector.py.
- Check that the DOWNGRADE_SESSIONS list has correct indentation and no missing commas between dict entries.
Why isn't my anomaly_detector.py running?
Why does the classifier miss this?
The Gradient Boosting classifier learned decision boundaries from individual feature values. TLS 1.2 scores a 4, AES-256-GCM scores a 5, SHA256withRSA scores a 3. None of these trigger the high-risk rules the classifier learned from weak sessions.
The danger is in the combination: RSA key exchange (no PFS) with a chain length of 1 never appears in the normal training data. The classifier does not model joint feature distributions. It cannot detect a combination that is individually normal but collectively suspicious.
Train the Isolation Forest to catch what the classifier misses
An Isolation Forest is an unsupervised anomaly detector. Instead of learning labeled risk categories, it learns what normal looks like. Anything that deviates from that baseline gets flagged as an anomaly.
Why Isolation Forest instead of One-Class SVM?
Isolation Forest works by randomly partitioning data points. Anomalies are isolated in fewer splits because they sit far from the dense clusters of normal data. This makes it faster than alternatives like Local Outlier Factor on higher-dimensional data.
Its scoring via path length maps cleanly to a risk narrative: shorter paths mean easier-to-isolate (more anomalous) sessions.
- In anomaly_detector.py, find the if __name__ == "__main__": line. Add the following function above that line:
def train_anomaly_detector(normal_sessions: list[dict]) -> IsolationForest:
"""Train Isolation Forest on normal (strong) TLS configurations."""
features_df = extract_features(normal_sessions)
iso = IsolationForest(
n_estimators=200,
contamination=0.05,
random_state=42,
n_jobs=-1,
)
iso.fit(features_df.values)
return iso
What does this function do?
- The function accepts only normal (low_risk) sessions. Training on clean data teaches the model what baseline traffic looks like.
- n_estimators=200 builds 200 isolation trees. More trees mean more stable anomaly scores.
- contamination=0.05 tells the model to expect about 5% of new data to be anomalous. This sets the decision threshold.
Now you need to call this function and test the Isolation Forest on the same downgrade sessions the classifier missed.
- In anomaly_detector.py, scroll to the end of the if __name__ block. Find the last line of the classifier's for loop:
print(f" Probabilities: {proba_dict}")
- Add the Isolation Forest testing code below that line, still inside the if __name__ block:
print("\n\n=== Step 2: Isolation Forest on same patterns ===\n")
normal_sessions = [
s for s, l in zip(sessions, labels) if l == "low_risk"
]
iso = train_anomaly_detector(normal_sessions)
print(f"Trained on {len(normal_sessions)} normal (low_risk) sessions")
downgrade_preds = iso.predict(downgrade_features.values)
downgrade_scores = iso.decision_function(downgrade_features.values)
print("\nIsolation Forest on downgrade patterns:")
for i, session in enumerate(DOWNGRADE_SESSIONS):
status = "ANOMALY" if downgrade_preds[i] == -1 else "NORMAL"
print(f"\n Session: {session['session_id']}")
print(
f" TLS: {session['tls_version']}, "
f"Cipher: {session['cipher_family']}, "
f"KeyEx: {session['key_exchange']}, "
f"Chain: {session['cert_chain_length']}"
)
print(f" Isolation Forest says: {status} (score: {downgrade_scores[i]:.4f})")
What is happening here?
- The list comprehension filters only low_risk sessions (1,000 of them) for training. The IF learns only what normal traffic looks like.
- predict() returns -1 for outliers and 1 for inliers.
- decision_function() returns a continuous score. Negative scores indicate outliers. The more negative, the more anomalous the session.
- Save anomaly_detector.py and run the script:
python anomaly_detector.py
You should now see two sections of output. The classifier still labels both downgrade sessions as medium_risk. But the Isolation Forest flags both as ANOMALY with negative decision scores. The combination of RSA key exchange with chain length 1 never appears in normal traffic, so the IF isolates these sessions as outliers.
Now add a sanity check to confirm the IF does not flag genuinely normal sessions, and save the trained model.
- In anomaly_detector.py, add the following code at the end of the if __name__ block, below the Isolation Forest downgrade loop:
normal_sample_features = extract_features(sessions[1000:1003])
normal_preds = iso.predict(normal_sample_features.values)
normal_scores = iso.decision_function(normal_sample_features.values)
print("\nSanity check - Isolation Forest on known normal sessions:")
for i in range(len(normal_preds)):
status = "ANOMALY" if normal_preds[i] == -1 else "NORMAL"
print(f" Sample {i}: {status} (score: {normal_scores[i]:.4f})")
joblib.dump(iso, "anomaly_detector.pkl")
print("\nAnomaly detector saved to anomaly_detector.pkl")
What does this code do?
- sessions[1000:1003] grabs three sessions from the strong (low_risk) portion of the training data. These should be labeled NORMAL by the IF.
- joblib.dump() saves the trained Isolation Forest to anomaly_detector.pkl so you can load it later without retraining.
- Save anomaly_detector.py.
Before you run the final script, what do you expect the sanity check to show? Should the known normal sessions be flagged as ANOMALY or NORMAL?
- Run the complete anomaly detector by executing:
python anomaly_detector.py
The sanity check confirms all three normal samples are labeled NORMAL with positive decision scores. The model saved confirmation appears at the end.
Normal sessions flagged as ANOMALY?
- Make sure you are filtering only low_risk sessions for training the IF. If you pass all sessions (including weak and moderate), the model learns a different baseline.
- Check that you are slicing sessions[1000:1003] for the sanity check. Indices 1000-1002 fall in the strong config range of the generated dataset.
Why is my Isolation Forest flagging normal sessions as anomalies?
✔️ Awesome, I've got everything!
Great. Double check you have saved anomaly_detector.py.
ⓧ I'd like to double check the full code
Your anomaly_detector.py should contain these sections in order. Scroll up and confirm each one is present:
- The docstring, imports (joblib, IsolationForest, generate_dataset, extract_features, train_risk_classifier).
- DOWNGRADE_SESSIONS list with two session dicts (downgrade_001 and downgrade_002).
- train_anomaly_detector() function that creates and fits an IsolationForest.
- if __name__ == "__main__": block with: classifier test on downgrade sessions, Isolation Forest test on the same sessions, sanity check on normal sessions, and joblib.dump() saving the model.
Secret mission
Build a Fusion Scoring Engine
You have two models that each see something the other misses. The classifier knows risk levels but is blind to novel attacks. The Isolation Forest catches anomalies but cannot tell you how severe they are. In this mission, you will fuse both signals into a single scoring engine that produces a ranked risk report with severity tiers, escalating downgrade attacks from medium to critical.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally, so there are no ongoing costs or cloud resources to worry about. Decide whether to keep your project files for future reference, or delete them to free up disk space.
Resources you used:
- The tls-risk-scoring project directory on your Desktop, containing all Python source files.
- A Python virtual environment (venv) inside the project directory, with scikit-learn and pandas installed.
- Trained model files: risk_classifier.pkl, label_encoder.pkl, and anomaly_detector.pkl.
Keep everything running
No action needed. Choose this if you plan to extend the pipeline further or integrate it with Project 1's PCAP analyzer.
- Your project files, virtual environment, and trained models all stay in the tls-risk-scoring folder on your Desktop.
- To reactivate the virtual environment later, open a terminal, navigate to the project folder, and run the activate command again.
Pause - I'll come back to this later
Deactivate the virtual environment to free up your terminal session. Your files and installed packages stay on disk.
- If your virtual environment is still active, deactivate it by running:
deactivate
- Your project files, trained models, and installed packages remain in ~/Desktop/tls-risk-scoring. When you return, reactivate the virtual environment and pick up where you left off.
Delete - I don't want to use this again
Remove the entire project directory, including all source files, the virtual environment, and trained model files.
- If your virtual environment is still active, deactivate it first by running:
deactivate
- Delete the project folder by running this command on macOS/Linux:
rm -rf ~/Desktop/tls-risk-scoring
- On Windows, run this command instead:
rmdir /s /q %USERPROFILE%\Desktop\tls-risk-scoring
- This removes all Python source files, the venv folder (along with scikit-learn and pandas), and the trained .pkl model files.
Nice Work!
Nice Work!
You just built a hybrid ML pipeline that classifies TLS sessions by risk level and detects novel downgrade attacks that a supervised classifier alone would miss.
You've learned how to:
- Built a feature engineering pipeline that transforms raw TLS session data into 9 numerical features, generated a synthetic training dataset of 2,000 labeled sessions across weak, moderate, and strong cryptographic profiles, and used a composite weakness score to capture interactions between individual security signals.
- Trained a Gradient Boosting risk classifier with over 90% accuracy that ranks which TLS attributes drive risk the most, and saved the model with joblib for reuse.
- Demonstrated a real-world gap where the classifier labels subtle TLS downgrade patterns as only medium risk, then trained an Isolation Forest anomaly detector that catches those patterns by learning what normal traffic looks like.
- Secret Mission: Tackled an optional challenge to push your security ML skills further.
Ready to quiz yourself?