1
0

update parts of the practical part

This commit is contained in:
surtur 2023-07-31 21:32:56 +02:00
parent 8319a9c07f
commit dea1a6f4fe
Signed by: wanderer
SSH Key Fingerprint: SHA256:MdCZyJ2sHLltrLBp0xQO0O1qTW9BT/xl5nXkDvhlMCI
2 changed files with 267 additions and 106 deletions

@ -1,7 +1,15 @@
% =========================================================================== %
\part{Practical part}
\n{1}{Kudos}
\n{1}{Introduction}
As a part of the task was to build an actual Password Compromise Monitoring
Tool, the development process, the tools used and the outcome are all described
in the following sections. There is also a section concerned with application
architecture, whereby relevant choices and decision motifs are explained.
\n{2}{Kudos}
The program that has been developed as part of this thesis used and utilised a
great deal of free (as in \textit{freedom}) and open-source software in the
@ -63,7 +71,8 @@ the following commands (the \% sign denotes the shell prompt):
\vspace{\parskip}
\begin{lstlisting}[language=bash, caption={Verifying signature of a git commit},
label=gitverif, basicstyle=\linespread{0.9}\footnotesize\ttfamily]
label=gitverif, basicstyle=\linespread{0.9}\small\ttfamily,
backgroundcolor=\color{lstbg}]
% cd <cloned project dir>
% git show --show-signature <commit>
% # alternatively:
@ -85,7 +94,8 @@ from inside of the cloned repository:
\vspace{\parskip}
\begin{lstlisting}[language=bash, caption={Prepare allowed signers file and signature format for git},
label=gitsshprep, basicstyle=\linespread{0.9}\footnotesize\ttfamily]
label=gitsshprep, basicstyle=\linespread{0.9}\small\ttfamily,
backgroundcolor=\color{lstbg}]
% # set the signature format for the local repository.
% git config --local gpg.format ssh
% # save the public key.
@ -168,26 +178,25 @@ kernel version 6.3.x.
\n{2}{Source code repositories}\label{sec:repos}
The git repository containing source code of the \texttt{pcmt} project:\\
\url{https://git.dotya.ml/mirre-mt/pcmt.git}.
The git repository hosting the \texttt{pcmt} configuration schema:\\
\url{https://git.dotya.ml/mirre-mt/pcmt-config-schema.git}.
The repository containing the \LaTeX{} source code of this thesis:\\
\url{https://git.dotya.ml/mirre-mt/masters-thesis.git}.
All of the pertaining source code was published in repositories on a publicly
available git server operated by the author, the reasoning \emph{pro}
self-hosting being that it is the preferred way of guaranteed autonomy over
one's source code, as opposed to large silos owned by big corporations having a
track record of arguably not always deciding with user's best interest in mind
(although recourse has been observed~\cite{ytdl}). When these providers act on
impulse or under public pressure they can potentially at least temporarily
disrupt their user's operations, thus not only beholding their user to their
lengthy \emph{terms of service} that \emph{can change at any time}, but also
factors outside their control. Granted, decentralisation can take a toll on
discoverability of the project, but that is not a concern here.
The git repository containing source code of the \texttt{pcmt} project:\\
\url{https://git.dotya.ml/mirre-mt/pcmt.git}.
The git repository hosting the \texttt{pcmt} configuration schema:\\
\url{https://git.dotya.ml/mirre-mt/pcmt-config-schema.git}.
The repository containing the \LaTeX{} source code of this thesis:\\
\url{https://git.dotya.ml/mirre-mt/masters-thesis.git}.
impulse or under public pressure they can potentially (at least temporarily)
disrupt operations of their users. Thus they are not only beholding their users
to lengthy \emph{terms of service} that \emph{are subject to change at any
given moment}, but also outside factors beyond their control. Granted,
decentralisation can take a toll on discoverability of the project, but that is
only a concern if rapid market penetration is a goal.
\n{2}{Toolchain}
@ -195,19 +204,21 @@ The repository containing the \LaTeX{} source code of this thesis:\\
Throughout the creation of this work, the \emph{then-current} version of the Go
programming language was used, i.e. \texttt{go1.20}.
To read more on why Go was chosen, see Appendix~\ref{appendix:whygo}.
Nix/\texttt{devenv} tools have also aided heavily during development, see
Appendix~\ref{appendix:whynix} to learn more.
To read more on why Go was chosen in particular, see
Appendix~\ref{appendix:whygo}. Equally, Nix and Nix-based tools such as
\texttt{devenv} have also aided heavily during development, more on those is
written in Appendix~\ref{appendix:whynix}.
\tab{Tool/Library-Usage Matrix}{tab:toolchain}{1.0}{ll}{
\textbf{Name} & \textbf{Usage} \\
\textbf{Tool/Library} & \textbf{Usage} \\
Go programming language & program core \\
Dhall configuration language & program configuration \\
Echo & HTTP handlers, controllers, web server \\
Echo & HTTP handlers, controllers \\
ent & ORM using graph-based modelling \\
pq & Pure-Go Postgres drivers \\
bluemonday & sanitising HTML \\
TailwindCSS & stylesheets using a utility-first approach \\
PostgreSQL & persistently storing data \\
TailwindCSS & utility-first approach to cascading style sheets \\
PostgreSQL & persistent data storage \\
}
Table~\ref{tab:depsversionmx} contains the names and versions of the most
@ -219,27 +230,35 @@ application.
\texttt{echo} (\url{https://echo.labstack.com/}) & 4.10.2 \\
\texttt{go-dhall} (\url{https://github.com/philandstuff/dhall-golang}) & 6.0.2\\
\texttt{ent} (\url{https://entgo.io/}) & 0.11.10 \\
\texttt{pq} (\url{https://github.com/lib/pq/}) & 1.10.7 \\
\texttt{bluemonday} (\url{https://github.com/microcosm-cc/bluemonday}) & 1.0.23 \\
\texttt{tailwindcss} (\url{https://tailwindcss.com/}) & 3.3.0 \\
\texttt{PostgreSQL} (\url{https://www.postgresql.org/}) & 15.2 \\
}
Additionally, dependency-version mapping can be inferred from looking at the
\texttt{go.mod} file's first \textit{require} block at any point in time.
\n{1}{Application architecture}
The application is written in Go and uses \textit{gomodules}. The full name of
the module is \texttt{git.dotya.ml/mirre-mt/pcmt}.
\n{2}{Package structure}
The source code of the main module is organised into smaller, self-contained Go
The source code of the module is organised into smaller, self-contained Go
\emph{packages} appropriately along a couple of domains: logging, core
application, web routers, configuration and settings, etc. In Go, packages are
delimited by folder structure -- each folder can be a package.
Generally speaking, the program aggregates decision points into central places,
such as \texttt{run.go}, which then imports child packages that facilitate each
of the task of loading the configuration, connecting to the database and
of the tasks of loading the configuration, connecting to the database and
running migrations, consolidating flag, environment variable and
configuration-based values into canonical \emph{settings}, setting up routes
and handling graceful shutdown.
configuration-based values into canonical \emph{settings} \texttt{struct},
setting up web routes, authenticating requests, or handling \texttt{signals}
and performing graceful shutdowns.
\n{3}{Internal package}
@ -250,10 +269,11 @@ package to prevent accidental imports.
\n{2}{Logging}
The program uses dependency injection to share a single logger instance,
The program uses \emph{dependency injection} to share a single logger instance,
similar applies to the database client. These are passed around as a pointer,
so the underlying data stays the same. As a rule of thumb, every larger
\texttt{struct} that needs to be passed around is passed around as a pointer.
so the underlying data stays the same. As a rule of thumb throughout the
application, every larger \texttt{struct} that needs to be passed around is
passed around as a pointer.
\n{2}{Authentication}
@ -446,7 +466,10 @@ application whenever the application is determined to be ready for it.
\smallskip
% \vspace{\baselineskip}
\begin{lstlisting}[language=Haskell, caption={Dhall configuration schema version 0.0.1-rc.2},
label=dhallschema, basicstyle=\linespread{0.9}\footnotesize\ttfamily]
label=dhallschema, basicstyle=\linespread{0.9}\footnotesize\ttfamily,
backgroundcolor=\color{lstbg},
morekeywords={Text, Natural, Optional, Type}
]
let Schema =
{ Type =
{ Host : Text
@ -508,26 +531,31 @@ allow for grouping of related functionality. For instance, configuration
settings pertaining mailserver setup are grouped in a record named
\textbf{Mailer}. Its attribute \textbf{Enabled} is annotated as \textbf{Bool},
which was deemed appropriate for a on-off switch-like functionality, with the
only permissible values being either \emph{True} or \emph{False}. Do note that
in Dhall $true != True$, since \textbf{True} is internally a Bool constant,
which is built into Dhall (check out ``The Prelude''~\cite{dhallprelude}),
while \textbf{true} is evaluated as an \emph{unbound} variable, that is, a
variable \emph{not} defined in the current \emph{scope} and thus not
\emph{present} in the current scope.
only permissible values being either \emph{True} or \emph{False}.
Another one of Dhall specialties is that `$==$' and `$!=$' (in)equality
Do note that in Dhall $true\ != True$, since internally \textbf{True} is a
\texttt{Bool} constant built directly into Dhall (see ``The
Prelude''~\cite{dhallprelude} for reference), while \textbf{true} is evaluated
as an \emph{unbound} variable, that is, a variable \emph{not} defined in the
current \emph{scope} and thus not \emph{present} in the current scope.
Another one of Dhall's specialties is that `$==$' and `$!=$' (in)equality
operators \textbf{only} work on values of type \texttt{Bool}, which for example
means that variables of type \texttt{Natural} (\texttt{uint}) or \texttt{Text}
(\texttt{string}) cannot be compared directly as in other languages, which
either leaves the work for a higher-level language (such as Go), or from the
perspective of the Dhall authors, \emph{enums} are promoted when the value
matters.
(\texttt{string}) cannot be compared directly as is the case in other
languages. That either leaves the comparing work for a higher-level language
(such as Go). Alternatively, from the perspective of the Dhall authors
\emph{enums} are the promoted way to solve this when the value matters, i.e.\
derive a custom \emph{named} type from a primitive type and compare
\emph{that}.
\newpage
% \vspace{\parskip}
\begin{lstlisting}[language=Haskell, caption={Dhall configuration defaults for
schema version 0.0.1-rc.2},
label=dhallschemadefaults, basicstyle=\linespread{0.9}\footnotesize\ttfamily]
label=dhallschemadefaults, basicstyle=\linespread{0.9}\footnotesize\ttfamily,
backgroundcolor=\color{lstbg},
]
, default =
-- | have sane defaults.
{ Host = ""
@ -668,18 +696,22 @@ following:
These methods can further be imported into other packages and this makes
working with the database a morning breeze.
TODO: save admin search queries in application
\n{1}{Deployment}
\textbf{TODO}: mention how \texttt{systemd} aids in running the pod.
\textbf{TODO}: mention how \texttt{systemd} aids in running the pod.\\
\\
A deployment setup as suggested in Section~\ref{sec:deploymentRecommendations}
is already partially covered by the multi-stage \texttt{Containerfile} that is
available in the main sources. Once built, the resulting container image only
contains a handful of things it absolutely needs:
\begin{itemize}
\item a statically linked copy of the program
\item a statically linked copy of the program (contains parts of the Go
stdlib)
\item a default configuration file and corresponding Dhall expressions cached
at build time
\item a recent CA certs bundle
@ -687,13 +719,11 @@ contains a handful of things it absolutely needs:
Since the program also needs a database for proper functioning, an example
scenario includes the application container being run in a Podman \textbf{pod}
together with the database. That results in not having to expose the database
to the entire host or out of the pod at all, it is only be available over pod's
\texttt{localhost}.
It goes without saying that the default values of any configuration secrets
should be substituted by the application operator with new, securely generated
ones.
(as in a pea pod or pod of whales) together with the database. That results in
not having to expose the database to the entire host or out of the pod at all,
it is only be available over pod's \texttt{localhost}. Hopefully it goes
without saying that the default values of any configuration secrets should be
substituted by the application operator with new, securely generated ones.
\n{2}{Rootless Podman}
@ -717,67 +747,91 @@ Podman, i.e.\ it has \texttt{UID}/\texttt{GID} mapping entries in
Podman commands.
% \newpage
\begin{lstlisting}[language=bash, caption={Example application deployment using
rootless Podman},
label=podmanDeployment, basicstyle=\linespread{0.9}\small\ttfamily]
label=podmanDeployment, basicstyle=\linespread{0.9}\small\ttfamily,
backgroundcolor=\color{lstbg}, commentcolor=\color{Gray},
morekeywords={mkdir,podman,just},
]
# From inside the project folder, build the image locally using kaniko.
just kaniko
# Create a pod.
podman pod create --userns=keep-id -p3005:3000 --name pcmt
# Create a pod, limit the amount of memory/CPU available to its containers.
podman pod create --replace --name pcmt \
--memory=100m --cpus=2 \
--userns=keep-id -p3005:3000
# Run the database in the pod.
# Create the database folder and run the database in the pod.
mkdir -pv ./tmp/db
podman run --pod pcmt --replace -d --name "pcmt-pg" --rm \
-e POSTGRES_INITDB_ARGS="--auth-host=scram-sha-256 \
--auth-local=scram-sha-256" \
-e POSTGRES_PASSWORD=postgres -v $PWD/tmp/db:/var/lib/postgresql/data \
-e POSTGRES_PASSWORD=postgres \
-v $PWD/tmp/db:/var/lib/postgresql/data:Z \
--health-cmd "sh -c 'pg_isready -U postgres -d postgres'" \
--health-on-failure kill \
--health-retries 3 \
--health-interval 10s \
--health-timeout 1s \
--health-start-period=5s \
docker.io/library/postgres:15.2-alpine3.17
# Run the application in the pod.
# Run the application itself in the pod.
podman run --pod pcmt --replace --name pcmt-og -d --rm \
-e PCMT_LIVE=False \
-e PCMT_DBTYPE="postgres" \
-e PCMT_CONNSTRING="host=pcmt-pg port=5432 sslmode=disable \
user=postgres dbname=postgres password=postgres"
-v $PWD/config.dhall:/config.dhall:ro \
-v $PWD/config.dhall:/config.dhall:Z,ro \
docker.io/immawanderer/pcmt:testbuild -config /config.dhall
\end{lstlisting}
\vspace*{-\baselineskip}
To summarise Listing~\ref{podmanDeployment}, first, the application
container is built from inside the project folder using \texttt{kaniko}.
Alternatively, the container image could be pulled from the container
repository, but it makes more sense showing the image being built from sources
since the listing depicts a \texttt{:testbuild} tag being used.
To summarise Listing~\ref{podmanDeployment}, first, the application container
is built from inside the project folder using \texttt{kaniko}. Alternatively,
the container image could be pulled from a container repository, but it makes
more sense showing the image being built from sources with the listing
depicting a \texttt{:testbuild} tag being used.
Next, a \emph{pod} is created and given a name, setting the port binding for
the application. Then, the database container is started inside the pod.
the application. Then, the database container is started inside the pod,
configured with a healthchecking mechanism.
As a final step, the application container itself is run inside the pod. The application configuration named \texttt{config.dhall} located in
\texttt{\$PWD} is mounted as a volume into container's \texttt{/config.dhall},
providing the application with a default configuration. The default container
does contain a default configuration for reference, however, running the
container as is without additional configuration would fail as it does not
contain the necessary secrets.
As a final step, the application container itself is run inside the pod. The
application configuration named \texttt{config.dhall} located in \texttt{\$PWD}
is mounted as a volume into container's \texttt{/config.dhall}, providing the
application with a default configuration. The default container does contain a
default configuration for reference, however, running the container without
additionally providing the necessary secretes would fail.
\n{3}{Sanity checks}
Do also note that the application connects to the database using its
\emph{container} name, i.e.\ not the IP address. That is possible thanks to
Podman setting up DNS inside the pod in such a way that all containers in the
pod can reach each other using their (container) names. Interestingly,
connecting via \texttt{localhost} would also work, as from inside the pod, any
container in the pod can reach any other container in the same pod via pod's
\texttt{localhost}.
In fact, \emph{pinging} the database or application containers from an ad-hoc
\texttt{alpine} container added to the pod yields:
Also do note that the application connects to the database using its
\emph{container} name, i.e.\ not the IP address. This is possible thanks to
Podman setting up DNS resolution inside pods using default networks in such a
way that all containers in the pod can reach each other using their (container)
names.
Interestingly, connecting via \texttt{localhost} from containers inside the pod
would also work. Inside the pod, any container in the pod can reach any other
container in the same pod via \emph{pod's} own \texttt{localhost}, thanks to a
shared network name space~\cite{podmanNet}.
In fact, \emph{pinging} (sending ICMP packets using the \texttt{ping} command)
the database and application containers from an ad-hoc Alpine Linux container
that just joined the pod temporarily yields:
\vspace{\parskip}
\begin{lstlisting}[language=bash, caption={Pinging pod containers using their
names}, label=podmanPing, basicstyle=\linespread{0.9}\small\ttfamily]
user@containerHost % podman run --rm -it --user=0 --pod=pcmt \
names}, label=podmanPing, basicstyle=\linespread{0.9}\small\ttfamily,
backgroundcolor=\color{lstbg},
morekeywords={podman,ping}
]
user@containerHost % podman run --rm -it \
--user=0 \
--pod=pcmt \
docker.io/library/alpine:3.18
/ # ping -c2 pcmt-og
/ % ping -c2 pcmt-og
PING pcmt-og (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=42 time=0.072 ms
64 bytes from 127.0.0.1: seq=1 ttl=42 time=0.118 ms
@ -785,7 +839,7 @@ PING pcmt-og (127.0.0.1): 56 data bytes
--- pcmt-og ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.072/0.095/0.118 ms
/ # ping -c2 pcmt-pg
/ % ping -c2 pcmt-pg
PING pcmt-pg (127.0.0.1): 56 data bytes
64 bytes from 127.0.0.1: seq=0 ttl=42 time=0.045 ms
64 bytes from 127.0.0.1: seq=1 ttl=42 time=0.077 ms
@ -793,18 +847,27 @@ PING pcmt-pg (127.0.0.1): 56 data bytes
--- pcmt-pg ping statistics ---
2 packets transmitted, 2 packets received, 0% packet loss
round-trip min/avg/max = 0.045/0.061/0.077 ms
/ #
/ %
\end{lstlisting}
\vspace*{-\baselineskip}
The pod created in Listing~\ref{podmanDeployment} only set the binding for a
port used by the application (\texttt{5005/tcp}). The Postgres default port
Was the application deployed in a traditional manner instead of using Podman,
the use of FQDNs or IPs would be probably be necessary, as there would be no
magic resolution of container names happening transparently in the background.
\n{3}{Database isolation from the host}
A keen observer has undoubtedly noticed that the pod constructed in
Listing~\ref{podmanDeployment} did only create the binding for a port used by
the application (\texttt{5005/tcp}). The Postgres default port
\texttt{5432/tcp} is not among pod's port bindings, as can be seen in the pod
creation command. This can also easily be verified using the command in
Listing~\ref{podmanPortBindings}:
creation command in the said listing. This can also easily be verified using
the command in Listing~\ref{podmanPortBindings}:
\begin{lstlisting}[language=bash, caption={Podman pod port bindings},
label=podmanPortBindings, basicstyle=\linespread{0.9}\small\ttfamily]
\begin{lstlisting}[language=bash, caption={Podman pod port binding inspection},
label=podmanPortBindings, basicstyle=\linespread{0.9}\small\ttfamily,
backgroundcolor=\color{lstbg},
morekeywords={podman},
]
user@containerHost % podman pod inspect pcmt \
--format="Port bindings: {{.InfraConfig.PortBindings}}\n\
Host network: {{.InfraConfig.HostNetwork}}"
@ -813,20 +876,100 @@ Host network: false
\end{lstlisting}
\vspace*{-\baselineskip}
To be absolutely sure, trying to connect to the database from outside of the
pod (i.e. from the container host) should \emph{fail}, unless, of course, there
is another process listening on that port:
To be absolutely sure that the database is available only internally in the pod
(unless, of course, there is another process listening on the subject port),
and that connecting to the database from outside of the pod (i.e. from the
container host) really \emph{does} fail, the following commands can be issued:
\begin{lstlisting}[language=bash, caption={In-pod database is unreachable from
the host}, breaklines=true, label=podDbUnreachable,
basicstyle=\linespread{0.9}\small\ttfamily]
basicstyle=\linespread{0.9}\small\ttfamily,
backgroundcolor=\color{lstbg},
]
user@containerHost % curl localhost:5432
--> curl: (7) Failed to connect to localhost port 5432 after 0 ms: Couldn't connect to server
\end{lstlisting}
\vspace*{-\baselineskip}
The error in Listing~\ref{podDbUnreachable} is expected, as it is the result of
the database port not been exposed from the pod.
The error in Listing~\ref{podDbUnreachable} is indeed expected, as it is the
result of the database port not been exposed from the pod.
Of course, since a volume (essentially a bind mount) from the host is used, the
actual data is still accessible on the host, both to privileged users and the
user running the pod. On the host with SELinux support, the \texttt{:Z} volume
addendum at least ensures that the content of the volume is directly
inaccessible to other containers, including the application container running
inside the same pod, via SELinux labelling.
\n{3}{Health checks}
Running the containers with health checks can be counted among the few crucial
settings. That way
the container runtime can periodically \emph{check} that the application
running inside the container is behaving correctly and instructions can be
provided on what action should be taken, should the health of the application
evaluate unsatisfyingly. Furthermore, different sets of health checking
commands can be passed with Podman for start-up and runtime.
\n{2}{Reverse proxy configuration}
If the application is deployed behind a reverse proxy, such as NGINX, the
configuration snippet in Listing~ref{nginxSnip} might apply. Kindly do note how the
named upstream server \texttt{pcmt} references the port that was exposed from
the pod created in Listing~\ref{podmanDeployment}
\begin{lstlisting}[caption={Example reverse proxy configuration snippet},
breaklines=true, label=nginxSnip, basicstyle=\linespread{0.9}\small\ttfamily,
backgroundcolor=\color{lstbg},
morekeywords={upstream,server,return,listen,server_name,add_header,access_log,error_log,location,proxy_pass,proxy_set_header,allow,include,more_set_headers,ssl_buffer_size,ssl_dhparam,ssl_certificate,ssl_certificate_key},
]
upstream pcmt {
server 127.0.0.1:5005;
}
server {
return 301 https://<pcmt domain>$request_uri;
listen 80;
listen [::]:80;
server_name: <pcmt domain> www.<pcmt domain>;
return 404;
add_header Referrer-Policy "no-referrer, origin-when-cross-origin";
}
server {
server_name <pcmt domain>;
access_log /var/log/nginx/<pcmt domain>.access.log;
error_log /var/log/nginx/<pcmt domain>.error.log;
location / {
proxy_pass http://pcmt;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-For $proxy_add_forwarded_for;
}
location /robots.txt {
allow all;
add_header Content-Type "text/plain; charset=utf-8";
add_header X-Robots-Tag "all, noarchive, notranslate";
return 200 "User-agent: *\nDisallow: /";
}
include sec-headers.conf;
add_header X-Real-IP $remote_addr;
add_header X-Forwarded-For $proxy_add_x_forwarded_for;
add_header X-Forwarded-Proto $scheme;
more_set_headers 'Early-Data: $ssl_early_data';
listen [::]:443 ssl http2;
listen 443 ssl http2;
ssl_certificate /etc/letsencrypt/live/<pcmt domain>/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/<pcmt domain>/privkey.pem;
include /etc/letsencrypt/options-ssl-nginx.conf;
ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;
# reduce TTFB
# ssl_buffer_size 4k;
ssl_buffer_size 4k;
}
\end{lstlisting}
\vspace*{-\baselineskip}
\n{1}{Validation}
@ -835,8 +978,8 @@ the database port not been exposed from the pod.
Unit testing is a hot topic for many people and the author does not count
himself to be a staunch supporter of neither extreme. The ``no unit tests''
seems to discount any benefit there is to unit testing, while a ``
TDD-only''\footnotemark{} approach can be a little too much for some people's
opinion seems to discount any benefit there is to unit testing, while a
``TDD-only''\footnotemark{} approach can be a little too much for some people's
taste. The author tends to prefer a \emph{middle ground} approach in this
particular case, i.e. writing enough tests where meaningful but not necessarily
testing everything or writing tests prior to business logic code. Arguably,
@ -930,7 +1073,11 @@ function is again checked and if everything goes well, the value of the
\smallskip
\smallskip
\begin{lstlisting}[language=Go, caption={Example integration test.},
label=integrationtest,basicstyle=\linespread{0.9}\scriptsize\ttfamily]
label=integrationtest,basicstyle=\linespread{0.9}\scriptsize\ttfamily,
% numbers=left, numberstyle=\tiny\ttfamily\color{codegray},
backgroundcolor=\color{lstbg},
morekeywords=any
]
// modules/user/user_test.go
package user
@ -1025,8 +1172,13 @@ a testing instance; therefore, limits to prevent abuse might be imposed.
\n{3}{Deployment validation}
TODO: show the results of testing the app in prod using
\url{https://testssl.sh/}.
TODO: show the results of testing the app in prod using:
\begin{itemize}
\item \url{https://testssl.sh/}.
\item \url{https://www.ssllabs.com/ssltest/analyze.html?d=testpcmt.dotya.ml}
\item \url{https://securityheaders.com/?q=https%3A%2F%2Ftestpcmt.dotya.ml}
\item \url{https://cryptcheck.fr/https/testpcmt.dotya.ml}
\end{itemize}
% =========================================================================== %

@ -86,6 +86,15 @@ used to validate the correctness of program's behaviour.}
% Následující příkaz nastaví standard PDF/A-1b
\aplikujpdfa
\definecolor{lstbg}{rgb}{.96,.96,.96}
\lstdefinestyle{yaml}{
keywords={true,false,null,y,n,---,...},
rulecolor=\color{black},
string=[s]{'}{'},
string=[s]{"}{"},
comment=[l]{#},
commentstyle=\color{Gray},
}
% =========================================================================== %
\begin{document}