\section{Dataset Creation}
The construction of the dataset required defining which information could identify a client in a stable manner during a session.
As discussed earlier, relying on a single feature such as IP address or User-Agent is not sufficiently reliable because of NAT, IP rotation, and User-Agent rotation (see Section~\ref{session-identification}). \\[6pt]
For this reason, the dataset creation relies on the combination of both identifiers.
This approach reduces the risk of grouping unrelated clients within the same session or splitting a single client into multiple artificial ones. \\[6pt]
User activity is not constant. It may contain short bursts of requests followed by periods of inactivity and then resume.
To capture this pattern, the requests are aggregated per minute.
This representation preserves the chronological order of actions and separates active periods from those without traffic.
It also prevents Elasticsearch from returning a single large document per client, which would be difficult to process.
At this stage, minute buckets provide only a temporal view of the traffic and do not define the logical session. \\[6pt]
To extract the data, Elasticsearch is queried through a composite aggregation over the IP and the User-Agent.
The composite aggregation returns groups of client keys in batches, which avoids loading all records into memory at once.
Within each key, a date histogram with a fixed interval of one minute collects the requests occurring during that time range.
The query used is shown below:
\begin{verbatim}
"size": 0,
"query": {
    "bool": {
        "must": [
            {"range": {"@timestamp": {"gte": start_time, "lte": end_time}}},
            {"term": {"octofence.service-id": service_id}}
        ]
    }
},
"aggs": {
    "all_clients": {
        "composite": {
            "size": 100,
            "sources": [
                {"real_client_ip": {"terms": {"field": "real_client_ip"}}},
                {"user_agent": {"terms": {"field": "request.headers.user-agent"}}}
            ]
        },
        "aggs": {
            "per_minute": {
                "date_histogram": {
                    "field": "@timestamp",
                    "fixed_interval": "1m",
                    "min_doc_count": 1
                }
            }
        }
    }
}
\end{verbatim}
An example of the response is:
\begin{verbatim}
"key": {
    "real_client_ip": "X.X.X.X",
    "user_agent": "Mozilla/5.0 (X11; Linux i686) ..."
},
"doc_count": 1,
"per_minute": {
    "buckets": [
        {
            "key_as_string": "2025-07-31T19:24:00.000Z",
            "key": 1753989840000,
            "doc_count": 1
        },
        {
            "key_as_string": "2025-07-31T19:31:00.000Z",
            "key": 1753990260000,
            "doc_count": 1
        }
    ]
}
\end{verbatim}
This output shows two requests issued by the same client.
They appear in two different minute buckets because they were sent seven minutes apart.
However, this does not imply that they correspond to two distinct sessions.
The minute buckets are only a representation of the temporal distribution of activity. \\[6pt]
The actual session reconstruction is performed in Python.
All consecutive requests generated by the same \((\text{IP}, \text{User-Agent})\) pair are assigned to the same session as long as the temporal gap between them is less than one hour.
If the gap exceeds one hour, a new session is created.
In the previous example, the two requests are separated by seven minutes, therefore they are assigned to the same session.
This logic reflects typical user behaviour: brief pauses are common, whereas longer periods of inactivity usually indicate that the interaction has ended. \\[6pt]
The one-hour threshold was selected primarily because Sicuranext also protects e-commerce platforms.
In this context, users often take more time before generating the next request.
They may read product pages, compare alternatives, or leave the page open while deciding what to purchase.
Shorter time limits would fragment this behaviour into many separate sessions, even though it corresponds to a single visit.
The one-hour window maintains slow navigation within the same session and prevents artificial interruptions in the user journey. \\[6pt]
Elasticsearch cannot directly manage session information because the logs contain no field that defines it.
The system stores only individual HTTP requests, each with its own timestamp and metadata.
Consequently, Elasticsearch can group data by fields or time windows, but it cannot detect when a user stops browsing and resumes later.
Since the concept of \emph{session} does not exist in the raw data, the session boundaries must be computed externally in Python after the aggregated records have been retrieved.

% Features selected and why %
\subsection{Features}
The feature set contains 31 elements.  
They are organised by purpose, and their identification is based on the considerations already discussed in Section~\ref{feature-identification}.  
The intention is not to introduce features only because they are technically available, but to select those that can provide information useful to distinguish normal behaviour from suspicious traffic.  
For this reason, the dataset includes both features already explored in previous research and new ones tailored to the context of this work. \\[6pt]
The first group captures basic traffic volume and request types:
\begin{itemize}
    \item total\_number\_requests
    \item get
    \item post
    \item non\_get\_post
    \item unassigned\_referer
\end{itemize}
These features describe the overall structure of the session.  
For example, an unusual number of non-GET requests is often associated with automated scripts or brute force attacks rather than interactive browsing.  
Similarly, a missing or undefined \texttt{referer} may indicate non-standard navigation patterns or direct URL probing, which are less common in regular user behavior. \\[6pt]
The \texttt{Sec-Fetch-*} headers are used to enrich this view.  
As presented earlier, they contain browser-generated information about the context and origin of a request.  
This information makes it possible to approximate actions that are likely initiated by a human user, as opposed to requests issued by the browser in the background or by automated tools.  
This distinction is important because many raw HTTP requests do not reflect real user intent.  
For example, loading images, fonts, or JavaScript files may produce dozens of requests even if the user is only visiting a single page. \\[6pt]
Using this filtering logic, two behavioral metrics are derived: \texttt{max\_requests\_per\_minute} and \texttt{max\_page\_ratio}.  
They are computed only from requests that match one of the following values:  
\texttt{sec-fetch-user = ?1}, \texttt{sec-fetch-site = none}, \texttt{sec-fetch-dest = document}, or \texttt{sec\-fetch-mode = navigate}.  
These values correlate with active navigation events, such as opening a page or clicking a link, and therefore increase the reliability of the measurements. \\[6pt]
The first metric captures bursts of interaction: \texttt{max\_requests\_per\_minute}. 
Without filtering, this metric would largely reflect how many static assets the browser retrieves. 
Instead, in this work, it focuses on requests that are more closely aligned with user intent. 
The second metric measures concentration:
\[
\texttt{max\_page\_ratio} = \frac{\text{requests for the most frequent page}}{\text{total requests}}
\]
A high value suggests repetitive interactions with a specific endpoint, which may be legitimate (for example a dashboard refresh) or malicious (for example a brute force login attempt). Previous studies proposed similar indicators~\cite{unsupervised-clustering-of-web-sessions-to-detect-malicious-and-non-malicious-website-users, wsad-an-unsupervised-web-session-anomalydetection-method}, but in this thesis the use of the \texttt{Sec-Fetch} heuristic increases their interpretability in real-user contexts. \\[6pt]
The following features represent the distribution of \texttt{Sec-Fetch} values over the session:
\begin{itemize}
    \item sfd\_image (sec-fetch-dest)
    \item sfd\_document (sec-fetch-dest)
    \item sfd\_script (sec-fetch-dest)
    \item sfd\_font (sec-fetch-dest)
    \item sfm\_cors (sec-fetch-mode)
    \item sfm\_navigate (sec-fetch-mode)
    \item sfm\_no\_cors (sec-fetch-mode)
    \item sfm\_same\_origin (sec-fetch-mode)
    \item sfs\_cross\_site (sec-fetch-site)
    \item sfs\_same\_origin (sec-fetch-site)
    \item sfs\_same\_site (sec-fetch-site)
    \item sfs\_none (sec-fetch-site)
    \item sfu\_?1 (sec-fetch-user)
    \item xml\_http\_requests
    \item max\_requests\_per\_minute
    \item max\_page\_ratio
\end{itemize}
These indicators describe the "shape" of a session.  
Normal browsing tends to mix document loads, script retrievals, and embedded resources from related origins.  
On the contrary, automated tools often show simpler patterns:  
repeated navigation to a single endpoint, an unusual ratio of cross-site requests, or an absence of resources associated with page rendering. \\[6pt]
Response content types were previously addressed in Section~\ref{feature-identification}.  
They are useful to distinguish sessions originating from webpages, which typically return \texttt{text/html}, from those caused by programmatic APIs, which frequently return \texttt{application/json}.  
Only the top-level category is considered:
\begin{itemize}
    \item ct\_image
    \item ct\_font
    \item ct\_application
    \item ct\_text
\end{itemize}
HTTP error codes provide further insight.  
Instead of merging all 4xx responses into one group, they are separated, because each type explains a different behaviour.  
Scanning activities usually produce many \texttt{404 Not Found} responses, as the attacker explores unknown paths.  
In contrast, attempts to bypass access controls are more likely to produce \texttt{403 Forbidden}.  
This separation has more diagnostic value than the single 4xx indicator used in several previous works:
\begin{itemize}
    \item error\_status\_code\_401
    \item error\_status\_code\_403
    \item error\_status\_code\_404
    \item error\_status\_code\_other
\end{itemize}
The \texttt{avg\_response\_content\_length} feature was already introduced in Section~\ref{feature-identification}.  
The \texttt{time\allowbreak\_interval} measures the duration of the session, which helps separate short automated attacks from longer activities typical of human users.  
When considered together, these features provide multiple perspectives on the session and support the model in identifying behaviour that deviates from normal patterns:
\begin{itemize}
    \item avg\_response\_content\_length
    \item time\_interval
\end{itemize}




% pre-processing %
\subsection{Pre-Processing}
Before clustering, the raw dataset contains 37 features.  
This initial set includes identification fields such as \texttt{real\_client\_ip} and \texttt{user\_agent}, temporal boundaries such as \texttt{start\_time} and \texttt{end\_time}, and operational indicators such as \texttt{num\_asn\_blocks}.  
The last feature records how many requests belonging to the session were blocked because the ASN of the client had a poor reputation.  
The role of ASN and its impact on traffic quality are described in Chapter~\ref{infrastructure}. \\[6pt]
The rationale behind this first filtering step is that sessions originating from untrusted ASNs have a high probability of being malicious or at least not representative of normal user behavior.  
Keeping them in the dataset would introduce noise and affect the interpretation of the clustering output.  
Therefore, if a session contains at least one blocked request, the entire session is excluded.  
Once the filtering is done, the \texttt{num\_asn\_blocks} feature has no remaining purpose and is removed. \\[6pt]
A second exclusion rule is applied based on session size.  
Sessions containing fewer than five requests do not provide enough information to derive meaningful behaviour.  
Such short traces often correspond to accidental visits, crawler probes, or incomplete navigation, and including them would weaken the model rather than improve it.  
For this reason, they are discarded. \\[6pt]
The next transformation concerns feature scaling relative to traffic volume.  
Most indicators represent counts, and large sessions would naturally show larger values even if their behaviour is normal.  
To avoid bias toward users who simply interact more, all features except \texttt{total\_number\_requests}, \texttt{time\_interval}, \texttt{max\_requests\_per\_minute}, \texttt{max\_page\_ratio}, and \texttt{avg\_response\_content\_length} are divided by the total request count.  
This step converts raw frequencies into ratios and makes sessions comparable regardless of size. \\[6pt]
After normalization, feature redundancy is inspected through the correlation matrix (Section~\ref{exploratory-feature-analysis}).  
Highly correlated variables generally reflect the same underlying phenomenon,
therefore they provide limited additional information. Their inclusion increases
the dimensionality of the dataset without necessarily improving the separation
between clusters, and in distance-based models this may even distort the results.
Nevertheless, in certain contexts such variables can capture subtle or
complementary aspects that contribute to distinguishing one group from another,
so their relevance ultimately depends on the specific analytical scenario.
\begin{figure}[H]
  \centering
  \includegraphics[width=1\textwidth]{images/correlation-matrix}
  \caption{Correlation Matrix}
  \label{fig:correlation-matrix}
\end{figure}
\noindent
As shown in Figure~\ref{fig:correlation-matrix}, the features \texttt{get}, \texttt{unassigned\_referer}, \texttt{sfm\_cors}, \texttt{sfm\_navigate}, \texttt{sfm\allowbreak\_no\_cors}, \texttt{sfu\_?1}, \texttt{ct\_image}, \texttt{ct\_application} and \texttt{xml\_http\_requests} exhibit strong linear correlations, with coefficients of at least $0.9$.  
A reduced dataset is therefore produced where these features are removed to evaluate whether clustering performance improves.  
This step is not only a dimensionality reduction process, but also a way to assess which signals are actually necessary to represent distinct behaviours. \\[6pt]
Finally, features are standardized using the Z-score transformation,
\[
z = \frac{x - \mu}{\sigma},
\]
so that variables with large numeric ranges do not dominate the clustering algorithm.  
Standardization ensures that all features contribute with equal weight and that distance metrics reflect differences in behavior rather than scale.

\section{Dip Test for Multimodality}
It is necessary to understand whether the dataset is likely to contain more than one underlying group.  
The Dip Test is a statistical tool that helps detect multimodality by analysing the distribution of pairwise distances between samples (see Section~\ref{dip-test}).  
If the distance distribution contains more than one significant peak, the dataset is unlikely to be homogeneous and a single-cluster model would not be appropriate.  
This step therefore acts as a preliminary validation that can prevent misleading interpretations in later stages. \\[6pt]
In practice, computing the full pairwise distance matrix for this dataset is not feasible.  
With 87,942 samples and 31 features, evaluating all combinations would generate $\frac{87942 \times 87941}{2}$ distance values, approximately $3.87$ billion entries.  
Since each \texttt{float64} element requires 8 bytes, the total memory usage would reach about 28.81~GB.  
The workstation used in this project has 16~GB of RAM, so a full calculation cannot be performed. \\[6pt]
A possible workaround is to apply the Dip Test to a lower dimensional representation.  
One common solution is to run PCA and apply the test only to the first principal component.  
This approach assumes that most of the structure of the dataset is captured by a small number of linear components.  
If the data were approximately linear, the first two or three principal components should explain most of the variance. \\[6pt]
For this reason, PCA is used to explore whether the dataset behaves linearly:
\begin{verbatim}
def _explore_dataset_linearity(self):
    def plot_cumulative_variance(explained_variance_ratio: np.ndarray):
        plt.figure(figsize=(8, 5))
        plt.plot(
            range(1, len(explained_variance_ratio) + 1),
            explained_variance_ratio,
            marker="o",
            linestyle="--",
        )
        plt.title("Elbow Method for PCA")
        plt.xlabel("Number of Principal Components")
        plt.ylabel("Cumulative Explained Variance")
        plt.grid(True)
        plt.show()

    X = self._X
    pca = PCA(n_components=0.9)
    pca.fit(X)
    eigenvalues = pca.explained_variance_ratio_
    explained_variance_ratio = np.cumsum(eigenvalues)
    logging.info(f"Eigenvalues: {np.round(eigenvalues, 5)}")
    plot_cumulative_variance(explained_variance_ratio=explained_variance_ratio)
\end{verbatim}
The expected outcome, if the dataset were roughly linear, is that only the first few eigenvalues would be significantly larger than zero.  
Equivalently, two or three principal components should capture around 90\% of the variance.  
However, in this case 16 components are needed to reach this threshold, which indicates that the information is distributed across many directions:

\begin{lstlisting}
Eigenvalues: [0.24604 0.1277  0.09541 0.06468 0.04382 0.03985 0.03947 0.03472
             0.03413 0.0319  0.02983 0.02965 0.02731 0.02537 0.02475 0.02021]
\end{lstlisting}

\begin{figure}[H]
  \centering
  \includegraphics[width=1\textwidth]{images/pca_explained_variance}
  \caption{PCA Explained Variance}
  \label{fig:pca-explained-variance}
\end{figure}
\noindent
Since the dataset does not exhibit a clear linear structure, applying the Dip Test only to the first principal component would ignore most of the information.  
This would produce results that are not reliable and could misrepresent the real geometry of the data.  
For this reason, the PCA--Dip approach is discarded. \\[6pt]
The alternative is to keep the original feature space but reduce the number of samples in a meaningful way.  
A naive sampling method, such as randomly selecting a subset of sessions, would risk removing relevant behaviors and modifying the underlying distribution.  
The goal is not just to reduce the volume of data, but to preserve the relative distances between samples so that the Dip Test remains interpretable. \\[6pt]
To achieve this, a SOM is adopted.  
When correctly trained, a SOM preserves the topology of the input space: samples that are close in the original dataset remain close on the resulting grid.  
This allows replacing individual sessions with the weight vectors of SOM neurons.  
These weight vectors keep the original dimensionality, so this procedure is not a form of dimensionality reduction.  
Instead, it compresses the dataset into a smaller set of representative prototypes, reducing the number of points on which the Dip Test is applied while maintaining the global structure of the data.

\section{Self-Organizing Map}
Before setting the parameters, it is important to clarify the role of the SOM within the proposed solution. The SOM is not employed to predict or classify. Instead, it provides a low-dimensional representation of the data, where sessions that behave in a similar way should be mapped close to one another. The quality of this representation depends on the ability of the map to preserve distances from the original space and reflect meaningful neighbourhood relations. \\[6pt]
From a design perspective, the SOM parameters must support these goals. Therefore, the selection of the map size, neighbourhood radius, distance metric and training policy is not arbitrary. They affect how the model learns the structure of the dataset and how stable the resulting representations are. \\[6pt]
The Python library used in this thesis to construct the SOM is \textbf{MiniSom}~\cite{minisom}. The SOM has several parameters that must be defined, such as:
\begin{itemize}
    \item \textbf{x} and \textbf{y}: the dimensions of the map. In this work, $x = y$, resulting in a square map.
    \item \textbf{input\_len}: the dimensionality of the input vectors (i.e., the number of features).
    \item \textbf{sigma}: the initial spread of the neighborhood function.
    \item \textbf{learning\_rate}: the initial learning rate.
    \item \textbf{decay\_function}: the method used to reduce the learning rate over time.
    \item \textbf{neighborhood\_function}: the kernel used to define how neighboring neurons are influenced during training.
    \item \textbf{topology}: the map topology (e.g., rectangular or hexagonal).
    \item \textbf{activation\_distance}: the metric used to compute distances when assigning input vectors to neurons.
    \item \textbf{random\_seed}: random initialization seed to ensure reproducibility.
    \item \textbf{sigma\_decay\_function}: the method used to update the neighborhood radius during training.
\end{itemize}
Starting from the dimensional perspective, the topographic product does not provide substantial information, as already discussed in Section~\ref{som}. Since the dataset is not linear, the topographic product oscillates between small negative and positive values, often remaining close to zero. Therefore, this metric cannot be used to determine the optimal size of the map. Moreover, because the computation of the topographic product is computationally demanding, only the map sizes of 10, 15, 20, 25, 30, 35, and 40 were evaluated.
\begin{figure}[H]
  \centering
  \includegraphics[width=1\textwidth]{images/tp}
  \caption{Topographic Product}
  \label{fig:topographic-product}
\end{figure}
\noindent
A common rule of thumb to define the map size is \( x = y = 5\sqrt{M} \), where \(M\) denotes the number of data samples; in this case \(M = 39\). This rule is adopted in the present work, as it yields satisfactory results in terms of quantization error and topographic error. These two errors were already examined in Section~\ref{som}, and lower values indicate better performance. \\[6pt]
Unfortunately, in version 2.3.5 of MiniSom, the batch training mode is not available, and the function named \texttt{train\_batch} in that version does not correspond to Kohonen's batch training algorithm is not available. Since the author of the Self Organizing Map, Kohonen, introduced this algorithm in 2013, in this thesis the batch version is implemented so that it is possible to evaluate whether it performs better than the standard online training. It should be noted that the batch algorithm does not require a learning rate. \\[6pt]
Regarding the remaining parameters, such as \texttt{sigma}, the \texttt{learning\_rate} in the case of standard training, the \texttt{neighborhood\_function} and the \texttt{topology}, they remain unchanged. This choice is mainly motivated by the heuristics provided by Kohonen, according to which the initial \texttt{sigma} should be approximately one half of the map side length in the case of a quadratic topology, while for the online method the \texttt{learning\_rate} should start at a relatively high value. The Gaussian neighborhood function is the classical formulation discussed in the original works. These recommendations are described in Kohonen's articles. What is optimized through Optuna in order to determine the best configuration are the \texttt{decay\_function}, the \texttt{sigma\_decay\_function} and the \texttt{activation\_distance}. \\[6pt]
The hyperparameter tuning procedure operates as follows. The two algorithms are executed under different training configurations. In the online training setting, the number of iterations is set to $1000$, and the batch size is equal to $1$. Regarding the initialization of the model parameters, the online training approach employs random weight initialization, whereas the batch training approach uses initialization based on principal component analysis (PCA). \\[6pt]
It is important to note that a single iteration of the batch training procedure is not equivalent to one iteration of the online training procedure, since the batch update internally cycles through the entire dataset. After multiple runs, the configuration that produces the best performance is selected, and the corresponding hyperparameters are retained. Eventually, both algorithms converged toward the same optimal set of parameters, namely:
\begin{lstlisting}
{'decay_function': 'asymptotic_decay', 'sigma_decay_function': 'inverse_decay_to_one', 'activation_distance': 'cosine'}
\end{lstlisting}
Both training methods effectively train the map using the appropriate number of iterations. In the case of online training, the number of iterations is $500 \cdot x \cdot y$ where \(x\) and \(y\) are the dimensions of the map. In the case of batch training, the number of iterations is \(20\). These values are not chosen arbitrarily, they are suggested in the work of Kohonen.
\begin{figure}[H]
  \centering
  \includegraphics[width=0.7\textwidth]{images/train_vs_train-batch}
  \caption{Online Training versus Batch Training}
  \label{fig:train_vs_train-batch}
\end{figure}
\noindent
As shown in Figure~\ref{fig:train_vs_train-batch}, batch training achieves consistently very good results, and it performs better than online training.
\begin{lstlisting}
[train] QE = 1.77805 | TE = 0.14707
[train batch] QE = 0.96502 | TE = 0.04284
\end{lstlisting}
To assess whether the obtained results are satisfactory, the quantization error (QE) and the topographic error (TE) should be as low as possible (see Section~\ref{som}). \\[6pt]
Before applying any clustering method, it is necessary to verify whether the weights are likely to form distinct groups. To this end, the Dip Test is employed to assess the null hypothesis of unimodality. The procedure was presented in Section~\ref{dip-test}. The resulting $p$-value is:
\begin{lstlisting}
dip p-value: 0.02567051939734566
\end{lstlisting}
Since the obtained p\textendash value is lower than the conventional significance threshold of $0.05$, the null hypothesis of unimodality can be rejected. Consequently, the weight distribution of the SOM is likely to be multimodal, which supports the application of a clustering algorithm. \\[6pt]
However, as shown below, the distribution of the pairwise distances, along with the empirical cumulative distribution function (ECDF), exhibits a unimodal behavior, as no evident flat regions are present.
\begin{figure}[H]
  \centering
  \includegraphics[width=1\textwidth]{images/distribution_ecdf_som_weights}
  \caption{Distribution of the pairwise distances and ECDF of SOM weights}
  \label{fig:distribution_ecdf_som_weights}
\end{figure}
\noindent
At this stage, a new SOM map was trained on the dataset after the correlated features had been removed, and this approach appears to yield better results. A secondary, smaller mode emerges in the resulting distribution, which may indicate a tendency toward multimodality.
\begin{figure}[H]
  \centering
  \includegraphics[width=1\textwidth]{images/distribution_ecdf_som_weights-cfr}
  \caption{Distribution of the pairwise distances and ECDF of SOM weights with correlated features removed}
  \label{fig:distribution_ecdf_som_weights-cfr}
\end{figure}
\noindent
To verify this result, the $p$-value of the Dip Test is reported below:
\begin{lstlisting}
dip p-value: 0.0
\end{lstlisting}
The clustering results may also be examined, even though these indices have already been discussed in detail in Section~\ref{internal-validation-indices}, where it was shown that they do not always coincide with expert judgement. \\[6pt]
In the present case, the validation indices do not provide consistent indications. The best values of both the Silhouette coefficient and the Davies--Bouldin index are obtained at $k = 23$, whereas the DuNN OWA method identifies $k = 3$ as the optimal number of clusters. \\[6pt]
In contrast, for the SOM trained with correlated features removed, the silhouette score and the DuNN\textendash OWA metric are coherent. These metrics have performed reliably in experiments using the \textit{make\_moons} and \textit{make\_blobs} datasets available in scikit\textendash learn. Moreover, The silhouette value is greater than $0.5$, which indicates satisfactory clustering quality. \\[6pt]
The SOM trained without the correlated features performs better than the SOM trained using all features, the map is reported below (Figure~\ref{fig:som_map_cfr}). The interpretation of the SOM map is that the neurons shown in black are very distant from the neighbouring neurons, which indicates that they are different, and possibly that the sessions belonging to them behave differently.
\begin{figure}[H]
    \centering
    \begin{subfigure}{0.48\textwidth}
        \centering
        \includegraphics[width=\textwidth]{images/clustering-som-weights}
        \caption{Internal metrics for SOM weights}
        \label{fig:clustering-som-weights}
    \end{subfigure}
    \hfill
    \begin{subfigure}{0.48\textwidth}
        \centering
        \includegraphics[width=\textwidth]{images/clustering-som-weights-crf}
        \caption{Internal metrics for SOM weights with correlated features removed}
        \label{fig:clustering-som-weights-crf}
    \end{subfigure}
    \caption{Comparison of internal metrics for SOM weights before and after removing correlated features}
    \label{fig:clustering-som-comparison}
\end{figure}

\begin{figure}[H]
  \centering
  \includegraphics[width=0.8\textwidth]{images/som_map_cfr.png}
  \caption{SOM map with correlated features removed}
  \label{fig:som_map_cfr}
\end{figure}
\noindent
Two distinct clusters are expected to exhibit different behaviours. It is plausible that one cluster represents human navigation, or at least traffic patterns that mimic human navigation, while the other appears to be automated. To assess this hypothesis, a subset of neurons is analysed manually.
\begin{lstlisting}
Neuron: (0, 10)
=== Session 2744 ===
user_agent                                       Go-http-client/1.1
start_time                                 2025-11-16T11:32:00.000Z
end_time                                   2025-11-16T11:51:00.000Z
total_number_requests                                            59
get                                                              59
unassigned_referer                                               59
error_status_code_403                                            59
time_interval                                                  19.0

=== Session 2812 ===
user_agent                                   python-requests/2.31.0
start_time                                 2025-11-13T21:20:00.000Z
end_time                                   2025-11-13T21:21:00.000Z
total_number_requests                                            51
get                                                              43
post                                                              8
unassigned_referer                                               51
error_status_code_403                                            51
time_interval                                                   1.0

Neuron: (1, 10)
=== Session 34278 ===
user_agent                     Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Ge...
start_time                                              2025-11-16T05:31:00.000Z
end_time                                                2025-11-16T05:57:00.000Z
total_number_requests                                                          8
get                                                                            8
unassigned_referer                                                             7
ct_text                                                                        3
error_status_code_403                                                          6
avg_response_content_length                                          8371.333333
time_interval                                                               26.0
\end{lstlisting}
These sessions are generated by automated scripts. The first two can be identified from the user agent, while the third can be inferred from unassigned referrers and from the fact that the number of HTTP~403 errors is almost equal to the total number of requests. It is also possible to understand that it is an automated script because it does not include the Sec--Fetch headers and it is likely not a genuine Mozilla client, but a script that modifies the User--Agent. This highlights the importance of these new features, which help distinguish legitimate traffic from automated traffic. \\[6pt]
In contrast, a normal navigation process is observed. Almost all of the Secure Fetch (Sec--Fetch) headers are present, only a single HTTP~404 error occurs, and the total number of requests is $151$.
\begin{lstlisting}
Neuron: (35, 0)
=== Session 252 ===
user_agent                     Mozilla/5.0 (Windows NT 10.0; Win64; x64) Appl...
start_time                                              2025-11-12T07:35:00.000Z
end_time                                                2025-11-12T07:46:00.000Z
total_number_requests                                                        151
get                                                                          117
post                                                                          34
unassigned_referer                                                           114
sfd_image                                                                    114
sfd_document                                                                   1
sfm_cors                                                                      35
sfm_navigate                                                                   1
sfm_no_cors                                                                  115
sfs_same_origin                                                              150
sfs_none                                                                       1
sfu_?1                                                                         1
ct_image                                                                     114
ct_font                                                                        0
ct_application                                                                35
ct_text                                                                        2
xml_http_requests                                                             35
max_requests_per_minute                                                      2.0
error_status_code_404                                                          1
avg_response_content_length                                          1048.925926
max_page_ratio                                                          0.006623
time_interval                                                               11.0
\end{lstlisting}
In this case, such an additional step is not required. However, when the number of clusters increases, the manual analysis becomes time consuming. As already emphasized multiple times, clustering evaluation metrics do not provide definitive verdicts. Since $k = 23$ yields the best results in the SOM representation using all features, an experiment with a large language model (LLM) is conducted to evaluate whether it is capable of understanding the context and identifying whether a session is malicious.

\section{LLM for labelling}
\subsection{Design Rationale for the Labeling Approach}
The goal of the labeling phase is to provide a meaningful interpretation of the clusters produced by K-Means and the Self-Organizing Map. A cluster without an explanation is difficult to use in any practical scenario. For this reason, the design of the labeling method must consider both the structure of the data and the way in which patterns emerge at the session level. \\[6pt]
The first design decision concerns the nature of the labels. Instead of focusing on very specific features (for example, large numbers of image requests or repeated failures), the labels should describe a higher-level intent or behaviour. This abstraction makes the results easier to compare across different clusters and different datasets. It also supports future work, since behavioral labels can be reused even if the underlying raw features change. \\[6pt]
A second design choice is the use of representative samples rather than full session histories. Full inspection of every session would be impractical, especially when the number of neurons grows. The model is therefore asked to infer the dominant behaviour from a limited number of sessions per cluster and a few raw HTTP requests for each session. This reduces the computational cost, but it also creates a trade-off: some contextual information is removed, and small variations may become invisible. The design accepts this limitation because the objective is to produce an initial hypothesis, not to provide a definitive classification. \\[6pt]
The third design element consists of treating previously assigned labels as part of the system state. When a label has already been assigned, it should be reused if a similar pattern emerges. This choice is motivated by the need for consistency, since assigning independent names to similar clusters would increase ambiguity and hinder comparison. Reusing existing labels encourages the model to connect current evidence with previously acquired knowledge. New labels are introduced only when the observed behaviour clearly differs from the existing descriptions. This principle has already been addressed in \cite{text-clustering-as-classification-with-llms}. \\[6pt]
Finally, the method assumes that automatic labeling cannot fully replace expert judgment. The model is used to reduce the manual workload and to highlight potential behavioural patterns, but the final evaluation remains a human task. This is important because the system may extract superficial signals from the data, or it may generalize based on incomplete evidence. Therefore, the design explicitly includes a validation phase in which the analyst compares session statistics, raw requests, and known traffic patterns to decide whether the assigned label is credible. \\[6pt]
Since the map containing all the features shows the best values of the Silhouette score and the Davies--Bouldin Index at $k = 23$, the K-Means algorithm is executed with $k = 23$. The resulting map is presented below.
\begin{figure}[H]
  \centering
  \includegraphics[width=1\textwidth]{images/som_map}
  \caption{SOM Map}
  \label{fig:som_map}
\end{figure}
\noindent
The neurons display clear coloration, which makes it possible to observe that neurons located farther away, represented by darker colors, form the boundaries of the clustering. This effect does not appear in the previous example, where only two clusters are present. In the current case, manually examining each cluster is very time-consuming, particularly because a large number of neurons must be inspected. \\[6pt]
To address this issue, large language models (LLMs) can assist in automating the labeling process. In this work, only three clusters are analyzed, and for each cluster five sessions are considered. For every session, five raw requests are randomly selected and examined. This procedure serves merely as a preliminary test, since a full analysis would require an excessive number of tokens and represents a promising direction for improvement, possibly by employing a local LLM.
Below, the system prompt is defined, as it is an important component when working with large language models:
\begin{lstlisting}
system_prompt = """
Self-Organizing Map Neuron Labeling - Behavioral/Intent Categories

You are an analyst working with a Self-Organizing Map (SOM) that clusters web sessions based on HTTP traffic patterns. Each neuron in the SOM represents a group of sessions with similar characteristics. Your task is to assign a meaningful, high-level behavioral label to a single neuron based on the data provided.

You will receive:
- Some sessions mapped to the same cluster, each with summarized statistics.
- For one session, a sample of some raw HTTP requests.
- A list of previously assigned labels and their descriptions (to avoid duplicates or near-duplicates).

Labeling guidelines:
- Focus on the main purpose, behavioral pattern, or security-relevant signals reflected by these sessions.
- Do not simply summarize atomic features (such as "onlyimage" or "manyget"). Instead, infer the higher-level intent or behavior suggested by the features.
- Consider whether the sessions resemble typical human browsing, automated bots, crawlers, background tasks, or other patterns of interest for traffic and security analysis.
- If the behavior matches or is very similar to an existing label (including its description), reuse that label and description exactly. Only create a new label and description if no existing label is suitable.
- Descriptions must be a single brief sentence and reused if similar patterns arise.

The very first labeling task will not include any existing labels and will be used to define the initial label set.

Expected output format:
Return a JSON object exactly as follows (no explanations, no extra text):
{
"label": <one-word category label>,
"description": <very brief explanation, suitable for cluster analysis, of why this label was chosen>
}
"""
\end{lstlisting}
An example of the user prompt is provided below. In the actual system, empty fields or fields containing 0 are included, even though they are omitted here:
\begin{lstlisting}
--- Session Summary ---
{
  "total_number_requests": 525,
  "get": 525,
  "unassigned_referer": 525,
  "ct_application": 241,
  "ct_text": 284,
  "max_requests_per_minute": 0.0,
  "error_status_code_401": 284,
  "time_interval": 281.0
}
--- Raw Requests Sample ---
[
  {
    "method": "GET",
    "uri_path": "[REDACTED]",
    "content-type": "application/json",
    "http_code": 200
  },
  {
    "method": "GET",
    "uri_path": "[REDACTED]",
    "content-type": "text/html",
    "http_code": 401
  },
  {
    "method": "GET",
    "uri_path": "[REDACTED]",
    "content-type": "text/html",
    "http_code": 401
  },
  {
    "method": "GET",
    "uri_path": "[REDACTED]",
    "content-type": "application/json",
    "http_code": 200
  },
  {
    "method": "GET",
    "uri_path": "[REDACTED]",
    "content-type": "application/json",
    "http_code": 200
  }
]
\end{lstlisting}

Only three clusters are labeled by the LLM. The labels and their descriptions are the following:
\begin{lstlisting}
{
  "12": {
    "label": "pageview",
    "description": "Multiple GET-only requests for same-origin images, styles and scripts with occasional navigations, consistent with normal browser page loads."
  },
  "0": {
    "label": "pageview",
    "description": "Multiple GET-only requests for same-origin images, styles and scripts with occasional navigations, consistent with normal browser page loads."
  },
  "1": {
    "label": "api-probing",
    "description": "Repeated unauthenticated GET requests to API endpoints with many 401 responses and no referer, consistent with automated API scanning or probing."
  }
}
\end{lstlisting}
Then, it is possible to manually analyze these clusters and evaluate whether both the clustering and the labeling are appropriate. \\[6pt]
For cluster 1, the neurons are located at $(2,37)$ and $(2,38)$, the remaining neurons are empty, and the results are consistent with the labels and the description.
\begin{lstlisting}
user_agent                                             axios/1.13.2
start_time                                 2025-11-14T06:20:00.000Z
end_time                                   2025-11-14T11:30:00.000Z
total_number_requests                                            58
get                                                              58
unassigned_referer                                               58
ct_text                                                          58
error_status_code_401                                            58
time_interval                                                 310.0

user_agent                     Mozilla/5.0 (Linux; Android 10; K) AppleWebKit...
start_time                                              2025-11-12T12:55:00.000Z
end_time                                                2025-11-12T13:18:00.000Z
total_number_requests                                                          8
get                                                                            5
post                                                                           3
unassigned_referer                                                             4
ct_application                                                                 4
ct_text                                                                        4
xml_http_requests                                                              4
error_status_code_401                                                          3
time_interval                                                               23.0
\end{lstlisting}
In the first case, the User-Agent information helps to identify the type of session, and it is also relevant because the total number of requests is equal to the number of HTTP 401 errors. The second session was grouped into this cluster due to the presence of HTTP 401 errors. However, this evidence alone does not allow us to conclude that the session is malicious. Some fields, such as \texttt{ct\_application} and \texttt{xml\_http\_requests}, appear consistent with legitimate activity. It is possible that the attacker modified the headers. While this is a session in the cluster 0, labeled as pageview:
\begin{lstlisting}
=== Session 37357 ===
user_agent                     Mozilla/5.0 (Windows NT 10.0; Win64; x64) Appl...
start_time                                              2025-11-12T17:13:00.000Z
end_time                                                2025-11-12T17:16:00.000Z
total_number_requests                                                         34
get                                                                           34
unassigned_referer                                                            28
sfd_image                                                                     11
sfd_document                                                                   2
sfd_script                                                                    12
sfd_font                                                                       2
sfm_cors                                                                       2
sfm_navigate                                                                   2
sfm_no_cors                                                                   30
sfs_cross_site                                                                 2
sfs_same_origin                                                               32
sfu_?1                                                                         2
ct_image                                                                      11
ct_application                                                                 2
ct_text                                                                       21
max_requests_per_minute                                                      1.0
max_page_ratio                                                          0.058824
time_interval                                                                3.0
\end{lstlisting}
The presence of the \texttt{Sec-Fetch-*} headers suggests that the request originates from a real browser operated by a human user. In particular, the header \texttt{Sec-Fetch-Mode: navigate} indicates that a navigation action was performed, which may correspond to an intentional visit to the page labeled as a \textit{pageview}.
