# Technical documentation — vulnerability_scan

Evidence date: 2026-09-29. Revision: `main` `de29c65` (2025-04-02).

Credential values are omitted. Environment variable names, image names, ports, and the Sonar project key below are taken from source.

## What this repository is

A single Bitbucket repository that holds a Python CLI and a Docker Compose recipe for Greenbone Community Edition. The CLI drives gvmd, builds an HTML vulnerability report, and can send it with the `O365` library or insert it into Supabase.

There is one branch, `main`. 17 commits. About 20 tracked files. The large file is `cli_app/test.json` (a saved report, not a test harness).

`sonarqube_scan.bat` was listed by the onboarding scan and is not in git.

Authors on the commits: Jaco Brits (repository shell, Sonar, pipeline, 2025-02-26) and joshgatdew `joshua@overdrive.co.za` (application, 2025-02-28 through 2025-04-02).

## Repository layout

```text
vulnerability_scan/
  readme.md                          Operator install and usage
  bitbucket-pipelines.yml            SonarQube on main
  sonar-project.properties           Project key only
  .gitignore                         Ignores .env, .venv, o365_token.txt
  documentation/                     Two PNG screenshots of a Stack Overflow answer
  cli_app/
    cli.py                           The application (one script)
    requirements.txt                 Pinned Python dependencies
    install_requirements.sh          python3 -m venv .venv && pip install
    Dockerfile                       debian:stable-slim image
    entrypoint.sh                    venv + gosu gvm ./cli.py
    test.json                        Captured GMP report, not read by cli.py
  green_bone_docker/
    docker-compose.yml               Project name greenbone-community-edition
    install_greenbone.sh
    start.sh
    stop.sh
    restart.sh
    streamlogs.sh
    cli_app.sh
    getGvmCLI.sh
    getGvmDockerCLI.sh
```

## Languages and runtime

| Piece | Language | Runtime | Notes |
|---|---|---|---|
| CLI | Python 3 | Host `python3`, or Debian `stable-slim` `python3` in the image | README says Python 3.x. The image's minor version was not built during inspection |
| Packaging | Bash, Dockerfile, Compose | Docker Compose v2 plugin (`docker compose`, not `docker-compose`) | README install is Ubuntu-specific |
| Pipeline | Bitbucket Pipelines YAML | `sonarsource/sonarqube-scan:3.0.2` | `clone.depth: full`. Branch filter `{main}` |

No application framework. Argument parsing is the standard library `getopt`. XML uses `lxml`. GMP uses `python-gvm` `26.1.0`.

`cli.py` is one module. Execution starts at import time: `load_dotenv()`, then argv handling, then a top-level `with GMP(...)` block. There is no `if __name__ == "__main__"` guard.

## Python dependencies

Direct libraries used by `cli.py`:

| Package | Pin | Use |
|---|---|---|
| `python-gvm` | `26.1.0` | GMP client, `UnixSocketConnection`, `TLSConnection`, `SSHConnection`, `EtreeTransform` |
| `lxml` | `5.3.1` | XML tree to dict |
| `python-dotenv` | `1.0.1` | `load_dotenv()` |
| `simple-term-menu` | `1.6.6` | Interactive menus |
| `O365` | `2.1.0` | Microsoft 365 mail |
| `supabase` | `2.14.0` | `create_client` and table insert |

Also pinned, and not imported by `cli.py`: `paramiko` (pulled in for SSH support inside python-gvm), `beautifulsoup4`, and the Supabase client stack (`gotrue`, `postgrest`, `httpx`, and the rest of `requirements.txt`). `smtplib` is imported and only referenced from the commented SMTP function.

`install_requirements.sh` creates `.venv` in the working directory and installs that file with `pip`.

## GMP connection

`create_connection` supports three types:

- `socket` — `UnixSocketConnection` with `path=socketpath`.
- `tls` — `TLSConnection` with hostname, port, and optional cert files.
- anything else — `SSHConnection`.

The live call is socket only:

```text
socket_file = /run/gvmd/gvmd.sock
if /tmp/gvm/gvmd/gvmd.sock is a file:
    socket_file = /tmp/gvm/gvmd/gvmd.sock
connection = create_connection("socket", socketpath=socket_file, timeout=DEFAULT_TIMEOUT)
```

`DEFAULT_TIMEOUT` comes from `gvm.connections`. The SSH example (hostname `127.0.0.1`, port `22`, username `ssh-user`) is a comment. The password in that comment is a placeholder.

`cli_app.sh` bind-mounts the host directory `/tmp/gvm/gvmd` onto `/run/gvmd` in the container and does not mount `/tmp/gvm/gvmd` inside the container. The image creates an empty `/tmp/gvm/gvmd`. **Inferred:** inside that `docker run`, `/tmp/gvm/gvmd/gvmd.sock` is absent, so the CLI uses `/run/gvmd/gvmd.sock`, which is the host socket directory.

A host process running `python3 cli.py` (not in Docker) uses `/tmp/gvm/gvmd/gvmd.sock` when the compose bind has created that file. **Inferred** from the same `isfile` test.

## Authentication

GMP `authenticate(username, password)`.

| Source | Variable |
|---|---|
| Environment / `.env` | `USERN`, `PASS` |
| Prompt | `input` / `getpass.getpass` when the key is absent |

Three attempts. Status attribute other than `400` is treated as success. The local `password` string is cleared. `.env` is gitignored.

Microsoft 365, separate from GMP:

| Variable | Used |
|---|---|
| `ms_app_id` | Yes, as the O365 client id |
| `ms_app_secret_val` | Yes, as the client secret |
| `ms_tenant_id` | Read into a local variable and not passed to `Account` |
| Token file | `./o365_token.txt` (gitignored). Created only if missing, via `account.authenticate` |

Scopes: `IMAP.AccessAsUser.All`, `POP.AccessAsUser.All`, `SMTP.Send`, `Mail.Send`, `offline_access`. Second `Account` uses `auth_flow_type='authorization'` and `FileSystemTokenBackend`.

SMTP variables `mail_host`, `mail_port`, `mail_user`, `mail_pass`, `mail_from` appear in the README and in comments. The loader is commented out.

Supabase:

| Variable | Used |
|---|---|
| `SUPABASE_URL` | Assigned to `supabase_url` |
| `SUPABASE_KEY` | Assigned to `supabase_url`, replacing the URL |

`create_client` is then called with `(supabase_url, supabase_url)`. `supabase_pass` is unused. This is the code on `main`, not a confirmed working integration.

`cli_app.sh` does not pass `--env-file`. The compose `cli-app` service does not declare `env_file` or a mount for `.env`. **Unknown / Requires Confirmation** how operators inject credentials into the container. A host run loads `.env` from the process working directory via `load_dotenv()`.

## Report pipeline

1. `get_report_formats(details=True)`, select `name == "XML"`.
2. `get_report(report_id, report_format_id, ignore_pagination=True, details=True)`.
3. `elem2dict` walks the lxml tree. A repeated child tag becomes a list. A single child stays a dict. Several loops normalise a lone dict into a one-element list (`targets`, `tasks`, `results`, `hosts`, report formats, NVT refs).
4. HTML is built with string concatenation. Severity sections are High, Medium, Low, Log, and threat `False`.
5. The overview table resets `summary_rows` on each host and appends to `summary_headers`. For a report with one host this is one row. For several hosts, headers accumulate and only the last host's cells are kept. **Implemented.** The sample report has one host, so it does not demonstrate the multi-host case.
6. NVT tags are split on `|` and then on the first `=`. The HTML reads `summary`, `insight`, `solution_type`, and `solution` with direct key access.

The sample `cli_app/test.json` records:

- GMP version `22.6` inside the report.
- Task and target name `timesheets.php8`.
- Owner name `admin`.
- `hosts.count` of `1`.
- Scan start `2025-03-03T05:51:49Z`.
- A filter term that includes `min_qod=70` and `levels=hml`. That filter is inside the saved report. `cli.py` does not send it.

The compose feed pin is `24.10`, and several images use tag `stable`. The sample predates the Docker image commit (`f5d71af`, 2025-03-25). Which GMP version a freshly pulled `stable` image speaks is **Unknown / Requires Confirmation**.

## Supabase row

Table name in code: `scan_que`.

| Column written | Value |
|---|---|
| `created_at` | Scan start, `T` replaced with space, last character dropped |
| `type` | `greenbone` |
| `command` | `" ".join(sys.argv)` |
| `start_at` | Same transformation as `created_at` |
| `end_at` | Same transformation of scan end |
| `results` | HTML string |
| `to_start` | `False` |
| `intervals` | `False` |
| `result_summary` | `False` |

No schema migration is in the repository. No other table is named.

## Greenbone compose stack

Project name: `greenbone-community-edition`.

Registry: `registry.community.greenbone.net/community/`.

| Service | Image | Role |
|---|---|---|
| `vulnerability-tests` | `vulnerability-tests` | VT feed, `FEED_RELEASE=24.10`, volume `vt_data_vol` |
| `notus-data` | `notus-data` | Notus feed volume |
| `scap-data` | `scap-data` | SCAP volume |
| `cert-bund-data` | `cert-bund-data` | CERT volume |
| `dfn-cert-data` | `dfn-cert-data` | Same CERT volume, after `cert-bund-data` |
| `data-objects` | `data-objects` | `FEED_RELEASE=24.10` |
| `report-formats` | `report-formats` | `FEED_RELEASE=24.10`, after `data-objects` |
| `gpg-data` | `gpg-data` | GnuPG volume |
| `redis-server` | `redis-server` | Redis socket volume, `restart: on-failure` |
| `pg-gvm` | `pg-gvm:stable` | Postgres data and socket volumes |
| `gvmd` | `gvmd:stable` | Manager. Depends on Postgres started and the feed one-shots completed |
| `gsa` | `gsa:stable` | Web UI, `127.0.0.1:9392:80` |
| `configure-openvas` | `openvas-scanner:stable` | Writes `openvas.conf` (`table_driven_lsc = yes`, `openvasd_server = http://openvasd:80`) and a log config |
| `openvas` | `openvas-scanner:stable` | Tails `/var/log/openvas/openvas.log` |
| `openvasd` | `openvas-scanner:stable` | `OPENVASD_MODE=service_notus`, listen `0.0.0.0:80` inside the network. Host port `127.0.0.1:3000:80` is commented out |
| `ospd-openvas` | `ospd-openvas:stable` | Scanner daemon, socket mode argument `666` |
| `gvm-tools` | `gvm-tools` | Interactive tools container |
| `cli-app` | build `../cli_app/Dockerfile` | This repository's CLI image |

`gvmd` volume mounts include both `gvmd_socket_vol:/run/gvmd` and `/tmp/gvm/gvmd:/run/gvmd`. Two mounts share that container path. Which one is visible at runtime is **Unknown / Requires Confirmation**. GSA mounts only `gvmd_socket_vol` at `/run/gvmd`. The host CLI and `getGvmCLI.sh` use `/tmp/gvm/gvmd/gvmd.sock`.

`ospd-openvas` adds capabilities `NET_ADMIN` and `NET_RAW`, and sets `seccomp=unconfined` and `apparmor=unconfined`. The comment in the file says those are for packet capture and raw sockets used by alive detection.

`cli-app` `depends_on` uses `condition: service_completed_successfully` for `gvmd` and `scap-data`. `scap-data` is a feed job that is expected to exit. `gvmd` is not. The one-shot operator path is `cli_app.sh`, which `docker run`s the image `greenbone-community-edition-cli-app` and does not use that `depends_on`.

Dockerfile details:

- Base `debian:stable-slim`.
- Installs `gosu`, `python3`, `python-is-python3`, `pipx`, `python3-pip`.
- Copies the build context to `/sili_cli/` and `entrypoint.sh` to `/usr/local/bin/entrypoint`.
- Group and user `gvm`, gid/uid `1001`, system user, shell `/bin/false`.
- Runs `install_requirements.sh` at build time.
- `chmod -R 777 /tmp/gvm`.
- `ENTRYPOINT ["/usr/local/bin/entrypoint"]`.
- `CMD ["tail -f /dev/null"]` is a single JSON argument, not three words. `entrypoint.sh` executes `gosu gvm ./cli.py "$@"`, so a container started with no CLI arguments receives that string as the target. `cli_app.sh` passes the operator's arguments instead.

`restart.sh` is `docker compose up -d` with no `down` and no `--force-recreate`.

## Data stores

| Store | Where | What this repo does with it |
|---|---|---|
| Greenbone Postgres | Volume `psql_data_vol` on `pg-gvm` | Not accessed by `cli.py`. gvmd owns it |
| Redis | Socket volume for OpenVAS | Not accessed by `cli.py` |
| Feed volumes | VT, Notus, SCAP, CERT, data objects, GPG | Populated by the community images |
| Supabase `scan_que` | External, optional | Insert only, and only with `--supabase` |
| `o365_token.txt` | Working directory | O365 refresh token. Gitignored |
| `.env` | Working directory | Gitignored |

There is no database migration, ORM, or local SQLite file.

## Background work

The CLI process is synchronous. It blocks in a poll loop until `in_use` is not `1` or until ten poll errors.

Greenbone's own containers (`gvmd`, `ospd-openvas`, `openvas`, `redis-server`, `pg-gvm`, `gsa`) keep running after the CLI exits. Feed services are one-shot (`service_completed_successfully` on the gvmd dependencies).

No queue worker, cron file, or scheduler is in git. The README mentions cron as a reason to store GVM credentials in `.env`.

## HTTP and other interfaces

This repository does not expose an HTTP API.

| Interface | Bind | Owner |
|---|---|---|
| GSA | `127.0.0.1:9392` to container port 80 | `gsa` |
| openvasd | Container port 80 on the compose network. Host publish is commented (`127.0.0.1:3000:80`) | `openvasd` |
| gvmd GMP | Unix socket | `gvmd` |
| ospd | Unix socket volume | `ospd-openvas` |

Outbound calls from the CLI are GMP on the socket, Microsoft 365 when mailing, and Supabase when the flag is set. Greenbone images pull feeds from Greenbone's distribution. Those feed URLs are not pinned in this compose file.

## Pipeline, quality, and tests

`bitbucket-pipelines.yml` runs one step, `SonarQube analysis`, on branch `main`, using the SonarSource pipe `3.0.2` and repository or workspace variables `SONAR_HOST_URL` and `SONAR_TOKEN`.

`sonar.projectKey=overdrive_vulnerability_scan_491f4e22-35d3-4703-8862-c62d23d68958`. No other Sonar properties (sources, exclusions, tests) are set.

There is no test step, no lint step, and no deploy step.

`cli_app/test.json` is not referenced by `cli.py`, by a test runner, or by the pipeline. It is a saved report from 2025-03-03.

## Security-relevant implementation details

These are observations from the files, not a pentest and not a policy.

- `/tmp/gvm` is mode `777` in `install_greenbone.sh`, `start.sh`, and the Dockerfile.
- `ospd-openvas` drops seccomp and AppArmor confinement and adds `NET_ADMIN` and `NET_RAW`. The socket argument includes mode `666`.
- GSA is bound to loopback. openvasd's host port is not published.
- `.env` and `o365_token.txt` are gitignored. The README and the script header still contain placeholder passwords and empty secret markers. Do not treat those placeholders as live credentials.
- `send_ms_email` requests mailbox scopes (`IMAP.AccessAsUser.All`, `POP.AccessAsUser.All`) as well as send scopes.
- The Supabase `command` column stores the full argument vector, which includes target names and mail addresses when those were passed.
- The CLI prints the GMP authentication response body when all three logins fail.

## Development process

Documented host setup is Ubuntu, Docker's apt repository, then `install_greenbone.sh` and `start.sh`. A host-only CLI install is `cli_app/install_requirements.sh` after cloning `https://bitbucket.org/overdrive/vulnerability_scan.git`.

No contribution guide, no version tag, and no changelog file. Release history is the git log on `main`.

## Discrepancies to confirm

| Topic | Documented or scanned | On `main` at `de29c65` |
|---|---|---|
| `sonarqube_scan.bat` | Onboarding file list | Not in git |
| Script name `cli-app.py` | `readme.md` | File is `cli_app/cli.py` |
| SSH toggle at lines 355 and 357 | `readme.md` | Socket selection is lines 384–387. SSH call is a comment at line 389 |
| SMTP `.env` keys | `readme.md` | Loader and `send_email` are comments. Active mail is `O365` |
| `restart.sh` | Name | `docker compose up -d` only |
| Supabase client | README implies URL and key | Key overwrites the URL variable; both `create_client` arguments are that variable |
| `cli-app` compose service | Present in compose | `depends_on` waits for long-running `gvmd` to complete successfully |
| GMP version | Sample report `22.6` | Compose feed `24.10` and image tag `stable` |
